亚洲激情专区-91九色丨porny丨老师-久久久久久久女国产乱让韩-国产精品午夜小视频观看

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Android中FlowLayout組件如何實現瀑布流效果

發布時間:2022-01-20 09:26:59 來源:億速云 閱讀:184 作者:小新 欄目:開發技術

這篇文章將為大家詳細講解有關Android中FlowLayout組件如何實現瀑布流效果,小編覺得挺實用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。

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

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

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

    Android中FlowLayout組件如何實現瀑布流效果

    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);
        }

    其實就是網上的這張圖

    Android中FlowLayout組件如何實現瀑布流效果

    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組件如何實現瀑布流效果”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,使各位可以學到更多知識,如果覺得文章不錯,請把它分享出去讓更多的人看到。

    向AI問一下細節

    免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    AI

    明水县| 高要市| 白银市| 浦县| 淮阳县| 江永县| 和龙市| 灯塔市| 阜康市| 新乡县| 郎溪县| 云南省| 新巴尔虎左旗| 浏阳市| 东丽区| 贵定县| 克什克腾旗| 澜沧| 合肥市| 名山县| 买车| 东丰县| 宁津县| 蕉岭县| 思茅市| 泾阳县| 青铜峡市| 环江| 澳门| 井陉县| 松原市| 西乌| 迭部县| 定州市| 酒泉市| 泾源县| 青岛市| 运城市| 大渡口区| 汪清县| 崇阳县|