Android 自定義多狀態(tài)提示輸入布局 ColorTextInputLayout

本文主要是講解在 ViewGroup 中混合使用 addView()xml布局文件 的方式來配置UI,自定義屬性等入門知識請移步其他博客。

前言

因為項目需要決定自定義一個 ViewGroup,但是在選定方案以后,全網(wǎng)搜索以后卻找不到一個addView() + xml布局文件 混合添加View的使用方式的講解。能提到混合使用的文章就寥寥無幾(實際上就看到一篇,被到處Ctrl CV,但這篇文章對混合使用方式也只是說了句在研究),真正講解如何混合使用的教程,我反正是沒看到的。

無奈之下只能對著Google官方提供的與我需求相似度20%的 TextInputLayout 慢慢摸索,最后好歹算是搞出來了。想了想,一方面為了總結(jié)和記錄;另一方面也是想補上這個缺口,使后來人不必再如我一般走彎路,特此寫下此文,希望能有所作用。


需求

最近項目需要一個多種提示狀態(tài)的輸入框來替代現(xiàn)有的登錄注冊框,由于提示文本的位置、手機號涉及到國際區(qū)號的選擇,加上考慮到后期布局可能變得更加復雜化,自定義 EditText 不能滿足需求,只能決定采用自定義 ViewGroup 的方式來實現(xiàn),由 ViewGroup 來對提示文本、輸入框狀態(tài)、狀態(tài)指示器等進行控制。

需求樣板

xuqiu.jpg

最終效果

效果圖.gif


方案選型

XML中使用自定義 ViewGroup 的方式

  1. 通過addView()方法將需要的控件添加到ViewGroup

  2. 通過在xml布局文件中配置需要包含的控件

  3. 混合布局:1+2

方案優(yōu)缺點對比

  • 方案1優(yōu)點在于簡潔,完全不需要另外配置xml布局文件,但也正是因此,它不具備在xml布局文件中動態(tài)配置子View的擴展性

  • 方案2優(yōu)點是具有完全的可配置性,可在xml布局文件中任意配置子View,但缺點也很明顯:

    1. 通用的子 View仍然需要在每個使用該 ViewGroupxml布局文件 中配置,過于繁瑣

    2. xml布局文件中配置在 ViewGroup下的子 View都會被 add到 ViewGroup下,無法進一步分配

  • 方案3的具有方案2的可配置性,同時又能通過方案1的方式來解決方案2的第1個缺點,也可以通過重寫 ViewGroupaddView() 方法來解決方案2的第2個缺點。

結(jié)論

由于前述的需求,我需要1個通用的 TextView 來作為提示文本,同時又需要一個 ViewGroup 來存放從xml布局文件中加載進來的子View,這2個正對應(yīng)方案2的2個問題,方案3正好能滿足需求。


項目實施

在編寫的過程中,有參考Google官方的 TextInputLayout,這個控件很有意思,它結(jié)合 EditText 一起使用的時候可以對hint文本進行移動,具體情況想了解可以搜一下看看。

TextInputLayout 重寫的addView() 方法:

public void addView(View child, int index, LayoutParams params) {
    if (child instanceof EditText) {
        FrameLayout.LayoutParams flp = new FrameLayout.LayoutParams(params);
        flp.gravity = 16 | flp.gravity & -113;
        this.inputFrame.addView(child, flp);
        this.inputFrame.setLayoutParams(params);
        this.updateInputLayoutMargins();
        this.setEditText((EditText)child);
   } else {
        super.addView(child, index, params);
   }
}

這里其實我走了一段彎路,開始參考TextInputLayout源碼時,陷入它的2個坑里:

  1. addView(View child, int index, LayoutParams params)方法可能是因為編譯后的緣故,Override標志沒了,我誤以為此方法并非重寫的方法。
  2. params 沒有標注完整的具體類型,因為TextInputLayout本身是繼承的LinearLayout,我想當然的把它當作了LinearLayout.LayoutParams,而實際上它始終都是ViewGroup.LayoutParams。

本來錯誤2很容易發(fā)現(xiàn),但是在錯誤1的加持下,這個問題被掩蓋了,我花了幾個小時在錯誤的方法上面,最后一無所獲。無奈之下改為使用onLayout()方法,最終從肉眼視覺上達到想要的效果。這個方案相比直接在加載View的時候按需配置View樹顯然會差一些,不論是感覺上還是性能上均如此。

好消息是:次日我不甘心就這么算了,再次嘗試addView() 方法,終于給我發(fā)現(xiàn)了上述的2個坑,從而成功的使用addView() 方法做到了想要的效果。

這里說說TextInputLayout 給我的2個重要啟發(fā):

  1. addView() 方法在控件從xml布局文件轉(zhuǎn)化為View過程中發(fā)揮的作用

  2. setAddStatesFromChildren() 方法

我們先看 addView() 方法,setAddStatesFromChildren() 方法會在后面進行講解。

addView() 方法

