Android中FlowLayout組件實現瀑佈流效果

紙上得來終覺淺,絕知此事要躬行。

動手實踐是學習的最好的方式,對於自定義View來說,聽和看隻能是過一遍流程,能掌握個30%、40%就不錯瞭,而且很快就會遺忘,想變成自己的東西必須動手來寫幾遍,細細體會其中的細節和系統API的奧秘、真諦。

進入主題,今天來手寫一個瀑佈流組件FlowLayout,溫習下自定義view的流程和關鍵點,先來張效果圖

FlowLayout實現關鍵步驟:

1、創建一個view繼承自ViewGroup

class ZSFlowLayout : ViewGroup {
    constructor(context: Context) : super(context) {}
 
    /**
     * 必須的構造函數,系統會通過反射來調用此構造方法完成view的創建
     */
    constructor(context: Context, attr: AttributeSet) : super(context, attr) {}
 
    constructor (context: Context, attr: AttributeSet, defZStyle: Int) : super(
        context,
        attr,
        defZStyle
    ) {
    }
 
}

  這裡註意兩個參數的構造函數是必須的構造函數,系統會通過反射來調用此構造方法完成view的創建,具體調用位置在LayoutInflater 的 createView方法中,如下(基於android-31):

省略瞭若幹不相關代碼,並寫瞭重要的註釋信息,請留意

 public final View createView(@NonNull Context viewContext, @NonNull String name,
            @Nullable String prefix, @Nullable AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        Objects.requireNonNull(viewContext);
        Objects.requireNonNull(name);
 
        //從緩存中取對應的構造函數
        Constructor<? extends View> constructor = sConstructorMap.get(name);
       
        Class<? extends View> clazz = null;
 
        try {
            
            if (constructor == null) {
                // 通過反射創建class對象
                clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                        mContext.getClassLoader()).asSubclass(View.class);
 
                //創建構造函數  這裡的mConstructorSignature 長這個樣子
                //static final Class<?>[] mConstructorSignature = new Class[] {
                //        Context.class, AttributeSet.class};
                //看到瞭沒 就是我們第二個構造方法
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
                //緩存構造方法
                sConstructorMap.put(name, constructor);
            } else {
                ...
            }
 
            
            try {
                //執行構造函數 創建出view
                final View view = constructor.newInstance(args);
                ...
                return view;
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        } catch (Exception e) {
            ...
        } finally {
            ...
        }
    }

 對LayoutInflater以及setContentView、DecorView、PhoneWindow相關一整套源碼流程感興趣的可以看下我這篇文章:

Activity setContentView背後的一系列源碼分析

2、重寫並實現onMeasure方法

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
 
}

(1)先瞭解下 MeasureSpec的含義

MeasureSpec是View中的內部類,基本都是二進制運算。由於int是32位的,用高兩位表示mode,低30位表示size。

(2)重點解釋下 兩個參數widthMeasureSpec 和 heightMeasureSpec是怎麼來的

這個是父類傳給我們的尺寸規則,那父類是如何按照什麼規則生成的widthMeasureSpec、heightMeasureSpec呢?

答:父類會結合自身的情況,並且結合子view的情況(子類的寬是match_parent、wrap_content、還是寫死的值)來生成的。生成的具體邏輯 請見:ViewGroup的getChildMeasureSpec方法

相關說明都寫在瞭註釋中,請註意查看:

