android 自動換行FlowLayout

ios 自動換行FlowLayout


最近產品需要實現自動換行功能,在gitHub看了一下,雖然有不少,但都有那麼一點不滿足需求的,或者感覺用着不方便的。所以乾脆自己寫了一份,順便有時間寫了一份ios的版本,有興趣的可點擊上面鏈接。


在這裏先提供下載地址:https://github.com/lanqi-x/flowLayout

然後來個圖先:



實現概要思路爲1、繼承ViewGroup,實現對子view進行佈局,不進行其他處理,不跟業務掛鉤,以保證其靈活性。2、支持adapter方式,仿recycleview的adapter,使用觀察者模式。(需要注意的是,1該控件並未實現子view的複用,2不建議同時使用adapter和自己在代碼中直接調用addView)

實現該控件,我寫了三個類,分別爲FlowLayout、FlowAdapter和FlowDataSetObserver,這裏按照簡易度,簡單的介紹下。(如想只看FlowLayout的實現可點擊這裏


1、FlowDataSetObserver

先貼下代碼:

    public void onChanged() {
        // Do nothing
    }

    public void onItemRangeChanged(int positionStart, int itemCount) {
        // do nothing
    }

    public void onItemRangeInserted(int positionStart, int itemCount) {
        // do nothing
    }

    public void onItemRangeRemoved(int positionStart, int itemCount) {
        // do nothing
    }

    public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
        // do nothing
    }

這是一個觀察者,很簡單都是空實現,相信看過recycleView中Adapter源碼的同學都很熟悉了,就是對應幾個數據改變的事件,沒什麼好說的。


2、FlowAdapter

這個代碼有點多,就不貼太多代碼了,具體思路跟recycleView的Adapter差不多。

常用的幾個方法有以下幾個和數據改變的notifyItemRangeChanged等,意義也跟recycleView的一樣就不解釋了

   public abstract T onCreateViewHolder(FlowLayout flowLayout, int viewType);

    public abstract void onBindViewHolder(T view, int position);

    public long getItemId(int position) {
        return position;
    }

    public abstract Object getItem(int position);

    public int getItemViewType(int position) {
        return 0;
    }
    public abstract int getItemCount();

然後內部類AdapterDataObservable繼承java.util.ArrayList包下的Observable實現被觀察者,看過源碼的同學應該都知道Observable裏有一個ArrayList數組,registerObserve()方法既是將Observer對象存在該數組中,所以被觀察者即實現跟觀察者對應的幾個方法,對觀察者們進行一個個的回調(個人覺得觀察者模式就是被觀察者對觀察者的接口回調),貼個方法做例子:

public void notifyItemRangeChanged(int positionStart, int itemCount) {
    for (int i = mObservers.size() - 1; i >= 0; i--) {
        mObservers.get(i).onItemRangeChanged(positionStart, itemCount);
    }
}

3、FlowLayout

準備都做好了,終於來到自定義控件了。(其實實際做的時候,是先簡單的實現的FlowLayout,後來纔想要來個Adapter模式再一點點加上去的)

首先繼承ViewGroup,然後對子View進行擺放,大家都知道自定義ViewGroup主要的處理onMeasure和onLayout方法,所以這裏也主要講這兩個方法。首先是測量方法onMeasure。這裏主要是計算每個子View的寬度,確定每一行有幾個子View,爲了不重複計算和方便服用,跟其他同學的實現方式一樣,封裝了一個內部類Line,來負責裝每一行的子View計算每一行的高度和對子View進行擺放。

