【学习】Android中View的工作原理(下)——View的工作流程和自定义View

View的工作流程

measure、layout、draw三大流程

1.View的measure过程

View的measure过程由其measure方法来完成,measure方法是一个final类型的方法,这意味着子类不能重写此方法,在View的measure方法中会去调用View的onMeasure方法

image.png

setMeasuredDimension方法会设置View宽高的测量值,我们看一下getDefaultSize方法

getDefaultSize

image.png
getDefaultSize返回的大小就是measureSpec中的specSize就是View测量后的大小,这里多次提到测量后的大小,是因为View最终的大小是在layout阶段确定的,但是几乎所有情况下View的测量大小和最终大小是相等的。至于UNSPECIFIED情况,一般用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize的第一个参数size,即宽高分别为getSuggestedMinimumWidth和getSuggestedMinimumHeight这两个方法的返回值。

image.png

分析上图第二个,如果View没有设置背景,那么View的宽度为mMinWidth,而mMinWidth对应于android:minWidth这个属性所指定的值,因此View的宽度即为android:minWidth属性所指定的值。如果这个属性不指定,那么mMinWidth则默认为0,如果View指定了背景,则View的宽度为max(mMinWidth,mBackground.getMinimumWidth()).

getMinimumWidth方法

image.png
该方法返回的就是Drawable的原始宽度,前提是这个View有原始宽度,否则返回0。

总结getSuggestedMinimum

如果View没有设置背景,那么返回android:minWidth这个属性所指定的值,这个值可以为0,如果View设置了背景,则返回android:minWidth和背景这两者中的最大值。getSuggestedMinimum的返回值就是View在UNSPECIFIED情况下的测量宽高。从getDefaultSize方法来看,View的宽高由specSize决定,所以直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身的大小,否则在布局中使用wrap_content就相当于使用match_parent。从上述代码中我们可以知道如果View在布局中使用了wrap_content,那么他的specmode是AT_MOST模式,在这种模式下它的宽高等于specSize。查表可得此时specSize为parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器当前剩余空间大小,显然,View的宽高就等于父容器当前剩余的空间大小,这种效果和布局中使用match_parent完全一致。

解决wrap_content失效问题

我们只需要给View指定一个默认的内部宽高(mWidth mHeight),并在wrap_content时设置此宽高即可,对于非wrap_content情形,我们沿用系统的测量值。这个默认的内部宽高大小如何指定根据需求。 image.png

ViewGroup的measure过程

对于ViewGroup来说,除了完成自己的measure过程,还会去遍历调用所有子元素的measure方法,各个子元素再递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,所以它没有重写View的onMeasure方法,但提供了一个measureChildren的方法。

measureChildren

image.png 上图中ViewGroup对每一个子元素进行measure

measureChild

image.png
measureChild取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传给View的measure方法处理。

ViewGroup并没有定义其测量的具体过程,因为不同的ViewGroup子类有不同的布局特性,导致它们的细节各不相同。

分析LinearLayout的onMeasure

LineaarLayout的onMeasure

image.png

measureVertical的源码

由于太长只展示部分

image.png 系统会遍历每个子元素并执行measureChildBeforeLayout方法,这个方法内部会调用子元素的measure方法,这样各个子元素就开始依次进入measure过程,并且系统会通过mTotalLength这个变量来存储LinearLayout在竖直方向的初步高度。每测量一个子元素,mTotalLength就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方向上的margin等。当子元素测量完毕后,LinearLayout会测量自己的大小,如下图。
image.png
当子元素测量完毕后,LinearLayout会根据子元素的情况来测量自己的大小。针对竖直的LinearLayout而言,它在水平方向的测量过程遵循View的测量过程,在竖直方向的测量过程和View有所不同。具体指如果它的布局中高度采用match_parent或者具体数值,那么它的的量过程与View一致,即高度为SpecSize,如果布局中高度采用wrap_content,那么它的高度是所有子元素所占用的高度总和,但是仍然不能超过它的父容器的剩余空间,最终高度还要考虑其在竖直方向的padding。

