Android读书笔记(3)—— View的事件体系
一、View的基础知识
1、View的位置参数
1.1、两种坐标系
Android坐标系:以屏幕左上角点作为坐标系原点。
View坐标系:以View的左上角点作为坐标系原点。
1.2、View的位置属性
View的位置主要由四个属性决定:top、left、right、bottom。从Android3.0开始,还增加了x、y、translationX、translationY。这几个参数都是相对于父容器坐标系而言。
width = right - left
height = bottom - top
x = left + translationX //left不会变
y = top + translationY //top不会变
x、y是View的左上角坐标
translationX、translationY是View的左上角相对于父容器的偏移量,默认值是0
2、MotionEvent
典型的事件类型
- ACTION_DOWN 手指刚接触屏幕
- ACTION_MOVE 手指在屏幕上移动
- ACTION_UP 手指从屏幕上松开
MotionEvent的getX()
和getY()
是相对于发生事件的View本身坐标系而言的,getRawX()
和getRawY()
是相对于Android坐标系而言的。
若在View处按下,View接收到了MotionEvent对象,移到View上方时,getY()返回负数,移到View下方时,getY()将返回的值大于getHeight(),getX()也是类似的。
3、TouchSlop
系统所能识别出的被认为是滑动的最小距离,这是一个常量,与设备有关,可通过以下方法获得
ViewConfiguration.get(getContext()).getScaledTouchSloup()
当我们处理滑动时,比如滑动距离小于这个值,我们就可以过滤这个事件(系统会默认过滤),从而有更好的用户体验。
4、VelocityTracker
速度追踪,用于追踪手指在滑动过程中的速度,包括水平放向速度和竖直方向速度。使用方法:
- 在View的onTouchEvent方法中追踪当前事件的速度
VelocityRracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
- 计算速度,获得水平速度和竖直速度
velocityTracker.computeCurrentVelocity(1000);//计算速度。获取速度之前,必须调用。
int xVelocity = (int)velocityTracker.getXVelocity();
int yVelocity = (int)velocityTracker.getYVelocity();
这里的速度是指一段时间内手指滑过的像素数,1000指的是1000ms,得到的是1000ms内滑过的像素数。速度可正可负:速度 = ( 终点位置 - 起点位置) / 时间段
- 当不需要使用的时候,需要调用clear()方法重置并回收内存
velocityTracker.clear();
velocityTracker.recycle();
5、GestureDetector
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。
使用过程
- 创建一个GestureDetector对象并实现OnGestureListener(或OnDoubleTapListener)接口:
GestureDetector mGestureDetector = new GestureDetector(this);
//解决长按屏幕后无法拖动的现象
mGestureDetector.setIsLongpressEnabled(false);
2.接管目标View的onTouchEvent方法
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
OnGestureListener和OnDoubleTapListener接口中的方法:
其中常用的方法有:onSingleTapUp
(单击)、onFling
(快速滑动)、onScroll
(拖动)、onLongPress
(长按)和onDoubleTap
( 双击)。建议:如果只是监听滑动相关的,可以自己在onTouchEvent中实现,如果要监听双击这种行为,那么就使用GestureDetector。
2、View的滑动
三种方式实现滑动:①通过View本身提供的scrollTo/scrollBy方法。②通过动画对View施加平移效果。③通过改变View的LayoutParams使得View重新布局来实现滑动。
2.1、使用scrollTo/scrollBy
View的两个属性:mScrollX和mScollY。
mScrollX = View布局x(左边缘) - View内容x(内容左边缘)可能为负数。
scrollTo/scrollBy 只能改变View内容的位置而不能改变View在布局中的位置。
View内容:若View是一个ViewGroup,指的就是其子元素。若View如Buttom,那么指的就是text值。
scrollTo(int x, int y)
scrollBy(int x, int y)
getScrollX()
getScrollY()
2.2、使用动画
使用动画移动View,主要是操作View的translationX和translationY属性,既可以采用传统的View动画,也可以采用属性动画,如果使用属性动画,为了能够兼容3.0以下的版本,需要采用开源动画库nineolddandroids。
2.3、改变参数布局
LinearLayout.MarginLayoutParams params //取决于button的父容器是什么布局
= (LinearLayout.MarginLayoutParams) button.getLayoutParams();
params.width = 100;
params.height = 200;
params.leftMargin = 100;
button.requestLayout();//或者 button.setLayoutParams(params)
ViewParent
View需要与其父ViewGroup进行交互时的API,基本所有的View都实现了这个接口
重要方法:
View的getParent() ViewParent
ViewParent的requestLayout()
requeLayout()
: 子View调用requestLayout方法,会标记当前View及父容器,同时逐层向上提交,直到ViewRootImpl处理该事件,ViewRootImpl会调用三大流程,从measure开始,对于每一个含有标记位的view及其子View都会进行测量、布局、绘制。
invalidate()
:当子View调用了invalidate方法后,会为该View添加一个标记位,同时不断向父容器请求刷新,父容器通过计算得出自身需要重绘的区域,直到传递到ViewRootImpl中,最终触发performTraversals方法,进行开始View树重绘流程(只绘制需要重绘的视图)。
postInvalidate()
:这个方法与invalidate方法的作用是一样的,都是使View树重绘,但两者的使用条件不同,postInvalidate是在非UI线程中调用,invalidate则是在UI线程中调用。
layout()
:对控件进行重新定位执行onLayout()这个方法,比如要做一个可回弹的ScrollView,思路就是随着手势的滑动子控件滑动,那么我们可以将ScrollView的子控件调用layout(l,t,r,b)这个方法就行了。
Android View 深度分析requestLayout、invalidate与postInvalidate
2.4、各种滑动方式的对比
- scrollTo/scrollBy:操作简单,适合对View内容的滑动;
- 动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果;
- 改变布局参数:操作稍微复杂,适用于有交互的View。
3、弹性滑动
共同思想:将一次大的滑动分成若干次小的滑动,并在一定时间段内完成。
3.1、使用Scroller
使用Scroller实现弹性滑动的典型使用方法如下
Scroller scroller = new Scroller(mContext);
//缓慢移动到指定位置
private void smoothScrollTo(int destX,int dextY){
int scrollX = getScrollX();
int deltaX = destX - scrollX;
//1000ms内滑向destX,效果就是缓慢滑动
mScroller.startSscroll(scrollX,0,deltaX,0,1000);//仅仅保存了传递的参数,并不会滑动
invalidate();//View会进行重绘
}
@override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
invalidate()
导致View重绘,在View的draw方法中调用了computeScroll()
,computeScroll()
在View中是一个空的实现,需要我们自己去实现。computeScrollOffset()
会根据时间流逝去计算当前的mScrollX和mScrollY,并调用scrollTo
方法实现滑动,接着又调用postInvalidate()
进行第二次重绘。如此反复,直到绘制结束。
Scroller方法:
startScroll(int startX, int startY, int dx, int dy, int duration)
-
boolean computeScrollOffset()
//返回true,代表滑动未结束 -
int getCurrX()
//当前时刻应该所处的位置
3.2、通过动画
方法一
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start()
方法二
//当然,我们也可以利用动画来模仿Scroller实现View弹性滑动的过程:
final int startX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new AnimatorUpdateListener(){
@override
public void onAnimationUpdate(ValueAnimator animator){
float fraction = animator.getAnimatedFraction();
mButton1.scrollTo(startX + (int) (deltaX * fraction) , 0);
}
});
animator.start();
3.3、使用延时策略
延时策略的核心思想是通过发送一系列延时信息从而达到一种渐近式的效果,具体可以通过Hander和View的postDelayed方法,也可以使用线程的sleep方法。 下面以Handler为例:
private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELATED_TIME = 33;
private int mCount = 0;
@suppressLint("HandlerLeak")
private Handler handler = new handler(){
public void handleMessage(Message msg){
switch(msg.what){
case MESSAGE_SCROLL_TO:
mCount ++ ;
if (mCount <= FRAME_COUNT){
float fraction = mCount / (float) FRAME_COUNT;
int scrollX = (int) (fraction * 100);
mButton1.scrollTo(scrollX,0);
mHandelr.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO , DELAYED_TIME);
}
break;
default : break;
}
}
}
四、事件的分发机制
1、基础认知
当用户触摸屏幕时将产生MotionEvent对象
典型的事件类型:
MotionEvent.ACTION_DOWN:按下View(所有事件的开始)
MotionEvent.ACTION_MOVE:滑动View
MotionEvent.ACTION_CANCEL:非人为原因结束本次事件
MotionEvent.ACTION_UP:抬起View(与DOWN对应)
事件分发的本质:即当一个点击事件发生后,系统需要将这个事件传递给一个具体的View去处理。这个事件传递的过程就是分发过程。由三个重要方法来共同完成。
boolean dispatchTouchEvent(MotionEvent event)
用来进行事件的分发
boolean onInterceptTouchEvent(MotionEvent ev)
用来判断是否拦截事件
boolean onTouchEvent(MotionEvent event)
用来处理事件
他们之间的关系,可以用如下伪代码表示:
public boolean dispatchTouchEvent (MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvnet(ev)){
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEnvet(ev);
}
return consume;
}
事件传递
事件分发机制的重要结论:
- 同一个事件序列以down事件开始,中间包含数量不定的move事件,最终以up事件结束。
- 正常情况下,一个事件序列只能由一个View拦截并消耗。
- 某个View拦截了事件后,该事件序列只能由它去处理,并且它的onInterceptTouchEvent不会再被调用。
- 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件( onTouchEvnet返回false) ,那么同一事件序列中的其他事件都不会交给他处理,并且事件将重新交由他的父元素去处理,即父元素的onTouchEvent被调用。好比一个程序员,如果这件事没有处理好,短期内上级不会再把事情交给他处理。
- 如果View不消耗ACTION_DOWN以外的其他事件,那么这个事件将会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终消失的点击事件会传递给Activity去处理。
- ViewGroup默认不拦截任何事件。
- View没有onInterceptTouchEvent方法。一旦事件传递给它,它的onTouchEvent方法会被调用。
- View的onTouchEvent默认消耗事件,除非他是不可点击的( clickable和longClickable同时为false) 。View的longClickable属性默认false,clickable默认属性分情况(如TextView为false,button为true)。
- View的enable属性不影响onTouchEvent的默认返回值。
- onClick会发生的前提是当前View是可点击的,并且收到了down和up事件。
- 事件传递过程总是由外向内的,即事件总是先传递给父元素,然后由父元素分发给子View,通过
requestDisallowInterceptTouchEvent
方法可以在子元素中干预父元素的分发过程,但是ACTION_DOWN事件除外。 -
onTouch
(dispatchTouchEvent中调用)优先于onTouchEvent
执行,onClick
优先级最低。onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。