Android中FlowLayout组件如何实现瀑布流效果(android,flowlayout,开发技术)

时间:2024-05-05 00:03:31 作者 : 石家庄SEO 分类 : 开发技术
  • TAG :

    纸上得来终觉浅,绝知此事要躬行。

    动手实践是学习的最好的方式,对于自定义View来说,听和看只能是过一遍流程,能掌握个30%、40%就不错了,而且很快就会遗忘,想变成自己的东西必须动手来写几遍,细细体会其中的细节和系统API的奥秘、真谛。

    进入主题,今天来手写一个瀑布流组件FlowLayout,温习下自定义view的流程和关键点,先来张效果图

    Android中FlowLayout组件如何实现瀑布流效果

    FlowLayout实现关键步骤:

    1、创建一个view继承自ViewGroup

    classZSFlowLayout: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):

    省略了若干不相关代码,并写了重要的注释信息,请留意

    publicfinalViewcreateView(@NonNullContextviewContext,@NonNullStringname,@NullableStringprefix,@NullableAttributeSetattrs)throwsClassNotFoundException,InflateException{Objects.requireNonNull(viewContext);Objects.requireNonNull(name);//从缓存中取对应的构造函数Constructor<?extendsView>constructor=sConstructorMap.get(name);Class<?extendsView>clazz=null;try{if(constructor==null){//通过反射创建class对象clazz=Class.forName(prefix!=null?(prefix+name):name,false,mContext.getClassLoader()).asSubclass(View.class);//创建构造函数这里的mConstructorSignature长这个样子//staticfinalClass<?>[]mConstructorSignature=newClass[]{//Context.class,AttributeSet.class};//看到了没就是我们第二个构造方法constructor=clazz.getConstructor(mConstructorSignature);constructor.setAccessible(true);//缓存构造方法sConstructorMap.put(name,constructor);}else{...}try{//执行构造函数创建出viewfinalViewview=constructor.newInstance(args);...returnview;}finally{mConstructorArgs[0]=lastContext;}}catch(Exceptione){...}finally{...}}

    对LayoutInflater以及setContentView、DecorView、PhoneWindow相关一整套源码流程感兴趣的可以看下我这篇文章:

    Activity setContentView背后的一系列源码分析

    2、重写并实现onMeasure方法

    overridefunonMeasure(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设置的对应的宽、高*/publicstaticintgetChildMeasureSpec(intspec,intpadding,intchildDimension){//获取父类的尺寸模式intspecMode=MeasureSpec.getMode(spec);//获取父类的尺寸大小intspecSize=MeasureSpec.getSize(spec);//去掉padding后的大小最小不能低于0intsize=Math.max(0,specSize-padding);intresultSize=0;intresultMode=0;switch(specMode){//如果父类的模式是MeasureSpec.EXACTLY(精确模式,父类的值是可以确定的)caseMeasureSpec.EXACTLY:if(childDimension>=0){//此时子view的大小就是我们设置的值,超过父类也没事,开发人员自定义设置的//比如父view的宽是100dp,子view宽你非要设置200dp,那就给200dp,这么做有什么//意义?这样是可以扩展的,不至于限制死,比如子view可能具有滚动属性或者其他高级//玩法resultSize=childDimension;resultMode=MeasureSpec.EXACTLY;}elseif(childDimension==LayoutParams.MATCH_PARENT){//MATCH_PARENT则子view和父view大小一致模式是确定的resultSize=size;resultMode=MeasureSpec.EXACTLY;}elseif(childDimension==LayoutParams.WRAP_CONTENT){//WRAP_CONTENT则子view和父view大小一致模式是最大不超过这个值resultSize=size;resultMode=MeasureSpec.AT_MOST;}break;//ParenthasimposedamaximumsizeonuscaseMeasureSpec.AT_MOST:if(childDimension>=0){//按子view值执行,确定模式resultSize=childDimension;resultMode=MeasureSpec.EXACTLY;}elseif(childDimension==LayoutParams.MATCH_PARENT){//按父view值执行模式是最多不超过指定值模式resultSize=size;resultMode=MeasureSpec.AT_MOST;}elseif(childDimension==LayoutParams.WRAP_CONTENT){//按父view值执行模式是最多不超过指定值模式resultSize=size;resultMode=MeasureSpec.AT_MOST;}break;//ParentaskedtoseehowbigwewanttobecaseMeasureSpec.UNSPECIFIED:if(childDimension>=0){//按子view值执行,确定模式resultSize=childDimension;resultMode=MeasureSpec.EXACTLY;}elseif(childDimension==LayoutParams.MATCH_PARENT){//按父view值执行模式是未定义resultSize=View.sUseZeroUnspecifiedMeasureSpec?0:size;resultMode=MeasureSpec.UNSPECIFIED;}elseif(childDimension==LayoutParams.WRAP_CONTENT){//按父view值执行模式是未定义resultSize=View.sUseZeroUnspecifiedMeasureSpec?0:size;resultMode=MeasureSpec.UNSPECIFIED;}break;}//noinspectionResourceTypereturnMeasureSpec.makeMeasureSpec(resultSize,resultMode);}

    其实就是网上的这张图

    Android中FlowLayout组件如何实现瀑布流效果

    3、重写并实现onLayout方法

    我们要在这个方法里面,确定所有被添加到我们的FlowLayout里面的子view的位置,这里没有特殊要注意的地方,控制好细节就可以。

    三个关键步骤介绍完了,下面上实战代码:

    ZSFlowLayout:

    /***自定义瀑布流布局系统核心方法*ViewGroupgetChildMeasureSpec获取子view的MeasureSpec信息*Viewmeasure对view进行测量测量以后就知道view大小了之后可以通过getMeasuredWidth、getMeasuredHeight来获取其宽高*ViewMeasureSpec.getMode获取宽或高的模式(MeasureSpec.EXACTLY、MeasureSpec.AT_MOST、MeasureSpec.UNSPECIFIED)*ViewMeasureSpec.getSize获取父布局能给我们的宽、高大小*ViewsetMeasuredDimension设置测量结果*Viewlayout(left,top,right,bottom)设置布局位置**几个验证点getMeasuredHeight、getHeight何时有值结论:分别在onMeasure和onLayout之后*子view是relativeLayout并有子view时的情况没问题*通过addView方式添加ok已验证*/classZSFlowLayout:ViewGroup{//保存所有子view按行保存每行都可能有多个view所有是一个listvarallViews:MutableList<MutableList<View>>=mutableListOf()//每个子view之间的水平间距valhorizontalSpace:Int=resources.getDimensionPixelOffset(R.dimen.zs_flowlayout_horizontal_space)//每行之间的间距valverticalSpace:Int=resources.getDimensionPixelOffset(R.dimen.zs_flowlayout_vertical_space)//记录每一行的行高onLayout时会用到varlineHeights: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){}overridefunonMeasure(widthMeasureSpec:Int,heightMeasureSpec:Int){//会测量次allViews.clear()lineHeights.clear()//保存每一行的viewvareveryLineViews:MutableList<View>=mutableListOf()//记录每一行当前的宽度,用来判断是否要换行varcurLineHasUsedWidth:Int=paddingLeft+paddingRight//父布局能给的宽valselfWidth:Int=MeasureSpec.getSize(widthMeasureSpec)//父布局能给的高valselfHeight:Int=MeasureSpec.getSize(heightMeasureSpec)//我们自己通过测量需要的宽(如果用户在布局里对ZSFlowLayout的宽设置了wrap_content就会用到这个)varselfNeedWidth=0//我们自己通过测量需要的高(如果用户在布局里对ZSFlowLayout的高设置了wrap_content就会用到这个)varselfNeedHeight=paddingBottom+paddingTopvarcurLineHeight=0//第一步先测量子view核心系统方法是Viewmeasure方法//(1)因为子view有很多,所以循环遍历执行for(iin0untilchildCount){valchildView=getChildAt(i)if(childView.visibility==GONE){continue}//测量view之前先把测量需要的参数准备好通过ViewGroupgetChildMeasureSpec获取子view的MeasureSpec信息valchildWidthMeasureSpec=getChildMeasureSpec(widthMeasureSpec,paddingLeft+paddingRight,childView.layoutParams.width)valchildHeightMeasureSpec=getChildMeasureSpec(heightMeasureSpec,paddingTop+paddingBottom,childView.layoutParams.height)//调用子view的measure方法来对子view进行测量childView.measure(childWidthMeasureSpec,childHeightMeasureSpec)//测量之后就能拿到子view的宽高了,保存起来用于判断是否要换行以及需要的总高度valmeasuredHeight=childView.measuredHeightvalmeasuredWidth=childView.measuredWidth//按行保存view保存之前判断是否需要换行,如果需要就保存在下一行的list里面if(curLineHasUsedWidth+measuredWidth>selfWidth){//要换行了先记录换行之前的数据lineHeights.add(curLineHeight)selfNeedHeight+=curLineHeight+verticalSpaceallViews.add(everyLineViews)//再处理当前要换行的view相关数据curLineHeight=measuredHeighteveryLineViews=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+=curLineHeightlineHeights.add(curLineHeight)}}//第二步测量自己//根据父类传入的尺寸规则widthMeasureSpec、heightMeasureSpec获取当前自身应该遵守的布局模式//以widthMeasureSpec为例说明下这个是父类传入的,那父类是如何按照什么规则生成的widthMeasureSpec呢?//父类会结合自身的情况,并且结合子view的情况(子类的宽是match_parent、wrap_content、还是写死的值)来生成//生成的具体逻辑请见:ViewGroup的getChildMeasureSpec方法//(1)获取父类传过来的我们自身应该遵守的尺寸模式valwidthMode=MeasureSpec.getMode(widthMeasureSpec)valheightMode=MeasureSpec.getMode(heightMeasureSpec)//(2)根据模式来判断最终的宽高valwidthResult=if(widthMode==MeasureSpec.EXACTLY)selfWidthelseselfNeedWidthvalheightResult=if(heightMode==MeasureSpec.EXACTLY)selfHeightelseselfNeedHeight//第三步设置自身的测量结果setMeasuredDimension(widthResult,heightResult)}overridefunonLayout(changed:Boolean,l:Int,t:Int,r:Int,b:Int){//设置所有view的位置varcurT=paddingTopfor(iinallViews.indices){valmutableList=allViews[i]//记录每一行view的当前距离父布局左侧的位置初始值就是父布局的paddingLeftvarcurL=paddingLeftif(i!=0){curT+=lineHeights[i-1]+verticalSpace}for(jinmutableList.indices){valview=mutableList[j]valright=curL+view.measuredWidthvalbottom=curT+view.measuredHeightview.layout(curL,curT,right,bottom)//为下一个view做准备curL+=view.measuredWidth+horizontalSpace}}}}

    在布局文件中使用:

    <?xmlversion="1.0"encoding="utf-8"?><ScrollViewxmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><TextViewandroid: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.ZSFlowLayoutandroid:id="@+id/activity_flow_flowlayout"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_margin="8dp"android:padding="7dp"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="吕布吕奉先"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="赵云赵子龙"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:paddingLeft="10dp"android:text="典韦"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="关羽关云长"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="马超马孟起"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="张飞张翼德"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="黄忠"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="徐褚徐仲康"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="孙策孙伯符"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="太史慈"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="夏侯惇"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="夏侯渊"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="张辽"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="张郃"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="徐晃徐功明"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="庞德"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="甘宁甘兴霸"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="周泰"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="魏延"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="张绣"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="文丑"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="颜良"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/shape_button_circular"android:text="邓艾"/><TextViewandroid: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(更接近实战,实战中数据多是后台请求而来)

    classFlowActivity:AppCompatActivity(){@BindView(id=R.id.activity_flow_flowlayout)varflowLayout:ZSFlowLayout?=null;overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)setContentView(R.layout.activity_customview_flow)BindViewInject.inject(this)for(iin1until50){valtv:TextView=TextView(this)tv.text="TextView$i"flowLayout!!.addView(tv)}}}

    其中BindViewInject是用反射+注解实现的一个小工具类

    objectBindViewInject{/***注入**@paramactivity*/@JvmStaticfuninject(activity:Activity){inject(activity,false)}funinject(activity:Activity,isSetOnClickListener:Boolean){//第一步获取class对象valaClass:Class<outActivity>=activity.javaClass//第二步获取类本身定义的所有成员变量valdeclaredFields=aClass.declaredFields//第三步遍历找出有注解的属性for(iindeclaredFields.indices){valfield=declaredFields[i]//判断是否用BindView进行注解if(field.isAnnotationPresent(BindView::class.java)){//得到注解对象valbindView=field.getAnnotation(BindView::class.java)//得到注解对象上的id值这个就是view的idvalid=bindView.idif(id<=0){Toast.makeText(activity,"请设置正确的id",Toast.LENGTH_LONG).show()return}//建立映射关系,找出viewvalview=activity.findViewById<View>(id)//修改权限field.isAccessible=true//第四步给属性赋值try{field[activity]=view}catch(e:IllegalAccessException){e.printStackTrace()}//第五步设置点击监听if(isSetOnClickListener){//这里用反射实现增加练习//第一步获取这个属性的值valbutton=field.get(activity)//第二步获取其class对象valjavaClass=button.javaClass//第三步获取其setOnClickListener方法valmethod=javaClass.getMethod("setOnClickListener",View.OnClickListener::class.java)//第四步执行此方法method.invoke(button,activity)}}}}}
    @Target(AnnotationTarget.FIELD)@Retention(RetentionPolicy.RUNTIME)annotationclassBindView(//value是默认的,如果只有一个参数,并且名称是value,外面传递时可以直接写值,否则就要通过键值对来传值(例如:value=1)//intvalue()default0;valid:Int=0)
     </div> <div class="zixun-tj-product adv-bottom"></div> </div> </div> <div class="prve-next-news">
    本文:Android中FlowLayout组件如何实现瀑布流效果的详细内容,希望对您有所帮助,信息来源于网络。
    上一篇:Java IO中字节输入输出流的示例分析下一篇:

    13 人围观 / 0 条评论 ↓快速评论↓

    (必须)

    (必须,保密)

    阿狸1 阿狸2 阿狸3 阿狸4 阿狸5 阿狸6 阿狸7 阿狸8 阿狸9 阿狸10 阿狸11 阿狸12 阿狸13 阿狸14 阿狸15 阿狸16 阿狸17 阿狸18