上图中resolveSizeAndState源码

image.png

setMeasuredDimension方法源码

image.png

measure完成后,通过fetMeasuredWidth/Height方法就可以正确获取到View的测量宽高。需要注意在某些极端情况下系统可能需要多次measure才能确定最终的测量宽高,在这种情况下在onMeasure拿到的测量宽高可能是不准确的。一个比较好的习惯是从onLayout方法中去获取View的测量宽高或者最终宽高。

问题

当我们想在Activity已启动的时候就做一件任务,但这件任务需要获取某个View的宽高。但是在onCreate、onStart、onResume中均无法正确得到某个View的宽高信息,这是因为View的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证。

解决办法

1.Activity/View#onWindowFocusChanged

这个方法的含义是:View已经初始化完毕了,宽高已经准备好了,这个时候去获取宽高是没有问题的。但是onWindowFocusChanged会被调用多次,当Activity的窗口得到焦点和失去焦点时都会被调用一次。也就是当Activity继续执行和暂停执行时,它都会被调用,如果频繁的进行onResume和onPause那么它也会被频繁调用。

image.png

所以可以通过重写该方法在其中获取宽高

image.png

image.png

2.view.post(runnable)

通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候View也初始化好了。 使用方法

image.png

3.ViewTreeObserver

使用Observer的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener这个接口,当View树的状态发生改变或者View树内部的View的客家逆行发生改变时,onGlobalLayout方法将被回调,因此这是获取View的宽高一个很好的时机。伴随着View树的状态改变等,onGlobalLayout会被调用多次。

使用方法 image.png image.png

4.view.measure(int widthMeasureSpec,int heightMeasureSpec)

通过手动对View进行measure来得到view的宽高,比较复杂,

要根据View的LayoutParams来分

1.match_parent

无法measure出具体的宽高,因为构造此种MeasureSpec需要parentSize而这个时候我们无法知道parentSize的大小。

2.具体数值

image.png

3.wrap_content

image.png 注意(1<<30)-1,View吃醋还能使用30位二进制表示,也就是说最大是30个1(2^30-1)也就是(1<<30)-1,在最大化模式下我们用View理论上能支持的最大值去构造MeasureSpec是合理的

measure的两种错误用法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JM47dySw-1654609301870)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1ffbb26095944ab8dfec10c11fbedb0~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image?)]

2.layout过程

ViewGroup用于确定子元素的位置,当ViewGroup位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中onLayout方法又会被调用。Layout过程和measure过程相比较简单。layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置。

下图为View的layout方法。 image.png

layout大致流程

先通过setFrame方法来设定View四个顶点的位置,即初始化mLeft,mRight,mTop,mBottom,View这四个顶点一旦确定,那么View在父容器中的位置也确定了,接着会调用onLayout方法,用于父容器决定子元素的位置,它的具体实现和具体的布局有关,所以View和ViewGroup都没有具体实现。 下图是LinearLayout的onLayout方法

image.png

选择layoutVertical讲解

image.png 该方法会遍历所有子元素并调用setChildFrame方法指定子元素的位置,其中childTop会逐渐增大,意味着后面的子元素会被放置在靠下的位置,正好符合竖直方向的LinearLayout的特性。setChildFrame只是调用子元素的layout方法而已,这样父容器在layout方法中完成自己的定位之后,就通过onLayout方法会调用子元素的layout方法,子元素有会通过自己的layout方法来确定自己的位置,这样一层一层完成了整个View树的layout过程。

以下是setChildFrame的源码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hHPpknZs-1654609301874)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d7d2842af4934d86a05da5631e43f117~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image?)] setChildFrame中的width和height实际上就是子元素的测量宽高

image.png 而在layout方法中会通过setFrame去设置子元素的四个顶点位置,在setFrame中有几句赋值语句来确定子元素的位置

image.png

View的测量宽高和最终宽高有什么区别?

这个问题可以具体为View的getMeasuredWidth和getWidth有什么区别 首先看一下getWidth的实现

