一、View的分发机制
应用场景一
- Down:TextView默认是不能获得焦点的,所以它的TouchEvent返回的肯定是false,那么会继续递归到上层交由Activity的TouchEvent消费掉。
- UP/Mve:而UP或Move的时候,直接就不会向下走逻辑的,直接在Activity的touchEvent中消费掉。UP和Move是根据Down之后的情况来确定的。
应用场景二
Down:由于Button是可以获取焦点的,那么它的onTouchEvent方法返回的是true,那么在Button的onTouchEvent是会消费掉事件的。
Move/Up:事件在down中被子视图消费掉的时候,Move/Up的时候它也会走和down一样的逻辑。
应用场景三
Down:如果Activity上有ViewGroup(ScrollView),ScrollView又包含Button,这时Button是会消费掉事件,在Down的情况也是如此。
Move/Up:如果我们按下后,又往下进行了滑动,这个时候外侧ViewGroup会响应到事件,所以Move和Up的时候,ViewGroup的TouchEvent
会响应到,而这时它默认又是false,所以最终交给上层的Activity或系统去响应,这时Button会收到一个action_cancle的事件响应。
注:View或ViewGroup中监听的touchEvent大多数情况下我们可以认为它和onTouchEvent是一样的,当他们同时存在的时候,
onTouchEvent会先去查看监听的touchEvent是否消费事件,然后再走自己的逻辑。
二、View滑动总结
1.1、View的弹性滑动
Android中实现滑动的方式有三种:
- 第一种是通过View本身提供的scrollTo/scrollBy方法来实现滑动。
- 第二种是通过动画给View施加平移效果来实现滑动。
- 第三种是通过改变View的LayoutParams使View重新布局从而实现滑动。
1、使用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的内容进行移动,并不会将View本身进行移动。
此时mScrollX = 100、mScrollY = 100。最大距离不会超出父容器的大小。
1.2、使用动画滑动
使用动画来移动View,主要操作View的translationX和translationY属性,既可以采用传统的View动画,也可以采用属性动画。
如果使用到属性动画,为了兼容3.0以下版本,需要采用开源动画库nineoldandroids。
示例:将一个View从原始位置向右下角移动100个像素。
传统动画方式:
属性动画方式:
/**同样可以采用链式调用的方式*/ObjectAnimator animator = ObjectAnimator.ofFloat(mView, "tanslationX", 0, 100);animator.setDuration(100);animator.start();
传统View动画不会真正改变View的位置和宽高信息,其实是通过父容器将子控件隐藏,然后不断绘制子控件的位置。
同时,我们需要保留动画的位置必须将fillAfter设置为true,如果为false,在动画完成的一刻会回到初始位置。
使用属性动画则不会有这样的问题,因为属性动画是真实的改变控件的位置信息。
1.3、改变布局参数
我们可以通过LayoutParams来完成滑动的效果,比如我们将一个button向右平移100px,只需要将LayoutParams里的marginLeft参数值
增加100px即可,这种方式是非常灵活的,可以根据我们所需要的情况做不同的处理。
MarginLayoutParams params = (MarginLayoutParams) mButton.getLayoutParams();params.width += 100;params.leftMargin += 100;mButton.requestLayout(); 或者 mButton.setLayoutParams(params);
1.4、三种滑动方式的区别
- scrollTo/scrollBy:它是View提供的原生方式,专门用于View的滑动,方便实现滑动效果却不影响单击事件等,但是只能滑动内容,不能滑动View本身。
- 动画滑动方式:如果是android3.0以上版本,并且使用的是属性动画则没有任何缺点,使用传统View动画则会影响单击事件等。
- 改变布局参数:除了使用起来比较麻烦外,没有明显的缺点,适用对象是一些具有交互性的View。
三种方式总结:
- scrollTo/scrollBy:操作简单,适合对View内容的滑动。
- 动画:操作简单,主要适用于没有交互的View和实现复杂动画效果。
- 改变布局参数:操作稍微复杂,适用于有交互的View。
下面我们来看看屏幕拖拽的一个案例,由于画笔和画布之类的比较简单,这里只演示onTouchEvent方法:
private int mLastX;private int mLastY;private Paint mPaint;@Overridepublic boolean onTouchEvent(MotionEvent event) { int x = (int) event.getRawX(); int y = (int) event.getRawY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: int detalX = x - mLastX; int detalY = y - mLastY; int translationX = (int) (ViewHelper.getTranslationX(this) + detalX); int translationY = (int) (ViewHelper.getTranslationY(this) + detalY); ViewHelper.setTranslationX(this, translationX); ViewHelper.setTranslationY(this, translationY); break; case MotionEvent.ACTION_UP: break; } mLastX = x; mLastY = y; return true;}
其中,需要注意ViewHelper需要引入NineOldAndroids库才能识别,用来兼容3.0之前版本使用属性动画。
1.2、View的滑动原理
1、使用Scroller
Scroller的典型使用方式在上方已经演示过,那么它为什么能实现弹性滑动呢?
Scroller mScroller = new Scroller(getContext());// 缓慢滚动到指定位置private void smoothScrollTo(int destX , int destY){ int scrollX = getScrollX(); int scrollY = getScrollY(); int delta = destX - scrollX; // 1000ms内滑向destX,缓慢移动 mScroller.startScroll(scrollX, 0, delta, 0, 1000); invalidate();}@Overridepublic void computeScroll() { super.computeScroll(); if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); }}
当我们构造一个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进行滑动的,让其产生滑动效果的是invalidate方法,该方法会导致View重绘,在View的draw方法中又会
去调用computeScroll方法,computeScroll方法在View中是一个空实现,因此,需要我们自己去实现。
当View重绘后会在draw方法中调用computeScroll,而computeScroll又会向Scroller获取当前scrollX和scrollY,然后通过scrollTo方法实现滑动。
接着又调用postInvalidate方法进行二次重绘,这个重绘的过程和第一次一样,还是会导致computeScroll方法被调用,然后继续向Scroller获取
当前的scrollX和scrollY,并通过scrollTo方法滑动到新的位置,如此反复,直到整个滑动过程结束。
下面我们来看下computeScrollOffset方法的实现:
/** * Call this when you want to know the new location. If it returns true, * the animation is not yet finished. */ public boolean computeScrollOffset() { 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和scrollY所改变的百分比来计算出当前的值。返回true表示滑动未结束,false则表示滑动结束。
2、使用动画
动画本身是一种渐进的过程,因为它具备天然的弹性滑动效果。
这里我们模仿Scroller来实现View的弹性滑动,利用动画的特性,我们可以采用如下方式实现:
final int startX = 0;final int detalX = 100;final ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);animator.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float fraction = animator.getAnimatedFraction(); mButton.scrollTo(startX + (int)(detalX * fraction), 0); }});animator.start();
动画本质上没有作用在任何对象上,只是在1000ms内完成整个动画过程,利用该特性,我们就可以在动画的每一帧到来时获取动画完成的比例。
然后根据这个比例计算出当前View所要滑动的距离。当然我们还可以在onAnimationUpdate方法中实现其他操作。
3、使用延时策略
延时策略的核心思想是通过发送一系列的延时消息从而达到一种渐进式的效果,具体可以使用Handler或View的postDelay方法以及线程的sleep方法。
下面我们以Handler为例,在1000ms内将View内容像左移动100像素:
private static final int MSG_SCROLL_TO = 0;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 MSG_SCROLL_TO: mCount++; if(mCount <= FRAME_COUNT){ float fraction = mCount / (float)FRAME_COUNT; int scrollX = (int) (fraction * 100); mButton.scrollTo(scrollX, 0); mHandler.sendEmptyMessageDelayed(MSG_SCROLL_TO, DELAYED_TIME); } break; } }};
4、使用ViewDragHelper
待续