Android View原理浅析——View的事件体系
View是什么
首先我们需要理解什么是View。View是安卓中所有控件的基类,无论是简单的TextView、Button,还是复杂的LinearLayout、ListView,它们的共同基类都是View,ViewGroup其实也是继承了View。Button是一个View,而LinearLayout是View的同时也是ViewGroup,ViewGroup的内部可以有子View,而子View同样可以是ViewGroup,以此类推。
View的位置参数
View的位置由它的四个顶点来决定,分别对应了View的四个属性——top、left、right、bottom。需要注意的是,这些坐标都是相对于View的父容器而说的,是一种相对坐标。View的坐标和父容器的关系如图。在Android中,x、y轴的正方向分包为右、下。
很容易得出,View宽高和坐标关系如下:
width = right - left
height = bottom - top
通过View的getLeft()、getRight()…方法可以得到这四个参数。
从Android 3.0开始,View增加了几个额外的参数:x.、y、translateX、translateY。x、y是View左上角的坐标,而translateX,translateY则是View左上角相对于父容器的偏移量。这几个参数也是相对父容器的坐标,并且translateX、translateY的默认值为0。与前面的参数一样,View也为它们提供了get/set方法。
需要注意:View平移过程中,top、left表示的是原始左上角位置,并不会改变。此时发生改变的是x、y、translatX、translateY这四个参数。
MotionEvent和TouchSlop
MotionEvent
手指接触屏幕后产生的一系列事件中,典型的事件类型有一下几种:
- ACTION_DOWN:手指刚刚接触屏幕
- ACTION_MOVE:手指在屏幕上移动
- ACTION_UP:手指从屏幕松开的一瞬间
正常情况,一次手指触碰屏幕的行为会发生一系列点击事件,比如如下情况:
- 点击屏幕后松开,事件顺序:DOWN->UP
- 点击屏幕滑动一会松开,事件顺序:DOWN->MOVE->…->MOVE->UP
上述的情况是典型的事件序列,同时通过MotionEvent对象我们可以得到发生点击事件的x,y坐标。为此,系统提供了两组方法 getX/getY 和 getRawX/getRawY。它们的区别:getX/getY返回的是相对于当前View左上角的x,y坐标。getRawX/getRawY返回的是相对于屏幕左上角的x,y。
TouchSlop
TouchSlop是系统能识别的被认为滑动的最小距离。如果滑动时距离小于这个常量,则系统不认为这是滑动。它的值在不同设备上可能不同,通过 ViewConfiguration.get(getContext()).getScaledTouchSlop() 即可获取这个常量。这个常量的可以帮助我们在处理滑动时做一些过滤。
在源码中可以找到这个常量的定义,它处于frameworks/base/core/res/res/values/config.xml文件中。
VelocityTracker、GestureDetector 和 Scroller
VelocityTracker
顾名思义,速度追踪,用于追踪手指在滑动时的速度,包括水平和竖直方向的速度。使用过程很简单,首先在View的onTouchEvent中追踪当前单击事件的速度:
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
接着,当我们想知道当前滑动速度,可以采用如下方式:
VelocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();
有两点需要注意:
一、获取速度前必须先计算速度,即先调用computeCurrentVelocity()方法。
二、此处的速度是指一段时间内手指划过的像素数,当手指从右往左划时,速度为负值。
最后,不需要使用它时,调用clear及recycle方法即可重置并回收内存。
velocityTracker.clear();
velocityTracker.recycle();
GestureDetector
手势检测,用于辅助用户单击、滑动、长按、双击等行为。
使用GestureDetector,首先需要创建一个GestureDetector对象并实现OnGestureListener接口,根据需要还可以实现OnDoubleTapListenr从而监听双击事件。
GestureDetector mGestureDetector = new GestureDetector(this);
//解决长按屏幕无法拖动的情况
mGestureDetector.setIsLongpressEnabled(false);
接着接管View的onTouchEvent方法,在其中添加如下实现:
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
之后,我们可以有选择地实现 OnGestureListener和OnDoubleTapListener中的方法了,这两个接口的方法介绍如表:
方法名 | 描述 | 所属接口 |
---|---|---|
onDown | 手指触摸屏幕一瞬间,由一个ACTION_DOWN触发 | OnGestureListener |
onShowPress | 手指轻轻触摸屏幕,未松开或拖动,由一个ACTION_DOWN触发(注意与onDown的区别,它强调的是没有松开或拖动的状态) | OnGestureListener |
onSingleTapUp | 手指触摸后松开,伴随一个MotionEvent ACTION_UP而触发,是单击行为 | OnGestureListener |
onScroll | 手指按下屏幕并拖动,由一个ACTION_DOWN,多个ACTION_MOVE触发,是拖动行为 | OnGestureListener |
onLongPress | 用户长按 | OnGestureListener |
onFling | 用户按下触摸屏,快速滑动后松开,由一个ACTION_DOWN,多个ACTION_MOVE和一个ACTION_UP触发,是快速滑动行为 | OnGestureListener |
onDoubleTap | 双击,由两次连续的单击组成,不可能与onSingleTapConfirmed共存 | OnDoubleTapListener |
onSingleTapConfirmed | 严格的单击行为(注意它与onSingleTapUp的区别,如果触发了onSingTapConfirmed,那么后面不可能再跟着一个单击行为,即只可能是双击,不可能是双击中的一次单击) | OnDoubleTapListener |
onDoubleTapEvent | 表示发生了双击行为,在双击期间ACTION_DOWN,ACTION_MOVE和ACTION_UP都会触发此回调 | OnDoubleTapListener |
日常开发中可以不使用GestureDetector,自己在View的onTouchEvent中实现所需的监听。如果是监听滑动相关的,最好在onTouchEvent中实现。如果监听是双击这种行为,就使用GestureDetector。
Scroller
弹性滑动对象,用于实现View的弹性滑动。我们知道(我们不知道!),当使用View的 scrollTo/ScrollBy 方法来滑动时,这个过程是瞬间完成的,这个没有过渡效果的滑动用户体验非常不好,这时就可用Scroller来山西爱你有过渡的滑动,这个过程不是瞬间完成的,而是在一定时间间隔内完成的。
Scroller本身无法让View弹性滑动,需要与View的computeScroll方法配合使用才能完成。典型代码如下:
Scroller mScroller = new Scroller(mContext);
//缓慢滑动到指定位置
private void smoothScrollTo(int desX, int desY){
int scrollX = getScrollX();
int delta = destX - scrollX;
//1000ms内滑向destX,效果就是慢慢滑动
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}
@Override
public void computeScroll(){
if (mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
View的滑动
Android设备上,滑动几乎是应用的标配,不论下拉刷新还是SlidingMenu,它们的基础均是滑动。通过三种方法可以实现View的滑动:
一、通过View本身的 scrollTo/scrollBy 方法
二、通过动画给View施加平移效果来实现滑动
三、通过改变View的LayoutParams使得View重新布局而实现滑动
使用 scrollTo/scrollBy
为了实现View的滑动,View提供了scrollTo和scrollBy方法来实现。
我们查看源码看看这两个方法的实现:
/**
* 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);
}
可以看出,scrollBy其实也是调用了scrollTo方法来实现的。它实现了基于当前位置的相对滑动,而scrollTo则实现了绝对滑动。使用scrollTo和scrollBy来实现View的滑动,不是一件困难的事,但要明白View的mScrollX和mScrollY的改变规则。
在滑动过程中,mScrollX的值等于View左边和View内容左边缘在水平方向的距离,mScrollY则等于View上边缘与View内容上边缘在竖直方向的距离。
因此,scrollTo和scrollBy只能改变View内容的位置而不能改变View在布局中的位置。当View左边缘在View内容左边缘右边时,mScrollX为正值,当View上边缘在View内容上边缘下边时,mScrollY为正值。
换句话说,如果从左往右滑动,mScrollX为负值,反之为正值。如果从上往下滑动,mScrollY为负值,反之为正值
使用scrollTo和scrollBy来实现View的滑动,只能将View内容进行移动,不能将View本身进行移动。也就是说不论怎样滑动,也不可能将当前View移动到附近View的区域
使用动画
通过动画我们可以让一个View进行平移。而平移就是一种滑动。用动画来移动View,主要是操作View的translateX和translateY属性。既可以采用传统View动画,也可以采用属性动画。
采用View动画,将View从原始位置在100ms内向右下角移动100像素。
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:zAdjustment="normal">
<translate
android:duration = "100"
android:fromXDelta="0"
android:fromYDelta="0"
android:interpolator="@android:anim/linear_interpolator"
android:toXDelta="100"
android:toYDelta="100"/>
</set>
如果使用属性动画:
ObjectAnimator.offFloat(targetView, "translateX", 0, 100).setDuration(100).start();
虽然两者可以达到一样的效果,但是需要注意的是,View动画是对View的影像做操作,不能真正改变View的位置参数,包括宽高。并且如果希望动画后状态改变需要将fillAfter属性设置为true。即使将fillAfter置为了true,实际上也仅仅是改变了View影像的位置,它的诸如onClick等事件仍然需要在原来的位置才可触发,也就是系统看来View的位置没有发生变化。
使用属性动画就解决了这一问题,它是通过改变View的属性值来达成的,因此系统看来View实际也是改变了的。关于属性动画,可以看我另外一篇博客:【Android】属性动画学习与总结
改变布局参数
这个很好理解,比如我们想把Button右移100px,我们可以将这个Button的LayoutParams里的marginLeft值增加100px。
还有一种方式就是在Button的左边放置一个空的View,要移动Button,只需要改变View的宽度即可。
MarginLayoutParams params = (MarginLayoutParams)mButton.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
mButton.requestLayout();
//或者mButton.setLayoutParams(params);
几种方式的对比
首先看scrollTo / scrollBy方法,它们是View提供的原生方法,可以比较方便地实现View的滑动。但它们的缺点很明显,只能滑动View的内容,不能滑动View本身。
再看动画,分为两种。如果是使用属性动画,采用这种方式没有明显的缺点。如果采用View动画,则不能改变View本身的属性。如果动画元素不需要相应用户的交互,使用动画来做滑动比较合适的,否则不太合适。不过动画实现滑动有个明显的优点:一些复杂效果必须通过动画实现。
再看改变布局的方式,除了使用麻烦,也没有明显的缺点。主要适用对象是一些有交互性的View。因为这些View需要与用户交互,用动画来实现会有问题,此时可以通过改变布局参数而实现。
做个总结
- scrollTo/scrollBy:操作简单,适合对View内容的滑动
- 动画:操作简单,适用于没有交互的View和实现复杂的动画效果。
- 改变布局参数:操作稍微复杂,适用有交互的View
弹性滑动
众所周知,比较生硬的滑动的用户体验实在太差了,因此我们需要实现渐进式滑动。渐进式滑动的实现方法非常多,但他们都有一个共同的思想:将大的滑动分成若干小的滑动,并在某一时间段完成。实现方式非常多,如用Handler.postDelayed方法及Thread.sleep方法等等。
使用Scroller
之前已经介绍了Scroller的使用方法。这里我们来分析它为什么能实现View的弹性滑动。
Scroller mScroller = new Scroller(mContext);
//缓慢滑动到指定位置
private void smoothScrollTo(int desX, int desY){
int scrollX = getScrollX();
int delta = destX - scrollX;
//1000ms内滑向destX,效果就是慢慢滑动
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}
@Override
public void computeScroll(){
if (mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
这里是Scroller的典型使用方法,下面先描述它的工作原理
当我们创建Scroller对象并调用它的startScroll方法,Scroller内部其实什么也没做,只是保存几个传递的参数,如下:
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
参数含义很清楚,startX、startY表示滑动起点,dx、dy表示滑动的距离,duration表示滑动的时间。可以看到,仅仅调用startScroll是无法让View滑动的,因为它内部没有做滑动相关的事情。
Scroller真正的滑动方法是下面的invalidate方法。它会导致View重绘,在View的draw方法中又会调用computeScroll方法(需要我们自己实现,上面已实现)。而computeScroll方法又会向Scroller获取当前scrollX和scrollY,之后通过scrollTo方法实现滑动。接着,调用postInvalidate方法进行二次重绘。这次重绘和第一次重绘一样,如此反复,直到滑动结束。
我们看看Scroller的computeScrollOffset方法:
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
...
}
return true;
}
这个方法会根据时间流逝计算当前scrollX的值(类似属性动画插值器)。它返回true代表滑动未结束,我们要继续进行View的滑动。
通过动画
动画本身就是一种渐进过程,比如如下代码可以让View100ms内向右移动100px:
ObejctAnimator.ofFloat(targetView, "translateX", 0F, 100F).setDuration(100).start();
通过ValueAnimator也可以实现:
ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
targetView.scrollTo(startX+(int)(deltaX*fraction),0);
}
});
animator.start();
具体使用动画如何解决滑动,可以参考我的另一个文章【Android】属性动画学习与总结
使用延时策略
另外一种实现弹性滑动的方法,也就是使用延时策略。它的核心思想是发送一系列延时消息达到渐进式效果。如使用Handler或View的postDelayed方法,或者使用线程的sleep方法。
下面用Handler做一个示范:
private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELAYED_TIME = 33;
private int mCount = 0;
private Handler mHandler = 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);
targetView.scrollTo(scrollX, 0);
mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
}
break;
default:
break;
};
};
}
广告时间
我是N0tExpectErr0r,一名广东工业大学的大二学生
欢迎来到我的个人博客,所有文章均在个人博客中同步更新哦
http://blog.N0tExpectErr0r.cn