image.png

结合mLeft等四个变量的赋值过程来看,getWidth方法的返回值刚好就是View的的测量宽度。在View的默认实现中,View的测量宽高和最终宽高是相等的,只不过测量宽高形成于View的measure过程,而最终宽高形成于View的layout过程,即两者的赋值时机不同,测量宽高的赋值时机要早一点。在日常开发中,我们可以认为View的测量宽高等于最终宽高,但是的确存在情况会导致两者不一致。

重写View的layout代码 image.png 上述代码会导致在任何情况下View的最终宽高总是比测量宽高大100px,虽然这么做会导致View显示不正常并且没有实际意义,但这证明了测量宽高的确可以不等于最终宽高。另外一种情况是在某些情况下View需要多次measure才能确定自己的测量宽高,在前几次的测量过程中,其得出的测量宽高有可能和最终宽高不一致,但最终来说两者相同。

draw过程

draw步骤

1.绘制背景 background.draw(canvas)
2.绘制自己 onDraw
3.绘制children dispatchDraw
4.绘制装饰 onDrawScrollBars //在新版本中已经改为onDrawForeground方法,在该方法中调用了onDrawScrollBars

draw源码

image.png View的绘制过程传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子元素的draw方法,如此draw事件就一层一层的传递下去

View的一个特殊方法setWillNotDraw

image.png
如果一个view不需要绘制任何内容那么设置这个标记位为true以后,系统会进行相关优化。ViewGroup默认启动该标记位,View默认不启动。当我们的自定义控件继承于ViewGroup并且自身并不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当明确知道一个ViewGroup需要通过onDraw来绘制内容时我们需要显式关闭这个标记位。

自定义View

自定义View的分类

1.继承View重写onDraw方法

主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态地现实一些不规则的图形。显然这需要通过绘制的方式来实现,即重写onDraw方法。采用这种方式需要自己直至wrap_content并且padding也要自己处理。

2.继承ViewGroup派生特殊的Layout

主要用于实现自定义布局,当某种效果看起来很像几种View组合在一起的时候,可以采用这种方法来实现。需要合适地处理ViewGroup的测量、布局两个过程,并同时处理子元素的测量和布局过程

3.继承特定的View

比较常见,一般是用于扩展某种已有的View的功能,比较容易,不需要自己支持wrap_content和padding

4.继承特定的ViewGroup

比较常见,不需要自己处理ViewGroup的测量和布局,理论上2能实现的效果方法4也能,但2更接近View的底层

自定义View须知

1.让View支持wrap_content

如果不对其做特殊处理那么当外界在布局使用它时就无法达到预期效果

2.若有必要让你的view支持padding

因为直接继承View的控件如果不在draw方法中处理padding,那么padding属性是无法起作用的。直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,不然会导致padding和子元素的margin失效。

3.尽量不要在View中使用Handler

因为View内部本身就提供了post方法完全可以替代Handler

4.View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow

如果有线程或者动画需要停止时那么onDetachedFromWindow是一个很好的时机。当包含此View的Activity退出或当前View被remove时,这个方法会被调用,和此方法对应的是onAttachedToWindow,它会在包含此View的Activity启动时调用。当View变得不可见时我们也需要停止,否则有可能造成内存泄漏

5.View带有滑动嵌套情形时,处理好滑动冲突

自定义View示例

自定义的Circle代码1.0

 import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.Nullable;

public class Circle extends View {

