您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關(guān)Android中FlowLayout組件如何實現(xiàn)瀑布流效果,小編覺得挺實用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。
紙上得來終覺淺,絕知此事要躬行。
動手實踐是學習的最好的方式,對于自定義View來說,聽和看只能是過一遍流程,能掌握個30%、40%就不錯了,而且很快就會遺忘,想變成自己的東西必須動手來寫幾遍,細細體會其中的細節(jié)和系統(tǒng)API的奧秘、真諦。
進入主題,今天來手寫一個瀑布流組件FlowLayout,溫習下自定義view的流程和關(guān)鍵點,先來張效果圖
class ZSFlowLayout : ViewGroup { constructor(context: Context) : super(context) {} /** * 必須的構(gòu)造函數(shù),系統(tǒng)會通過反射來調(diào)用此構(gòu)造方法完成view的創(chuàng)建 */ constructor(context: Context, attr: AttributeSet) : super(context, attr) {} constructor (context: Context, attr: AttributeSet, defZStyle: Int) : super( context, attr, defZStyle ) { } }
這里注意兩個參數(shù)的構(gòu)造函數(shù)是必須的構(gòu)造函數(shù),系統(tǒng)會通過反射來調(diào)用此構(gòu)造方法完成view的創(chuàng)建,具體調(diào)用位置在LayoutInflater 的 createView方法中,如下(基于android-31):
省略了若干不相關(guān)代碼,并寫了重要的注釋信息,請留意
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); //從緩存中取對應的構(gòu)造函數(shù) Constructor<? extends View> constructor = sConstructorMap.get(name); Class<? extends View> clazz = null; try { if (constructor == null) { // 通過反射創(chuàng)建class對象 clazz = Class.forName(prefix != null ? (prefix + name) : name, false, mContext.getClassLoader()).asSubclass(View.class); //創(chuàng)建構(gòu)造函數(shù) 這里的mConstructorSignature 長這個樣子 //static final Class<?>[] mConstructorSignature = new Class[] { // Context.class, AttributeSet.class}; //看到了沒 就是我們第二個構(gòu)造方法 constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); //緩存構(gòu)造方法 sConstructorMap.put(name, constructor); } else { ... } try { //執(zhí)行構(gòu)造函數(shù) 創(chuàng)建出view final View view = constructor.newInstance(args); ... return view; } finally { mConstructorArgs[0] = lastContext; } } catch (Exception e) { ... } finally { ... } }
對LayoutInflater以及setContentView、DecorView、PhoneWindow相關(guān)一整套源碼流程感興趣的可以看下我這篇文章:
Activity setContentView背后的一系列源碼分析
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { }
(1)先了解下 MeasureSpec的含義
MeasureSpec是View中的內(nèi)部類,基本都是二進制運算。由于int是32位的,用高兩位表示mode,低30位表示size。
(2)重點解釋下 兩個參數(shù)widthMeasureSpec 和 heightMeasureSpec是怎么來的
這個是父類傳給我們的尺寸規(guī)則,那父類是如何按照什么規(guī)則生成的widthMeasureSpec、heightMeasureSpec呢?
答:父類會結(jié)合自身的情況,并且結(jié)合子view的情況(子類的寬是match_parent、wrap_content、還是寫死的值)來生成的。生成的具體邏輯 請見:ViewGroup的getChildMeasureSpec方法
相關(guān)說明都寫在了注釋中,請注意查看:
/** * 這里的spec、padding是父類的尺寸規(guī)則,childDimension是子類的尺寸 * 舉個例子,如果我們寫的FlowLayout被LinearLayout包裹,那這里spec、padding就是LinearLayout的 * spec 可以是widthMeasureSpec 也可以是 heightMeasureSpec 寬和高是分開計算的,childDimension * 則是我們在布局文件中對FlowLayout設(shè)置的對應的寬、高 */ 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的大小就是我們設(shè)置的值,超過父類也沒事,開發(fā)人員自定義設(shè)置的 //比如父view的寬是100dp,子view寬你非要設(shè)置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值執(zhí)行,確定模式 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { //按父view值執(zhí)行 模式是最多不超過指定值模式 resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { //按父view值執(zhí)行 模式是最多不超過指定值模式 resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent asked to see how big we want to be case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // 按子view值執(zhí)行,確定模式 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 按父view值執(zhí)行 模式是未定義 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // 按父view值執(zhí)行 模式是未定義 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
其實就是網(wǎng)上的這張圖
我們要在這個方法里面,確定所有被添加到我們的FlowLayout里面的子view的位置,這里沒有特殊要注意的地方,控制好細節(jié)就可以。
三個關(guān)鍵步驟介紹完了,下面上實戰(zhàn)代碼:
ZSFlowLayout:
/** * 自定義瀑布流布局 系統(tǒng)核心方法 * 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 設(shè)置測量結(jié)果 * View layout(left,top,right,bottom) 設(shè)置布局位置 * * 幾個驗證點 getMeasuredHeight、getHeight何時有值 結(jié)論:分別在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) {} /** * 必須的構(gòu)造函數(shù),系統(tǒng)會通過反射來調(diào)用此構(gòu)造方法完成view的創(chuàng)建 */ 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的寬設(shè)置了wrap_content 就會用到這個) var selfNeedWidth = 0 //我們自己通過測量需要的高(如果用戶在布局里對ZSFlowLayout的高設(shè)置了wrap_content 就會用到這個) var selfNeedHeight = paddingBottom + paddingTop var curLineHeight = 0 //第一步 先測量子view 核心系統(tǒng)方法是 View measure方法 //(1)因為子view有很多,所以循環(huán)遍歷執(zhí)行 for (i in 0 until childCount) { val childView = getChildAt(i) if (childView.visibility == GONE) { continue } //測量view之前 先把測量需要的參數(shù)準備好 通過ViewGroup getChildMeasureSpec獲取子view的MeasureSpec信息 val childWidthMeasureSpec = getChildMeasureSpec( widthMeasureSpec, paddingLeft + paddingRight, childView.layoutParams.width ) val childHeightMeasureSpec = getChildMeasureSpec( heightMeasureSpec, paddingTop + paddingBottom, childView.layoutParams.height ) //調(diào)用子view的measure方法來對子view進行測量 childView.measure(childWidthMeasureSpec, childHeightMeasureSpec) //測量之后就能拿到子view的寬高了,保存起來用于判斷是否要換行 以及需要的總高度 val measuredHeight = childView.measuredHeight val measuredWidth = childView.measuredWidth //按行保存view 保存之前判斷是否需要換行,如果需要就保存在下一行的list里面 if (curLineHasUsedWidth + measuredWidth > selfWidth) { //要換行了 先記錄換行之前的數(shù)據(jù) lineHeights.add(curLineHeight) selfNeedHeight += curLineHeight + verticalSpace allViews.add(everyLineViews) //再處理當前要換行的view相關(guān)數(shù)據(jù) 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) } } //第二步 測量自己 //根據(jù)父類傳入的尺寸規(guī)則 widthMeasureSpec、heightMeasureSpec 獲取當前自身應該遵守的布局模式 //以widthMeasureSpec為例說明下 這個是父類傳入的,那父類是如何按照什么規(guī)則生成的widthMeasureSpec呢? //父類會結(jié)合自身的情況,并且結(jié)合子view的情況(子類的寬是match_parent、wrap_content、還是寫死的值)來生成 //生成的具體邏輯 請見:ViewGroup的getChildMeasureSpec方法 //(1)獲取父類傳過來的 我們自身應該遵守的尺寸模式 val widthMode = MeasureSpec.getMode(widthMeasureSpec) val heightMode = MeasureSpec.getMode(heightMeasureSpec) //(2)根據(jù)模式來判斷最終的寬高 val widthResult = if (widthMode == MeasureSpec.EXACTLY) selfWidth else selfNeedWidth val heightResult = if (heightMode == MeasureSpec.EXACTLY) selfHeight else selfNeedHeight //第三步 設(shè)置自身的測量結(jié)果 setMeasuredDimension(widthResult, heightResult) } override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { //設(shè)置所有view的位置 var curT = paddingTop for (i in allViews.indices) { val mutableList = allViews[i] //記錄每一行view的當前距離父布局左側(cè)的位置 初始值就是父布局的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="關(guān)羽關(guān)云長" /> <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>
也可以在代碼中動態(tài)添加view(更接近實戰(zhàn),實戰(zhàn)中數(shù)據(jù)多是后臺請求而來)
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是用反射+注解實現(xiàn)的一個小工具類
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, "請設(shè)置正確的id", Toast.LENGTH_LONG).show() return } //建立映射關(guān)系,找出view val view = activity.findViewById<View>(id) //修改權(quán)限 field.isAccessible = true //第四步 給屬性賦值 try { field[activity] = view } catch (e: IllegalAccessException) { e.printStackTrace() } //第五步 設(shè)置點擊監(jiān)聽 if (isSetOnClickListener) { //這里用反射實現(xiàn) 增加練習 //第一步 獲取這個屬性的值 val button = field.get(activity) //第二步 獲取其class對象 val javaClass = button.javaClass //第三步 獲取其 setOnClickListener 方法 val method = javaClass.getMethod("setOnClickListener", View.OnClickListener::class.java) //第四步 執(zhí)行此方法 method.invoke(button, activity) } } } } }
@Target(AnnotationTarget.FIELD) @Retention(RetentionPolicy.RUNTIME) annotation class BindView( //value是默認的,如果只有一個參數(shù),并且名稱是value,外面?zhèn)鬟f時可以直接寫值,否則就要通過鍵值對來傳值(例如:value = 1) // int value() default 0; val id: Int = 0 )
關(guān)于“Android中FlowLayout組件如何實現(xiàn)瀑布流效果”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,使各位可以學到更多知識,如果覺得文章不錯,請把它分享出去讓更多的人看到。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。