《Android开发艺术探索》笔记(三)——View事件体系

《Android开发艺术探索》笔记(三)——View事件体系

作者:cmad 时间:2016-05-12 分类:Android 评论:0条 浏览:556

1.View的基础知识

1.1 View的位置参数

  • left view左上角相对于父控件横坐标
  • top view左上角相对于父控件纵坐标
  • right view右下角相对于父控件横坐标
  • right view右下角相对于父控件纵坐标
  • x、y view左上角相对于父控件坐标
  • translationX、translationY view左上角相对于父控件的偏移量,默认为0

1.2 MotionEvent和TouchSlop

  • MotionEvent 手指接触屏幕后产生的事件。

    常用的有ACTION_DOWN、ACTION_MOVE、ACTION_UP等事件。

    通过MotionEvent可以获得点击事件发生的x、y坐标:getX/getY获得相对于当前View的坐标,getRawX/getRawY获得相对于屏幕的坐标。
  • TouchSlop 一个常量值,是系统所能识别出的被认为是滑动的最小距离,跟设备相关,在不同的设备上这个值可能不同。通过ViewConfiguration.get(getContext()).getScaledTouchSlop();获得。

1.3 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方法,传入参数为时间段,单位ms,比如这里为1000ms,及在1000ms的时间段里的滑动速度。

  最后当不需要使用VelocityTracker时调用clear方法来重置并回收内存:

    velocityTracker.clear();
velocityTracker.recycle();
GestureDetector

  手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。

  使用方法很简单,创建GestureDetector对象并实现OnGestureListener接口,然后在View的onTouchEvent方法里调用GestureDetector.onTouchEvent方法传入MotionEvent对象,即将滑动事件托管给GestureDetector来处理。

  OnGestureListener常用方法有onSingleTapUp(单击)、onFling(滑动)、onScroll(拖动)、onLongPress(长按)、onDoubleTap(双击);注意如果要使用onDoubleTap需要调用GestureDetector的setOnDoubleTapListener方法传入OnDoubleTapListener接口。

Scroller

  弹性滑动对象,用于实现View的弹性滑动。注意Scroller本身不能实现弹性滑动,需要配合View的scrollTo/scrollBy方法和computeScroll方法一起使用实现弹性滑动功能。

2.View的滑动

2.1 scrollTo/scrollBy

  scrollTo/scrollBy是View提供的滑动方法,scrollTo是滑动到指定位置,而scrollBy是相对于当前位置的滑动,scrollBy事件实际上也是调用的scrollTo方法,源码如下:

    public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}

  当前位置加上相对位置然后调用scrollTo方法。

2.2 动画

  用动属性动画实现滑动:

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

2.3 改变布局参数

  通过获取view的LayoutParams属性然后修改LayoutParams参数实现view的滑动:

        ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
view.setLayoutParams(params);

3.弹性滑动

3.1 Scroller

  Scroller结合scrollTo和computeScroll实现弹性滑动,首先创建Scroller对象,然后在需要弹性滑动的地方调用Scroller的startScroll方法然后调用View的invalidate方法重绘view:

Scroller mScroller = new Scroller(mContext);
mScroller.startScroll(startX, startY, dx, dy, duration);
invalidate();

  startX、startY是开始的x、y坐标,dx、dy是要滑动的距离,duration是滑动的时间。当我们调用invalidate方法时会导致View重绘,在View的方法里又会调用computeScroll方法。

  所以在View的computeScroll里通过scrollTo实现滑动操作:

@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}

  前面我们说了Scroller本身是不能实现滑动的,因为Scroller调用startScroll时只是保存了我们的参数,然后调用computeScrollOffset时根据当前时间计算当前要滑动到的坐标,我们通过Scroller的getCurrX/getCurrY方法获得当前要滑动到的坐标然后调用scrollTo来进行滑动,然后调用postInvalidate再次触发View重绘继续执行上一个流程。

  我们来看一下startScroll和computeScrollOffset的源码:

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;
}

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;
}

  我们看到startScroll方法里只是保存了我们传进来的参数,并保存了当前的时间戳。在computeScrollOffset首先判断当前时间是否超过了开始的时间加上我们设置的滑动时间,如果超过了返回false,否则通过时间计算出当前应该要滑动到的坐标mCurrX/mCurrY也就是getCurrX/getCurrY获得的值。

