第三章-View事件体系(View的滑动、弹性滑动)

View的滑动

  • 使用scrollTo/scrollBy
  • 使用动画
  • 改变布局参数
  • 各种滑动方式的对比
弹性滑动
  • 使用Scroller
  • 通过动画‘
  • 使用延时策略
在上一节介绍了View的一些基础知识和概念,本节开始介绍很重要的一个内容:View的滑动。在Android设备上,滑动几乎是应用的标配,不管是下拉刷新还是SlidingMenu,它们的基础都是滑动。从另外一方面来说,Android手机由于屏幕比较小,为了给用户呈现更多的内容,就需要使用滑动来隐藏和显示一些内容。基于上述两点,可以知道,滑动在Android开发中具有很重要的作用,不管一些滑动效果多么绚丽,归根结底,它们都是由不同的滑动外加一些特效所组成的。因此,掌握滑动的方法是实现绚丽的自定义控件的基础。
通过三种方式可以实现View的滑动:
第一种是通过View本身提供的scrollTo/scrollBy方法来实现滑动;
第二种是通过动画给View施加平移效果来实现滑动;
第三种是通过改变Viev的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); }

第三章-View事件体系(View的滑动、弹性滑动)
文章图片

第三章-View事件体系(View的滑动、弹性滑动)
文章图片

【第三章-View事件体系(View的滑动、弹性滑动)】2.使用动画
上一节介绍了采用scrollTo/scrollBy来实现View的滑动,本节课介绍另外一种滑动方式,就是使用动画,通过动画,我们来让一个View移动,而平移就是一种滑动,使用动画来移动View,主要是操作View的translationX,translationY属性,即可以采用传统的View动画,也可以采用属性动画,如果用属性动画的话,为了兼容3.0以下的版本需要使用开源库nineoldandroids(github上自行搜索)
采用View动画的代码,如下所示,此动画可以在100ms里让一个View从初始的位置向右下角移动100个像素
:android="http://schemas.android.com/apk/res/android" android:fillAfter="true" android:zAdjustment="normal">

如果采用属性动画的话,那就更简单了,我们可用这样
ObjectAnimator.ofFloat(testButton,"translationX",0,100).setDuration(100).start();


第三章-View事件体系(View的滑动、弹性滑动)
文章图片

3.改变布局参数
第三章-View事件体系(View的滑动、弹性滑动)
文章图片

ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) mButton.getLayoutParams(); layoutParams.width +=100; layoutParams.leftMargin +=100; mButton.requestLayout(); //或者mButton.setLayoutParams(layoutParams);

通过改变LayoutParams的方式去实现View的滑动同样是一种很灵活的方法,需要根据不同情况去做不同的处理。
4.各种滑动方式的对比
上面分别介绍了三种不同的滑动方式,它们都能实现View的滑动,那么它们之间的差别分别是什么呢?
先看scorllBy/To这种方式,他是View提供的原生方式,其作用是专门用于View的滑动,它可以比较方便地实现滑动效果并且不影响内部元素的单击事件。但是它的缺点也是很显然的:它只能滑动View的内容,并不能滑动View本身。
再看动画,通过动画来实现View的滑动,这要分情况。如果是Android3.0以上并采用属性动画,那么采用这种方式没有明显的缺点;如果是使用View动画或者在Android3.0以下使用属性动画,均不能改变View本身的属性。在实际使用中,如果动画元素不需要响应用户的交互,那么使用动画来做滑动是比较合适的,否则就不太适合**。但是动画有一很明显的优点,那就是一些复杂的效果必须要通过动画才能实现,**
改变布局方式,主要适用对象是一些具有交互性的View,因为这些View需要和用户交互,直接通过动画去实现会有问题,这在之前已经有所介绍,所以这个时候我们可以使用直接改变布局参数的方式去实现:
scrollTo/scrollBy:操作简单,适合对View内容的滑动:
动画:操作简单,主要适用于没有交互的Visw和实现复杂的动画效果
改变布局参数:操作稍微复杂,适用于有交互的View
下面我们来实现一个手滑动的效果,这是一个自定义的View,拖动他可以让他在整个屏幕上随意滑动,这个View实现起来很简单,我们只要重写他的onTouchEvent方法并且处理他的ACTION_MOVE事件,根据两次滑动之间的距离就可以实现它的滑动,为了实现全屏滑动,我们采用改变布局的方式来实现,这里只是演示,所以就选择了动画的方式,核心代码:
package com.example.testgesturedetector; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.LinearLayout; import com.nineoldandroids.view.ViewHelper; public class MyViewMoveextends LinearLayout { private static final String TAG = "MyViewMove"; public MyViewMove(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); }public MyViewMove(Context context, AttributeSet attrs) { super(context, attrs); init(context); }public MyViewMove(Context context) { super(context); init(context); }private void init(Context context) { }int mLastX = 0; int mLastY = 0; @Override public 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 deltaX = x - mLastX; int deltaY = y - mLastY; //这里这个ViewHelper是nineoldandroid包里面的工具类,需要在build.gradle中添加 //implementation "com.nineoldandroids:library:2.4.0" int trabslationX = (int) (ViewHelper.getTranslationX(this) + deltaX); int trabslationY = (int) (ViewHelper.getTranslationY(this) + deltaY); ViewHelper.setTranslationX(this,trabslationX); ViewHelper.setTranslationY(this,trabslationY); break; case MotionEvent.ACTION_UP:break; } mLastX = x; mLastY = y; return true; }}

