ScFlowLayout一款A(yù)ndroid基于ViewGroup實(shí)現(xiàn)的流式布局

ScFlowLayout一款A(yù)ndroid基于ViewGroup實(shí)現(xiàn)的流式布局

本文原創(chuàng)踩麦,轉(zhuǎn)載請(qǐng)注明出處背镇。歡迎關(guān)注我的 簡書玖媚。
安利一波我寫的開發(fā)框架:MyScFrame喜歡的話就給個(gè)Star
ScFlowLayout已經(jīng)加入到該框架內(nèi)

場(chǎng)景

最近在做一個(gè)聊天功能,其中需要給對(duì)方打標(biāo)簽钝计,第一時(shí)間想到的就是流式布局茉帅,目前項(xiàng)目上用的是鴻洋大神的FlowLayout糙申,功能很強(qiáng)大宾添,不過我項(xiàng)目上只用到了展示效果,讀了大神的源碼柜裸,給了我一些靈感缕陕,這里我也寫一個(gè)FlowLayout,并且參考了一些Recycler.Adapter的做法疙挺。

參考資料

hongyangAndroid/FlowLayout
Android流式布局(FlowLayout)
自定義View扛邑、動(dòng)畫

實(shí)現(xiàn)功能

  • 使用adapter的形式綁定并處理數(shù)據(jù)
  • 支持多種布局一同展示
  • 支持多行,單行,指定顯示行數(shù)
  • 支持Item左對(duì)齊,居中對(duì)齊,右對(duì)齊
  • 支持行布局頂部對(duì)齊,居中對(duì)齊,底部對(duì)齊
  • 支持選中狀態(tài)
  • 支持設(shè)置行間距
  • 支持設(shè)置item間距

ScFlowLayout

1.思路

使用SparseArray<LineDes> mLineDesArray;保存每行的數(shù)據(jù),其中包括行高,行寬以及該行中包含的View的集合铐然。
使用BaseTagFlowAdapter mAdapter;來管理數(shù)據(jù)加載蔬崩,點(diǎn)擊事件,選中事件搀暑。

2.onMeasure()中的業(yè)務(wù)邏輯

onMeasure()
首先遍歷測(cè)量子View沥阳,將子View的頂點(diǎn)坐標(biāo)通過view.setTag()方法保存,同時(shí)把每行的數(shù)據(jù)保存在LineDes中自点,這樣寫是為了后續(xù)在onLayout()好處理桐罕,不用重復(fù)計(jì)算。
接下來我們?cè)谡{(diào)用setMeasuredDimension()方法之前需要給出布局的寬跟高樟氢,我這邊是通過getLayoutParams().widthgetLayoutParams().height來判斷布局的寬高冈绊,至于為什么要這樣寫大家可以參考這篇文章,如果想要實(shí)現(xiàn)指定行數(shù)的話需要遍歷每行高度埠啃,然后累加到mMeasuredHeight中。
在計(jì)算高度的時(shí)候伟恶,由于我這里實(shí)現(xiàn)了自定義行間距碴开,因此實(shí)際計(jì)算高度的時(shí)候還需要加上行間距的高度。

        //由于計(jì)算子view所占寬度
        Map<String, Integer> compute = compute(widthSize, widthMeasureSpec, heightMeasureSpec);
        mMeasuredWidth = widthSize;
        mMeasuredHeight = heightSize;
        if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
            mMeasuredWidth = compute.get(ALL_CHILD_WIDTH);
        }
        if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
            mMeasuredHeight = compute.get(ALL_CHILD_HEIGHT);
            if (mLineDesArray.size() > 1) {
                //加上行間距
                mMeasuredHeight += mLineSpace * (mLineDesArray.size() - 1);
            }
        }
        if (mMaxShowRow != 0) {
            mMeasuredHeight = 0;
            int lineCount = Math.min(mLineDesArray.size(), mMaxShowRow);
            for (int i = 0; i < lineCount; i++) {
                mMeasuredHeight += mLineDesArray.get(i).rowsMaxHeight;
            }
            mMeasuredHeight += getPaddingBottom();
            if (lineCount > 1) {
                //加上行間距
                mMeasuredHeight += mLineSpace * (lineCount - 1);
            }
        }

Map<String, Integer> compute(int flowWidth, int widthMeasureSpec, int heightMeasureSpec)是遍歷子View的方法(整個(gè)控件都靠它了)
我們先要設(shè)置幾個(gè)參數(shù):

int lineIndex行數(shù)
int rowsWidth當(dāng)前行已占寬度
int columnHeight當(dāng)前行頂部已占高度
int rowsMaxHeight當(dāng)前行所有子元素的最大高度(用于換行累加高度)
LineDes lineDes保存每行數(shù)據(jù)的bean類

