View 的事件体系及工作原理

一 、View 的事件体系

知识储备

关于 View ,虽然不是四大组件之一,但是在Android 中 View 有着其不可或缺的地位,其为 Android 提供了丰富的控件,View 是所有视图对象的父类,例如 TextView、Button 等控件均是继承自 View 。

View 在 Android 源码中的文件位置:

platform_frameworks_base/core/java/android/view/

View 实现了 Drawable.callback (动画相关) 、KeyEvent.callback (按键相关)、AccessibilityEventSource (交互相关) 的接口,所有 View 可以处理动画、按键、交互相关的事件,在事件分发的过程中,View 有一套成熟的事件分发机制,可以用于解决事件冲突。

View 的子类不仅是控件,例如 LinearLayout、RelativeLayout 等的存在,Linearlayout 继承自 ViewGroup,顾名思义,可以理解为一组 View ,ViewGroup 是一个 abstract 类,继承自 View ,实现了 ViewParent (用户于父视图交互) 和 ViewManager (用于添加、删除、更新子视图到 Activity ) 的接口。

从 View 和 ViewGroup 的关系也可以看出,View 本身也可以是单个控件,也可以是是一个可以由多个控件组成的一组控件,所以在 ViewGroup 中是可以有子 View 的,子 View 同样也是可以拥有 ViewGroup 。

View 的位置参数

在 Android 中,坐标系的原点在左上角,x 轴的方向为从原点向右延伸, y 轴的方向为向下延伸,而 View 的位置一般由四个顶点来决定,分别对应四个属性:top、left、right、bottom ,四个属性的值为相对 View 父容器的位置,是一个相对坐标,top 对应 View 的左上角在父容器坐标系中的 y 坐标,left 为 x 坐标,right、bottom 为 View 右下角在父容器中对应的 x 和 y 坐标。在开发中经常会使用到的 width 和 height 两个属性的计算方式为

1
2
width = right -left;
height = bottom - top;

在 Android 的源码中,可以找到 mTop、mLeft、mRight、mBotton 四个由 protected 修饰的变量以及各自对应的 get 方法,同时还有 translationX 和 translatonY 两个参数,表示的为子 View 左上角对应父容器的偏移量,在当子 View 发生位移的时候,translationX 和 translationY 两个参数的值会发生改变,而 mTop 等的值并不会发生改变,表示的依旧是左上角的位置信息,存在如下的换算关系:

1
2
x = left + translationX;
y = top + translationY;

MotionEvent 和 TouchSlop

MotionEvent 为手指触摸屏幕所触发的一系列事件,主要为以下三种:

  • ACTION_DOWN 手指触摸到屏幕
  • ACTION_MOVE 手指在屏幕上滑动
  • ACTION_UP 手指离开屏幕的瞬间

通过 MotionEvent 对象提供的方法,可以得到点击事件的 x 和 y 坐标。

getX() \ getY() 获取被点击 View 的 x 坐标和 y 坐标

getRawX() \ getRawY() 获取相对于手机屏幕的 x 和 y 坐标

如何在 View 或 Activity 中对 MotionEvent 事件进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//在 View 或 Activity 中拦截 touch events,重写 onTouchEvent
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = MotionEventCompat.getActionMasked(event);
switch (action) {
case (MotionEvent.ACTION_DOWN):
//可以获取坐标进行业务处理等操作……
Log.d(TAG,"action down");
return true;
case (MotionEvent.ACTION_MOVE):
...
Log.d(TAG,"action move");
return true;
case (MotionEvent.ACTION_UP):
...
Log.d(TAG,"action up");
return true;
case (MotionEvent.ACTION_CANCEL):
...
Log.d(TAG,"action cancel");
return true;
case (MotionEvent.ACTION_OUTSIDE):
...
Log.d(TAG,"action outside");
return true;
default:
return super.onTouchEvent(event);
}
}
//View 中使用 setOnTouchListener() 监听touch events
View myView = findViewById(R.id.my_view);
myView.setOnTouchListener(new OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
// 事件处理操作
return true;
}
});

Touchslop 为系统所识别出的滑动的最小距离,为常量,不同设备中的值会不同。

1
2
//获取方式:
ViewConfiguration.get(getContext()).getScaledTouchSlop();

VelocityTracker、GestureDetector 和 Scroller

