IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android 自定义ViewGroup之FlowLayout流式布局 -> 正文阅读

[移动开发]Android 自定义ViewGroup之FlowLayout流式布局

简介

???????Android自定义View,ViewGroup是在开发中使用的比较多的,学习和掌握自定View,是中高级开发必须掌握的技能,本文使用一个自定义FlowLayout,来介绍自定义ViewGroup的流程与需要注意的地方

实践

  1. 我们先来看看最后的实现效果
    在这里插入图片描述

  2. 接下来我们来实现这个效果,首先我们新建一个类,继承至ViewGroup,并且重写其onMeasure和onLayout两个方法,ViewGroup有4个构造方法,我们可以根据需要,重写其构造方法

    class FlowLayout : ViewGroup {
    
        constructor(context: Context) : super(context)
        constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
        constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) 
        : super(context, attributeSet, defStyleAttr)
        /**
         * 测量控件大小
         */
        override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    
        }
        /**
         * 布局
         */
        override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
            
        }
    }
    

    onMeasure:测量控件大小的,我们定义的是ViewGroup,所以里面子view的大小都需要在这测量,测量后我们就可以拿到 控件的大小;

    注意:onMeasure是有可能执行多次的,像ViewPager就调用了两次,详细可以看ViewPager的onMeasure方法源码,onMeasure调用几次,取决于它的父ViewGroup,它的父ViewGroup调用了多次measure,onMeasure就会执行多次

    onLayout:布局,这个地方可以对控件进行布局,决定子view的排列方式,显示的位置;

  3. 测量子View的大小

    怎么测量子view的大小呢?其实ViewGroup已经给我提供了3个方法来测量子view的大小

    //循环遍历所有的子view,调用measureChild来测量子view大小
    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
            final int size = mChildrenCount;
            final View[] children = mChildren;
            for (int i = 0; i < size; ++i) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                    measureChild(child, widthMeasureSpec, heightMeasureSpec);
                }
            }
    }
    //测量单个view的大小,会考虑ViewGroup的padding
    protected void measureChild(View child, int parentWidthMeasureSpec,
                int parentHeightMeasureSpec) {
            final LayoutParams lp = child.getLayoutParams();
    
            final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                    mPaddingLeft + mPaddingRight, lp.width);
            final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                    mPaddingTop + mPaddingBottom, lp.height);
    
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    //测量单个view的大小,会考虑ViewGroup的padding  和子view的margin
    protected void measureChildWithMargins(View child,
                int parentWidthMeasureSpec, int widthUsed,
                int parentHeightMeasureSpec, int heightUsed) {
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    
            final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                    mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                            + widthUsed, lp.width);
            final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                    mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                            + heightUsed, lp.height);
    
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    

    因为我们需要考虑到子view的margin,所以我们可以使用第三个方法来测量子view的大小,当然我们也可以参考它的写法,自己来写
    首先我们要获取到自定义ViewGroup的padding,然后在循环遍历所有子view,测量每一个view的大小

    /**
     * 测量
     */
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        //获取控件设置的内边距
        val paddingLeft = paddingLeft
        val paddingRight = paddingRight
        val paddingTop = paddingTop
        val paddingBottom = paddingBottom
    
        //先测量子view的大小
        for (index in 0 until childCount) {
            val childView = getChildAt(index)
            //获取子view的LayoutParams --> MarginLayoutParams
            val childLP = childView.layoutParams as MarginLayoutParams
            //将LayoutParam转变成为MeasureSpec
            val childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                paddingLeft + paddingRight + childLP.leftMargin + childLP.rightMargin, childLP.width)
            val childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                paddingTop + paddingBottom + childLP.topMargin + childLP.bottomMargin, childLP.height )
            //测量子view的大小
            childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
        }
    }
    

    我们可以看到, 我们先获取到了子view的LayoutParams,然后使用getChildMeasureSpec()这个方法获取到了子view的MeasureSpec,然后调用子view的measure方法来度量子view的宽高

    那么getChildMeasureSpec方法,measure方法分别干了什么事情呢?MeasureSpec又是什么呢?详情可以看我另一篇文章
    自定义ViewGroup之measureSpec

    这个地方有一个需要注意的地方,childView.layoutParams拿到的LayoutParams是无法直接强转MarginLayoutParams的,而只有MarginLayoutParams才能获取到margin,所以 需要我们重写ViewGroup的这几个方法

    /**
         * 重写generateLayoutParams方法,返回自定义的LayoutParams
         * 使其可以获取Margin值
         */
        override fun generateLayoutParams(p: LayoutParams?): LayoutParams? {
            return MarginLayoutParams(p)
        }
    
        override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams? {
            return MarginLayoutParams(context, attrs)
        }
    
        override fun generateDefaultLayoutParams(): LayoutParams? {
            return MarginLayoutParams(
                LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT
            )
        }
    
  4. 测量完子view后,我们就可以获取到子view的大小了,获取到子view大小后,我们还需计算出我们自定义的FlowLayout的实际大小,并且调用setMeasuredDimension()方法保存起来

    在计算我们的FlowLayout的实际大小时,因为我们的子view显示不下的时候是需要换行的,所以我们高度就是所有行的高度之和,宽度就是最宽的那一行的宽度

    那我们怎么知道什么时候应该换行呢?就是当前行大于我们的FlowLayout的宽度的时候,就应该换行,但是我们现在又在计算FlowLayout的大小,那我们怎么获取呢?
    在onMeasure方法中有两个参数widthMeasureSpec和heightMeasureSpec,这个就是父ViewGroup给我们measureSpec,然后使用MeasureSpec.getSize()这个方法获取到父控件可以提供给你最大的布局空间,详情可以参考自定义ViewGroup之MeasureSpec,现在所有的问题都解决了,我们来看看代码:

    class FlowLayout : ViewGroup {
        //每一个Item横向间距
        private var mHorizontalSpacing = dp2px(0f)
    
        //每一行Item纵向间距
        private var mVerticalSpeaing = dp2px(0f)
    
        /**
         * 设置每个item的横向边距
         */
        fun setHorizontalSpacing(hSpacing: Float) {
            mHorizontalSpacing = dp2px(hSpacing)
        }
    
        /**
         * 设置每个item的纵向边距
         */
        fun setVerticalSpacing(vSpacing: Float) {
            mVerticalSpeaing = dp2px(vSpacing)
        }
    
        //保存每一行的view的数据
        val allLineViews: MutableList<MutableList<View>> = mutableListOf()
    
        //保存每一行的高度
        val allLineHeights: MutableList<Int> = mutableListOf()
    
    
        constructor(context: Context) : super(context)
    
        constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
    
        constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr)
    
        init {
            //初始化代码
    
        }
    
        /**
         * 测量
         */
        override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
            //清空上次保存的数据
            allLineHeights.clear()
            allLineViews.clear()
            //获取控件设置的内边距
            val paddingLeft = paddingLeft
            val paddingRight = paddingRight
            val paddingTop = paddingTop
            val paddingBottom = paddingBottom
            //保存一行中所有的view
            var lineViews: MutableList<View> = mutableListOf()
            var lineWidthUsed = 0 //记录这行已经使用了多宽的size
            var lineHeight = 0 //一行的行高
            //解析的父ViewGroup给我的参考宽度
            val selfWidth = MeasureSpec.getSize(widthMeasureSpec)
            //解析的父ViewGroup给我的参考高度
            val selfHeight = MeasureSpec.getSize(heightMeasureSpec)
            //所有子view中,最宽的值
            var parentNeededWidth = 0
            //所有子view的高度
            var parentNeededHeight = 0
    
            //记录一行,最大的topMargin和最大的bottomMargin
            var maxTopMargin = 0
            var maxBottomMargin = 0
    
            //先测量子view的大小
            for (index in 0 until childCount) {
                val childView = getChildAt(index)
                //获取子view的LayoutParams --> MarginLayoutParams
                val childLP = childView.layoutParams as MarginLayoutParams
                //将LayoutParam转变成为MeasureSpec
                val childWidthMeasureSpec = getChildMeasureSpec(
                    widthMeasureSpec,
                    paddingLeft + paddingRight + childLP.leftMargin + childLP.rightMargin, childLP.width
                )
                val childHeightMeasureSpec = getChildMeasureSpec(
                    heightMeasureSpec,
                    paddingTop + paddingBottom + childLP.topMargin + childLP.bottomMargin,
                    childLP.height
                )
                //测量子view的大小
                childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
    
                //获取子view测量后的宽高
                val childMeasuredWidth = childView.measuredWidth
                val childMeasuredHeight = childView.measuredHeight
                //这一行的宽度 还需要加上当前view的margin
                var lineWidth =
                    lineWidthUsed + childMeasuredWidth + mHorizontalSpacing + childLP.leftMargin + childLP.rightMargin
                //当这一行的宽度大于父布局的宽度时,需要换行
                if (lineWidth > selfWidth) {
                    //保存上一行的数据
                    allLineViews.add(lineViews)
                    allLineHeights.add(lineHeight)
    
                    //修改ViewGroup宽高 高度需要加上这一行最大的topMargin和最大的bottomMargin
                    parentNeededHeight += lineHeight + mVerticalSpeaing + maxTopMargin + maxBottomMargin
                    parentNeededWidth = Math.max(parentNeededWidth, lineWidth)
    
                    //初始化下一行的数据
                    lineWidth = childMeasuredWidth + mHorizontalSpacing + childLP.leftMargin + childLP.rightMargin
                    lineViews = mutableListOf()
                    lineHeight = 0
                    maxTopMargin = 0
                    maxBottomMargin = 0
    
                }
                //view是分行的layout,所以药记录每一行有哪些view
                lineViews.add(childView)
                //记录这一行,最大的topMargin和最大的bottomMargin
                maxTopMargin = Math.max(maxTopMargin, childLP.topMargin)
                maxBottomMargin = Math.max(maxBottomMargin, childLP.bottomMargin)
                //更新每一行的宽和高
                lineWidthUsed = lineWidth
                lineHeight = Math.max(lineHeight, childMeasuredHeight)
    
                //处理最后一行的数据
                if (index == childCount - 1) {
                    allLineViews.add(lineViews)
                    allLineHeights.add(lineHeight)
                    //修改宽高
                    parentNeededHeight += lineHeight + mVerticalSpeaing + maxTopMargin + maxBottomMargin
                    parentNeededWidth = Math.max(parentNeededWidth, lineWidth)
                }
    
            }
    
            //根据子view度量的结果,来重新度量自己ViewGroup
            //作为一个ViewGroup,它自己也是一个View,它的大小也要根据它的父View给他提供的宽高来度量
            val widthMode = MeasureSpec.getMode(widthMeasureSpec)
            val heigthMode = MeasureSpec.getMode(heightMeasureSpec)
            //如果是实际大小,则直接使用设置的具体值
            val realWidth = if (widthMode == MeasureSpec.EXACTLY) selfWidth else parentNeededWidth
            val realHeight = if (heigthMode == MeasureSpec.EXACTLY) selfHeight else parentNeededHeight
            //保存测量的大小
            setMeasuredDimension(realWidth, realHeight)
        }
    }
    

    这里需要特别注意的是,我们需要根据父ViewGroup给我们的widthMeasureSpec,调用MeasureSpec.getMode()方法,获取到MeasureSpec,如果MeasureSpec等于MeasureSpec.EXACTLY(确定的大小),则使用MeasureSpec.getSize()方法获取到的大小,如果是其它的,则使用我们计算出来的大小,最后调用setMeasuredDimension(realWidth, realHeight)保存我们ViewGroup的实际大小

  5. 测量完成后,我们就需要在onLayout里面确定子view的位置,在开始前,我们需要了解Android中的两种坐标系
    在这里插入图片描述
    一种是以Android屏幕左上角为原点,一种是根据父ViewGroup来确定位置,我们这里必须使用第二种,下面我们来看看代码

    /**
         * 布局
         */
        override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    
            var curL = paddingLeft
            var curT = paddingTop
    
            for ((index, itemViewList) in allLineViews.withIndex()) {
                //这一行的高度
                val lineHeight = allLineHeights[index]
                //记录这一行,最大的topMargin和最大的bottomMargin
                var maxTopMargin = 0
                var maxBottomMargin = 0
                for (view in itemViewList) {
                    //获取子view的LayoutParams --> MarginLayoutParams
                    val childLP = view.layoutParams as MarginLayoutParams
                    //需要加上margin
                    val left = curL + childLP.leftMargin
                    val top = curT + childLP.topMargin
                    val right = left + view.measuredWidth
                    val bottom = top + view.measuredHeight
                    view.layout(left, top, right, bottom)
                    //下一个view的左边距离需要加上当前view的rightMargin
                    curL = right + mVerticalSpeaing + childLP.rightMargin
                    //记录这一行,最大的topMargin和最大的bottomMargin
                    maxTopMargin = Math.max(maxTopMargin, childLP.topMargin)
                    maxBottomMargin = Math.max(maxBottomMargin, childLP.bottomMargin)
                }
                //下一行距离父布局top的距离需要加上 上一行最大的topMargin和最大的bottomMargin
                curT += lineHeight + mVerticalSpeaing + maxTopMargin + maxBottomMargin
                curL = paddingLeft
            }
        }
    

    好了,到这我们的自定义FlowLayout 就大功告成了,我们可以在布局中使用FlowLayout,在里面随便加一几个view,就实现文章开头所展示的效果啦
    代码:FlowLayout

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-08-12 16:42:49  更:2021-08-12 16:43:38 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 2:25:54-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码