所以onMeasure方法就會變得簡單一點,就對子View進行for循環,回去每個子View的寬度和內邊距之和,當其大於FlowLayout的寬度時,則過行,及new一個新的Line對象。關鍵代碼爲:

            int childWidth = child.getMeasuredWidth();
            mUsedWidth += childWidth;// 增加使用的寬度
            if (mUsedWidth <= sizeWidth) {// 使用寬度小於總寬度,該child屬於這一行。
                mLine.addView(child);// 添加child
                mUsedWidth += mHorizontalSpacing;// 加上間隔
                if (mUsedWidth >= sizeWidth) {// 加上間隔後如果大於等於總寬度,需要換行
                    if (!newLine()) {
                        break;
                    }
                }
            } else {// 使用寬度大於總寬度。需要換行
                if (mLine.getViewCount() == 0) {// 如果這行一個child都沒有,那麼就加上去,以保證每行都有至少有一個child
                    mLine.addView(child);// 添加child
                    if (!newLine()) {// 換行
                        break;
                    }
                } else {// 如果該行有數據了,就直接換行
                    if (!newLine()) {// 換行
                        break;
                    }
                    // 在新的一行,因爲這一行一個child都沒有,先加上去,以保證每行都有至少有一個child
                    mLine.addView(child);
                    mUsedWidth += childWidth + mHorizontalSpacing;
                }
            }
最後FlowLayout的高度根據配置,如是MATCH_PARENT和WRAP_CONTENT,則其高度爲每行高度和每行的行距之和,否則則爲用戶設置的高度

         for (int i = 0; i < linesCount; i++) {// 加上所有行的高度
             totalHeight += mLines.get(i).mHeight;
         }
        totalHeight += mVerticalSpacing * (linesCount - 1);// 加上所有間隔的高度
        totalHeight += getPaddingTop() + getPaddingBottom();// 加上padding
        // 設置佈局的寬高,寬度直接採用父view傳遞過來的最大寬度,而不用考慮子view是否填滿寬度,因爲該佈局的特性就是填滿一行後,再換行
        // 高度根據設置的模式來決定採用所有子View的高度之和還是採用父view傳遞過來的高度
        setMeasuredDimension(totalWidth,
                resolveSize(totalHeight, heightMeasureSpec));

對於onLayout方法,只要負責獲取每行的起始座標點傳給Line的layoutView方法即可。

而layoutView方法傳進來的起始座標,和子View的高寬度對子View進行layout即可,例如:

               for (int i = 0; i < count; i++) {
                    final View view = views.get(i);
                    int childWidth = view.getMeasuredWidth();
                    int childHeight = view.getMeasuredHeight();
                    // 計算出每個View的頂點,是由最高的View和該View高度的差值除以2,目的是爲了每個子View垂直居中
                    int topOffset = (int) ((mHeight - childHeight) / 2.0 + 0.5);
                    if (topOffset < 0) {
                        topOffset = 0;
                    }
                    // 佈局View
                    view.layout(left, top + topOffset, left + childWidth, top
                            + topOffset + childHeight);
                    left += childWidth + mHorizontalSpacing; // 爲下一個View的left賦值
                }

個人覺得關鍵的代碼就這些了,實際還實現了每行剩餘出來空間的分配方式、最多能顯示幾行和定義了FlowLayout的xml屬性等,如想了解,請自己看代碼吧!

不知道怎麼自定義控件的xml屬性的自己百度一下,這個有很多人介紹過,就不多說了,但在這裏補充一點,就是如果你想visibility屬性一樣,xml代碼提示出現的是gone、invisible等,而不是自己填的,可這樣實現

       <attr name="surplusSpacingMode" format="enum">
            <enum name="SURPLUSSPACINGMODE_AUTO" value="0" />
            <enum name="SURPLUSSPACINGMODE_SHARE" value="1" />
            <enum name="SURPLUSSPACINGMODE_SPACE" value="2"/>
        </attr>
即該屬性爲枚舉類型,在xml賦值的時候也只能選擇這幾個值的其中一個,代碼中獲取值,比如這裏的value是數字,那麼代碼中就可以直接typedArray.getInt()獲取了,從而避免傳入你不支持的值。如果你想用戶可以從這幾個值中選擇,或自己輸入數字,那麼可以將format="enum"改爲format="integer"就行了,不過在xml代碼提示中你定義的這幾個值會排到最後,value中的integer值會排在前面。

對了,最後提醒一句如要自定義的屬性在xml中有代碼提示,那麼<declare-styleable name="FlowLayout">這裏的name要跟你自定義的控件名一樣。

好,本文結束,如有說的不好的地方請多多包涵,也可在評論中指點一下。









發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章