3.2 动画

  使用属性动画实现弹性滑动:

final int startX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
view.scrollTo(startX + (int)(deltaX * fraction),0);
}
});
animator.start();

  使用ValueAnimator.ofInt(0,1)实现从0到1的值的变化,setDuration设置变化时间单位ms,addUpdateListener设置值的变化监听,在onAnimationUpdate获得值的变化比例,然后计算出当前要滑动的距离,然后调用scrollTo实现滑动。

3.3 延时策略

  通过发送一系列延时消息达到一种渐进式的滑动效果,具体方式是通过Handler或View的postDelayed方法实现没过一定时间调用一次scrollTo方法执行滑动操作。

上面讲了三种弹性滑动的方法,其实原理都是相同的,都是通过不同的方法获得我们当前要滑动到的坐标位置,然后调用scrollTo方法实现滑动操作。只是实现方式不同而已。

4.View的事件分发机制

  事件的分发过程由View的三个重要方法共同来完成:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。

  • public boolean dispatchTouchEvent(MotionEvent ev)

    用来进行事件的分发。如果事件传递到当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的 dispatchTouchEvent方法影响,表示是否消耗当前事件。
  • public boolean onInterceptTouchEvent(MotionEvent ev)

    在当前View的dispatchTouchEvent方法调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一事件序列当中此方法不会被再次调用,返回结果表示是否拦截当前事件。该方法只有ViewGroup中存在,View中没有该方法。
  • public boolean onTouchEvent(MotionEvent ev)

    在当前View的dispatchTouchEvent方法调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一事件序列中当前View无法再次接收到事件。

同一事件序列 是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间包含数量不定的move事件,最终以up事件结束。

事件传递流程

Activity->Window-View

  即事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View。顶级View接收事件后就会按照事件分发机制去分发事件。

  

  当View接收到事件后首先会调用dispatchTouchEvent方法,dispatchTouchEvent方法会根据onInterceptTouchEvent的返回值判断是否拦截该事件.

  如果onInterceptTouchEvent方法确定拦截则返回true,然后dispatchTouchEvent会调用onTouchEvent方法去处理事件.如果onInterceptTouchEvent不拦截返回false,则会调用子View的dispatchTouchEvent方法继续此流程.

  如果事件传递到View的onTouchEvent方法里,此时onTouchEvent方法返回false,那么他的父容器的onTouchEvent方法将被调用,以此类推.

  1)正常情况下一个事件序列只能被一个View拦截,一但一个元素拦截了此事件,那么同一序列内的所有事件都会直接交给它处理.并且它的onInterceptTouchEvent方法不会再调用。

  2)某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件,那么同一事件序列的其他事件都不会再交给它处理。

  3)ViewGroup默认不拦截任何事件。Android源码中ViewGroup中的onInterceptTouchEvent方法默认直接返回false。

  4)View没有onInterceptTouchEvent方法,一但有事件传递给他,那么他的onTouchEvent方法就会被调用.

  5)View如果设置了OnTouchListener,则OnTouchListener中的onTouch会被调用,onTouchEvent不会被调用。OnTouchListener的onTouch优先级别比OnTouchEvent高。

5.View的滑动冲突

  滑动冲突主要是因为两个滑动容器互相嵌套产生的,当父View跟子View都可滑动的时候,滑动屏幕时系统就不知道到底该滑动那一个View了。

  处理View滑动冲突的思路很简单,就是根据View的事件拦截来做,一般采用外部拦截法,也就是重写父容器的onInterceptTouchEvent方法,按照一定规则来判断父容器是否拦截滑动事件。

  当然也可以采用内部拦截法来做,即重写子容器的dispatchTouchEvent方法,不过这样比较麻烦,所以一般还是采用外部拦截法。

  下面举一个例子,父容器可左右滑动,子容器可上下滑动比如Listview,下面采用外部拦截法重写父容器的onInterceptTouchEvent来实现。

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
return intercepted;
}

  当滑动x方向距离大于y方向距离我们判断为左右滑动,此时我们拦截事件将滑动事件交给当前View处理即左右滑动,否则不拦截事件将事件交给子View处理,即子View进行上下滑动。

  

  其他的滑动事件处理思路一样,找到规则按照一定规则进行事件分发即可。


相关推荐
更多

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>