這里我重寫的是帶有3個參數(shù)的 addView(View child, int index, LayoutParams params),因為我需要用到第3個參數(shù),各參數(shù)分別表示:

  • child ????? 將要add進來的View
  • index ???? child將被add到的position,-1表示add到最后
  • params ?? 將在child上設(shè)置的LayoutParams參數(shù)(其實就是包含childxml布局文件中配置的屬性的LayoutParams

關(guān)于 addView() 方法在UI創(chuàng)建過程中的作用大概看了一下源碼和網(wǎng)上的解析文章,有了個粗略的了解。

簡單來說就是
在Android系統(tǒng)解析 xml布局文件 轉(zhuǎn)換成 View 的過程中,會調(diào)用當前正在解析的 ViewGroup 中的 addView() 方法,把 xml布局文件中該 ViewGroup 包含的 ViewViewGroup 一個個的 add 進來。


接下來進入到代碼解析階段:

show_code.jpg


1. 先看看構(gòu)造方法:

public ColorTextInputLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr, 0);
        setOrientation(VERTICAL);
        setWillNotDraw(true);
        mTvHint = new TextView(context);
        mTvHint.setTag(TAG_HINT);

        mFlInputPanel = new FrameLayout(context);
        mFlInputPanel.setTag(TAG_PANEL);
        mFlInputPanel.setAddStatesFromChildren(true);
        mFlInputPanel.setBackgroundResource(R.drawable.selector_color_hint_panel);

        mIvIndicator = new ImageView(context);
        FrameLayout.LayoutParams indicatorLp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.MATCH_PARENT);
        mFlInputPanel.addView(mIvIndicator, 0, indicatorLp);

        LinearLayout.LayoutParams rootLp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        addView(mTvHint, 0, rootLp);
        addView(mFlInputPanel, 1, rootLp);
        ...... 省略掉了OnGlobalFocusChangeListener監(jiān)聽代碼
}

代碼解析

這部分是 ColorTextInputLayout 的構(gòu)造方法,我在 ColorTextInputLayout 的構(gòu)造方法中 new TextView(提示文本 mTvHint) 和 FrameLayout(輸入面板 mFlInputPanel) 時分別給它們setTag(),以便在 addView() 方法中把它們與xml布局文件中的View區(qū)分開來。同時 new ImageView(狀態(tài)指示器 mIvIndicator),并add到 mFlInputPanel 中。最后,把 mTvHintmFlInputPanel add到 ColorTextInputLayout 中。

這里需要注意 mFlInputPanel.setAddStatesFromChildren(true) 這一行,這里我先不說它的作用,留到后面另一個使用到的地方再一起說。


2. 再來看看重寫的 addView() 方法

@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
    String tag = (String) child.getTag();
    //  提示和輸入面板add到本ViewGroup,其他的add到容器中
    if (TextUtils.equals(tag, TAG_HINT) || TextUtils.equals(tag, TAG_PANEL)) {
        super.addView(child, index, params);
    } else {
        //  輸入面板當前已經(jīng)add了圖標指示器,最多只能再add 1個子控件
        if (mFlInputPanel.getChildCount() > 1) {
            throw new IllegalStateException("ColorTextInputLayout can host only one child");
        }
        FrameLayout.LayoutParams flp = new FrameLayout.LayoutParams(params);
        MarginLayoutParams marginLp = (MarginLayoutParams) params;
        flp.leftMargin = marginLp.leftMargin == 0 ? marginLp.getMarginStart() : marginLp.leftMargin;
        flp.rightMargin = marginLp.rightMargin == 0 ? marginLp.getMarginEnd() : marginLp.rightMargin;
        flp.topMargin = marginLp.topMargin;
        flp.bottomMargin = marginLp.bottomMargin;
        mFlInputPanel.addView(child, flp);
        if (child instanceof EditText) {
            setEditText((EditText) child);
        } else if (child instanceof ViewGroup) {
            EditText edt = getEditTextFromViewGroup((ViewGroup) child);
            if (edt == null) {
                throw new IllegalStateException("The ViewGroup in ColorTextInputLayout must have one EditText");
            }
            setEditText(edt);

        } else {
            throw new IllegalStateException("ColorTextInputLayout can host only an EditText or a ViewGroup containing EditText");
        }
    }
}

代碼解析

if 分支表示這2個控件依然按照 ColorTextInputLayout 的父類的 addView() 方法添加到 ColorTextInputLayout 中;else 分支不用我說你們也都能猜到了,是的,這就是把 xml布局文件 中包裹在 ColorTextInputLayout 下的子View添加到輸入面板(mFlInputPanel)的代碼,這里說一下:

  • if (mFlInputPanel.getChildCount() > 1) 是用來限定 xml布局文件 中只能包含一個 child,類似 ScrollView。為什么是大于1而不是大于0呢?因為 mIvIndicator 已經(jīng)在構(gòu)造方法中add到 mFlInputPanel 里了。