VelocityTracker ,速度追踪者,Velocity 在物理学中代表着速度的矢量,Speed 代表速率,在这里,Velocity 也是一个矢量,追踪手指在滑动中的速度,包括 X 方向和 Y 方向的速度。

具体使用方法参考 VelocityTracker 官方文档

GestureDetector , 手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为,实例化 GestureDetectorCompat 对象,实现 OnGestureListener 接口,根据需要可以实现 OnDoubleTapListener 从而能监听双击行为,可以监听所有的手势,如果只需要监听部分手势可以继承 GestureDetector.SimpleOnGestureListener 类。

Scroller 弹性滑动对象,用于实现 View 的弹性滑动,在 View 滑动的过程中,如果没有中间过渡,那么用户会感觉很突兀,所以滑动是开发必备知识之一,另外滑动也是实现华丽的自定义动画和自定义 View 的基础。在 Android 中通过三种方式可以实现 View 的滑动:

  • 通过 View 本身提供的 scrollTo 和 scrollBy 方法实现
  • 通过动画给 View 添加平移效果来实现滑动
  • 通过改变 View 的 LayoutParams 让 View 重新布局从而实现滑动。

View 的滑动

滑动方式

  1. 通过 View 本身提供的 scrollTo 和scrollBy 方法。

    在 View 中为了满足组件的滑动需求,提供了上述两种方法,调用 View 中 scrollTo 和 scrollBy 方法是将 View 的内容移动,而非移动 View 本身的位置,两个方法的具体实现为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    /**
    * Set the scrolled position of your view. This will cause a call to
    * {@link #onScrollChanged(int, int, int, int)} and the view will be
    * invalidated.
    * @param x the x position to scroll to
    * @param y the y position to scroll to
    */
    public void scrollTo(int x, int y) {
    if (mScrollX != x || mScrollY != y) {
    int oldX = mScrollX;
    int oldY = mScrollY;
    mScrollX = x;
    mScrollY = y;
    invalidateParentCaches();
    onScrollChanged(mScrollX, mScrollY, oldX, oldY);
    if (!awakenScrollBars()) {
    postInvalidateOnAnimation();
    }
    }
    }
    /**
    * Move the scrolled position of your view. This will cause a call to
    * {@link #onScrollChanged(int, int, int, int)} and the view will be
    * invalidated.
    * @param x the amount of pixels to scroll by horizontally
    * @param y the amount of pixels to scroll by vertically
    */
    public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
    }

    从注释中也可以看到用于设置视图的滚动刷新,可以参考 ListView 的实现理解,滑动和更新的是 View 内容的位置,而 View 本身是不会移动。

    如果想要移动 View 本身的位置,可以采用 offsetLeftAndRight 方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /**
    * Offset this view's horizontal location by the specified amount of pixels.
    *
    * @param offset the number of pixels to offset the view by
    */
    public void offsetLeftAndRight(int offset) {
    //具体实现
    }
    /**

  2. 使用动画

    通过动画可以使一个 View 实现平移、旋转等操作,通过动画来移动 View 主要是操作 translationX 和 translationY 属性,动画方案可以采用 View 动画,也可以采用属性动画( Android 3.0 以上)。

    属性动画下降一个 View 在 100ms 中从原始位置向右移动 100 个像素:

    1
    ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();

    通过补间动画也可以实现类似操作,不做详解。

    需要注意的是在使用动画进行操作时,动画只是针对 View 的影像进行操作,是否保留动画后的状态,需要设置 fillafter 属性为 true;由于没有改变 View 本身的属性,所以在交互上需要做对应的处理,在使用属性动画进行类似操作时,由于直接对 translationX/Y 进行操作,不会存在以上问题,但是在 Android 3.0 以下使用属性动画,需要动画兼容库的支持。

  3. 改变布局参数

    相比前两种方式,改变布局参数就简单一些,通过改变 LayoutParams ,如果通过改变参数来实现 View 向右平移 100 个像素的话,那么只需要修改 LayoutParams 中的 marginLeft 参数即可,使用中可以灵活改变布局的参数来实现自己想要的效果。

滑动方式小结

  • scrollTo/scrollBy : 操作简单、适合对 View 内容的滑动
  • 动画:操作简单,主要适用于没有交互的 View 和实现复杂的动画效果
  • 改变布局参数: 操作稍微复杂,适用于有交互的 View

弹性滑动

由上述滑动方式,可以实现 View 的滑动效果,但是对于用户来说直来直去的滑动未免太过生硬,这时需要对滑动进行处理,实现所谓的“弹性滑动”。

  1. 使用 Scroller

    Scroller 本身并不能滑动,配合 View 的 computeScroll 方法实现弹性滑动的效果,通过不断的让 View 重绘,并且每次重绘都留有一定的时间间隔,在这个时间间隔内 Scroller 可以得出 View 当前的滑动位置,通过 scrollTo 方法来完成 View 的滑动,由此,每次的 View 重绘都会伴随着小幅度的滑动,由少到多,就组成了 View 的弹性滑动,完成了整个滑动的流程。

    具体 Scroller 的内部实现可以参考http://www.jianshu.com/p/2f90ae05e300

  2. 使用动画

    动画本身就是用来处理交互和组件运动的过程,所以可以通过动画的方案来实现弹性滑动,动画本身很强大,可以实现很多想要的交互风格和效果。

  3. 使用延时策略

    所谓延时策略,其实就是通过发送一系列的延时消息来达到一种渐进的效果,实现方案有 Handler、View 的 postDelayed 方法,或者线程的 sleep 方法。通过接连不断的发送延时消息,从而实现弹性滑动的效果,在 sleep 方案中,可以通过 while 循环来不断的滑动 View 和 Sleep ,从而实现弹性滑动的效果。

View 的事件分发机制

事件传递方法

  • public boolean dispatchTouchEvent(MotionEvent event)

    事件分发,将事件传递给子 View (返回值为 false)或者自己处理(返回值为 true)

  • public boolean onInterceptTouchEvent(MotionEvent event)

    是否拦截事件,存在于 ViewGroup ,View 中没有该方法

  • public boolean onTouchEvent(MotionEvent event)

    处理事件,在 dispatchTouchEvent 方法中调用

事件分发机制主要用到的便是以上三个方法,首先在一个点击事件产生后,传递流程为 Activity =》Window =》View,当 View 接收到事件后,会按照事件分发机制去分发。

在事件的分发过程中,如果仅有一层 View ,那么也没有必要分发,当前 View 决定是否处理即可。但对于一个根 ViewGroup 来说,它存在着许多子 View ,而子 View 中又可能嵌套着更多的子 View ,所以当事件发生时,根 ViewGroup 的 dispatchTouchEvent 将调用,如果该 ViewGroup 的 onInterceptEvent 返回值为 true ,那么拦截当前事件,事件会交给该 ViewGroup 处理,返回值为 false ,则将事件传递给子 View 的 dispatchTouchEvent 事件,重复上述过程,直到事件被处理。

事件处理所调用的方法便是 onTouchEvent ,但若当前 View 设置了 onTouchListener ,那么其优先级要高于 onTouchEvent ,需要看 onTouch 的返回值,如果返回 false,则 onTouchEvent 将会被调用,如果返回值为 true , 则 onTouch 方法不会被调用,如果当前 View 设置了 onClickListener ,由于 onClickListener 在 onTouchEvent 中调用,则 onClickListener 方法在事件分发中处于一个最低的优先级。

在 dispatchTouchEvent 方法的实现中,可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}

在 onTouchListener != null 时,dispatchTouchEvent 将直接返回 true ,从而 onTouchEvent 将不会执行。在 onTouchEvent 的源码中可以看到对点击的各种操作进行了处理,根据按下的不同时长提供了不同的操作,在这里有个小问题,在事件处理中,是否长按和点击不能同时存在呢? 答案是否定的,满足同时存在的条件是在 onLongClickListener 中将返回值设置为 false ,返回 true 则 onClickListener 不会执行。

小结

  • 在事件分发的流程中,返回值是几乎每个方法都有的存在,其中返回值为 true 则代表在这个位置事件进行了处理,不需要再向下或者向上传递,返回值为 false ,则说明事件处理的不够好或者未做处理,传递给其他“有能力”处理的组件。

  • ViewGroup 默认不拦截事件,在 ViewGroup 的 onInterceptTouchEvent 方法中默认返回值为 false。

  • View 没有 onIterceptTouchEvent 方法,一旦有事件传递过来,那么其 onTouchEvent 将会被调用

    onTouchEvent 默认返回值为 true , 即默认是处理事件

  • 事件传递的过程是由外向内的,即事件先传递给父元素,而后由父元素分发给子 View 。这里区别父元素和外部布局的区别,最开始学习中容易将概念混淆。

  • 时间原因暂未更深入探究源码实现,具体细节可以参考鸿洋大神文章 Android 事件分发机制源码解析

View 的滑动冲突

滑动冲突发生场景

  1. 外部滑动方向和内部滑动方向不一致

    通常出现在类似 ViewPager 与 Fragment 结合的情况下,横向滑动切换页面,而纵向滑动处理 Fragment 的内容,不过在使用的过程中 ViewPager 并没有出现和 ListView 之类组件的滑动冲突,因为在 ViewPager 中处理了这种冲突。但例如 Scroller 和 ListView 等的组合,就必须手动处理滑动冲突了,否则会造成仅有一层可以滑动,另一层无法滑动的情况出现。除此之外,还会有内部左右滑动,外部上下滑动的情况,但都为外部和内部滑动方向不一致的问题。

  2. 外部滑动方向和内部滑动方向一致

    内外两层滑动方向一致,两个滑动组件嵌套的时候回经常遇到类似的情况,系统无法直接判断是滑动的哪一个 ,从而导致只有一层可以正常滑动。

  3. 场景 1 和 2 的组合

    当在应用中存在内层有一个 1 中的冲突,外部有一个 2 的冲突,就会出现两种冲突的结合,但是个人并未经常遇到,其处理办法和上述 1 、 2 类似,分别处理内层和中层,中层和外层的滑动冲突即可。

滑动处理方案

针对上述 1 中的情况,可以采取当用户左右滑动时,让外部控件拦截滑动事件,在用户上下滑动时,让内部控件拦截滑动事件。

针对 2 中的情况,可以结合业务逻辑来处理滑动冲突,在不同的情况和状态下响应不同的滑动操作。

根据上述的解决方案,处理滑动冲突的大致方式为外部拦截内部拦截 两种办法,具体的实现不做解析,可以参考 《 Android 开发艺术探索 》 P157 解决方式。

事件体系小结

View 的事件体系主要体现 View 的事件的传递和处理机制,以及滑动相关的内容,View 作为与用户交互较多的前台组件,需要对它的整个事件体现掌握才能让程序和交互变得高效。

View 的事件分发逻辑为一个 V 型的事件传递机制,类似于公司的领导、中层和底层开发,在《Android 群英传》中医生给出了很形象的比喻,在此表示感谢。

View 的滑动冲突解决方案主体思想其实就是事件的拦截,苦于实践经历中暂未有太多需要解决滑动冲突的地方,故暂不锁详细探讨。

二、 View 的工作原理

ViewRoot、DecorView 及 MeasureSpec

View 在 Android 的地位不再强调了,在开发中,经常会遇到一些脑洞惊奇的产品或者设计搞出一些特别的显示效果,使用 Android 提供的开发组件不能满足需求,那么就需要自己去实现一些自定义 View ,通过自定义 View 可以实现许多五花八门的效果。为了实现这一目标,需要掌握 View 的底层工作原理,View 工作原理中核心便是测量、布局、绘制三大流程,在进入到核心过程之前,先对下面几个概念做初步的认识:

  • ViewRoot 和 DrecorView

    ViewRoot 是 View 绘制流程的开始,ViewRoot 对应源码文件中的 ViewRootImpl 类,在 WindowsManagerGlobal 文件的 addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) 方法中,创建了 ViewRootImpl 对象,将 ViewRootImpl 和 DecorView 相关联。

    1
    2
    3
    4
    root = new ViewRootImpl(view.getContext(), display);
    ...
    // view 是 PhoneWindow 的 DecorView
    root.setView(view, wparams, panelParentView);

    DecorView 作为顶级 View ,一般情况下内部会包含一个竖直方向的 LinearLayout ,根据 Android 不同版本的实现方案,该 LinearLayout 分为上下不同的部分,上部为标题栏,下部为内容区域。在 Activity 中所加载的 View 为内容区域,内容区域的 id 为 content ,通过 setContentView 方法名也可以看出加载的内容。

    View 的事件都要先经过 DecorView ,而后在传递给其他 View。

  • MeasureSpec

    MeasureSpec 顾名思义,是一个测量说明书之类的东西,MesasureSpec 是一个 32 位 int 值,高两位代表 SpecMode(测量模式),低 30 位代表 SpecSize(测量规格) ,MeasureSpec 在很大程度上决定着一个 View 的尺寸规格,在此过程中父容器会影响 View 的 MeasureSpec 创建过程。在测量过程中,系统会将 View 的 LayoutParams 根据父容器所施加的规则转换成对应的 MesaureSpec ,然后再根据 MeasureSpec 来测量出 View 的宽/高,此时的宽、高是测量宽高,不一定是最后 View 具体的宽高。

    MeasureSpec 将 SpecMode 和 SpecSize 打包成了一个 int 值来避免过多的对象内存分配,同时提供了打包和解包的方法。

    SpecMode 分为三类

    1. UNSPECIFIED 父容器不对 View 有任何限制,要多大给多大,一般用于系统内部。
    2. EXACTLY 父容器已经检测出 View 所需要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值。对应 LayoutParams 中的 match_parent 和具体数值这两种模式。
    3. 父容器指定了一个可用大小的 SpecSize, View 的大小不能大于这个值,具体是什么要看不同 View 的具体实现,对应 LayoutParams 中的 wrap_content。

    普通 View 的 MesaureSpec 创建规则

    getChildMeasureSpec 方法表格总结:

    table

    UNSPECIFIED 多用于系统内部多次 measure 的情形,暂时不关注。

    只要提供父容器的 MeasureSpec 和子元素的 LayoutParams ,就可以快速确定出子元素的 LayoutParams,有了 MeasureSpec 可以进一步确定出 子元素测量后的大小。

View 的绘制流程

View 的工作流程主要为 mesasure、layout、draw 三个过程,即测量、布局和绘制。Measure 过程主要是确定 View 测量的宽和高,layout 确定布局的位置,即 View 最终的大小和四个顶点的位置,draw 过程主要是将 View 绘制到屏幕上。

在开发中其实一般的需求基本可以通过原生的控件解决问题,而在某些情况下,例如设计师脑洞大开,思绪在异次元遨游的时候,那边需要自定义 View 了,往往要自己实现测量、布局和绘制的过程,这一切的基础,要清楚 View 的整个绘制流程。

在此之前,先来了解一波 Android UI 管理系统的层级关系,如下图所示:

ui_view

其中 PhoneView 是 Android 系统中最基本的窗口系统,每个 Activity 会创建一个 PhoneView 来作为 Activity 和 View 系统交互的接口,DecorView 本质上是一个 FrameLayout ,是 Activity 中所有 View 的父类,上面已经有提到。

当应用启动时候,最终会启动一个主 Activity ,应用启动流程可以参考文章 Android 系统及应用启动流程 后半部分,Android 会根据 Anctiviy 的布局来进行绘制,绘制会从根视图 ViewRoot 的 performTraversals() 方法开始,从上到下遍历整个视图树,每个 View 空间负责绘制自己,而 ViewGroup 还需要负责通知自己的子 View 去进行绘制操作,视图绘制过程可以分为三个步骤,就是 Measure、layout、draw .

对于 performTraversals() 逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
private void performTavelsals () {
...
int childWidthMesaurSpec = getRootMeasureSpec(mWidth,lp.width);
int childHeightMesaurSpec = get RootMeasureSpec(mHeight,lp.height);
...
// 测量流程
preformMeasure(childWidthMesaurSpec,childHeightMesaurSpec);
// 布局过程
preformLayout(lp,childWidthMesaurSpec,childHeightMesaurSpec)
// 绘制过程
preformDarw();
  • Measure 过程

    在测量的过程中,对于一个 View 而言,那么通过 measure 就完成了测量过程,如果对于 ViewGroup 而言,那么除了本身的测量过程之外,还将遍历调用所有子元素的 measure 方法,子元素递归执行这个流程。

    View 的测量

    View 的 measure 是一个由 final 修饰的不能重写的方法,会调用onMeasure 方法,onMeasure 两个形参为 widthMeasureSpec 和 heightMeasureSpec ,和 measure 方法形参一致,onMeasure 方法会调用 setMeasureDimension 方法设置 View 的测量值,可以根据需要选择是否重写 onMeasure 方法,如果没有重写的话测量值是通过 getDefaultSize 方法获取到 measureSpec 的 specSize,即 View 测量后的大小。View 测量后的大小是测量过程的值,而 layout 阶段最终才会确定 View 的大小,在绝大多数的情况下, View 的测量大小和最终大小是相等的。

    ViewGroup 的测量

    View 会将具体的测量操作分派给 ViewGroup,而ViewGroup 会在它的 measureChild 方法中传递给子 View 。ViewGroup 会通过遍历自身的所有的子 View 并逐个调用子 View 的 measure 方法,即上述 View 的测量过程,进而完成测量。

  • Layout 过程

    Layout 过程用来确定 View 在父容器的布局位置,它是由父容器获取子 View 的位置参数后,调用子 View 的 layout 方法并将位置参数传入实现的,ViewRootImpl 的 performLayout 方法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // ViewRootImpl
    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,int desiredWindowHeight) {
    ...
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ...
    }
    // View
    public void layout(int l,int t,int r,int b) {
    ...
    onLayout(changed,l,t,r,b);
    ...
    }
    // 方法体为空,如果子类是 ViewGroup 会重新该方法,实现 ViewGroup 对所有 View 控件的布局流程
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }
  • Draw 过程

    Draw 过程用来将控件绘制出来,绘制的流程从 performDraw 方法开始,核心逻辑如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // ViewRootImpl
    private void performDraw() {
    ...
    draw(fullRedrawNeeded);
    ...
    }
    // ViewRootImpl
    private void draw(boolean fullRedrawNeeded) {
    ...
    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
    return;
    }
    ...
    }
    // ViewRootImpl
    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
    boolean scalingRequired, Rect dirty) {
    ...
    mView.draw(canvas);
    ...
    }

    从上述过程中可以看到最终调用到每个 View 的 draw 方法绘制每个具体的 View ,绘制可以分为六步,流程如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public void draw(Canvas canvas) {
    // Step 1, 绘制 View 的背景
    ...
    if (!dirtyOpaque) {
    drawBackground(canvas);
    }
    ...
    // Step 2, 如果需要,保存 Canvas 图层(layer),为 fading 做准备
    // 常见情况会跳过 Step 2 & 5
    ...
    saveCount = canvas.getSaveCount();
    ...
    canvas.saveLayer(left, top, right, top + length, null, flags);
    ...
    // Step 3,绘制 View 的内容
    onDraw(canvas);
    // Step 4,绘制 View 的子 View
    dispatchDraw(canvas);
    // Step 5,如果需要的话,绘制 View 的 fading 边缘并恢复图层
    canvas.drawRect(left, top, right, top + length, p);
    ...
    canvas.restoreToCount(saveCount);
    ...
    // Setp 6,绘制 View 的装饰(如:scrollbars)
    onDrawScrollBar(canvas);
    }

    至此完成 View 的绘制流程。

自定义 View

写到这里其实已经拖了很久了,关于自定义 View ,看到了朱凯大佬的 Hencoder ,讲的很棒,总结的也很棒。链接:http://hencoder.com/ui-1-1/

参考资料

《 Andorid 开发艺术探索》

《 Android 群英传 》笔记

Android 事件分发机制

Android API 文档

题外话

记得几个月之前我接触 RxJava 的时候读的就是扔物线(朱凯)的《给 Android 开发者的 RxJava 详解》,在此基础上完成了第一个 MVP +RxJava+Retrofit 的 APP,该应用也就是我的毕业设计(传送门),后续零零星星的学习 Hencoder 里面的内容,“挣钱的事我自己负责,你就负责让你自己少花点钱、多学点有用的东西就好” 这句话很暖心,所以几天前在掘金上看到他的一本掘金小册就直接购买了,第一次为在网上分享的技术而付费,希望简简单单的一杯咖啡钱,能代表作为读者的感谢。

同时也很感谢我读过的博客和开源项目。

虽然进度都是零零星星,但我也会继续分享自己的所学所思、所见所闻,处于一个学习他人总结自己的阶段,希望能走的更远。

Keep Going.

本文 11月 05 日开始,12 月 07 日完成。

坚持原创技术分享,您的支持将鼓励我继续创作!
0%