思路是先遍歷所有子View博秫,然后計(jì)算出每個(gè)子View所占用的寬高潦牛,child.getMeasuredWidth()計(jì)算出來的是包含子View中的Padding參數(shù),但是不包含Margin挡育,所以這里實(shí)際寬高還需要加上Margin不然會(huì)導(dǎo)致實(shí)際大小與計(jì)算出來的不符

            //遍歷去調(diào)用所有子元素的measure方法(child.getMeasuredHeight()才能獲取到值巴碗,否則為0)
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            //獲取元素測(cè)量寬度和高度
            int measuredWidth = child.getMeasuredWidth();
            int measuredHeight = child.getMeasuredHeight();
            //獲取元素的margin
            marginParams = (MarginLayoutParams) child.getLayoutParams();
            //子元素所占寬度 = MarginLeft+ child.getMeasuredWidth+MarginRight  注意此時(shí)不能child.getWidth,因?yàn)榻缑鏇]有繪制完成,此時(shí)wdith為0
            int childWidth = marginParams.leftMargin + marginParams.rightMargin + measuredWidth;
            int childHeight = marginParams.topMargin + marginParams.bottomMargin + measuredHeight;

得到每個(gè)子View的寬高后就要開始計(jì)算行數(shù)以及每行所存放的View的數(shù)量了
我們之前已經(jīng)有了一個(gè)rowsWidth參數(shù)即寒,默認(rèn)值是getPaddingLeft()橡淆,然后加上childWidth看是否會(huì)超過父布局的寬度召噩,這邊還需要減去一個(gè)getPaddingRight()切記切記!逸爵,如果超了具滴,表示這個(gè)View已經(jīng)無法存放在該行,需要換行师倔。最后使用Rect把子View的寬高賦值進(jìn)去构韵,然后保存在tag中,方便后續(xù)使用趋艘。

            //該布局添加進(jìn)去后會(huì)超過總寬度->換行
            if (rowsWidth + childWidth > flowWidth - getPaddingRight()) {
                getLineDesArray().put(lineIndex, lineDes);
                lineDes = new LineDes();
                lineIndex++;
                //重置行寬度
                rowsWidth = getPaddingLeft();
                //累加上該行子元素最大高度
                columnHeight += rowsMaxHeight;
                //重置該行最大高度
                rowsMaxHeight = childHeight;
            } else {
                rowsMaxHeight = Math.max(rowsMaxHeight, childHeight);
            }
            //累加上該行子元素寬度
            rowsWidth += childWidth;
            // 判斷時(shí)占的寬段時(shí)加上margin計(jì)算疲恢,設(shè)置頂點(diǎn)位置時(shí)不包括margin位置,
            // 不然margin會(huì)不起作用瓷胧,這是給View設(shè)置tag,在onlayout給子元素設(shè)置位置再遍歷取出
            Rect rect = new Rect(
                    rowsWidth - childWidth + marginParams.leftMargin,
                    columnHeight + marginParams.topMargin,
                    rowsWidth - marginParams.rightMargin,
                    columnHeight + childHeight - marginParams.bottomMargin);
            child.setTag(rect);
            lineDes.rowsMaxHeight = rowsMaxHeight;
            lineDes.rowsMaxWidth = rowsWidth;
            lineDes.views.add(child);
            //累加上item間距
            rowsWidth += mItemSpace;

3.onLayout()中的業(yè)務(wù)邏輯

onLayout()中冈闭,通過所需要實(shí)現(xiàn)的類型去做不同的排版
因?yàn)檫@里我們實(shí)現(xiàn)了行間上,中抖单,下與Item間的左萎攒,中,右對(duì)齊因此矛绘,這里需要有針對(duì)行與Item做兩次處理

我們先設(shè)置一個(gè)diffvalue用于存放位移參數(shù)耍休。
為了讓行內(nèi)所有布局都居中對(duì)齊或下對(duì)齊,那么我們要先知道每行有多少個(gè)元素货矮,以及行高與元素高度羊精,這個(gè)時(shí)候LineDes就派上用場(chǎng)了,之前在onMeasure()中我們已經(jīng)計(jì)算并保存了LineDes囚玫,現(xiàn)在只需要遍歷LineDes即可喧锦,由于系統(tǒng)在繪制的時(shí)候就是使用頂部對(duì)齊,因此LINE_GRAVITY_TOP不需要做處理抓督,我們只需要處理LINE_GRAVITY_CENTERLINE_GRAVITY_BOTTOM即可

LINE_GRAVITY_CENTERdiffvalue = (lineDes.rowsMaxHeight - childWidth) / 2;
LINE_GRAVITY_BOTTOMdiffvalue = lineDes.rowsMaxHeight - childWidth;

再來說一下Item間的排版燃少,同樣的TAG_GRAVITY_LEFT可以不做處理

