Android Scroller完全解析

Android Scroller完全解析,第1张

概述在Android中,任何一个控件都是可以滚动的,因为在View类当中有scrollTo()和scrollBy()这两个方法,如下图所示:

在AndroID中,任何一个控件都是可以滚动的,因为在VIEw类当中有scrollTo()和scrollBy()这两个方法,如下图所示:

这两个方法的主要作用是将VIEw/VIEwGroup移至指定的坐标中,并且将偏移量保存起来。另外:

mScrollX 代表X轴方向的偏移坐标
mScrollY 代表Y轴方向的偏移坐标

这两个方法都是用于对VIEw进行滚动的,那么它们之间有什么区别呢?简单点讲,scrollBy()方法是让VIEw相对于当前的位置滚动某段距离,而scrollTo()方法则是让VIEw相对于初始的位置滚动某段距离。

关于偏移量的设置我们可以参看下源码:

public class VIEw {   ....   protected int mScrollX; //该视图内容相当于视图起始坐标的偏移量,X轴方向     protected int mScrollY; //该视图内容相当于视图起始坐标的偏移量,Y轴方向   //返回值   public final int getScrollX() {     return mScrollX;   }   public final int getScrollY() {     return mScrollY;   }   public voID scrollTo(int x,int y) {     //偏移位置发生了改变     if (mScrollX != x || mScrollY != y) {       int oldX = mScrollX;       int oldY = mScrollY;       mScrollX = x; //赋新值,保存当前便宜量       mScrollY = y;       //回调onScrollChanged方法       onScrollChanged(mScrollX,mScrollY,oldX,oldY);       if (!awakenScrollbars()) {         invalIDate(); //一般都引起重绘       }     }   }   // 看出区别了吧 。 mScrollX 与 mScrollY 代表我们当前偏移的位置 , 在当前位置继续偏移(x,y)个单位   public voID scrollBy(int x,int y) {     scrollTo(mScrollX + x,mScrollY + y);   }   //... } 

于是,在任何时刻我们都可以获取该VIEw/VIEwGroup的偏移位置了,即调用getScrollX()方法和getScrollY()方法。

下面我们写个例子看下它们的区别吧:

<?xml version="1.0" enCoding="utf-8"?><linearLayout xmlns:androID="http://schemas.androID.com/apk/res/androID"  xmlns:tools="http://schemas.androID.com/tools"  androID:ID="@+ID/layout"  androID:layout_wIDth="match_parent"  androID:layout_height="match_parent"  androID:orIEntation="vertical">  <button    androID:ID="@+ID/scroll_to_btn"    androID:layout_wIDth="wrap_content"    androID:layout_height="wrap_content"    androID:text="scrollTo"/>  <button    androID:ID="@+ID/scroll_by_btn"    androID:layout_wIDth="wrap_content"    androID:layout_height="wrap_content"    androID:layout_margintop="10dp"    androID:text="scrollBy"/></linearLayout>

外层使用了一个linearLayout,在里面包含了两个按钮,一个用于触发scrollTo逻辑,一个用于触发scrollBy逻辑。

public class MainActivity extends AppCompatActivity {  private linearLayout layout;  private button scrollToBtn;  private button scrollByBtn;  @OverrIDe  protected voID onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    setContentVIEw(R.layout.activity_main);    layout = (linearLayout) findVIEwByID(R.ID.layout);    scrollToBtn = (button) findVIEwByID(R.ID.scroll_to_btn);    scrollByBtn = (button) findVIEwByID(R.ID.scroll_by_btn);    scrollToBtn.setonClickListener(new VIEw.OnClickListener() {      @OverrIDe      public voID onClick(VIEw v) {        layout.scrollTo(getResources().getDimensionPixelOffset(R.dimen.horizontal_scroll),getResources().getDimensionPixelOffset(R.dimen.horizontal_scroll));      }    });    scrollByBtn.setonClickListener(new VIEw.OnClickListener() {      @OverrIDe      public voID onClick(VIEw v) {        layout.scrollBy(getResources().getDimensionPixelOffset(R.dimen.horizontal_scroll),getResources().getDimensionPixelOffset(R.dimen.horizontal_scroll));      }    });  }}<resources>  <dimen name="horizontal_scroll">-20dp</dimen>  <dimen name="vertical_scroll">-30dp</dimen></resources>

当点击了scrollTo按钮时,我们调用了linearLayout的scrollTo()方法,当点击了scrollBy按钮时,调用了linearLayout的scrollBy()方法。那有的朋友可能会问了,为什么都是调用的linearLayout中的scroll方法?这里一定要注意,不管是scrollTo()还是scrollBy()方法,滚动的都是该VIEw内部的内容,而linearLayout中的内容就是我们的两个button,如果你直接调用button的scroll方法的话,那结果一定不是你想看到的。

