1. 程式人生 > >【Android】自定義FlowLayout,支援多種佈局優化--android-flowlayout

【Android】自定義FlowLayout,支援多種佈局優化--android-flowlayout

前言

flow layout, 流式佈局, 這個概念在移動端或者前端開發中很常見,特別是在多標籤的展示中, 往往起到了關鍵的作用。然而Android 官方, 並沒有為開發者提供這樣一個佈局, 於是有很多開發者自己做了這樣的工作,github上也出現了很多自定義FlowLayout。 最近, 我也實現了這樣一個FlowLayout,自己感覺可能是當前最好用的FlowLayout了(捂臉),在這裡做一下分享。
專案地址:https://github.com/lankton/android-flowlayout

展示


第一張圖, 展示向FlowLayout中不斷新增子View
第二張圖, 展示壓縮子View, 使他們儘可能充分利用空間


第三張圖, 展示調整子View之間間隔, 使各行左右對齊


這張圖,截斷flowlayout到指定行數。--20160520更新。

##基本的流式佈局功能##
在佈局檔案中使用FlowLayout即可:

<cn.lankton.flowlayout.FlowLayout
        android:id="@+id/flowlayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="10dp"
        app:lineSpacing
="10dp" android:background="#F0F0F0">
</cn.lankton.flowlayout.FlowLayout>

可以看到, 提供了一個自定義引數lineSpacing, 來控制行與行之間的間距。

壓縮

flowLayout.relayoutToCompress();

壓縮的方式, 是通過對子View重新排序, 使得它們能夠更合理的擠佔空間, 後面會做詳細說明。

對齊

flowLayout.relayoutToAlign();

對齊, 不會改變子View的順序, 也不會起到壓縮的作用。

截斷

flowLayout.specifyLines(int
)

實現

流式佈局的實現

重寫generateLayoutParams方法

@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
    return new MarginLayoutParams(p);
}

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs)
{
    return new MarginLayoutParams(getContext(), attrs);
}

重寫該方法的2種過載是有必要的。這樣子元素的LayoutParams就是MarginLayoutParam, 包含了margin 屬性, 正是我們需要的。

重寫onMeasure

主要有2個目的, 第一是測量每個子元素的寬高, 第二是根據子元素的測量值, 設定的FlowLayout的測量值。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int mPaddingLeft = getPaddingLeft();
    int mPaddingRight = getPaddingRight();
    int mPaddingTop = getPaddingTop();
    int mPaddingBottom = getPaddingBottom();

    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    int lineUsed = mPaddingLeft + mPaddingRight;
    int lineY = mPaddingTop;
    int lineHeight = 0;
    for (int i = 0; i < this.getChildCount(); i++) {
        View child = this.getChildAt(i);
        if (child.getVisibility() == GONE) {
            continue;
        }
        measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, lineY);
        MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();
        int childWidth = child.getMeasuredWidth();
        int childHeight = child.getMeasuredHeight();
        int spaceWidth = mlp.leftMargin + childWidth + mlp.rightMargin;
        int spaceHeight = mlp.topMargin + childHeight + mlp.bottomMargin;
        if (lineUsed + spaceWidth > widthSize) {
            //approach the limit of width and move to next line
            lineY += lineHeight + lineSpacing;
            lineUsed = mPaddingLeft + mPaddingRight;
            lineHeight = 0;
        }
        if (spaceHeight > lineHeight) {
            lineHeight = spaceHeight;
        }
        lineUsed += spaceWidth;
    }
    setMeasuredDimension(
            widthSize,
            heightMode == MeasureSpec.EXACTLY ? heightSize : lineY + lineHeight + mPaddingBottom
    );
}

程式碼邏輯很簡單, 就是遍歷子元素, 計算累計長度, 超過一行可容納寬度, 就將累計長度清0,同時假設繼續向下一行放置子元素。為什麼是假設呢, 因為真正在FlowLayout中放置子元素的過程, 是在onLayout方法中的。
重點在最後的setMeasuredDimension方法。在日常使用FlowLayout中, 我們的寬度往往是固定值, 或者match_parent, 不需要根據內容而改變, 所以寬度值直接用widthSize, 即從傳進來的測量值獲得的寬度。
高度則根據MeasureSpec的mode來判斷, EXACTLY意味著和寬度一樣, 直接用測量值的寬度即可, 否則,則是wrap_content, 需要用子元素排布出來的高度進行判斷。