LINE_GRAVITY_CENTERdiffvalue = (mMeasuredWidth - getPaddingRight() - lineDes.rowsMaxWidth) / 2;
LINE_GRAVITY_BOTTOMdiffvalue = mMeasuredWidth - lineDes.rowsMaxWidth - getPaddingRight();

改完重新寫入Rect中并傳入子View的layout()中即可。

    private synchronized void formatAboveLine(int lineGravity) {
        int lineIndex = getLineDesArray().size();
        for (int i = 0; i < lineIndex; i++) {
            LineDes lineDes = getLineDesArray().get(i);
            List<View> views = lineDes.views;
            int viewIndex = views.size();
            for (int j = 0; j < viewIndex; j++) {
                View child = views.get(j);
                Rect rect = (Rect) child.getTag();
                int childWidth = (rect.bottom - rect.top);
                //如果是當(dāng)前行的高度大于了該view的高度話铃在,此時(shí)需要重新放該view了
                int diffvalue = 0;
                if (childWidth < lineDes.rowsMaxHeight) {
                    switch (lineGravity) {
                        case LINE_GRAVITY_TOP:
                            break;
                        case LINE_GRAVITY_CENTER:
                            diffvalue = (lineDes.rowsMaxHeight - childWidth) / 2;
                            rect.top += diffvalue;
                            rect.bottom += diffvalue;
                            break;
                        case LINE_GRAVITY_BOTTOM:
                            diffvalue = lineDes.rowsMaxHeight - childWidth;
                            rect.top += diffvalue;
                            rect.bottom += diffvalue;
                            break;
                        default:
                            break;
                    }
                }
                switch (mTagGravity) {
                    case TAG_GRAVITY_LEFT:
                        break;
                    case TAG_GRAVITY_CENTER:
                        diffvalue = (mMeasuredWidth - getPaddingRight() - lineDes.rowsMaxWidth) / 2;
                        if (diffvalue > 0) {
                            rect.left += diffvalue;
                            rect.right += diffvalue;
                        }
                        break;
                    case TAG_GRAVITY_RIGHT:
                        diffvalue = mMeasuredWidth - lineDes.rowsMaxWidth - getPaddingRight();
                        rect.left += diffvalue;
                        rect.right += diffvalue;
                        break;
                    default:
                        break;
                }
                //加上行間距
                rect.top += mLineSpace * i;
                rect.bottom += mLineSpace * i;
                child.layout(rect.left, rect.top, rect.right, rect.bottom);
            }
        }
        getLineDesArray().clear();
    }

適配器

參考BaseRecyclerViewAdapterHelper實(shí)現(xiàn)的一個(gè)AdapterViewHolder用于綁定相關(guān)數(shù)據(jù)阵具,并處理點(diǎn)擊,選中等事件定铜。

1.思路

使用SparseIntArray mLayoutResIds保存layoutId阳液,實(shí)現(xiàn)多布局樣式。
使用SparseArray<ArrayList<Integer>> mCheckedStateViewResIds保存需要實(shí)現(xiàn)選中狀態(tài)的子ViewId
使用HashMap<Integer, TagView> mCheckedPosList保存選中的View揣炕,實(shí)現(xiàn)單選帘皿,多選等功能

2.加載布局

RecyclerView.Adapter一樣,我們把data傳進(jìn)來畸陡,然后遍歷數(shù)據(jù)鹰溜,通過ViewType來判斷到底使用mLayoutResIds中的哪個(gè)布局虽填,并且遍歷 mCheckedStateViewResIds對(duì)需要做選中狀態(tài)變更的view設(shè)置setDuplicateParentStateEnabled(true),然后把實(shí)例出來的View傳入ViewHolder最后加載出來奉狈。

    private void addNewView() {
        mFlowLayout.removeAllViews();
        mCheckedPosList.clear();
        TagView tagViewContainer = null;
        K baseViewHolder = null;
        T data = null;
        int viewType = DEFAULT_VIEW_TYPE;
        for (int i = 0; i < getCount(); i++) {
            data = getItem(i);
            viewType = getDefItemViewType(data);
            baseViewHolder = onCreateViewHolder(mFlowLayout, viewType, i);
            tagViewContainer = new TagView(mContext);
            //關(guān)鍵代碼,使得內(nèi)部View可以使用TagView的狀態(tài)
            if (mCheckedStateViewResIds != null) {
                ArrayList<Integer> viewResId = mCheckedStateViewResIds.get(viewType, new ArrayList<Integer>());
                for (Integer stateViewId : viewResId) {
                    View stateView = baseViewHolder.getView(stateViewId.intValue());
                    if (stateView != null) {
                        stateView.setDuplicateParentStateEnabled(true);
                    }
                }
            }
            baseViewHolder.itemView.setDuplicateParentStateEnabled(true);
            if (baseViewHolder.itemView.getLayoutParams() != null) {
                tagViewContainer.setLayoutParams(baseViewHolder.itemView.getLayoutParams());
            } else {
                ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(
                        ViewGroup.LayoutParams.WRAP_CONTENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT);
                lp.setMargins(leftMargin, topMargin, rightMargin, bottomMargin);
                tagViewContainer.setLayoutParams(lp);
            }
            ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
            baseViewHolder.itemView.setLayoutParams(lp);
            tagViewContainer.addView(baseViewHolder.itemView);

            //處理選中與非選中邏輯
            if (setDefSelected(data, i)) {
                if (mSelectedMax == 1 && mCheckedPosList.size() > 0) {
                    int oldSelected = 0;
                    TagView oldTagView;
                    for (Map.Entry<Integer, TagView> entry : mCheckedPosList.entrySet()) {
                        oldSelected = entry.getKey();
                        oldTagView = entry.getValue();
                        setChildUnChecked(oldSelected, oldTagView);
                    }
                    mCheckedPosList.clear();
                }
                mCheckedPosList.put(i, tagViewContainer);
                setChildChecked(i, tagViewContainer);
            }

            mFlowLayout.addView(tagViewContainer);
            convert(baseViewHolder, data);
            bindViewClickListener(tagViewContainer, baseViewHolder);
        }
    }

