安卓高级UI之自定义ViewGroup2.0(深入理解)

这一篇博客也算是对第一篇博客的一个深入补充吧。虽然第一篇介绍的东西多一点,但是有个别知识还是没有真正解释透,所以在这里又整理了一下。
上一篇博客

自定义View小总结

自定义View涉及三个方法,分别是onDrawonMeasureonLayoutonDraw里面经常涉及属性动画ObjectAnimation的应用,以及在set方法中进行重绘方法的调用。onMeasure里面的知识点有,父ViewGrouponMeasure调用子Viewmeasure方法,measure方法再调用自己的onMeasure方法。如果父ViewGroup里面还嵌套了子ViewGroup,那么子ViewGroup再调用子Viewmeaure方法,这是一个递归的过程。通过setMeasureDimision来告诉父ViewGroup自己的测量结果。父ViewGroup在调用子Viewmeasure方法的时候,会传入限制,有三种,分别是不限制,有上限和固定值。

扯点其他的

Java和kotlin是语言基础,自定义view是安卓基础。

所以,绘制UI的能力其实是入门的功夫。

一.什么是自定义View

一个效果只要它能够在手机上面实现,你就应该具备实现它的能力

1.自定义View包含了一些什么东西?

布局:onMeasure onLayout
显示 :onDraw:(画布(canvas),画笔(paint),matrix clip,rect,animation,path(贝塞尔曲线等等),line等等)
事件分发 (交互):onTouchEvent:用的最多的是组合的ViewGroup
这些类似于是二十六个字母,音标,这是最基本的。但是学会这些还不能说英语,认全字母之后还需要再去背单词。也就是大量的练习。

2.自定义View的分类?

①自定义View:在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View,SurfaceView或其他的View
②自定义ViewGroup:自定义ViewGroup一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout

3.自定义View的绘制流程

在这里插入图片描述

4.什么是流式布局?

FlowLayout,流式布局,这个概念在移动端或者前端开发中很常见,特别是在多标签的展示中,往往起到了关键的作用。然而Android官方,并没有为开发者提供这样一个布局。一般也是学习自定义ViewGroup的入门级项目。
效果如下在这里插入图片描述

5.两个必须

在自定义ViewGroup中必须实现onMeasure和onLayout方法
在自定义View中实现onMeasure和onDraw方法

二.onMeasure

1.onMeasure思想

总体来说,测量的过程是一个递归算法。绝大部分是先度量孩子再度量自己(ViewPager除外)

父ViewGroup在触发onMeasure方法的时候会调用子View的onMeasure方法,并且传给他两个参数,也就是我们经常用到的widthMeasureSpec和heightMeasureSpec参数。子View测量完了之后会进行一个保存。

那按理说onMeasure函数应该有返回值呀,应该return个数给父控件呀,但是为什么返回值是void呢?

这是因为父控件可以通过childView.getMeasuredWidth与childView.getMeasuredHeight获得子控件保存的值。

那父亲给的两个参数究竟是什么?
那首先就要知道MeasureSpec是个什么东西:

2.MeasureSpec

它是View里面的一个静态类,基本都是二进制运算。由于int是32位的,所以MeasureSpec用高两位表示mode,低30位表示size。MODE_SHIFT=30的作用是移位。
其中三种mode分别为:

UNSPECIFIED:不对View大小做限制,系统使用
EXACTLY:确切的大小,如100dp
AT_MOST:大小不可超过某个值,如MATCH_PARENT

3.如何把LayoutParams转化为MeasureSpec呢?

LayoutParams是什么?

LayoutParams把控件的layout_width和layout_height以及他们的值解析出来

两者如何进行转换?

那就要用到一个非常重要的算法:getChildMeasureSpec

他们之间转换的规则表如下
在这里插入图片描述
这个图的形象表示就是:
在这里插入图片描述
从源码来分析,比如拿出前三个情况(最后三种情况基本遇不到)

case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

解释:

在确定子控件的MeasureSpec时:当父亲是个确切的值的时候,如果子控件是个确切的值,那么子控件的size就是子控件layoutParams得到的size,mode就是EXACTLY。如果子控件是MATCH_PARENT,那么子控件的size就是父控件的size,mode也是EXACTLY,因为这也是明确的全部给你。如果子控件是WRAP_CONTENT,那么子控件的size就是父控件的size,mode是AT_MOST。其他的情况都是类似的道理。

从这里也可以看出,getChildMeasureSpec得到的是一个参考的的值而不是一个具体的值。具体的值等measure完了之后才能得到。也就是说measure传入的是一个参考值,子控件onMeasure方法两个参数就是这两个参考值。

所以,在这里再次理解一下getChildMeasureSpec的三个参数,就会恍然大悟:

getChildMeasureSpec(int spec, int padding, int childDimension)第一个参数是父亲的spec,用于得到父亲的size和mode,第二个参数是间距,只有减去这个间距才是父控件”真正“留给子控件的大小。(形象一点来说就是父控件要给自己留一点养老金),第三个参数是子控件的LayoutParams

所以现在就可以回答一开始的那个问题了

这个onMeasure的两个参数,对于当前我写的这个ViewGroup来说,如果把它当作父亲,那么这两个参数就是爷爷给它传过来的参考值,而具体值需要等最后setMeasuredDimension方法执行完了之后才确定。

此时的层级关系是:
在这里插入图片描述

此时TextView的Mode就是AT_MOST,FlowLayout的Mode也是AT_MOST

那么是先度量孩子还是先度量自己呢?

这是不一定的。
先度量自己再度量孩子:ViewPager
先度量孩子再度量自己:绝大部分

4.1度量孩子的步骤:

①先获取每个孩子
②通过LayoutParams把孩子的layout_width和layout_height存放的值变成-1那些值。具体为:
在这里插入图片描述

③通过getChildMeasureSpec将孩子的layoutParams值转化MeasureSpec
④进行度量

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
            for(i in 0 until childCount){
                var childView = getChildAt(i)

                //得到子控件的LayoutParams
                var childLP = childView.layoutParams

                //将layoutParams转化为MeasureSpec
                var childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,paddingLeft+paddingRight,childLP.width)
                var childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,paddingTop+paddingBottom,childLP.height)

                childView.measure(childWidthMeasureSpec,childHeightMeasureSpec)
            }
    }

4.2度量父亲的步骤:

		var widthMode = MeasureSpec.getMode(widthMeasureSpec)
        var heightMode = MeasureSpec.getMode(heightMeasureSpec)
        
        var realWidth = if(widthMode == MeasureSpec.EXACTLY) selfWidth else (parentNeededWidth + paddingLeft + paddingRight)
        var realHeight = if(heightMode == MeasureSpec.EXACTLY) selfHeight else (parentNeededHeight + paddingTop + paddingBottom)
        
        setMeasuredDimension(realWidth,realHeight)

这里比较难的地方就是realWidth和realHeight的来源。

selfWidth 和selfHeight代表爷爷给ViewGroup(父控件)传过来的参考size,parentNeededWidth和parentNeededHeight代表测量完子控件之后父控件真正需要的大小(当然还要加上padding才是真真正正的大小)。当父控件的mode是EXACTLY时,比如宽和高都是100dp,那么它就根本不用管子控件多大,直接就可以得出测量自己的结果了。
那么又有问题了:如果子控件宽或者高大小超过100dp怎么办?其实很简单,多出去的部分不显示不就得了。所以如果mode是EXACTLY的话就不用管子控件,直接用爷爷给父控件传过来的参考值就是最后的结果。如果mode是其他的,那就要管子控件了。

三.onLayout

1.安卓的两种坐标系

在介绍布局之前,要先掌握Android的两种坐标系
一种是Android屏幕坐标系,一种是视图坐标系。他们的使用规则如下:
在这里插入图片描述
视图坐标系以父view的左上角为原点,我们自定义ViewGroup进行布局的时候,使用的就是视图坐标系。

2.getMeasuredWidth和getWidth有什么区别

①getMeasuredWidth在measure()过程结束后就可以获取到对应的值,这个值是通过setMeasuredDimension()方法来进行设置的。
②而getWidth是在layout()过程结束后才能获取到的,这个值是通过视图右边的坐标减去左边的坐标计算出来的。
简而言之,就是如果生命周期在onMeasure和onLayout之间就用前者,如果生命周期在onLayout和onDraw之间就用后者

3.补充的知识点:

margin和padding
我找到了一篇很棒的关于margin和padding的博客

4.流式布局效果演示

在这里插入图片描述

5.流式布局代码

package com.example.test01

import android.content.Context
import android.icu.util.Measure
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup

class FlowLayout:ViewGroup {
    //定义两控件之间的间距
    var mHorizontalSpace = 30
    var mVerticalSpace = 20

    //保存所有的View
    var allLineViews: MutableList<MutableList<View>> = mutableListOf()
    //保存所有的高度
    var everyLineHeights: MutableList<Int> = mutableListOf()

    constructor(context: Context):super(context){}
    constructor(context: Context,attrs: AttributeSet):super(context,attrs){}

    //清空函数
    private fun clearFun(){
        allLineViews.clear()
        everyLineHeights.clear()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        //清空,而不是重新申请空间,防止内存抖动
        clearFun()

        //得到爷爷给此ViewGroup的参考值
        var selfWidth = MeasureSpec.getSize(widthMeasureSpec)
        var selfHeight = MeasureSpec.getSize(heightMeasureSpec)
        //记录此ViewGroup实际需要多宽多高
        var parentNeededWidth = 0
        var parentNeededHeight = 0

        //保存这一行的行宽以及行高
        var lineWidthUsed = 0
        var lineHeightUsed = 0
        //保存这一行所有的View
        var oneLineViews: MutableList<View> = mutableListOf()

        //测量孩子
        for(i in 0 until childCount){
            var childView = getChildAt(i)

            //得到layoutParams
            var childLP = childView.layoutParams

            //得到MeasureSpec
            var childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,paddingLeft+paddingRight,childLP.width)
            var childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,paddingTop+paddingBottom,childLP.height)

            //进行测量
            childView.measure(childWidthMeasureSpec,childHeightMeasureSpec)

            if(lineWidthUsed + childView.measuredWidth + mHorizontalSpace +paddingLeft + paddingRight> selfWidth){
                //换行
                allLineViews.add(oneLineViews)
                everyLineHeights.add(lineHeightUsed)
                parentNeededWidth = Math.max(parentNeededWidth,lineWidthUsed)
                parentNeededHeight += lineHeightUsed

                //数据重置或者清0
                lineWidthUsed = 0
                lineHeightUsed = 0
                oneLineViews = mutableListOf()
            }

            oneLineViews.add(childView)
            lineWidthUsed += childView.measuredWidth + mHorizontalSpace
            lineHeightUsed = Math.max(lineHeightUsed,childView.measuredHeight + mVerticalSpace)

            //保存最后一行的数据(这个很容易忘记)
            if(i == childCount - 1){//注意这个判断条件
                //换行
                allLineViews.add(oneLineViews)
                everyLineHeights.add(lineHeightUsed)
                parentNeededWidth = Math.max(parentNeededWidth,lineWidthUsed)
                parentNeededHeight += lineHeightUsed
            }
        }
        //测量父亲
        //首先得到父亲的mode,看看是不是EXACTLY
        var parentWidthMode = MeasureSpec.getMode(widthMeasureSpec)
        var parentHeightMode = MeasureSpec.getMode(heightMeasureSpec)

        var realWidth = if(parentWidthMode == MeasureSpec.EXACTLY) selfWidth else (parentNeededWidth + paddingLeft + paddingRight) 
        var realHeight = if(parentHeightMode == MeasureSpec.EXACTLY) selfHeight else (parentNeededHeight + paddingTop + paddingBottom)

        //这里要加上间距
        setMeasuredDimension(realWidth,realHeight)

    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        var padL = paddingLeft
        var padT = paddingTop

        for(i in 0 until allLineViews.size){
            for(j in 0 until allLineViews.get(i).size){
                //注意:下面这里不是getChildAt,而是得到二维数组的每一个值
                var childView = allLineViews.get(i).get(j)
                var left = padL
                var top = padT
                var right = left + childView.measuredWidth
                var bottom = top + childView.measuredHeight
                childView.layout(left,top,right,bottom)

                padL += childView.width + mHorizontalSpace
            }
            //一定要注意,左起始点要进行归0
            padL = paddingLeft
            padT += everyLineHeights[i]
        }
    }
}

隔了四个月之后又写了一个,感觉这个更标准一些,上面的可以不看

有的更改的地方,其实影响不大,重要的是总体思路要正确

package com.example.a202133;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;
import java.util.List;

public class FlowLayout extends ViewGroup {
    int mHorizontalSpacing = 80;//每个View之间的间隔
    int mVerticalSpacing = 10;//行与行之间的间隔

    private List<List<View>> allLines = new ArrayList<>();//记录所有的行,一行一行的存储,用于layout
    List<Integer> lineHeights = new ArrayList<>();//记录每一行的行高,用于layout

    private void clearMeasureParams(){
        allLines.clear();
        lineHeights.clear();
    }

    public FlowLayout(Context context) {
        super(context);
    }

    public FlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    //度量
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //一定要在onMeasure中进行清0,因为onMeasure可能调用多次,应该在每次调用Measure的时候清空
        //initMeasureParams();
        //但是可能会出现内存抖动问题!因为频繁的初始化会产生大量的内存碎片
        //所以应该是情空,而不是重新初始化
        clearMeasureParams();

        //先度量孩子
        int childCount =  getChildCount();
        //获取父亲的“养老地方”
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();

        int selfWidth = MeasureSpec.getSize(widthMeasureSpec);//从具有mode和size的measurespec中解析出ViewGroup的宽度
        int selfHeight = MeasureSpec.getSize(heightMeasureSpec);

        int parentNeededWidth = 0;//measure过程中给,子View要求的父ViewGroup的宽
        int parentNeededHeight = 0;

        List<View> lineViews = new ArrayList<>();//保存一行中所有的View
        int lineWidthUsed = 0;//记录这行已经使用了多宽的size
        int lineHeight = 0;//一行的行高

        for(int i = 0;i < childCount;i++){
            View childView = getChildAt(i);
            LayoutParams childLP = childView.getLayoutParams();
            if(childView.getVisibility() != View.GONE){
                //将LayoutParams转变成为measureSpec
                int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,paddingLeft+paddingRight,childLP.width);
                int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,paddingTop+paddingBottom,childLP.height);

                childView.measure(childWidthMeasureSpec,childHeightMeasureSpec);

                //获取子View的度量宽高,这个值是上面measure之后得到的
                int childMeasuredWidth = childView.getMeasuredWidth();
                int childMeasuredHeight = childView.getMeasuredHeight();

                //判断是否需要换行
                if(childMeasuredWidth + lineWidthUsed + mHorizontalSpacing +paddingLeft + paddingRight> selfWidth){
                    //一旦换行,我们就可以判断当前行需要的宽和高了,所以此时需要记录下来
                    allLines.add(lineViews);
                    lineHeights.add(lineHeight);

                    //这里留有疑问,值得推敲一下
                    parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;
                    parentNeededWidth = Math.max(parentNeededWidth,lineWidthUsed + paddingLeft + paddingRight);

                    //换行了,需要清0
                    lineViews = new ArrayList<>();
                    lineWidthUsed = 0;
                    lineHeight = 0;
                }
                //View是分行Layout的,所以要记录每一行有哪些View,这样可以方便layout布局
                lineViews.add(childView);
                //设置行宽和行高
                lineWidthUsed = lineWidthUsed + childMeasuredWidth + mHorizontalSpacing;//这个10是一行中每个View之间的间距
                lineHeight = Math.max(lineHeight,childMeasuredHeight);

                //处理最后一行的数据
                if(i == childCount - 1){
                    allLines.add(lineViews);
                    lineHeights.add(lineHeight);
                    parentNeededHeight = parentNeededHeight + lineHeight  + paddingBottom + paddingTop;//别忘了把上间距和下间距算上
                    parentNeededWidth = Math.max(parentNeededWidth,lineWidthUsed + paddingLeft + paddingRight);
                }
            }
        }
        //根据子View的度量结果,来重新度量自己ViewGroup
        //作为一个ViewGroup,它自己也是一个View,它的大小也需要根据它的父亲给他提供的宽高来度量
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        int realWidth = (widthMode == MeasureSpec.EXACTLY) ? selfWidth : parentNeededWidth;
        int realHeight = (heightMode == MeasureSpec.EXACTLY) ? selfHeight : parentNeededHeight;
        /**
         * 别忘了这里selfWidth和selfHeight是前面MeasureSpec.getSize得到的。是经过MeasureSpec算法之后得到的大小
         */

        //再度量自己,保存
        setMeasuredDimension(realWidth,realHeight);
        /**
         * 这个parent其实是ViewGroup自己,之所以是parent其实是相对于子View来说的。
         */
    }

    //布局
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int lineCount = allLines.size();

        int curL = getPaddingLeft();
        int curT = getPaddingTop();
        for(int i = 0;i < lineCount;i++){
            List<View> lineViews = allLines.get(i);
            int lineHeight = lineHeights.get(i);
            for(int j = 0;j < lineViews.size();j++){
                View view = lineViews.get(j);
                int left = curL;
                int top = curT;

                int right = left + view.getMeasuredWidth();
                int bottom = top + view.getMeasuredHeight();

                view.layout(left,top,right,bottom);
                curL = right + mHorizontalSpacing;
            }
            curT = curT + lineHeight + mVerticalSpacing;
            curL = getPaddingLeft();
        }
    }
}

效果图如下
在这里插入图片描述
为什么下面间距这么大呢?因为这里
在这里插入图片描述
我设置的是80

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值