這個限定的作用是什么?
分析了項目可能的使用情況,最簡單的使用情況就只包含一個 EditText,另外的情況就是包含多個控件。但是這里我并沒有辦法知到使用多個控件時到底想要什么樣的擺放方式,甚至可能不同的地方需要不同的擺放方式,此外多個控件的擺放過于復雜,綜合以上因素,我決定效仿 ScrollView:限定只允許包含1個child。要么僅包含一個 EditText;要么包含一個包含 EditTextViewGroup,ViewGroup 內(nèi)部的布局方式我不關(guān)心。

  • 接著往下看,到 mFlInputPanel.addView(child, flp) 為止,這部分就是從子View配置在xml布局文件中的 params 中取出 marinXXX 屬性,然后按照這些屬性重新把子View add到 mFlInputPanel 中。

  • 再往下直到結(jié)束就是取 EditText 的過程了:若xml布局文件中,ColorTextInputLayout 當前包裹的是 EditText 則直接 setEditText();若包裹的是 ViewGroup,先通過 getEditTextFromViewGroup() 方法取出 ViewGroup 中的 EditText,再 setEditText();如果以上2種條件都不滿足,則拋出異常,提示 xml布局文件 中包裹的子View類型錯誤。setEditText() 就是一個將View賦值給全局變量 mEditText 的方法,這里不多說。

3. 下面看看getEditTextFromViewGroup() 方法

/**
 * 使用遞歸來遍歷View樹,從ViewGroup中取出EditText(強烈建議只包含一個EditText)
 *
 * @param viewGroup
 * @return 從ViewGroup中取出的EditText,若ViewGroup包含多個EditText,將始終只返回取到的第一個;
 */
private EditText getEditTextFromViewGroup(ViewGroup viewGroup) {
    //  必須給ViewGroup添加此方法,否則輸入面板設(shè)置的此方法不會生效
    viewGroup.setAddStatesFromChildren(true);
    EditText editText = null;
    int childCount = viewGroup.getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = viewGroup.getChildAt(i);
        //  EditText越靠前,在View樹中同級靠后的就不用處理
        if (!(child instanceof ViewGroup)) {
            if (child instanceof EditText) {
                editText = (EditText) child;
                break;
            }
        } else {
            editText = getEditTextFromViewGroup((ViewGroup) child);
        }
    }
    return editText;
}

代碼解析

總體上是通過遞歸的方式從 ViewGroup 中取出 EditText。這里有2個點要提一下:

1.在 ViewGroup 中,EditText 盡可能的放到前面

在多層 ViewGroup 嵌套的情況下,假定View樹上同級的View總數(shù)不變,EditText 越靠前,與它同級但比它靠后的View就越多,這些View不用再處理,理論上能提升一部分性能。當然,這是我這個遞歸方法不完美導致的。理論上最優(yōu)方法就是從根節(jié)點一層一層的往葉子節(jié)點找,而不是找到一個 ViewGroup 就進入。算法學的不太好,這里后期再優(yōu)化吧。

  1. 必須給 ViewGroup 添加此方法,否則構(gòu)造方法中 mFlInputPanel 設(shè)置的此方法不會生效。

項目實施 中我提到 TextInputLayout 給我 2個啟發(fā), setAddStatesFromChildren() 就是另一個,它的作用如下:
它將設(shè)置父View與子View的背景聯(lián)動,實質(zhì)就是在構(gòu)建 ViewGroupdrawableState 時,會將子View的所有 drawableState 合并在一起交給父View,并在子View刷新drawable時通知父View

什么意思呢?就是子ViewdrawableState 發(fā)生變化時,ViewGroup 也會同步到此 drawableState 狀態(tài)。

lizi.jpg

以我現(xiàn)在的需求來說,我需要在 EditText 獲取到焦點時,將 mFlInputPanel 的背景圖設(shè)置為獲取焦點的狀態(tài)圖。按嘗龜

changgui.jpg

啊不,常規(guī)做法就是:對 EditText 設(shè)置焦點監(jiān)聽事件,在焦點變化時更換 mFlInputPanel 背景圖。但是這樣一來很繁瑣;另外如果 ColorTextInputLayout 外部也需要監(jiān)聽 EditText 焦點狀態(tài),二者就會沖突了。

而有了 setAddStatesFromChildren() 方法以后,一切都簡單了:mFlInputPanel 首先設(shè)置一個 drawable 類型的 selector_xx.xml 背景,然后調(diào)用 setAddStatesFromChildren() 方法。這樣,在 EditTextdrawableState 狀態(tài)(包括焦點狀態(tài))變化時,mFlInputPanel 將會收到通知,自動選擇 selector_xx.xml 中對應(yīng)狀態(tài)的背景。

需要注意的是,setDuplicateParentStateEnabled() 方法與 setAddStatesFromChildren() 剛好相反,二者不可以一起使用,否則可能引起崩潰。
此外,經(jīng)過我自己的使用發(fā)現(xiàn),多層嵌套時,如果響應(yīng)的 ViewGroup 與 想監(jiān)聽的 View 之間還有嵌套的 ViewGroup ,那么需要在每一層 ViewGroup 都調(diào)用 setAddStatesFromChildren() 方法,響應(yīng)的 ViewGroup 才會生效。所以,在getEditTextFromViewGroup() 方法開頭我也調(diào)用了 setAddStatesFromChildren() 方法。


尾聲

到這里,本次自定義 ViewGroupxml布局文件 + addView() 混合添加View的使用就講完了,感謝閱讀,有什么不對的地方也請指正。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內(nèi)容