3.ViewHolder

ViewHolder里面只是保存一些常用數(shù)據(jù),方便在使用的時(shí)候調(diào)用

    private final SparseArray<View> views;
    private final LinkedHashSet<Integer> childClickViewIds;//需要添加點(diǎn)擊事件的子View
    private final LinkedHashSet<Integer> itemChildLongClickViewIds;//需要添加點(diǎn)擊事件的子View
    private final HashSet<Integer> nestViews;//需要添加兩種點(diǎn)擊事件的子View
    public final View itemView;
    private BaseTagFlowAdapter adapter;
    private int position = -1;
    private int viewType = BaseTagFlowAdapter.DEFAULT_VIEW_TYPE;

    public BaseTagFlowViewHolder(final View view) {
        this.itemView = view;
        this.views = new SparseArray<>();
        this.childClickViewIds = new LinkedHashSet<>();
        this.itemChildLongClickViewIds = new LinkedHashSet<>();
        this.nestViews = new HashSet<>();
    }

歡迎大家留言指出我的不足卤唉。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市仁期,隨后出現(xiàn)的幾起案子桑驱,更是在濱河造成了極大的恐慌,老刑警劉巖跛蛋,帶你破解...
    沈念sama閱讀 212,718評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件熬的,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡赊级,警方通過查閱死者的電腦和手機(jī)押框,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來理逊,“玉大人橡伞,你說我怎么就攤上這事〗唬” “怎么了兑徘?”我有些...
    開封第一講書人閱讀 158,207評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長羡洛。 經(jīng)常有香客問我挂脑,道長,這世上最難降的妖魔是什么欲侮? 我笑而不...
    開封第一講書人閱讀 56,755評(píng)論 1 284
  • 正文 為了忘掉前任崭闲,我火速辦了婚禮,結(jié)果婚禮上威蕉,老公的妹妹穿的比我還像新娘刁俭。我一直安慰自己,他們只是感情好忘伞,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評(píng)論 6 386
  • 文/花漫 我一把揭開白布薄翅。 她就那樣靜靜地躺著,像睡著了一般氓奈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鼎天,一...
    開封第一講書人閱讀 50,050評(píng)論 1 291
  • 那天舀奶,我揣著相機(jī)與錄音,去河邊找鬼斋射。 笑死育勺,一個(gè)胖子當(dāng)著我的面吹牛但荤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播涧至,決...
    沈念sama閱讀 39,136評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼腹躁,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了南蓬?” 一聲冷哼從身側(cè)響起纺非,我...
    開封第一講書人閱讀 37,882評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎赘方,沒想到半個(gè)月后烧颖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,330評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡窄陡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評(píng)論 2 327
  • 正文 我和宋清朗相戀三年炕淮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片跳夭。...
    茶點(diǎn)故事閱讀 38,789評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡涂圆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出币叹,到底是詐尸還是另有隱情润歉,我是刑警寧澤,帶...
    沈念sama閱讀 34,477評(píng)論 4 333
  • 正文 年R本政府宣布套硼,位于F島的核電站卡辰,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏邪意。R本人自食惡果不足惜九妈,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評(píng)論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望雾鬼。 院中可真熱鬧萌朱,春花似錦、人聲如沸策菜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至搓侄,卻和暖如春献酗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背寒匙。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評(píng)論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留躏将,地道東北人锄弱。 一個(gè)月前我還...
    沈念sama閱讀 46,598評(píng)論 2 362
  • 正文 我出身青樓考蕾,卻偏偏與公主長得像,于是被迫代替她去往敵國和親会宪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子肖卧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評(píng)論 2 351

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