    private int mColor= Color.BLUE;
    private Paint mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);

    private void init(){
        mPaint.setColor(mColor);
    }

    public Circle(Context context) {
        super(context);
        init();
    }

    public Circle(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public Circle(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public Circle(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width=getWidth();
        int height=getHeight();
        int radius=Math.min(width,height)/2;
        canvas.drawCircle(width/2,height/2,radius,mPaint);
    }
} 

上面的代码定义了一个具有圆形效果的自定义View,它会在自己的中心点以宽高的最小值为直径绘制一个蓝色的实心圆

Activity_main.xml代码

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <com.rdc.testforlearn.Circle
        app:layout_constraintTop_toTopOf="parent"
        android:id="@+id/circle"
        android:background="@color/black"
        android:layout_width="match_parent"
        android:layout_height="100dp"/>

</androidx.constraintlayout.widget.ConstraintLayout> 

展示效果

image.png

再设置20dp的margin

 app:layout_constraintTop_toTopOf="parent"
    android:id="@+id/circle"
    android:background="@color/black"
    android:layout_margin="20dp"
    android:layout_width="match_parent"
    android:layout_height="100dp"/> 

显示效果

image.png

设置20dp的padding

 app:layout_constraintTop_toTopOf="parent"
    android:id="@+id/circle"
    android:background="@color/black"
    android:layout_margin="20dp"
    android:padding="20dp"
    android:layout_width="match_parent"
    android:layout_height="100dp"/> 

显示效果

image.png padding没有生效,因为我们前面提到过直接继承自View和ViewGroup的控件,padding是默认无法生效的,需要自己处理

修改width的模式(顺便修改了一下布局)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <com.rdc.testforlearn.Circle
        android:id="@+id/circle"
        android:background="@color/black"
        android:layout_margin="20dp"
        android:padding="20dp"
        android:layout_width="wrap_content"
        android:layout_height="100dp"/>

</LinearLayout> 

显示效果

image.png

wrap_content并没有起到预期效果,通过对比我们可以发现使用match_parent和wrap_content没有任何区别。这一点在前面也已经提到过:对于直接继承自View的控件,如果不对wrap_content做特殊处理就相当于使用match_parent。

解决办法

1.wrap_content

为其指定一个wrap_content模式下的默认宽高即可

在Circle类中增加如下代码

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecMode=MeasureSpec.getMode(widthMeasureSpec);
    int heightSpecMode=MeasureSpec.getMode(heightMeasureSpec);
    if (widthSpecMode==MeasureSpec.AT_MOST&&heightSpecMode==MeasureSpec.AT_MOST){//当宽高都为wrap_content时
        setMeasuredDimension(200,200);//设置默认值
    }else if(widthSpecMode==MeasureSpec.AT_MOST){//当宽为wrap_content时
        setMeasuredDimension(200,200);
    }else if(heightSpecMode==MeasureSpec.AT_MOST){//当高为wrap_content时
        setMeasuredDimension(200,200);
    }
} 

显示效果

image.png

2.padding

修改onDraw如下

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int paddingLeft=getPaddingLeft();
    int paddingRight=getPaddingLeft();
    int paddingTop=getPaddingLeft();
    int paddingBottom=getPaddingLeft();
    int width=getWidth()-paddingLeft-paddingRight;
    int height=getHeight()-paddingTop-paddingBottom;
    int radius=Math.min(width,height)/2;
    canvas.drawCircle(paddingLeft+width/2,paddingTop+height/2,radius,mPaint);
} 

中心思想就是绘制时考虑到View四周的空白即可,其中圆心和半径都会考虑到View四周的padding

显示效果

image.png

自定义属性

添加步骤 1.在values目录下创建自定义属性的XML,比如attrs.xml,也可以选择类似attrs_circle_view.xml等这种以attrs_开头的文件名,文件名并没有什么限制,可以随便取。 创建attrs.xml

image.png 在上面的xml中声明了一个自定义属性集合Circle,在这个集合里面可以有很多自定义属性,这里的格式color指的是颜色。除了颜色格式,自定义属性还有其他格式,比如reference指的是资源id,dimension指尺寸,而像string integer boolean这种指基本数据类型等等。

2.在View的构造方法中解析自定义属性的值并做相应处理

image.png 首先加载自定义属性集合Circle,接着解析Circle属性集合中的circle_color。在这一步骤中,如果在使用时没有指定circle_color这个属性,那么就会选择红色作为默认的颜色,解析完属性后,通过recycle方法来实现资源。