重寫onLayout

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int mPaddingLeft = getPaddingLeft();
    int mPaddingRight = getPaddingRight();
    int mPaddingTop = getPaddingTop();

    int lineX = mPaddingLeft;
    int lineY = mPaddingTop;
    int lineWidth = r - l;
    usefulWidth = lineWidth - mPaddingLeft - mPaddingRight;
    int lineUsed = mPaddingLeft + mPaddingRight;
    int lineHeight = 0;
    for (int i = 0; i < this.getChildCount(); i++) {
        View child = this.getChildAt(i);
        if (child.getVisibility() == GONE) {
            continue;
        }
        MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();
        int childWidth = child.getMeasuredWidth();
        int childHeight = child.getMeasuredHeight();
        int spaceWidth = mlp.leftMargin + childWidth + mlp.rightMargin;
        int spaceHeight = mlp.topMargin + childHeight + mlp.bottomMargin;
        if (lineUsed + spaceWidth > lineWidth) {
            //approach the limit of width and move to next line
            lineY += lineHeight + lineSpacing;
            lineUsed = mPaddingLeft + mPaddingRight;
            lineX = mPaddingLeft;
            lineHeight = 0;
        }
        child.layout(lineX + mlp.leftMargin, lineY + mlp.topMargin, lineX + mlp.leftMargin + childWidth, lineY + mlp.topMargin + childHeight);
        if (spaceHeight > lineHeight) {
            lineHeight = spaceHeight;
        }
        lineUsed += spaceWidth;
        lineX += spaceWidth;

    }
}

這段程式碼也很好理解, 逐個判斷子元素,是繼續在本行放置, 還是需要換行放置。這一步和onMeasure一樣, 基本上所有的FlowLayout都會進行重寫, 我的自然也沒什麼特別的新意, 這兩塊就不重點介紹了。下面重點介紹一下2種佈局優化的實現。

壓縮的實現