另外还有一点需要注意,就是两个scroll方法中传入的参数,第一个参数x表示相对于当前位置横向移动的距离,正值向左移动,负值向右移动。第二个参数y表示相对于当前位置纵向移动的距离,正值向上移动,负值向下移动。
运行一下程序:

当我们点击scrollTo按钮时,两个按钮会一起向右下方滚动,之后再点击scrollTo按钮就没有任何作用了,界面不会再继续滚动,只有点击scrollBy按钮界面才会继续滚动,并且不停点击scrollBy按钮界面会一起滚动下去。

Scroller类

从上面例子运行结果可以看出,利用scrollTo()/scrollBy()方法把一个VIEw偏移至指定坐标(x,y)处,整个过程是直接跳跃的,没有对这个偏移过程有任何控制,对用户而言不太友好。于是,基于这种偏移控制,Scroller类被设计出来了,该类的主要作用是为偏移过程制定一定的控制流程,从而使偏移更流畅,更完美。
我们分析下源码里去看看Scroller类的相关方法,其源代码(部分)如下: 路径位于 \frameworks\base\core\Java\androID\Widget\Scroller.java

public class Scroller {   private int mStartX;  //起始坐标点,X轴方向   private int mStartY;  //起始坐标点,Y轴方向   private int mCurrX;   //当前坐标点 X轴, 即调用startScroll函数后,经过一定时间所达到的值   private int mCurrY;   //当前坐标点 Y轴, 即调用startScroll函数后,经过一定时间所达到的值   private float mDeltaX; //应该继续滑动的距离, X轴方向   private float mDeltaY; //应该继续滑动的距离, Y轴方向   private boolean mFinished; //是否已经完成本次滑动 *** 作, 如果完成则为 true   //构造函数   public Scroller(Context context) {     this(context,null);   }   public final boolean isFinished() {     return mFinished;   }   //强制结束本次滑屏 *** 作   public final voID forceFinished(boolean finished) {     mFinished = finished;   }   public final int getCurrX() {     return mCurrX;   }    /* Call this when you want to kNow the new location. If it returns true,* the animation is not yet finished. loc will be altered to provIDe the    * new location. */    //根据当前已经消逝的时间计算当前的坐标点,保存在mCurrX和mCurrY值中   public boolean computeScrollOffset() {     if (mFinished) { //已经完成了本次动画控制,直接返回为false       return false;     }     int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);     if (timePassed < mDuration) {       switch (mMode) {       case SCRolL_MODE:         float x = (float)timePassed * mDurationReciprocal;         ...         mCurrX = mStartX + Math.round(x * mDeltaX);         mCurrY = mStartY + Math.round(x * mDeltaY);         break;       ...     }     else {       mCurrX = mFinalX;       mCurrY = mFinalY;       mFinished = true;     }     return true;   }   //开始一个动画控制,由(startX,startY)在duration时间内前进(dx,dy)个单位,即到达坐标为(startX+dx,startY+dy)出   public voID startScroll(int startX,int startY,int dx,int dy,int duration) {     mFinished = false;     mDuration = duration;     mStartTime = AnimationUtils.currentAnimationTimeMillis();     mStartX = startX;    mStartY = startY;     mFinalX = startX + dx; mFinalY = startY + dy;     mDeltaX = dx;      mDeltaY = dy;     ...   } } 

其中比较重要的两个方法为:

public boolean computeScrollOffset()
函数功能说明:根据当前已经消逝的时间计算当前的坐标点,保存在mCurrX和mCurrY值中。

public voID startScroll(int startX,int duration)
函数功能说明:开始一个动画控制,由(startX,dy)个单位,到达坐标为(startX+dx,startY+dy)处。

computeScroll()方法介绍:
为了易于控制滑屏控制,AndroID框架提供了 computeScroll()方法去控制这个流程。在绘制VIEw时,会在draw()过程调用该方法。因此, 再配合使用Scroller实例,我们就可以获得当前应该的偏移坐标,手动使VIEw/VIEwGroup偏移至该处。
computeScroll()方法原型如下,该方法位于VIEwGroup.java类中

/**    * Called by a parent to request that a child update its values for mScrollX and mScrollY if necessary. This will typically be done if the child is animating a scroll using a {@link androID.Widget.Scroller Scroller}    * object.    * 由父视图调用用来请求子视图根据偏移值 mScrollX,mScrollY重新绘制 */  public voID computeScroll() { //空方法 ,自定义viewGroup必须实现方法体       } 

为了实现偏移控制,一般自定义view/VIEwGroup都需要重载该方法 。其调用过程位于VIEw绘制流程draw()过程中,如下:

@OverrIDe protected voID dispatchDraw(Canvas canvas){   ...   for (int i = 0; i < count; i++) {     final VIEw child = children[getChildDrawingOrder(count,i)];     if ((child.mVIEwFlags & VISIBIliTY_MASK) == VISIBLE || child.getAnimation() != null) {       more |= drawChild(canvas,child,drawingTime);     }   } } protected boolean drawChild(Canvas canvas,VIEw child,long drawingTime) {   ...   child.computeScroll();   ... } 

实例演示

VIEwPager相信每个人都再熟悉不过了,因此它实在是太常用了,我们可以借助VIEwPager来轻松完成页面之间的滑动切换效果,但是如果问到它是如何实现的话,我感觉大部分人还是比较陌生的。其实说到VIEwPager最基本的实现原理主要就是两部分内容,一个是事件分发,一个是Scroller。对于事件分发,不了解的同学可以参考我这篇博客Android事件的分发、拦截和执行。
接下来我将结合事件分发和Scroller来实现一个简易版的VIEwPager。首先自定义一个VIEwGroup,不了解的可以参考Android自定义ViewGroup(一)之CustomGridLayout这篇文章。平滑偏移的主要做法如下:

第一、调用Scroller实例去产生一个偏移控制(对应于startScroll()方法)
第二、手动调用invalID()方法去重新绘制,剩下的就是在computeScroll()里根据当前已经逝去的时间,获取当前应该偏移的坐标(由Scroller实例对应的computeScrollOffset()计算而得)
第三、当前应该偏移的坐标,调用scrollBy()方法去缓慢移动至该坐标处。

新建一个ScrollerLayout并让它继承自VIEwGroup来作为我们的简易VIEwPager布局,代码如下所示:

public class ScrollerLayout extends VIEwGroup {  private Scroller mScroller; //用于完成滚动 *** 作的实例  private VeLocityTracker mVeLocityTracker = null ; //处理触摸的速率  public static int SNAP_VELociTY = 600 ; //最小的滑动速率  private int mtouchSlop = 0 ;      //最小滑动距离,超过了,才认为开始滑动  private float mLastionMotionX = 0 ;  //上次触发ACTION_MOVE事件时的屏幕坐标  private int curScreen = 0 ; //当前屏幕  private int leftborder;  //界面可滚动的左边界  private int rightborder; //界面可滚动的右边界  //两种状态: 是否处于滑屏状态  private static final int touch_STATE_REST = 0; //什么都没做的状态  private static final int touch_STATE_SCRolliNG = 1; //开始滑屏的状态  private int mtouchState = touch_STATE_REST; //默认是什么都没做的状态  public ScrollerLayout(Context context,AttributeSet attrs) {    super(context,attrs);    // 创建Scroller的实例    mScroller = new Scroller(context);    //初始化一个最小滑动距离    mtouchSlop = VIEwConfiguration.get(context).getScaledtouchSlop();  }  @OverrIDe  protected voID onMeasure(int wIDthMeasureSpec,int heightmeasureSpec) {    super.onMeasure(wIDthMeasureSpec,heightmeasureSpec);    int childCount = getChildCount();    for (int i = 0; i < childCount; i++) {      VIEw childVIEw = getChildAt(i);      // 为ScrollerLayout中的每一个子控件测量大小      measureChild(childVIEw,wIDthMeasureSpec,heightmeasureSpec);    }  }  @OverrIDe  protected voID onLayout(boolean changed,int l,int t,int r,int b) {    if (changed) {      int childCount = getChildCount();      for (int i = 0; i < childCount; i++) {        VIEw childVIEw = getChildAt(i);        // 为ScrollerLayout中的每一个子控件在水平方向上进行布局        childVIEw.layout(i * childVIEw.getMeasureDWIDth(),(i + 1) * childVIEw.getMeasureDWIDth(),childVIEw.getMeasuredHeight());      }    }    // 初始化左右边界值    leftborder = getChildAt(0).getleft();    rightborder = getChildAt(getChildCount() - 1).getRight();  }  @OverrIDe  public boolean onIntercepttouchEvent(MotionEvent ev) {    final int action = ev.getAction();    //表示已经开始滑动了,不需要走该Action_MOVE方法了(第一次时可能调用)。    //该方法主要用于用户快速松开手指,又快速按下的行为。此时认为是处于滑屏状态的。    if ((action == MotionEvent.ACTION_MOVE) && (mtouchState != touch_STATE_REST)) {      return true;    }    final float x = ev.getX();    switch (action) {      case MotionEvent.ACTION_MOVE:        final int xDiff = (int) Math.abs(mLastionMotionX - x);        //超过了最小滑动距离,就可以认为开始滑动了        if (xDiff > mtouchSlop) {          mtouchState = touch_STATE_SCRolliNG;        }        break;      case MotionEvent.ACTION_DOWN:        mLastionMotionX = x;        mtouchState = mScroller.isFinished() ? touch_STATE_REST : touch_STATE_SCRolliNG;        break;      case MotionEvent.ACTION_CANCEL:      case MotionEvent.ACTION_UP:        mtouchState = touch_STATE_REST;        break;    }    return mtouchState != touch_STATE_REST;  }  public boolean ontouchEvent(MotionEvent event){    super.ontouchEvent(event);    //获得VeLocityTracker对象,并且添加滑动对象    if (mVeLocityTracker == null) {      mVeLocityTracker = VeLocityTracker.obtain();    }    mVeLocityTracker.addMovement(event);    //触摸点    float x = event.getX();    switch(event.getAction()){      case MotionEvent.ACTION_DOWN:        //如果屏幕的动画还没结束,你就按下了,我们就结束上一次动画,即开始这次新ACTION_DOWN的动画        if(mScroller != null){          if(!mScroller.isFinished()){            mScroller.abortAnimation();          }        }        mLastionMotionX = x ; //记住开始落下的屏幕点        break ;      case MotionEvent.ACTION_MOVE:        int detaX = (int)(mLastionMotionX - x ); //每次滑动屏幕,屏幕应该移动的距离        if (getScrollX() + detaX < leftborder) {  //防止用户拖出边界这里还专门做了边界保护,当拖出边界时就调用scrollTo()方法来回到边界位置          scrollTo(leftborder,0);          return true;        } else if (getScrollX() + getWIDth() + detaX > rightborder) {          scrollTo(rightborder - getWIDth(),0);          return true;        }        scrollBy(detaX,0);//开始缓慢滑屏咯。 detaX > 0 向右滑动 , detaX < 0 向左滑动        mLastionMotionX = x ;        break ;      case MotionEvent.ACTION_UP:        final VeLocityTracker veLocityTracker = mVeLocityTracker ;        veLocityTracker.computeCurrentVeLocity(1000);        //计算速率        int veLocityX = (int) veLocityTracker.getXVeLocity() ;        //滑动速率达到了一个标准(快速向右滑屏,返回上一个屏幕) 马上进行切屏处理        if (veLocityX > SNAP_VELociTY && curScreen > 0) {          // Fling enough to move left          snapToScreen(curScreen - 1);        }        //快速向左滑屏,返回下一个屏幕        else if(veLocityX < -SNAP_VELociTY && curScreen < (getChildCount()-1)){          snapToScreen(curScreen + 1);        }        //以上为快速移动的 ,强制切换屏幕        else{          //我们是缓慢移动的,因此先判断是保留在本屏幕还是到下一屏幕          snapToDestination();        }        //回收VeLocityTracker对象        if (mVeLocityTracker != null) {          mVeLocityTracker.recycle();          mVeLocityTracker = null;        }        //修正mtouchState值        mtouchState = touch_STATE_REST ;        break;      case MotionEvent.ACTION_CANCEL:        mtouchState = touch_STATE_REST ;        break;    }    return true ;  }  //我们是缓慢移动的,因此需要根据偏移值判断目标屏是哪个  private voID snapToDestination(){    //判断是否超过下一屏的中间位置,如果达到就抵达下一屏,否则保持在原屏幕    //公式意思是:假设当前滑屏偏移值即 scrollCurX 加上每个屏幕一半的宽度,除以每个屏幕的宽度就是我们目标屏所在位置了。    int destScreen = (getScrollX() + getWIDth() / 2 ) / getWIDth() ;    snapToScreen(destScreen);  }  //真正的实现跳转屏幕的方法  private voID snapToScreen(int whichScreen){    //简单的移到目标屏幕,可能是当前屏或者下一屏幕,直接跳转过去,不太友好,为了友好性,我们在增加一个动画效果    curScreen = whichScreen ;    //防止屏幕越界,即超过屏幕数    if(curScreen > getChildCount() - 1)      curScreen = getChildCount() - 1 ;    //为了达到下一屏幕或者当前屏幕,我们需要继续滑动的距离.根据dx值,可能向左滑动,也可能向右滑动    int dx = curScreen * getWIDth() - getScrollX() ;    mScroller.startScroll(getScrollX(),dx,Math.abs(dx) * 2);    //由于触摸事件不会重新绘制VIEw,所以此时需要手动刷新VIEw 否则没效果    invalIDate();  }  @OverrIDe  public voID computeScroll() {    //重写computeScroll()方法,并在其内部完成平滑滚动的逻辑    if (mScroller.computeScrollOffset()) {      scrollTo(mScroller.getCurrX(),mScroller.getCurrY());      invalIDate();    }  }}

代码比较长,但思路比较清晰。
(1)首先在ScrollerLayout的构造函数里面我们创建Scroller的实例,由于Scroller的实例只需创建一次,因此我们把它放到构造函数里面执行。另外在构建函数中我们还初始化的touchSlop的值,这个值在后面将用于判断当前用户的 *** 作是否是拖动。
(2)接着重写onMeasure()方法和onLayout()方法,在onMeasure()方法中测量ScrollerLayout里的每一个子控件的大小,在onLayout()方法中为ScrollerLayout里的每一个子控件在水平方向上进行布局,布局类似于方向为horizontal的linearLayout。
(3) 接着重写onIntercepttouchEvent()方法, 在这个方法中我们记录了用户手指按下时的X坐标位置,以及用户手指在屏幕上拖动时的X坐标位置,当两者之间的距离大于touchSlop值时,就认为用户正在拖动布局,置状态为touch_STATE_SCRolliNG,当用户手指抬起,重置状态为touch_STATE_REST。这里当状态值为touch_STATE_SCRolliNG时返回true,将事件在这里拦截掉,阻止事件传递到子控件当中。
(4)那么当我们把事件拦截掉之后,就会将事件交给ScrollerLayout的ontouchEvent()方法来处理。
如果当前事件是ACTION_MOVE,说明用户正在拖动布局,那么我们就应该对布局内容进行滚动从而影响拖动事件,实现的方式就是使用我们刚刚所学的scrollBy()方法,用户拖动了多少这里就scrollBy多少。另外为了防止用户拖出边界这里还专门做了边界保护,当拖出边界时就调用scrollTo()方法来回到边界位置。
如果当前事件是ACTION_UP时,说明用户手指抬起来了,但是目前很有可能用户只是将布局拖动到了中间,我们不可能让布局就这么停留在中间的位置,因此接下来就需要借助Scroller来完成后续的滚动 *** 作。首先计算滚动速率,判断当前动作是scroll还是fling。如果是fling,再根据fling的方向跳转到上一页或者下一页,调用函数snapToScreen。如果是scroll,就调用函数snapToDestination,函数中首先根据当前的滚动位置来计算布局应该继续滚动到哪一页,滚动到哪一页同样调用snapToScreen。再来看看snapToScreen写法吧,其实是调用startScroll()方法来滚动数据,紧接着调用invalIDate()方法来刷新界面。
(5)重写computeScroll()方法,并在其内部完成平滑滚动的逻辑 。在整个后续的平滑滚动过程中,computeScroll()方法是会一直被调用的,因此我们需要不断调用Scroller的computeScrollOffset()方法来进行判断滚动 *** 作是否已经完成了,如果还没完成的话,那就继续调用scrollTo()方法,并把Scroller的curX和curY坐标传入,然后刷新界面从而完成平滑滚动的 *** 作。

现在ScrollerLayout已经准备好了,接下来我们修改activity_main.xml布局中的内容,如下所示:

<?xml version="1.0" enCoding="utf-8"?><com.hx.scroller.ScrollerLayout xmlns:androID="http://schemas.androID.com/apk/res/androID"  androID:layout_wIDth="match_parent"  androID:layout_height="match_parent">  <ImageVIEw    androID:layout_wIDth="match_parent"    androID:layout_height="200dp"    androID:background="@drawable/crazy_1" />  <ImageVIEw    androID:layout_wIDth="match_parent"    androID:layout_height="200dp"    androID:background="@drawable/crazy_2" />  <ImageVIEw    androID:layout_wIDth="match_parent"    androID:layout_height="200dp"    androID:background="@drawable/crazy_3" /></com.hx.scroller.ScrollerLayout>

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持编程小技巧。

总结

以上是内存溢出为你收集整理的Android Scroller完全解析全部内容,希望文章能够帮你解决Android Scroller完全解析所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/1147910.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-31
下一篇 2022-05-31

发表评论

登录后才能评论

评论列表(0条)

保存