最近学习了 Android 开发艺术探索的三、四章,本来是想写一篇关于 VIew 的文章。翻了几遍书,又看一些相关文章,觉得题目太大,知识量也不够,不如老实把View相关两章写个学习笔记,除了列出一些结论性知识便于查阅之外,也尝试把一些概念性的东西用自己的方式叙述出来,算是检验与巩固一下所学。
View 基础知识
View 是所有控件的基类,其子类 ViewGroup 是控件组的基类。本节主要讲了 View 的基础知识、点击事件和几个常用对象。
View的位置参数
View 的位置主要由四个顶点决定,对应属性:top 、left、right、bottom,其坐标以相对于父容器位置为标准。对应View源码中mTop 、mLeft、mRight、mBottom 四个成员变量。获取方式类似于Left = getLeft()
。
View的宽高由位置参数得出:
width = right - left
height = bottom - top。
Android 3.0 开始,View新增表示位移后位置信息的 4 个变量:x、y、translationX、translationY。View提供了相应 get/set 方法。
与left、top的换算关系如下:
x = left + translationX
y = top + translationY
translationX、translationY 的默认值为 0。View在位移后top、left不会发生改变。
MotionEvent(点击事件)
事件类型:
ACTION_DOWN:手指刚接触到屏幕。
ACTION_MOVE:手指在屏幕上移动。
ACTION_UP:手指离开屏幕。
事件序列,指从点击屏幕到离开屏幕的一次操作期间,发生一系列不同类型的点击事件。常见例子如下:
点击后离开屏幕:事件序列为DOWN->UP。
点击后滑动一会再离开屏幕:事件序列为DOWN->MOVE->...->MOVE->UP。
MotionEvent对象可以获取点击事件发生的 x、y 坐标。
getX / getY 方法获取相对当前 View 左上角的 x、y 坐标。
getRawX / getRawY 方法获取相对手机屏幕左上角的 x、y 坐标。
TouchSlop(最小滑动距离)
系统能识别的最小滑动距离。
获取方法:ViewConfiguration.get(getContext()).getScaledTouchSlop()
。
VelocityTracker(速度追踪)
用于追踪手指滑动速度的对象。用法如下:
@Override public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
//建立对象,添加事件
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
//计算速度参数为时间段 单位为ms
//速度 = (起点位置 - 终点位置)/ 时间段
velocityTracker.computeCurrentVelocity(1000);
int xV = (int) velocityTracker.getXVelocity(); //单位为像素 下同
int yV = (int) velocityTracker.getYVelocity();
Log.e(TAG, "onTouchEvent: ------------->" + xV + " " + yV);
break;
case MotionEvent.ACTION_UP:
//重置、回收内存
velocityTracker.clear();
velocityTracker.recycle();
break;
default:
break;
}
return true;
}
GestureDetector(手势检测)
用于检测双击、长按等行为。
//该类实现onGestureListener
GestureDetector mGestureDetector=new GestureDetector(this);
//解决长按后无法拖动
mGestureDetector.setIsLongpressEnabled(false);
//设定双击监听
mGestureDetector.setOnDoubleTapListener(....);
------------------------------------
//在View的onTouchEvent中实现以下,以接管View的onTouchEvent方法
mGestureDetector.setIsLongpressEnabled(false);
boolean consume=mGestureDetector.onTouchEvent(event);
return consume;
实现以上步骤后,只需要在GestureListener 、DoubleTapListener中实现相应方法即可。相应表格在书的 p127 上。
View的滑动
3种滑动方式对比
- scrollTo/scrollBy :操作简单,适合对View内容滑动。
- 动画 :操作简单,交互上比较麻烦,属性动画则无此缺点。
- 改变参数 :操作较复杂。
scrollTo/scrollBy
mScrollX、mScrollY 两个参数表示 View 内容与 View 左、上边缘的距离,单位为像素,上、左方向为正值。
scrollTo(int x,int y)
实现基于传入参数绝对滑动(本质上是把mScrollX、mScrollY改变为传入参数)。
scrollBy(int x,int y)
实现基于当前位置的相对滑动(实际上也是调用了scrollTo,做了参数处理而已)。
该方法只能改变mScrollX、mScrollY ,即只能改变View内容的位置,改变不了View本身位置。且滑动是瞬时完成体验不佳。
动画
View动画存在问题是:View动画不能改变View的位置(参数),需要保留动画效果的话要设定动画的 fillAfter
属性为 true,且 View 在使用动画移动后,由于位置没变,交互上会出现问题。
Android 3.0后提供了属性动画,解决了上述问题。动画的执行类可以设置动画操作的对象的属性、持续时间,开始和结束的属性值,时间差值等,然后系统会根据设置的参数动态的变化对象的属性。
//使 Button 的 "translationX" 属性在 3000 ms 时间内从 0 增加到 300
ObjectAnimator.ofFloat(mButton, "translationX", 0, 300).setDuration(3000).start();
布局参数
直接改变View的布局参数,从而实现滑动或其他效果。
ViewGroup.MarginLayoutParams params =
(ViewGroup.MarginLayoutParams) mButton.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
mButton.setLayoutParams(params);
View的弹性滑动
弹性滑动其实就是渐进式滑动,可以得到较好用户体验。实现方式很多,如 Scroller、Handler 的postDelayed、Thread 的 sleep等,共同的思路是:将一次完整滑动分成多次小滑动,并在一定时间段内完成。
Scroller
Scroller是一个用于记录滑动行为起始位置、经历时间的对象,且可以根据时间计算相应的值,需配合View的computeScroll方法才能实现弹性滑动。
private void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();
int scrollY = getScrollY();
int deltaX = destX - scrollX;
int deltaY = destY - scrollY;
//使得Scroller记录滑动行为相关信息 并不是进行滑动
mScroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000);
//使View重绘,调用draw方法,其中调用了computeScroll方法
invalidate();
}
//View的draw方法中被调用 重写前是一个空实现
@Override public void computeScroll() {
//判断滑动是否结束 具体时间位置等信息由 Scroller # startScroll方法决定
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
//在滑动完成前会再次重绘 循环直到if条件不成立(滑动完毕)
postInvalidate();
}
}
动画
动画本身就是带渐进效果的。
ObjectAnimator.ofFloat(mButton, "translationX", 100, 300).setDuration(3000).start();
另外,也可以利用动画的特性,来实现一些动画不能实现的效果。
final int startX = 0;
final int deltaX = 100;
final ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animator.getAnimatedFraction();
mButton.scrollTo(startX + (int) (deltaX * fraction), 0);
}
});
animator.start();
上面这段代码动画并不作用于任何对象上,我们只是利用了其1000ms内完成动画的过程,在没一帧到来时根据动画时间比例去使用 scrollTo 方法,思路和使用 Scroller 类似。
延时策略
思路是通过发送延时消息达到效果。
View 或者 Handler 的 postDelayed 方法,发送延时消息,记录接收消息次数,在消息中根据次数比例滑动,且再次发送消息,如此循环实现弹性滑动。
sleep方法则可通过 while 循环不断滑动 View 和 sleep,从而实现弹性滑动效果。
View的事件分发机制
传递规则
点击事件的事件分发实际上就是对MotionEvent事件的分发过程,即一个 MotionEvent 产生后,系统把这个事件传递给一个具体 View 的过程。
该过程主要由 3 个方法共同完成:
- dispatchTouchEvent:事件只要能传给当前View,必定调用。返回值表示是否消耗事件,受另外两个方法影响。
- onInterceptTouchEvent: 上个方法内部调用,判断是否拦截事件。一旦拦截,同一事件序列中不会再次调用。
- onTouchEvent:第一个方法中调用,用于具体处理事件。返回值表示是否消耗该事件。若不消耗,同一事件序列中,当前View无法再次接收到事件。
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
3 个方法间的关系可以参照上面伪代码和图片。
需要注意,onTouchListener 的 onTouch 有可能屏蔽掉 onTouchEvent 方法。
11个结论
(1)同一事件序列指手指接触屏幕到离开屏幕期间产生系列事件。从 down 开始,中间有多个 move,以 up 结束。
(2)正常情况下,一个事件序列只能被一个View拦截且消耗。
(3)一个View拦截事件后,同一事件序列都会交由其处理。即同一事件序列中事件不能由两个 View 同时处理。但是,通过特殊手段可以实现。比如,在 onTouchEvent 中强行传递给其他 View。
(4)某个View一旦开始处理事件,如果不消耗 ACTION_DOWN 事件(onTouchEvent 返回了 false),那么同一事件序列其他事件不会再交给它处理,且将事件重新交由其父 View 处理(即调用父 View 的onTouchEvent)。
(5)如果View不消耗除 ACTION_DOWN 以外其他事件,那么这个点击事件会消失,View 可以接收后续事件。最终消失的事件交由 Activity 处理。
(6)ViewGroup 默认不拦截任何事件。
(7)View 没有 onInterceptTouchEvent 方法,一旦有事件传递给它,就会调用 onTouchEvent 方法。
(8)View 的onTouchEvent 方法默认消耗事件(返回 true)。除非它设定为不可点击(即clickable 和 longClickable 同时为 false)。longClickable 默认为 false。clickable 部分View(如 Button)为 true。
(9)View 的 enable 属性不影响 onTouchEvent 的默认返回值。
(10)onClick 会发生的前提上当前 View 是可点击的,且收到了 down 和 up 事件。
(11)事件传递过程是由外向内、由父到子的。但是,requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外。
比较值得注意4、5 条结论,可以理解为一个事件序列传递过来,且其 down 事件传递到某个 View 的onTouchEvent 方法中,且返回 false,事件(包括后续事件,即整个事件序列)会向上传递,依次调用父 View 的 onTouchEvent 方法,直到事件处理或者到达最顶层的 Activity 为止。
如果除了 down 事件的其他事件不被处理(也就是说 View 已经处理的 down 事件),View 可以继续接收后续事件,相当于这个事件序列依然是由该View处理,只是部分不处理的事件会直接交由 Activity 处理。
源码分析
这部分只能自己照着看了,要详细写可以写多一篇了,就只提出一些单独的知识点。
Activity对事件的分发过程:
Acitivity -> Window -> DecorView ->顶级View
Window:可以控制顶级 View 的外观和行为策略,唯一实现类位于 android.policy.PhoneWindow中。
DecorView:可以通过 getWindow().getDecorView
获取,继承于 FrameLayout,是一个 ViewGroup。
顶级 View:我们平常通过setContentView 方法设置的 View,可以通过 ((ViewGroup)getWindow().getDecorView.findViewById(android.R.id.content)).getChildAt(0)
获取。
ViewGroup 有一个 requestDisallowInterceptTouchEvent
方法值得注意,该方法用于设定 FLAG_DISALLOW_INTERCEPT
标记位,一旦设置该标记位,ViewGroup 将无法拦截除 down 之外(down 事件会重置标记位,即只对当前事件序列有效,下次事件序列到来时会重置)所有事件。一般用于子 View 中,解决滑动冲突的内部拦截法也需要这个方法才可以实现。
View的滑动冲突
外部拦截法
比较简单好复用的方法,重写滑动冲突外部容器的 onInterceptTouchEvent 方法即可。基本思路是,父容器需要事件就拦截,不需要就不拦截。
@Override public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//必须返回false 否则后续事件只能向外传递
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要当前点击事件的条件) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
//必须返回false 否则影响子View接收事件
// 且父容器的特性决定,一旦其开始拦截事件,后续事件都由其处理,即使此处返回false
intercepted = false;
break;
default:
break;
}
mLastXIntercept = x;
mLastXIntercept = y;
return intercepted;
}
内部拦截法
利用 ViewGroup 的 requestDisallowInterceptTouchEvent
方法,使得父容器不拦截任何事件,子容器接收事件后消耗,否则交由父容器处理。
父容器中重写 onInterceptTouchEvent :
@Override public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return false;
} else {
// 拦截除 down 之外所有事件,
// 但是由于子 View 会调用 requestDisallowInterceptTouchEvent 方法
// 实际上只有特定条件下才会拦截
return true;
}
}
子容器中重写 dispatchTouchEvent :
@Override public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) getX();
int y = (int) getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//设置父容器不拦截任何事件
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (需要交给父容器的条件) {
//设置父容器拦截事件
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}