# 读 Android 开发艺术探索 &6

关键词:View / 滑动 / 动画 / 手势 / 坐标 / measue / layout / draw

1. View 的事件体系 #

(1)关于 View 需要知道的几点

  • View 是 Android 中所有控件的基类,ViewGroup 也是继承了 View;
  • View 是一种界面层的控件的一种抽象,代表了一个控件;
  • View 的位置由四个顶点决定,它们的坐标都是相对于父容器来说,是一种相对坐标。top:左上角纵坐标,left:左上角横坐标;right:右下角横坐标;bottom:右下角纵坐标;
  • 几个参数:
    x = left + translationXy = top + translationY ,x,y 是 View 左上角的坐标(left/top 是它们的原始坐标),translationX,translationY 是左上角相对于父容器的偏移量默认为 0 ;
  • 通过 MotionEvent(ACTION_DOWN/MOVE/UP) 我们可以得到点击事件发生的 x 和 y 坐标,系统提供了两组方法:
    getX/getY 返回相对于当前 View 左上角的 x 和 y 坐标
    getRawX/getRawY 返回的是相对于手机屏幕左上角的 x 和 y 坐标;
  • TouchSlop 是系统能识别出来的被认为是滑动操作的最小距离,是滑动距离的临界值,未达到这个临界值,就被认为不是滑动,为了更好的用户体验;
  • VelocityTracker 追踪速度,一段时间内手指所滑过的像素数,速度 = (终点位置 - 起点位置)/ 时间段,手指逆着坐标划,速度值为负值;
  • GestureDetector 手势检测,用于辅助用户的单击、滑动、长按、双击等行为;
  • 如果只是监听滑动相关的,建议自己在 onTouchEvent 中实现,如果要监听双击这种行为,就使用 GuestDetector;
  • Scroller 弹性滑动对象,用于实现 View 的弹性滑动;有过渡效果的滑动,但是需要和 View 的 computeScroll 方法配合使用才能完成;

(2)关于 View 的滑动需要知道的几点:

  • 各种滑动效果 = 不同的滑动 + 特效;
  • 掌握滑动的方法是我实现自定义控件的基础;
  • 有三种方式可以实现 View 的滑动:
    1.通过 View 本身提供的 scrollTo/scrollBy 方法来实现滑动;
    2.通过动画给 View 施加平移效果来实现滑动,动画本身就是一种渐近的过程;
    3.通过改变 View 的 LayoutParams 使得 View 重新布局从而实现滑动;
  • 1.使用 scrollTo 和 scrollBy 来实现的滑动,只能将 View 的内容进行滑动,并不能将 View 本身进行移动;
  • 2.通过动画我们能够让一个 View 进行平移,而平移是一种滑动。使用动画来移动 View 主要是操作 View 的 translationX 和 translationY 属性,分为传统的 View 动画 Or 属性动画;View 动画是对 View 的影响做操作,并不能真正改变 View 的位置参数,包括宽/高,(在系统眼里,View 动画操作的 View 并没有发生任何改变,它的真身仍然在原始位置,新位置上只是 View 的影响而已,无法在新位置上触发事件),属性动画可以解决这个问题;
  • 3.通过改变布局参数即改变 LayoutParams 来实现 View 滑动,(改变本身的参数大小 Or 旁边放一个空 View 专门用来变化参数起到增减空间大小的作用);

(3)各种滑动的对比

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

(4)弹性滑动

  • 将一次大的滑动分成若干次小的滑动并在一个时间段内完成
  • 弹性滑动的具体实现方式有很多,比如通过 Scroller、Handler#postDelayed 以及 Thread#sleep 等

Scroller 的工作机制:
Scroller 本身并不能实现 View 的滑动,它需要配合 View 的 computeScroll 方法才能完成弹性滑动的效果。它不断地让 View 重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔 Scroller 就可以得出 View 当前的滑动位置,知道了滑动位置就可以通过 scrollerTo 方法来完成 View 的滑动。就这样,每一次重绘都会导致 View 进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动。

还有一种实现弹性滑动的方法是 “延时策略”。核心思想是,通过发送一些列延时消息从而达到一种渐近式的效果,可以使用 Handler 或 View 的 postDelayed 方法,也可以使用线程的 sleep 方法;

(5)View 的事件分发机制

MotionEvent 即点击事件。点击事件的事件分发,就是对 MotionEvent 事件的分发过程,即当一个 MotionEvent 产生以后,系统需要把这个事件传递给一个具体的 View,而这个传递的过程就是分发过程。点击事件的分发过程由三个很重要的方法来共同完成:dispatchTouchEvent、onInterceptTouchEvent 和 onTouchEvent。

当一个点击事件产生的时候,它的传递过程遵循如下顺序:Activity → Window → View,即事件总是先传递给 Activity,Activity 再传递给 Window,最后 Window 再传递给顶级 View。顶级 View 接收到事件之后,就会按照事件分发机制去分发事件。