/**
 * 這裡的spec、padding是父類的尺寸規則,childDimension是子類的尺寸
 * 舉個例子,如果我們寫的FlowLayout被LinearLayout包裹,那這裡spec、padding就是LinearLayout的
 * spec 可以是widthMeasureSpec 也可以是 heightMeasureSpec 寬和高是分開計算的,childDimension
 * 則是我們在佈局文件中對FlowLayout設置的對應的寬、高
 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        
        //獲取父類的尺寸模式
        int specMode = MeasureSpec.getMode(spec);
        //獲取父類的尺寸大小
        int specSize = MeasureSpec.getSize(spec);
 
        //去掉padding後的大小 最小不能低於0
        int size = Math.max(0, specSize - padding);
 
        int resultSize = 0;
        int resultMode = 0;
 
        switch (specMode) {
        // 如果父類的模式是MeasureSpec.EXACTLY(精確模式,父類的值是可以確定的)
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                //此時子view的大小就是我們設置的值,超過父類也沒事,開發人員自定義設置的
                //比如父view的寬是100dp,子view寬你非要設置200dp,那就給200dp,這麼做有什麼
                //意義?這樣是可以擴展的,不至於限制死,比如子view可能具有滾動屬性或者其他高級 
                //玩法                
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // MATCH_PARENT 則子view和父view大小一致 模式是確定的
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // WRAP_CONTENT 則子view和父view大小一致 模式是最大不超過這個值
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
 
        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // 按子view值執行,確定模式
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //按父view值執行 模式是最多不超過指定值模式
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //按父view值執行 模式是最多不超過指定值模式
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
 
        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // 按子view值執行,確定模式
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 按父view值執行 模式是未定義
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 按父view值執行 模式是未定義
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

其實就是網上的這張圖

3、重寫並實現onLayout方法

我們要在這個方法裡面,確定所有被添加到我們的FlowLayout裡面的子view的位置,這裡沒有特殊要註意的地方,控制好細節就可以。

三個關鍵步驟介紹完瞭,下面上實戰代碼:

ZSFlowLayout:

/**
 * 自定義瀑佈流佈局 系統核心方法
 * ViewGroup getChildMeasureSpec  獲取子view的MeasureSpec信息
 * View measure 對view進行測量 測量以後就知道view大小瞭 之後可以通過getMeasuredWidth、getMeasuredHeight來獲取其寬高
 * View MeasureSpec.getMode 獲取寬或高的模式(MeasureSpec.EXACTLY、MeasureSpec.AT_MOST、MeasureSpec.UNSPECIFIED)
 * View MeasureSpec.getSize 獲取父佈局能給我們的寬、高大小
 * View setMeasuredDimension 設置測量結果
 * View layout(left,top,right,bottom) 設置佈局位置
 *
 * 幾個驗證點 getMeasuredHeight、getHeight何時有值 結論:分別在onMeasure 和 onLayout之後
 * 子view是relativeLayout 並有子view時的情況  沒問題
 * 通過addView方式添加  ok  已驗證
 */
class ZSFlowLayout : ViewGroup {
 
    //保存所有子view 按行保存 每行都可能有多個view 所有是一個list
    var allViews: MutableList<MutableList<View>> = mutableListOf()
 
    //每個子view之間的水平間距
    val horizontalSpace: Int =
        resources.getDimensionPixelOffset(R.dimen.zs_flowlayout_horizontal_space)
 
    //每行之間的間距
    val verticalSpace: Int = resources.getDimensionPixelOffset(R.dimen.zs_flowlayout_vertical_space)
 
    //記錄每一行的行高 onLayout時會用到
    var lineHeights: MutableList<Int> = mutableListOf()
 
    constructor(context: Context) : super(context) {}
 
    /**
     * 必須的構造函數,系統會通過反射來調用此構造方法完成view的創建
     */
    constructor(context: Context, attr: AttributeSet) : super(context, attr) {}
 
    constructor (context: Context, attr: AttributeSet, defZStyle: Int) : super(
        context,
        attr,
        defZStyle
    ) {
    }
 
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        //會測量次
        allViews.clear()
        lineHeights.clear()
 
        //保存每一行的view
        var everyLineViews: MutableList<View> = mutableListOf()
        //記錄每一行當前的寬度,用來判斷是否要換行
        var curLineHasUsedWidth: Int = paddingLeft + paddingRight
        //父佈局能給的寬
        val selfWidth: Int = MeasureSpec.getSize(widthMeasureSpec)
        //父佈局能給的高
        val selfHeight: Int = MeasureSpec.getSize(heightMeasureSpec)
        //我們自己通過測量需要的寬(如果用戶在佈局裡對ZSFlowLayout的寬設置瞭wrap_content 就會用到這個)
        var selfNeedWidth = 0
        //我們自己通過測量需要的高(如果用戶在佈局裡對ZSFlowLayout的高設置瞭wrap_content 就會用到這個)
        var selfNeedHeight = paddingBottom + paddingTop
        var curLineHeight = 0
 