關於如何實現壓縮, 這個問題開始的確很讓我頭疼。因為我的腦子裡只有大致的概念,那就是壓縮應該是一個什麼樣的效果, 而這個模糊的概念很難轉換成具體的數學模型。沒有數學模型, 就無法用程式碼解決這個問題,簡直恨不得回到大學重學演算法。。但有一個想法是明確的, 那就是解決這個問題, 實際上就是對子元素的重新排序。
後來決定簡化思路, 用類似貪心演算法的思維解決問題,那就是:逐行解決, 每一行都爭取最大程度的佔滿。
1. 從第一行開始, 從子元素集合中,選出一部分, 使得這一部分子元素可以最大程度的佔據這一行;
2. 將這部分已經選出的從集合中拿出, 繼續對下一行執行第一步操作。
這個思路確立了, 那我們如何從集合中選出子集, 對某一行進行最大程度的佔據呢?
我們已知的條件:
1. 子元素集合
2. 每行可容納寬度
3. 每個子元素的寬度
這個時候, 腦子裡就想到了01揹包問題:
已知
1. 物品集合
2. 揹包總容量
3. 每個物品的價值
4. 每個物品的體積
求揹包包含物品的最大價值(及其方案
有朋友可能有疑問, 二者確實很像, 但不是還差著一個條件嗎?嗯 ,是的。。但是在當前狀況下,因為我們要儘可能的佔滿某一行, 那麼每個子元素的寬度就不僅僅是限制了, 也是價值所在。
這樣, 該狀況就完全和01揹包問題一致了。之後就可以用動態規劃解決問題了。 關於如何用動態規劃解決01揹包問題, 其實我也忘的差不多了, 也是在網上查著資料, 一邊回顧,一邊實現的。所以這裡我自己就不展開介紹了, 也不貼自己的程式碼了(感興趣的可以去github檢視), 放一個連結。我感覺這個連結裡的講解對我回顧相關知識點幫助很大,有興趣的也可以看看~
揹包問題——“01揹包”詳解及實現(包含揹包中具體物品的求解)

對齊的實現

這個功能,我最早是在bilibili的ipad客戶端上看到的,如下。

當時覺得挺好看的,還想過一陣怎麼做, 但一時沒想出來。。。這次實現FlowLayout, 就順手將這種對齊樣式用自己的想法實現了一下。

public void relayoutToAlign() {
    int childCount = this.getChildCount();
    if (0 == childCount) {
        //no need to sort if flowlayout has no child view
        return;
    }
    int count = 0;
    for (int i = 0; i < childCount; i++) {
        View v = getChildAt(i);
        if (v instanceof BlankView) {
            //BlankView is just to make childs look in alignment, we should ignore them when we relayout
            continue;
        }
        count++;
    }
    View[] childs = new View[count];
    int[] spaces = new int[count];
    int n = 0;
    for (int i = 0; i < childCount; i++) {
        View v = getChildAt(i);
        if (v instanceof BlankView) {
            //BlankView is just to make childs look in alignment, we should ignore them when we relayout
            continue;
        }
        childs[n] = v;
        MarginLayoutParams mlp = (MarginLayoutParams) v.getLayoutParams();
        int childWidth = v.getMeasuredWidth();
        spaces[n] = mlp.leftMargin + childWidth + mlp.rightMargin;
        n++;
    }
    int lineTotal = 0;
    int start = 0;
    this.removeAllViews();
    for (int i = 0; i < count; i++) {
        if (lineTotal + spaces[i] > usefulWidth) {
            int blankWidth = usefulWidth - lineTotal;
            int end = i - 1;
            int blankCount = end - start;
            if (blankCount > 0) {
                int eachBlankWidth = blankWidth / blankCount;
                MarginLayoutParams lp = new MarginLayoutParams(eachBlankWidth, 0);
                for (int j = start; j < end; j++) {
                    this.addView(childs[j]);
                    BlankView blank = new BlankView(mContext);
                    this.addView(blank, lp);
                }
                this.addView(childs[end]);
                start = i;
                i --;
                lineTotal = 0;
            }
        } else {
            lineTotal += spaces[i];
        }
    }
    for (int i = start; i < count; i++) {
        this.addView(childs[i]);
    }
}

程式碼很長, 但說起來很簡單。獲得子元素列表,從頭開始, 逐一判斷哪些子元素在同一行。即每一次的start 到 end。 然後計算這些子元素裝滿一行的話, 還差多少, 設為d。則每兩個子元素之間需要補上的間距為 d / (end - start)。 如果設定間距呢, 首先我們肯定不能去更改子元素本身的性質。那麼, 就只能在兩個子元素中間補上一個寬度為d / (end - start) 的BlankView了。
至於這個BlankView是個什麼鬼, 定義如下:

class BlankView extends View {

    public BlankView(Context context) {
        super(context);
    }
}

你看, 根本什麼也沒做。 那我新寫一個類繼承View的意義是什麼呢? 其實從上邊對齊的程式碼裡也能看到,這樣我們在遍歷FlowLayout的子元素時, 就可以通過 instance of BlankView 來判斷是真正需要處理、計算的子元素,還是我們後來加上的補位View了

截斷的實現

假設要截斷為N行, 則取子元素列表中,前N行的,重新佈局。詳見github程式碼。

總結

這個專案, 肯定還是有很多需要優化的地方, 歡迎各位提出各種意見或者建議,也期待能夠被大家使用。
可以的話,也順求star~ 謝謝。

更新

釋出到JCenter-20160519

為方便使用,已將library釋出到JCenter,開發者可以使用gradle或者maven進行依賴的配置。

gradle

compile 'cn.lankton:flowlayout:1.0.1'

maven

<dependency>
  <groupId>cn.lankton</groupId>
  <artifactId>flowlayout</artifactId>
  <version>1.0.1</version>
  <type>pom</type>
</dependency>

相關推薦

Android定義FlowLayout支援多種佈局優化--android-flowlayout

前言 flow layout, 流式佈局, 這個概念在移動端或者前端開發中很常見,特別是在多標籤的展示中, 往往起到了關鍵的作用。然而Android 官方, 並沒有為開發者提供這樣一個佈局, 於是有很多開發者自己做了這樣的工作,github上也出現了很多自定義

Android定義控制元件-仿QQ聯絡人側滑條目右側滑選單。

一直沒有寫部落格的習慣,一直都是看別人的部落格,學習別人的東西。平時工作中總會遇到或大或小的問題,往往是上百度CSDN查詢答案。今天嘗試著寫部落格,一是更加深入地熟悉一下部落格;二是轉變一下學習方式;三是把自己所學的東西分享出來,幫助別人的同時也提升了自己!

Android定義控制元件實現帶百分比顯示進度條定義顏色

介紹 前天做了一個帶百分比顯示的條形進度條,效果如下: 實現 這個自定義進度條, 看起來簡單, 做起來。。。其實也很簡單: 主要通過繼承View類, 並重寫其onDraw方法實現。 思路分為3步: 1. 畫進圖條背景(圖中灰色部分 2. 根據

Android定義錄音、播放動畫View讓你的錄音浪起來

前言 先看效果圖 嗯,然後大致就是這樣,按住錄音,然後有一個倒計時,最外層一個進度條,還有一個類似模擬聲波的動畫效果(其實中間的波浪會根據聲音的大小浪起來的~) 實現思路 然後,我們適當的來分析一下這個錄音動畫的實現方式。這個肯定是通過自定義控制元件

Android定義標題欄底部欄

為了簡化起見,只寫關鍵屬性,具體需要可以自己慢慢調 頂部標題title_layout.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout android:backg

Android定義帶進度條的WebView修復不彈出軟鍵盤的BUG

記錄下最近開發中研究的帶進度條的WebView 自定義類吧。 其實網上有不少這樣的帖子,但是都沒有一個完整的好用的例子,最關鍵的是,用網上的例子後有一個很明顯的bug,就是遇到輸入框的話沒法彈出軟鍵盤。研究了好久總算搞定了。特此記錄下。 直接上原始碼,關於程式碼的解釋,個人

pytorch定義讀取資料集使用txt文字

使用txt文字讀入資料可以減少記憶體的需要,有時候自定義載入資料集是非常必要的,我下面的程式碼是針對影象的,並且帶有label的有監督的影象。先看程式碼: import numpy as np import os import torch.nn as nn from PIL import Ima

Highchart定義儀表盤配置檔案儀表盤分段及漸變色

highchart配置出儀表盤 chart: { type: 'gauge', plotBackgroundColor: null, plotBackgroundImage: null, plotBorderWi

Android定義控制元件實現可滑動的開關(switch)

介紹 昨天晚上寫了一個Android的滑動開關, 即SlideSwitch。效果如下: 實現 實現的思路其實很簡單,監聽控制元件上的touch事件,並不斷重新整理,讓滑塊在手指的位置上繪出,達到滑塊跟著手指滑動的顯示效果。 先看一下程式碼

AndroidStudio定義gradle外掛:無需釋出僅用於當前專案

最近由於工作需要,想要做個gradle外掛輔助一下,各種難易程度的講解文章也看了不少,腦子裡的資訊比較亂,在這抽個時間整理一下。 因為是最近剛接觸gradle外掛的製作,剛開始除錯的時候構造的是可釋出的外掛模組,每次修改都得重新發布到本地,麻煩死了~ 在這裡提供一個免釋出

Android 定義ImageView支援圓角和直角

使用自定義ImageView,實現圓角功能 1.自定義屬性attrs.xml <?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="RoundC

Android定義透明dialog 去除邊緣陰影 黑邊

在style中定義透明dialog 沒繼承 @android:style/Theme.Dialog  後 出現如上圖 紅色框裡面有條黑色的陰影的現象  解決辦法:     <item name="android:windowContentOverlay">

android定義ProgressDialog實現暫時隱藏進度值並顯示等待狀態(附原始碼下載)

有時,我們需要訪問網路才能獲取到需要操作的任務數(例如下載的檔案數),而在伺服器返回任務數之前要想隱藏進度百分比和進度數值,就需要我們自己重寫ProgressDialog。等到獲取到任務數後再把進度值和百分比顯示出來。先上效果圖: 關鍵程式碼: public clas

Android定義控制元件——仿天貓Indicator

今天來說說類似天貓的Banner中的小圓點是怎麼做的(圖中綠圈部分) 在學習自定義控制元件之前,我用的是非常二的方法,直接在佈局中放入多個ImageView,然後程式碼中根據Pager切換來改變圖片。這種方法雖然可以在切換完成後正確顯示小圓點,但是卻做不到如下圖中的切換

Android定義Dialog如何設定點選事件

我一直用findViewById,結果檢視log,總是提示我沒有獲取到控制元件,讓我疑惑了幾天,上網查了下。dialog.getWindow().findViewById(R.id.cancel_tv)

Android定義View-為文字新增動態閃動效果

一、概述 昨天我簡單的為View添加了一個邊框,邊框的顏色和大小都是可以自行設定的。今天我想在文字方面做一些簡單的修改,我想讓文字閃動起來。我們可以利用LinearGradient的Shader渲染器和Matrix矩陣來實現閃動效果。 二、知識說明

4定義下拉框

order viewport down jquer pos bottom last png 下拉框 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8

python web框架補充定義web框架

數據大小 路徑 .py 用戶 ipa clr 接受 values 規範 http協議 HTTP簡介 HTTP協議是Hyper Text Transfer Protocol(超文本傳輸協議)的縮寫,是用於從萬維網(WWW:World Wide Web )服務器傳輸超文本到本

flask第二十四篇——模板6定義過濾器

def 定義 lazy .py highlight abs nbsp tps word 請關註孟船長的公眾號:自動化測試實戰 大家想了解其他過濾器可以參考這裏: http://jinja.pocoo.org/docs/dev/templates/#builtin-fil

Zabbix定義監控項 key 值。

功能 介紹 ffffff 根據 -o 最新 http 執行c 分享圖片 zabbix自帶的默認模版裏包括了很多監控項,有時候為了滿足業務需求,需要根據自己的監控項目自定義監控項,這裏介紹一種自定義監控項的方式。 1,首先編寫自定義監控腳本,本文以監控httpd進程是否存在為