关于事件机制需要知道的几点:
1. 从手指按下到手指离开会产生一系列事件,这个事件序列以 down 事件开始,中间含有数量不定的 move 事件,最终以 up 事件结束;
2. 正常情况下,一个事件序列只能被一个 View 拦截并且消耗,因为一旦一个元素拦截了某此事件,那么同一个事件序列内的所有事件都会直接交给它处理掉,正常情况下,同一个事件序列中的事件不能分别由两个 View 同时处理。
3. 某个 View 一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件能够传递给它的话),并且它的 onInterceptTouchEvent 不会再被调用,即不会再去询问它是否要拦截了。
4. 某个 View 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件,onTouchEvent 返回了 false,那么同一事件序列中的其它事件都不会再交给它来处理,并且事件将重新交给它的父元素去处理,即父元素的 onTouchEvent 会被调用。(事件一旦交给一个 View 处理,那么它就必须要被消耗掉的);
5. 除 ACTION_DOWN 以外,如果 View 不消耗其它事件,那么这个点击事件会消失,此时父元素的 onTouchEvent 并不会被调用,并且当前 View 可以持续收到后续的事件,最终这些消失的点击事件会传递给 Activity 处理;
6. ViewGroup 默认不拦截任何事件,源码中它的 onInterceptTouchEvent 方法默认返回 false;
7. View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,它的 onTouchEvent 方法会被调用;
8. View 的 onTouchEvent 方法默认都会消耗事件,即返回 true,除非它是不可点击的;
9. View 的 enable 属性不影响 onTouchEvent 的默认返回值;
10. onClick 会发生的前提是当前的 View 可点击,并且收到了 down 和 up 的事件;
11. 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子 View,通过 requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件奋发过程,但是 ACTION_DOWN 事件除外;

  • decor view 一般是指当前界面的底层容器,即 setContentView 所设置的 View 的容器;
  • Window 的唯一实现是 PhoneWindow;

(6)关于滑动冲突
- 解决滑动冲突有固定的套路
- 有三个场景:外部滑动方向和内部滑动方向不一致 / 外部滑动方向和内部滑动方向一致 / 以上两种的嵌套;

外部拦截法:点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突问题。符合点击事件的分发机制,需要重写父容器的 onInterceptTouchEvent 方法,在内部做相应的拦截即可。
内部拦截法:父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则交给父容器进行处理,与事件分发机制不同,比外部拦截法较复杂。

2. View 的工作原理 #

View 是 Android 在视觉上的呈现,我们可以自定义 View,即自定义控件。为了更好的自定义控件,需要掌握 View 的底层工作原理:测量流程、布局流程以及绘制流程。View 的常见回调方法:构造方法、onAttach、onVisibilityChanged、onDetach 等。自定义 View 有几种固定的类型:直接继承自 View 和 ViewGroup,有的继承现有的系统控件

关于 View 的三大流程需要知道的几点:
- View 的三个过程:measure / layout / draw ;
- View 的三大流程均是通过 ViewRoot 来完成的,ViewRoot 是连接 WindowManager 和 DecorView 的纽带;
- ViewRootImpl 对象和 DecorView 建立关联;
- measure 用来测量 View 的宽和高;layout 用来确定 View 在父容器中的放置位置;draw 负责将 View 绘制在屏幕上,决定了 View 的显示;

关于 DecorView 需要知道的几点:
- DecorView 作为顶级 View,一般情况下,它的内部包括一个竖直方向的 LinearLayout(标题栏 + 内容栏)ViewGroup content = (ViewGroup)findViewById(android.R.id.content);
- DecorView 其实是一个 FrameLayout,View 层的事件都先经过 DecorView,然后才传递给我们的 View;

关于 MeasureSpec 需要知道的几点:
- 很大程度上决定了一个 View 的尺寸规格,当然,这个过程还受父容器的影响,因为父容器影响 View 的 MeasureSpec 的创建过程;
- MeasureSpec 代表一个 32 位的 int 值,高 2 位代表 SpecMode,低 30 位代表 SpecSize,SpecMode 指的是测量模式(有三种:unspecified / exactly / at_most),SpecSize 指的是在某种测量模式下的规格大小;
- EXACTLY 对应于 LayoutParams 中的 match_parent 和具体的数值这两种模式;
- AT_MOST 对应于 LayoutParams 中的 wrap_content;
- LayoutParams 需要和父容器一起才能决定 View 的 MeasureSpec;MeasureSpec 一旦确定之后,onMeasure 中就可以确定 View 的测量宽 / 高;
- 对于普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定

需要知道的几点:
- 直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content 就相当于在使用 match_parent。
- 对于 ViewGroup 来说,除了完成自己的 measure 过程之外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行这个过程;
- 与 View 不同的是,ViewGroup 是一个抽象类,因此没有重写 View 的 onMeasure 方法,但是提供了一个叫做 measureChildren 的方法;
- View 的 measure 过程与 Activity 的生命周期方法不是同步执行的,无法保证 Activity 执行了 onCreate、onStart、onResume 的时候某个 View 已经测量完毕了,如果 View 还没有测量完毕,那么获得的宽 / 高就是 0 ;

关于 layout 需要知道的几点:
- layout 的作用是 ViewGroup 用来确定子元素的位置,当 ViewGroup 的位置被确定之后,它在 onLayout 中会遍历所有的子元素并调用其 layout 方法,在 layout 方法中 onLayout 方法又会被调用(onLayout 方法的作用就是父容器确定子元素的位置)。

关于 draw 需要知道的几点:
- 作用是将 View 绘制到屏幕上面;
- View 的绘制过程遵循如下几步:
1.绘制背景 background.draw(canvas)
2.绘制自己 (onDraw)
3.绘制 children(dispatchDraw)
4.绘制装饰 (onDrawScrollBars)
- View 的绘制过程的传递是通过 dispatchDraw 来实现的,dispatchDraw 会遍历调用所有子元素的 draw 方法,如此 draw 事件就一层层地传递了下去;

End.

Note by HF.
Learn from 《Android 开发艺术探索》


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值