        //第一步 先測量子view 核心系統方法是 View measure方法
        //(1)因為子view有很多,所以循環遍歷執行
        for (i in 0 until childCount) {
            val childView = getChildAt(i)
            if (childView.visibility == GONE) {
                continue
            }
            //測量view之前 先把測量需要的參數準備好 通過ViewGroup getChildMeasureSpec獲取子view的MeasureSpec信息
            val childWidthMeasureSpec = getChildMeasureSpec(
                widthMeasureSpec,
                paddingLeft + paddingRight,
                childView.layoutParams.width
            )
            val childHeightMeasureSpec = getChildMeasureSpec(
                heightMeasureSpec,
                paddingTop + paddingBottom,
                childView.layoutParams.height
            )
            //調用子view的measure方法來對子view進行測量
            childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
 
            //測量之後就能拿到子view的寬高瞭,保存起來用於判斷是否要換行 以及需要的總高度
            val measuredHeight = childView.measuredHeight
            val measuredWidth = childView.measuredWidth
 
            //按行保存view 保存之前判斷是否需要換行,如果需要就保存在下一行的list裡面
            if (curLineHasUsedWidth + measuredWidth > selfWidth) {
                //要換行瞭 先記錄換行之前的數據
                lineHeights.add(curLineHeight)
                selfNeedHeight += curLineHeight + verticalSpace
                allViews.add(everyLineViews)
 
                //再處理當前要換行的view相關數據
                curLineHeight = measuredHeight
                everyLineViews = mutableListOf()
                curLineHasUsedWidth = paddingLeft + paddingRight + measuredWidth + horizontalSpace
            } else {
                //每一行的高度是這一行view中最高的那個
                curLineHeight = curLineHeight.coerceAtLeast(measuredHeight)
                curLineHasUsedWidth += measuredWidth + horizontalSpace
            }
            everyLineViews.add(childView)
            selfNeedWidth = selfNeedWidth.coerceAtLeast(curLineHasUsedWidth)
 
            //處理最後一行
            if (i == childCount - 1) {
                curLineHeight = curLineHeight.coerceAtLeast(measuredHeight)
                allViews.add(everyLineViews)
                selfNeedHeight += curLineHeight
                lineHeights.add(curLineHeight)
            }
        }
 
        //第二步 測量自己
        //根據父類傳入的尺寸規則 widthMeasureSpec、heightMeasureSpec 獲取當前自身應該遵守的佈局模式
        //以widthMeasureSpec為例說明下 這個是父類傳入的,那父類是如何按照什麼規則生成的widthMeasureSpec呢?
        //父類會結合自身的情況,並且結合子view的情況(子類的寬是match_parent、wrap_content、還是寫死的值)來生成
        //生成的具體邏輯 請見:ViewGroup的getChildMeasureSpec方法
        //(1)獲取父類傳過來的 我們自身應該遵守的尺寸模式
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        //(2)根據模式來判斷最終的寬高
        val widthResult = if (widthMode == MeasureSpec.EXACTLY) selfWidth else selfNeedWidth
        val heightResult = if (heightMode == MeasureSpec.EXACTLY) selfHeight else selfNeedHeight
        //第三步 設置自身的測量結果
        setMeasuredDimension(widthResult, heightResult)
    }
 
    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        //設置所有view的位置
        var curT = paddingTop
        for (i in allViews.indices) {
            val mutableList = allViews[i]
            //記錄每一行view的當前距離父佈局左側的位置 初始值就是父佈局的paddingLeft
            var curL = paddingLeft
            if (i != 0) {
                curT += lineHeights[i - 1] + verticalSpace
            }
            for (j in mutableList.indices) {
                val view = mutableList[j]
                val right = curL + view.measuredWidth
                val bottom = curT + view.measuredHeight
                view.layout(curL, curT, right, bottom)
                //為下一個view做準備
                curL += view.measuredWidth + horizontalSpace
            }
        }
    }
}

