FlowLayout:Android自定義ViewGroup實(shí)現(xiàn)

需求

我們需要實(shí)現(xiàn)一個(gè)自定義的Layout,該Layout可以容納若干個(gè)寬高不等的子元素吹榴,元素按照從左到右的順序排列亭敢,當(dāng)元素超出屏幕顯示范圍時(shí),換一行繼續(xù)顯示图筹,like this


View和ViewGroup

Android的界面都是由View帅刀、ViewGroup及其派生類(lèi)組合而成,其中远剩,View是ViewGroup及其他UI組件的基類(lèi)劝篷。ViewGroup是放置View的容器,在編寫(xiě)xml布局文件的時(shí)候民宿,View所有以layout開(kāi)頭的屬性都是提供給ViewGroup的,ViewGroup根據(jù)這些屬性來(lái)給childView計(jì)算出測(cè)量模式和建議的寬高像鸡,并將childView繪制在屏幕上的適當(dāng)位置

UI是怎樣被繪制出來(lái)的

UI組件渲染過(guò)程可分為三個(gè)階段:測(cè)量活鹰、布局、繪制.

Measure過(guò)程

Measure過(guò)程的任務(wù)是根據(jù)ViewGroup給的參數(shù)計(jì)算出視圖自身的大小只估,在View中與Measure過(guò)程相關(guān)的方法有measure()志群、onMeasure()setMeasureDimension(),其中onMeasure()是我們需要在自定義視圖的時(shí)候重寫(xiě)的方法蛔钙,在measure()方法中锌云,onMeasure()被調(diào)用,在onMeasure()計(jì)算完畢后吁脱,調(diào)用setMeasureDimension()設(shè)置自身大小桑涎。
自身大小的計(jì)算結(jié)果取決于視圖本身所占區(qū)域的大小及ViewGroup傳遞過(guò)來(lái)的MeasureMode值,其中MeasureMode可能取值為UNSPECIFIED兼贡、EXACTLYAT_MOST攻冷。

UNSPECIFIED

表示childView可將自身大小設(shè)置為自身想要的任意大值,一般出現(xiàn)于AdapterView的item的高度屬性中

EXACTLY

表示childView應(yīng)該將自身大小設(shè)置為ViewGroup指定的大小遍希,當(dāng)View指定了自身寬或高為精確的值或match_parent時(shí)等曼,ViewGroup會(huì)傳入該Mode

AT_MOST

表示childView可以在一個(gè)限定的最大值范圍內(nèi)設(shè)置自己的大小,當(dāng)View指定自身寬或高為wrap_content時(shí),ViewGroup會(huì)傳入該Mode
Measure過(guò)程結(jié)束后禁谦,視圖大小即被確定胁黑。

Layout過(guò)程

Layout過(guò)程的任務(wù)是決定視圖的位置,framework調(diào)用View的layout()方法來(lái)計(jì)算位置州泊,在layout()方法中丧蘸,onLayout()方法會(huì)被調(diào)用,這個(gè)方法是需要View的派生類(lèi)重寫(xiě)的拥诡,在此實(shí)現(xiàn)布局邏輯触趴。
Layout是一個(gè)自頂向下遞歸的過(guò)程,先布局容器渴肉,再布局子視圖冗懦,因此,ViewGroup的位置一定程度上決定了它的childView的位置仇祭。
Layout過(guò)程結(jié)束后披蕉,視圖在屏幕上的位置即被確定。

Draw過(guò)程

Draw過(guò)程的任務(wù)是根據(jù)視圖的尺寸和位置乌奇,在相應(yīng)的區(qū)域內(nèi)繪制自身樣式没讲。同樣的,framework會(huì)調(diào)用onDraw()方法礁苗,我們需要重寫(xiě)onDraw()方法實(shí)現(xiàn)繪制邏輯爬凑,在View中,可以通過(guò)調(diào)用invalidate()方法觸發(fā)視圖重繪试伙。

讓View支持Padding和Margin

如上文所說(shuō)嘁信,所有以layout開(kāi)頭的屬性都是交由容器處理的,layout_margin就是這樣一個(gè)屬性疏叨,在自定義View中可通過(guò)getLayoutParams()返回的LayoutParams對(duì)象來(lái)獲取到視圖各個(gè)方向的margin值潘靖,容器只需在layout過(guò)程中將margin值作為偏移量加入即可實(shí)現(xiàn)將視圖放置在正確位置。
Padding是視圖的自有屬性蚤蔓,描述其各個(gè)方向邊界到內(nèi)部?jī)?nèi)容的距離卦溢,在代碼中可通過(guò)getPaddingTop(/Left/Right/Bottom)()來(lái)獲取各個(gè)方向的padding值,在Measure過(guò)程中秀又,需要注意View尺寸包含內(nèi)容區(qū)域加上padding區(qū)域单寂,padding區(qū)域內(nèi)的內(nèi)容將不會(huì)被繪制。

Step by Step實(shí)現(xiàn)需求

