读Android源码之View分析

读Android源码之View分析

一、View的生命周期

View生命周期

View的生命周期方法有什么作用呢?

其实这些方法在我们自定义View的时候发挥着很大的作用,我们来举几种应用场景。

场景1:在Activity启动时获取View的宽高,但是在onCreateonStartonResume均无法获取正确的结果。这是因为在Activity的这些方法里,View的绘制可能还没有完成,我们可以在View的生命周期方法里获取。

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if(hasFocus){
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
    }
}

场景2:在Activity生命周期发生变化时,View也要做响应的处理,典型的有VideoView保存进度和恢复进度。

@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
    super.onVisibilityChanged(changedView, visibility);
    //TODO do something if activity lifecycle changed if necessary
    //Activity onResume()
    if(visibility == VISIBLE){

    }
    //Activity onPause()
    else {

    }
}

@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
    super.onWindowFocusChanged(hasWindowFocus);

    //TODO do something if activity lifecycle changed if necessary
    //Activity onResume()
    if (hasWindowFocus) {
    }
    //Activity onPause()
    else {
    }
}

场景3:释放线程、资源

@Override
protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    //TODO release resources, thread, animation
}

二、Android应用程序窗口的渲染流程

  • Android应用视图的渲染流程:首先测量流程用来确定视图的大小,然后布局流程用来确定视图的位置,最后绘制流程将视图绘制在应用窗口上。
  • Android应用程序窗口UI首先是使用Skia图形库API来绘制在一块画布上,实际地是绘制在这块画布里面的一个图形缓冲区中,这个图形缓冲区最终会被交给SurfaceFlinger服 务,而SurfaceFlinger服务再使用OpenGL图形库API来将这个图形缓冲区渲染到硬件帧缓冲区中。

2.1 View的测量流程

View是一个矩形区域,它有自己的位置、大小与边距。

  • View位置:有左上角坐标(getLeft()getTop())决定,该坐标是以它的父View的左上角为坐标原点,单位是pixels
  • View大小:View的大小有两对值来表示。getMeasuredWidth()/getMeasuredHeight()这组值表示了该View在它的父View里期望的大小值,在measure()方法完成后可获得。 getWidth()/getHeight()这组值表示了该View在屏幕上的实际大小,在draw()方法完成后可获得。
  • View内边距:View的内边距用padding来表示,它表示View的内容距离View边缘的距离。通过getPaddingXXX()方法获取。需要注意的是我们在自定义View的时候需要单独处理 padding,否则它不会生效,这一块的内容我们会在View自定义实践系列的文章中展开。
  • View内边距:View的外边距用margin来表示,它表示View的边缘离它相邻的View的距离。

在介绍测量流程之前,我们先来介绍下MeasureSpec,它用来把测量要求从父View传递给子View。我们知道View的大小最终由子View的LayoutParams与父View的测量要求公共决定,测量要求指的 就是这个MeasureSpec,它是一个32位int值。

  • 高2位:SpecMode 通过 MeasureSpec.getMode(measureSpec)方法获取测量模式
  • 低30位:SpecSize 通过 MeasureSpec.getSize(measureSpec) 方法获取特定测量模式下的大小

普通View的MeasureSpec的创建规则。

  • 当View采用固定宽高的时候,不管父容器的MeasureSpec是什么,resultSize都是指定的宽高,resultMode都是MeasureSpec.EXACTLY
  • 当View的宽高是match_parent,当父容器是MeasureSpec.EXACTLY,则View也是MeasureSpec.EXACTLY,并且其大小就是父容器的剩余空间。当父容器是MeasureSpec.AT_MOST则View也是MeasureSpec.AT_MOST,并且大小不会超过父容器的剩余空间。
  • 当View的宽高是wrap_content时,不管父容器的模式是MeasureSpec.EXACTLY还是MeasureSpec.AT_MOST,View的模式总是MeasureSpec.AT_MOST,并且大小都不会超过父类的剩余空间。

了解了MeasureSpec的概念之后,我就就可以开始分析测量流程了。

  • 对于顶级View(DecorView)其MeasureSpec由窗口的尺寸和自身的LayoutParams共同确定的。
  • 对于普通View其MeasureSpec由父容器的Measure和自身的LayoutParams共同确定的。

在做测量的时候,measure()方法被父View调用,在measure()中做一些准备和优化工作后,调用onMeasure()来进行实际的自我测量。对于onMeasure(),View和ViewGroup有所区别:

  • View:View 在 onMeasure()中会计算出自己的尺寸然后保存;
  • ViewGroup:ViewGroup在onMeasure()中会调用所有子View的measure()让它们进行自我测量,并根据子View计算出的期望尺寸来计算出它们的实际尺寸和位置然后保存。同时,它也会 根据子View的尺寸和位置来计算出自己的尺寸然后保存。

View的onMeasure()方法实现比较简单,它调用setMeasuredDimension()方法来设置View的测量大小,测量的大小通过getDefaultSize()方法来获取。

注:你可以自己尝试一下自定义一个View,然后不重写onMeasure()方法,你会发现只有设置match_parentwrap_content效果是一样的,事实上TextViewImageView等系统组件都在wrap_content上有自己的处理,可以去翻一翻源码。

ViewGroup继承于View,是一个抽象类,它并没有重写onMeasure()方法,因为不同布局类型的测量 流程各不相同,因此onMeasure()方法由它的子类来实现,例如FrameLayout

以上便是Measure的整个流程,该流程完成以后,我们可以通过getMeasuredWidth()getMeasuredHeight()来获得View的宽高。但是在某些情况下,系统需要经过多次Measure才能确定 最终的宽高,因此在onMeasure()方法中拿到的宽高很可能是不正确的,比较好的做法是在onLayout()方法中获取View的宽高。

