开发艺术之旅 | View的工作原理

1、基础认识

1.1 ViewRoot 和 DecorView

  • ViewRoot对应ViewRootImpl,是连接WindowManager 和 DecorView的纽带,View的三大流程都是通过ViewRoot完成
  • 当Activity对象创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl,并且将ViewRootImpl(ViewRoot的实现类)和DecorView建立关联
  • View的绘制流程从ViewRoot的performTraversals开始的,经过measure、layout、draw三个过程将View绘制出来
  • performTraversals会依次调用 performMeasure、performLayout、performDraw完成顶层View的绘制;其中performMeasure中会调用measure方法,measure又会调用onMeasure方法,在onMeasure方法中会对所有的子元素进行measure过程;子元素再次重复,就完成了整个View树的遍历。其他两个的流程也是类似的,具体如下图
    在这里插入图片描述

1.2 MeasureSpec类

  • MeasureSpec 是一个32位的int值,高2位表示代表SpecMode测量模式,低30位代码SpecSize规格大小。
  • SpecMode有三类:

UNSPECIFIED:父容器不对View有限制,一般用于系统内部
EXACTLY:父容器已经检测出View所需要的精确大小,View最终大小就是SpecSize的值,对应LayoutParams的match_parent和具体数字这两种模式
AT_MOST:父容器指定了一个最大的值SpecSize,View的大小不能超过这个值,对应LayoutParams的wrap_content

  • 决定因素:子View的LayoutParams和父View的MeasureSpec共同决定,具体如下表
    (同时也是getChildMeasureSpec的逻辑)
    在这里插入图片描述
    可以看到 设置的match_parent 和 wrap_content是一个效果,所以自定义View需要重写onMeasure 以实现wrap_content效果
    实现代码见 下文3.1

2、View的工作流程

2.2 Mesure过程

2.2.1 View的mesure过程

View 的测量流程

  • measure会调用 onMeasure方法,传入父容器测量好的宽/高measureSpec
  • 如果不是系统内部的测量流程,会返回父容器测量好的specSize
  • 如果是系统内部的测量流程,则调用getSuggestedMinimumWidth()方法返回一个值
  • 如果View没有设置背景,则会返回 android:minWidth 设置的值;如果设置了背景,则会返回android:minWidth和背景的最小宽度两者中的最大值,也是View在UNSPECIFIED测量的宽/高

在这里插入图片描述

2.2.2 ViewGroup的mesure过程
  • ViewGroup不仅要完成自己的测量流程,还会遍历调用所有子View的measure方法。
  • ViewGroup是个抽象类,没有重写onMeasure方法,但是提供measureChildren方法
  • measureChildren方法具体操作逻辑是:取出子元素的LayoutParams,通过getChildMeasureSpec来创建子元素的MeasureSpec,getChildMeasureSpec的逻辑如上表

2.3 Layout过程

  • 当ViewGroup的位置被确定后,会在onLayout遍历所有的子元素并调用它的layout方法
  • 在layout方法中,它的onLayout方法又会被调用
  • onLayout的具体实现,根据不同的布局在各个实现类当中不同的实现
  • getWidth 和 getHeight 是获取最终的宽度;getMeasureWidth和getMeasureHeight 是获取测量的高度

以LinearLayout为例
在这里插入图片描述

2.4 Draw过程

  • 绘制背景 background.draw(cancva)
  • 绘制自己 onDraw
  • 绘制children dispatchDraw
  • 绘制装饰 onDrawScrollBars

2.5 四种方法获取View的宽高

Activity/View# onWindowFocusChange

此时View已经初始化完毕了,可以获得宽高;但是此方法会被调用多次,当获得/失去焦点时(即进行 onPause 和 onResume)

 @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        
        if (hasWindowFocus){
            int width = view.getWidth();
            int height = view.getHeight();
        }
        
    }
view.post(Runable runable)

通过post将一个runable投递到消息队列的尾部,等到Looper调用此runable时,View已经完成初始化

@Override
    protected void onStart() {
        super.onStart();
        view.post(new Runnable() {
            @Override
            public void run() {
                int width = view.getWidth();
                int height = view.getHeight();
            }
        });
    }
ViewTreeObserver

给View添加ViewTreeObserver;但是随着View树的状态改变等,onGlobalLayoutListener会被调用多次,所以获取后需要移除

    @Override
    protected void onStart() {
        super.onStart();
        
        ViewTreeObserver observer = view.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int width = view.getWidth();
                int height = view.getHeight();
            }
        });
        
    }
view.measure(int widthMeasureSpec,int heightMeasureSpec)

手动对View进行measure得到宽高,只有具体数值以及wrap_content模式下才可以获得

3、自定义View

四类:

  • 继承View重写onDraw,需要自己绘制;需要wrap_content 和 padding
  • 继承ViewGroup派生特殊的Layout,重新定义一种新的布局,需要合适的处理ViewGroup的测量、布局两个过程,并同时处理子元素的测量和布局过程
  • 继承特定的View(比如TextView),拓展已有的View的功能
  • 继承特定的ViewGroup(例如LinearLayout)

3.1 需要注意的点:

  • 让View支持wrap_content

实现代码:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
x
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);

        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(DEFAULT_WIDTH,DEFAULT_HEIGHT);
        }else if (widthMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(DEFAULT_WIDTH,heightSize);
        }else if (heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSize,DEFAULT_HEIGHT);
        }
    }
  • 支持padding (通过getPaddingTop()等方法,绘制时考虑上这个距离)
  • 尽量不要使用Handler,使用View内部的post方法
  • 及时关闭动画、线程(在onDetachedFromWindow回调方法)
  • 处理好滑动冲突(内部、外部解决法)

自定义属性

可以帮助我们在xml文件更好的初始化值

  1. 在values文件夹创建类似attrs.xml 文件
  2. 定义自定义属性,名字要和自定义View的名字一样,否则有些情况会报错
  3. 在View的构造方法解析相应的值
  4. 在xml使用

具体实现:

第一步,在attrs.xml定义
<resources>
    <declare-styleable name="MyView">
        <attr name="color" format="color"/>
        <attr name="name" format="string"/>
        <attr name="isShow" format="boolean"/>
    </declare-styleable>
</resources>

第二步,在构造函数获得值

public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView, defStyleAttr, 0);
        mColor= a.getResourceId(R.styleable.MyView_color,R.color.white);
        mName= a.getResourceId(R.styleable.MyView_name, "defaultName");
        mIsShow= a.getResourceId(R.styleable.MyView_isShow,false);
      
        a.recycle();// 记得回收
    }

第三步,在xml文件使用

    <com.hjl.commonlib.base.MyView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:name = "lihan">

    </com.hjl.commonlib.base.MyView>
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值