在佈局文件中使用:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
 
        <TextView
            android:layout_marginTop="10dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="@dimen/zs_flowlayout_title_marginL"
            android:text="三國名將"
            android:textColor="@android:color/black"
            android:textSize="18sp" />
 
        <com.zs.test.customview.ZSFlowLayout
            android:id="@+id/activity_flow_flowlayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:padding="7dp">
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="呂佈呂奉先" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="趙雲趙子龍" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:paddingLeft="10dp"
                android:text="典韋" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="關羽關雲長" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="馬超馬孟起" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="張飛張翼德" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="黃忠" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="徐褚徐仲康" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="孫策孫伯符" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="太史慈" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="夏侯惇" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="夏侯淵" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="張遼" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="張郃" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="徐晃徐功明" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="龐德" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="甘寧甘興霸" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="周泰" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="魏延" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="張繡" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="文醜" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="顏良" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="鄧艾" />
 
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="薑維" />
 
        </com.zs.test.customview.ZSFlowLayout>
 
    </LinearLayout>
 
</ScrollView>

也可以在代碼中動態添加view(更接近實戰,實戰中數據多是後臺請求而來)

class FlowActivity : AppCompatActivity() {
 
    @BindView(id = R.id.activity_flow_flowlayout)
    var flowLayout : ZSFlowLayout ? = null;
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_customview_flow)
        BindViewInject.inject(this)
 
        for (i in 1 until 50) {
            val tv:TextView = TextView(this)
            tv.text = "TextView $i"
            flowLayout!!.addView(tv)
        }
    }
}

其中BindViewInject是用反射+註解實現的一個小工具類

object BindViewInject {
 
 
    /**
     * 註入
     *
     * @param activity
     */
    @JvmStatic
    fun inject(activity: Activity) {
        inject(activity, false)
    }
 
    fun inject(activity: Activity, isSetOnClickListener: Boolean) {
        //第一步 獲取class對象
        val aClass: Class<out Activity> = activity.javaClass
        //第二步 獲取類本身定義的所有成員變量
        val declaredFields = aClass.declaredFields
        //第三步 遍歷找出有註解的屬性
        for (i in declaredFields.indices) {
            val field = declaredFields[i]
            //判斷是否用BindView進行註解
            if (field.isAnnotationPresent(BindView::class.java)) {
                //得到註解對象
                val bindView = field.getAnnotation(BindView::class.java)
                //得到註解對象上的id值 這個就是view的id
                val id = bindView.id
                if (id <= 0) {
                    Toast.makeText(activity, "請設置正確的id", Toast.LENGTH_LONG).show()
                    return
                }
                //建立映射關系,找出view
                val view = activity.findViewById<View>(id)
                //修改權限
                field.isAccessible = true
                //第四步 給屬性賦值
                try {
                    field[activity] = view
                } catch (e: IllegalAccessException) {
                    e.printStackTrace()
                }
                //第五步 設置點擊監聽
                if (isSetOnClickListener) {
                    //這裡用反射實現 增加練習
                    //第一步 獲取這個屬性的值
                    val button = field.get(activity)
                    //第二步 獲取其class對象
                    val javaClass = button.javaClass
                    //第三步 獲取其 setOnClickListener 方法
                    val method =
                        javaClass.getMethod("setOnClickListener", View.OnClickListener::class.java)
                    //第四步 執行此方法
                    method.invoke(button, activity)
                }
            }
        }
    }
}
@Target(AnnotationTarget.FIELD)
@Retention(RetentionPolicy.RUNTIME)
annotation class BindView( //value是默認的,如果隻有一個參數,並且名稱是value,外面傳遞時可以直接寫值,否則就要通過鍵值對來傳值(例如:value = 1)
    //    int value() default 0;
    val id: Int = 0
)

總結

到此這篇關於Android中FlowLayout組件實現瀑佈流效果的文章就介紹到這瞭,更多相關Android FlowLayout瀑佈流內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: