安卓高级UI之自定义ViewGroup(深入理解)

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

扯点其他的

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的绘制流程

安卓高级UI之自定义ViewGroup(深入理解)_第1张图片

4.什么是流式布局?

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

5.两个必须

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

二.onMeasure

1.onMeasure思想

总体来说,测量的过程是一个递归算法

父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

他们之间转换的规则表如下
安卓高级UI之自定义ViewGroup(深入理解)_第3张图片
这个图的形象表示就是:
安卓高级UI之自定义ViewGroup(深入理解)_第4张图片
从源码来分析,比如拿出前三个情况(最后三种情况基本遇不到)

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方法执行完了之后才确定。

此时的层级关系是:
安卓高级UI之自定义ViewGroup(深入理解)_第5张图片

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

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

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

4.1度量孩子的步骤:

①先获取每个孩子
②通过LayoutParams把孩子的layout_width和layout_height存放的值变成-1那些值。具体为:
安卓高级UI之自定义ViewGroup(深入理解)_第6张图片

③通过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屏幕坐标系,一种是视图坐标系。他们的使用规则如下:
安卓高级UI之自定义ViewGroup(深入理解)_第7张图片
视图坐标系以父view的左上角为原点,我们自定义ViewGroup进行布局的时候,使用的就是视图坐标系。

2.getMeasuredWidth和getWidth有什么区别

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

3.补充的知识点:

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

4.流式布局效果演示

安卓高级UI之自定义ViewGroup(深入理解)_第8张图片

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]
        }
    }
}

你可能感兴趣的:(安卓提升笔记,android)