2.2 View的布局流程

在进行布局的时候,layout()方法被父View调用,在layout()中它会保存父View传进来的自己的位置和尺寸,并且调用onLayout()来进行实际的内部布局。对于onLayout(),View和ViewGroup有所区别:

  • View:由于没有子 View,所以 View 的 onLayout() 什么也不做。
  • ViewGroup:ViewGroup在onLayout()中会调用自己的所有子View的layout()方法,把它们的尺寸和位置传给它们,让它们完成自我的内部布局。

layout()方法用来确定View本身的位置,onLayout()方法用来确定子元素的位置。
onLayout()的实现依赖于具体的布局,所以View/ViewGroup并没有实现这个方法,可由子类实现或者不实现,例如TextView/FrameLayout,一些继承自View的控件并不实现onLayout(),只需要绘制(draw)自己就可以。

2.3 View的绘制流程

绘制从ViewRoot.draw()开始,它首先会创建一块画布,接着再在画布上绘制Android上的UI,再把画布的内容交给SurfaceFlinger服务来渲染。

三、View事件分发机制

在介绍View的事件分发机制之前,我们要先了解两个概念。

  • MotionEvent:Android中用来表示各种事件的对象,例如ACTION_DOWNACTION_MOVE等,我们还可以通过它获取事件发生的坐标,getX/getY获取相对于当前View左上角的坐标,getRawX/getRawY获取相对于屏幕左上角的坐标。
  • TouchSlop:系统所能识别的最小滑动距离,通过ViewConfiguration.get(context).getScaledTouchSlop()方法获取。

现在我们再来看看View里的事件分发机制,概括来说,可以用下面代码表示:

public boolean dispatchTouchEvent(MotionEvent event){
    boolean consume = false;
    //父View决定是否拦截事件
    if(onInterceptTouchEvent(event)){
        //父View调用onTouchEvent(event)消费事件
        consume = onTouchEvent(event);
    }else{
        //调用子View的dispatchTouchEvent(event)方法继续分发事件
        consume = child.dispatchTouchEvent(event);
    }
    return consume;
}

我们再来具体看看各个场景中的事件分发。

3.1 Activity的事件分发

当点击事件发生时,事件最先传递给Activity,Activity会首先将事件将诶所属的Window进行处理,即调用superDispatchTouchEvent()方法。

public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks2,
        Window.OnWindowDismissedCallback, WindowControllerCallback {

        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                onUserInteraction();
            }
            if (getWindow().superDispatchTouchEvent(ev)) {
                return true;
            }
            return onTouchEvent(ev);
        }
}

通过观察superDispatchTouchEvent()方法的调用链,我们可以发现事件的传递顺序:

  • PhoneWinodw.superDispatchTouchEvent()
  • DecorView.dispatchTouchEvent(event)
  • ViewGroup.dispatchTouchEvent(event)

事件一层层传递到了ViewGroup里,关于ViewGroup对事件的处理,我们下面会说,如果superDispatchTouchEvent()方法返回false,即没有 处理该事件,则会继续调用Activity的onTouchEvent(ev)方法来处理该事件。可见Activity的onTouchEvent(ev)在事件处理的优先级是最低的。

3.2 ViewGroup的事件分发

ViewGroup作为View容器,它需要考虑自己的子View是否处理了该事件,具体说来:

  • 如果ViewGroup拦截了事件,即它的onInterceptTouchEvent()返回true,则该事件由ViewGroup处理,如果ViewGroup调用了setOnTouchListener()则该接口的onTouch()方法会被调用 否则会调用onTouchEvent()方法。
  • 如果ViewGroup没有拦截事件,则该事件会传递给它的子View,子View的dispatchTouchEvent()会被调用,View.dispatchTouchEvent()的处理流程前面我们已经分析过。

3.3 View的事件分发

View没有子元素,无法向下传递事件,它只能自己处理事件,所以View的事件传递比较简单。

如果外界设置了OnTouchListenerOnTouchListener.onTouch(this, event)返回true,则表示该方法消费了该事件,则onTouchEvent(event)不再被调用。 可见OnTouchListener的优先级高于onTouchEvent(event),这样是为了便于外界处理事件。
关于onTouchEvent(MotionEvent event),有两点需要说明一下:

  1. View的disable属性不会影响onTouchEvent()方法的返回值,哪怕View是disable的,只要 View的clickable或者longClickable为true,onTouchEvent()方法还是会返回true。
  2. 只要clickable或者longClickable为true,onTouchEvent()方法就会消费这个事件
  3. 如果View设置了OnClickListener,则performClick()会调用它的onClick方法。

上面我们提到了viewFlags里的CLICKABLELONG_CLICKABLE,也就是xml或者代码里可以设置的clickablelongClickable,View的LONG_CLICKABLE默认为 true,CLICKABLE默认为false,值得一提的是setOnClickListener()方法和setOnLongClickListener()会将这两个值设置为true。

通过对源码的分析,我们已经掌握了各种场景下事件分发的规律,我们再来总结一下View事件分发的相关结论。

  • 事件的传递是按照Activity -> Window -> View的顺序进行的
  • 一般情况下,一个事件序列只能由一个View拦截并消耗,一旦一个View拦截了该事件,则该事件序列的后续事件都会交由该View来处理。
  • ViewGroup默认不拦截任何事件
  • View没有onInterceptTouchEvent()方法,一但有点击事件传递给它,它的ouTouchEvent()方法就会被调用。

原文地址

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值