onMeasure

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int contentWidth = MeasureSpec.getSize(widthMeasureSpec);
        int contentHeight = MeasureSpec.getSize(heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //Padding支持
        int topOffset = getPaddingTop();
        int leftOffset = getPaddingLeft();
        int selfWidth = 0, selfHeight = 0;
        int currentLineWidth = 0, currentLineHeight = 0;
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == GONE)
                continue;
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams();
            int childWidth = Math.max(child.getMeasuredWidth(), getSuggestedMinimumWidth()) + layoutParams.leftMargin + layoutParams.rightMargin;
            int childHeight = Math.max(child.getMeasuredHeight(), getSuggestedMinimumHeight()) + layoutParams.topMargin + layoutParams.bottomMargin;
            if (currentLineWidth + childWidth > contentWidth  - getPaddingLeft() - getPaddingRight()) {
                //需要另起一行
                currentLineWidth = Math.max(currentLineWidth,childWidth);
                selfWidth = Math.max(selfWidth, currentLineWidth);
                currentLineWidth = childWidth;
                selfHeight += currentLineHeight;
                currentLineHeight = childHeight;
                //Measure的時(shí)候順便把位置計(jì)算出來(lái)
                child.setTag(new Location(child, leftOffset, selfHeight + topOffset, childWidth + leftOffset, selfHeight + child.getMeasuredHeight() + topOffset));
            } else {
                //不需要換行
                child.setTag(new Location(child, currentLineWidth + leftOffset, selfHeight + topOffset, currentLineWidth + child.getMeasuredWidth() + topOffset, selfHeight + child.getMeasuredHeight() + topOffset));
                currentLineWidth += childWidth;
                currentLineHeight = Math.max(currentLineHeight, childHeight);
            }
            if (i == childCount - 1) {
                //到最后一個(gè)child的時(shí)候更新高度
                sselfWidth = Math.max(currentLineWidth, selfWidth) + getPaddingRight() + getPaddingLeft();
                selfHeight += currentLineHeight + getPaddingTop() + getPaddingBottom();
            }
        }
        setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? contentWidth : selfWidth,
                heightMode == MeasureSpec.EXACTLY ? contentHeight : selfHeight);
    }

經(jīng)過(guò)如上處理疲扎,ViewGroup的尺寸和childView的位置便被計(jì)算出來(lái)壹甥,并且ViewGroup可根據(jù)childView排列情況自動(dòng)調(diào)整自身寬高句柠。

onLayout

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                Location location = (Location) child.getTag();
                child.layout(location.left, location.top, location.right, location.bottom);
            }
        }
    }

onMeasure()中我們已經(jīng)順手計(jì)算出了各個(gè)childView的位置信息,所以在Layout步驟中只需將其按照位置擺放到相應(yīng)區(qū)域即可。

draw

draw方法是由framework調(diào)用僻族,在ViewGroup的onDraw()方法中繪制的內(nèi)容最后會(huì)被作為ViewGroup的背景述么,所以如果需要更改背景內(nèi)容可重寫(xiě)該方法。draw()中會(huì)遞歸調(diào)用childView的onDraw()方法敷钾,調(diào)用完畢后ViewGroup本身和childView都繪制完畢挠锥,一次渲染過(guò)程到此結(jié)束粱侣。

附加特性

可通過(guò)對(duì)ViewGroup設(shè)置LayoutAnimation來(lái)為childView顯示的過(guò)程附加動(dòng)畫(huà),一個(gè)簡(jiǎn)單的例子:

public FlowLayout(Context context) {
        super(context);
        setLayoutAnimation(
                new LayoutAnimationController(AnimationUtils.loadAnimation(
                        getContext(), R.anim.list_animation), 0.3f));
    }

可以重寫(xiě)addView()方法為動(dòng)態(tài)添加的View附加動(dòng)畫(huà)情妖,重寫(xiě)removeView()方法實(shí)現(xiàn)移除View時(shí)的附加動(dòng)畫(huà)。

實(shí)現(xiàn)效果

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末摇邦,一起剝皮案震驚了整個(gè)濱河市施籍,隨后出現(xiàn)的幾起案子法梯,更是在濱河造成了極大的恐慌夜惭,老刑警劉巖诈茧,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件这嚣,死亡現(xiàn)場(chǎng)離奇詭異吏垮,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)遗嗽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人搁料,你說(shuō)我怎么就攤上這事椒振÷睿” “怎么了夹供?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵灵份,是天一觀(guān)的道長(zhǎng)荠察。 經(jīng)常有香客問(wèn)我甩卓,道長(zhǎng),這世上最難降的妖魔是什么谎倔? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任鸟辅,我火速辦了婚禮氛什,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘匪凉。我一直安慰自己枪眉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布再层。 她就那樣靜靜地躺著贸铜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪树绩。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,749評(píng)論 1 289
  • 那天隐轩,我揣著相機(jī)與錄音饺饭,去河邊找鬼。 笑死职车,一個(gè)胖子當(dāng)著我的面吹牛瘫俊,可吹牛的內(nèi)容都是我干的鹊杖。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼扛芽,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼骂蓖!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起川尖,我...
    開(kāi)封第一講書(shū)人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤登下,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后叮喳,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體被芳,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年馍悟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了畔濒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡锣咒,死狀恐怖侵状,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情毅整,我是刑警寧澤趣兄,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站毛嫉,受9級(jí)特大地震影響诽俯,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜承粤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一暴区、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧辛臊,春花似錦仙粱、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至刃唤,卻和暖如春隔心,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背尚胞。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工硬霍, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人笼裳。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓唯卖,卻偏偏與公主長(zhǎng)得像粱玲,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子拜轨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

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