xml增加

第三章-View事件体系(View的滑动、弹性滑动)
文章图片

实现的效果就是我们可以拖动着这个红色的方块满屏幕跑(实际测试通过)
弹性滑动
知道了View的滑动,我们还要知道如何实现View的弹性滑动,比较生硬地滑动过去这种用户体验实在是太差了,因此我们要实现渐进式滑动,那么如何实现弹性滑动呢?其实实现方法也是有很多,但是他们都有一个共同的思想:将一次大的滑动分成若干个小的滑动,并且在一个时间段完成,实现方式很多,比如Scroller,Handler#PostDelayed以及Thread#Sleep,我们接下来一一介绍:
1.Scroller
Scroller的使用方法在之前就已经介绍了,我们来分析一下他的源码,从而探索为什么能实现View的弹性滑动
Scroller scroller = new Scroller(getContext()); private void smoothScrollTo(int destX,int destY){ int scrollX = getScrollX(); int delta = destX - scrollX; //1000ms内滑向destX,效果就是慢慢的滑动 scroller.startScroll(scrollX,0,delta,0,1000); invalidate(); }@Override public void computeScroll() { if(scroller.computeScrollOffset()){ scrollTo(scroller.getCurrX(),scroller.getCurrY()); postInvalidate(); } }

上面是Scroller的典型用法,这里先描述一下他的工作原理,当我们构建一个scroller对象并且调用它的startScroll方法,scroller内部其实并没有做什么,他只是保存了我们传递的参数,这几个参数从startScroll的原型就可以看出,如下的代码:
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; }

第三章-View事件体系(View的滑动、弹性滑动)
文章图片

我们再来看下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; case FLING_MODE: ... break; } } else { mCurrX = mFinalX; mCurrY = mFinalY; mFinished = true; } return true; }

第三章-View事件体系(View的滑动、弹性滑动)
文章图片

我们写个demo来测试下
实现的功能就是,点击view会进行scroller滑动(黑色的区域,绿色的滑块移动),因为是view的内容移动,所以只会移动xml中的内容,就是绿色滑块。
核心代码:
package com.example.testgesturedetector; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.LinearLayout; import android.widget.Scroller; public class MyViewScroller extends LinearLayout { private Scroller mScroller; private static final String TAG = "MyView"; public MyViewScroller(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); }public MyViewScroller(Context context, AttributeSet attrs) { super(context, attrs); init(context); }public MyViewScroller(Context context) { super(context); init(context); }private void init(Context context) { Log.d(TAG,"步骤1:初始化一个Scroller。"); mScroller = new Scroller(context); }@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_UP: Log.d(TAG,"点击自定义View,向右移动50。"); smoothScrollTo(-50,0); break; } Log.d(TAG,"一定要返回true才生效。"); return true; }public void smoothScrollTo(int destX,int destY){ Log.d(TAG,"步骤2:定义一个smoothScrollTo方法"); Log.d(TAG,"destX向左移动距离;destY向上移动的距离。如果要反向,设置为负值即可。"); mScroller.startScroll(getScrollX(),getScrollY(),destX,destY,1000); invalidate(); Log.d(TAG,"注意:移动的是自定义View里面的内容(所有子view一起移动),背景不会动。例如我们自定义View是继承LinearLayout,里面所有的子view都是移动"); }@Override public void computeScroll() { if(mScroller.computeScrollOffset()){ Log.d(TAG,"步骤3:重写View的computeScroll方法"); Log.d(TAG,"会频繁地调用这个方法,通过scrollTo方法到达缓慢滚动的目的。"); scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); postInvalidate(); }} }

xml配置(很重要,需要有子元素,否则看不出来滑动了)

运行的效果如下:
第三章-View事件体系(View的滑动、弹性滑动)
文章图片

2.通过动画
动画本身就是一种渐进的过程,因此通过他来实现滑动天然就具有弹性效果,比如以下代码让一个view在100ms内左移100像素
ObjectAnimator.ofFloat(testView, "translationX", 0, 100).setDuration(100).start();

不过这里想说的并不是这个问题,我们可用利用动画的特性来实现一些动画不能实现的效果,还拿scorllTo来说,我们想模仿scroller来实现View的弹性滑动,那么利用动画的特性我们可用这样做:
final int startX = 0; final int startY = 100; finalint deltaX = 0; final ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { float fraction = animator.getAnimatedFraction(); testView.scrollTo(startX + (int)(deltaX * fraction),0); } });

第三章-View事件体系(View的滑动、弹性滑动)
文章图片

3.使用延时策略
第三章-View事件体系(View的滑动、弹性滑动)
文章图片

private static finalint MESSAGE_SCROLL_TO = 1; private static finalint FRAME_COUNT = 30; private static finalint DELAYED_TIME = 33; private int count = 1; private Handler handler = new Handler(){ @Override public void handleMessage(Message msg) { switch (msg.what){ case MESSAGE_SCROLL_TO: count++; if(count <= FRAME_COUNT){ float fraction = count / (float)FRAME_COUNT; int scrollX = (int)(fraction * 100); testButton.scrollTo(scrollX,0); handler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO,DELAYED_TIME); } break; } } };

上面集中弹性滑动的实现方法,在介绍中侧重更多的是实现思想,在实际使用中过程中可以对其进行灵活的扩展从而实现更加复杂的效果。
到此为止,View的滑动了解的差不了。下面来聊聊View的事件分发传递。

    推荐阅读