image.png 上图是书中的构造方法,注意第二个构造函数调用了本类的第三个构造方法,在正常情况下,第一个构造函数在动态创建一个view的时候调用,第二个构造函数在xml静态调用的时候调用,第三个调用方法要在第一或者第二个构造函数显式调用才会执行。

xml修改如下

image.png 需要注意为了使用自定义属性,必须在布局文件中添加schemas声明:xmlns:app=“schemas.android.com/apk/res-aut…” app是自定义属性的前缀,也可以换成其他名字,相应的下方属性也得更改名字。

显示效果

image.png

文末

在这里如果你要想成为架构师或者技术更近一步,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
在这里插入图片描述
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

一、架构师筑基必备技能

1、深入理解Java泛型
2、注解深入浅出
3、并发编程
4、数据传输与序列化
5、Java虚拟机原理
6、高效IO
……

在这里插入图片描述

二、Android百大框架源码解析

1.Retrofit 2.0源码解析
2.Okhttp3源码解析
3.ButterKnife源码解析
4.MPAndroidChart 源码解析
5.Glide源码解析
6.Leakcanary 源码解析
7.Universal-lmage-Loader源码解析
8.EventBus 3.0源码解析
9.zxing源码分析
10.Picasso源码解析
11.LottieAndroid使用详解及源码解析
12.Fresco 源码分析——图片加载流程

在这里插入图片描述

三、Android性能优化实战解析

  • 腾讯Bugly:对字符串匹配算法的一点理解
  • 爱奇艺:安卓APP崩溃捕获方案——xCrash
  • 字节跳动:深入理解Gradle框架之一:Plugin, Extension, buildSrc
  • 百度APP技术:Android H5首屏优化实践
  • 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
  • 携程:从智行 Android 项目看组件化架构实践
  • 网易新闻构建优化:如何让你的构建速度“势如闪电”?

在这里插入图片描述

四、高级kotlin强化实战

1、Kotlin入门教程
2、Kotlin 实战避坑指南
3、项目实战《Kotlin Jetpack 实战》

  • 从一个膜拜大神的 Demo 开始

  • Kotlin 写 Gradle 脚本是一种什么体验?

  • Kotlin 编程的三重境界

  • Kotlin 高阶函数

  • Kotlin 泛型

  • Kotlin 扩展

  • Kotlin 委托

  • 协程“不为人知”的调试技巧

  • 图解协程:suspend

在这里插入图片描述

五、Android高级UI开源框架进阶解密

1.SmartRefreshLayout的使用
2.Android之PullToRefresh控件源码解析
3.Android-PullToRefresh下拉刷新库基本用法
4.LoadSir-高效易用的加载反馈页管理框架
5.Android通用LoadingView加载框架详解
6.MPAndroidChart实现LineChart(折线图)
7.hellocharts-android使用指南
8.SmartTable使用指南
9.开源项目android-uitableview介绍
10.ExcelPanel 使用指南
11.Android开源项目SlidingMenu深切解析
12.MaterialDrawer使用指南
在这里插入图片描述

六、NDK模块开发

1、NDK 模块开发
2、JNI 模块
3、Native 开发工具
4、Linux 编程
5、底层图片处理
6、音视频开发
7、机器学习

在这里插入图片描述

七、Flutter技术进阶

1、Flutter跨平台开发概述
2、Windows中Flutter开发环境搭建
3、编写你的第一个Flutter APP
4、Flutter开发环境搭建和调试
5、Dart语法篇之基础语法(一)
6、Dart语法篇之集合的使用与源码解析(二)
7、Dart语法篇之集合操作符函数与源码分析(三)

在这里插入图片描述

八、微信小程序开发

1、小程序概述及入门
2、小程序UI开发
3、API操作
4、购物商场项目实战……

在这里插入图片描述

全套视频资料:

一、面试合集
在这里插入图片描述
二、源码解析合集

在这里插入图片描述
三、开源框架合集

在这里插入图片描述
欢迎大家一键三连支持,若需要文中资料,可点击下方CSDN官方认证卡片免费获取。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值