Android Scroller大揭秘

Android Scroller大揭秘,第1张

概述在学习使用Scroller之前,需要明白scrollTo()、scrollBy()方法。一、View的scrollTo()、scrollBy()

在学习使用Scroller之前,需要明白scrollTo()、scrollBy()方法。

一、VIEw的scrollTo()、scrollBy()

scrollTo、scrollBy方法是VIEw中的,因此任何的VIEw都可以通过这两种方法进行移动。首先要明白的是,scrollTo、scrollBy滑动的是VIEw中的内容(而且还是整体滑动),而不是VIEw本身。我们的滑动控件如SrollVIEw可以限定宽、高大小,以及在布局中的位置,但是滑动控件中的内容(或者里面的childVIEw)可以是无限长、宽的,我们调用VIEw的scrollTo、scrollBy方法,相当于是移动滑动控件中的画布Canvas,然后进行重绘,屏幕上也就显示相应的内容。如下:

1、getScrollX()、getScrollY()

在学习scrollTo()、scrollBy()之前,先来了解一下getScrollX()、getScrollY()方法。

getScrollX()、getScrollY()得到的是偏移量,是相对自己初始位置的滑动偏移距离,只有当有scroll事件发生时,这两个方法才能有值,否则getScrollX()、getScrollY()都是初始时的值0,而不管你这个滑动控件在哪里。所谓自己初始位置是指,控件在刚开始显示时、没有滑动前的位置。以getScrollX()为例,其源码如下:

 public final int getScrollX() { return mScrollX;}

可以看到getScrollX()直接返回的就是mScrollX,代表水平方向上的偏移量,getScrollY()也类似。偏移量mScrollX的正、负代表着,滑动控件中的内容相对于初始位置在水平方向上偏移情况,mScrollX为正代表着当前内容相对于初始位置向左偏移了mScrollX的距离,mScrollX为负表示当前内容相对于初始位置向右偏移了mScrollX的距离。

这里的坐标系和我们平常的认知正好相反。为了以后更方便的处理滑动相关坐标和偏移,在处理偏移、滑动相关的功能时,我们就可以把坐标反过来看,如下图:

因为滑动控件中的内容是整体进行滑动的,同时也是相对于自己显示时的初始位置的偏移,对于VIEw中内容在偏移时的参考坐标原点(注意是内容视图的坐标原点,不是图中说的滑动控件的原点),可以选择初始位置的某一个地方,因为滑动时整体行为,在进行滑动的时候从这个选择的原点出进行分析即可。

2、scrollTo()、scrollBy()

scrollTo(int x,int y)移动的是VIEw中的内容,而滑动控件中的内容都是整体移动的,scrollTo(int x,int y)中的参数表示VIEw中的内容要相对于内容初始位置移动x和y的距离,即将内容移动到距离内容初始位置x和y的位置。正如前面所说,在处理偏移、滑动问题时坐标系和平常认知的坐标系是相反的。以一个例子说明scrollTo():

说明:图中黄色矩形区域表示的是一个可滑动的VIEw控件,绿色虚线矩形为滑动控件中的滑动内容。注意这里的坐标是相反的。(例子来源于:http://blog.csdn.net/bigconvience/article/details/26697645)

(1)调用scrollTo(100,0)表示将VIEw中的内容移动到距离内容初始显示位置的x=100,y=0的地方,效果如下图:

(2)调用scrollTo(0,100)效果如下图:

(3)调用scrollTo(100,100)效果如下图:

(4)调用scrollTo(-100,0)效果如下图:

通过上面几个图,可以清楚看到scrollTo的作用和滑动坐标系的关系。在实际使用中,我们一般是在ontouchEvent()方法中处理滑动事件,在MotionEvent.ACTION_MOVE时调用scrollTo(int x,int y)进行滑动,在调用scrollTo(int x,int y)前,我们先要计算出两个参数值,即水平和垂直方向需要滑动的距离,如下:

@OverrIDe public boolean ontouchEvent(MotionEvent event) {  int y = (int) event.getY();  int action = event.getAction();  switch (action){  case MotionEvent.ACTION_DOWN:  mLastY = y;  break;  case MotionEvent.ACTION_MOVE:  int dy = mLastY - y;//本次手势滑动了多大距离  int oldScrollY = getScrollY();//先计算之前已经偏移了多少距离  int scrollY = oldScrollY + dy;//本次需要偏移的距离=之前已经偏移的距离+本次手势滑动了多大距离  if(scrollY < 0){  scrollY = 0;  }  if(scrollY > getHeight() - mScreenHeight){  scrollY = getHeight() - mScreenHeight;  }  scrollTo(getScrollX(),scrollY);  mLastY = y;  break;  }  return true; } 

上面在计算参数时,分为了三步。第一是,通过int dy = mLastY - y;得到本次手势在屏幕上滑动了多少距离,这里要特别注意这个相减顺序,因为这里的坐标与平常是相反的,因此,手势滑动距离是按下时的坐标mLastY - 当前的坐标y;第二是,通过oldScrollY = getScrollY();获得滑动内容之前已经距初始位置便宜了多少;第三是,计算本次需要偏移的参数int scrollY = oldScrollY + dy; 后面通过两个if条件进行了边界处理,然后调用scrollTo进行滑动。调用完scrollTo后,新的偏移量又重新产生了。从scrollTo源码中可以看到:

public voID scrollTo(int x,int y) {  if (mScrollX != x || mScrollY != y) {  int oldX = mScrollX;  int oldY = mScrollY;  mScrollX = x;//赋值新的x偏移量  mScrollY = y;//赋值新的y偏移量  invalIDateParentCaches();  onScrollChanged(mScrollX,mScrollY,oldX,oldY);  if (!awakenScrollbars()) {  postInvalIDateOnAnimation();  }  }  } 

scrollTo是相对于初始位置来进行移动的,而scrollBy(int x,int y)则是相对于上一次移动的距离来进行本次移动。scrollBy其实还是依赖于scrollTo的,如下源码:

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

可以看到,使用scrollBy其实就是省略了我们在计算scrollTo参数时的第三步而已,因为scrollBy内部已经自己帮我加上了第三步的计算。因此scrollBy的作用就是相当于在上一次的偏移情况下进行本次的偏移。

一个完整的水平方向滑动的例子:

public class MyVIEwPager extends VIEwGroup {  private int mLastX;  public MyVIEwPager(Context context) {  super(context);  init(context);  }  public MyVIEwPager(Context context,AttributeSet attrs) {  super(context,attrs);  init(context);  }  public MyVIEwPager(Context context,AttributeSet attrs,int defStyleAttr) {  super(context,attrs,defStyleAttr);  init(context);  }  private voID init(Context context) {  }  @OverrIDe  protected voID onMeasure(int wIDthMeasureSpec,int heightmeasureSpec) {  super.onMeasure(wIDthMeasureSpec,heightmeasureSpec);  int count = getChildCount();  for(int i = 0; i < count; i++){  VIEw child = getChildAt(i);  child.measure(wIDthMeasureSpec,heightmeasureSpec);  }  }  @OverrIDe  protected voID onLayout(boolean changed,int l,int t,int r,int b) {  int count = getChildCount();  Log.d("TAG","--l-->"+l+",--t-->"+t+",-->r-->"+r+",--b-->"+b);  for(int i = 0; i < count; i++){  VIEw child = getChildAt(i);  child.layout(i * getWIDth(),t,(i+1) * getWIDth(),b);  }  }  @OverrIDe  public boolean ontouchEvent(MotionEvent ev) {  int x = (int) ev.getX();  switch (ev.getAction()){  case MotionEvent.ACTION_DOWN:  mLastX = x;  break;  case MotionEvent.ACTION_MOVE:  int dx = mLastX - x;  int oldScrollX = getScrollX();//原来的偏移量  int preScrollX = oldScrollX + dx;//本次滑动后形成的偏移量  if(preScrollX > (getChildCount() - 1) * getWIDth()){   preScrollX = (getChildCount() - 1) * getWIDth();  }  if(preScrollX < 0){   preScrollX = 0;  }  scrollTo(preScrollX,getScrollY());  mLastX = x;  break;  }  return true;  } } 

布局文件:

<?xml version="1.0" enCoding="utf-8"?> <linearLayout xmlns:androID="http://schemas.androID.com/apk/res/androID" androID:layout_wIDth="match_parent" androID:layout_height="match_parent" androID:orIEntation="vertical">  <com.scu.lly.vIEwtest.vIEw.MyVIEwPager androID:layout_wIDth="match_parent" androID:layout_height="300dp" >  <ImageVIEw androID:layout_wIDth="match_parent" androID:layout_height="match_parent" androID:scaleType="fitXY" androID:src="@drawable/test1" />  <ImageVIEw androID:layout_wIDth="match_parent" androID:layout_height="match_parent" androID:scaleType="fitXY" androID:src="@drawable/test2" />  <ImageVIEw androID:layout_wIDth="match_parent" androID:layout_height="match_parent" androID:scaleType="fitXY" androID:src="@drawable/test3" />  <ImageVIEw androID:layout_wIDth="match_parent" androID:layout_height="match_parent" androID:scaleType="fitXY" androID:src="@drawable/test4" />  </com.scu.lly.vIEwtest.vIEw.MyVIEwPager> </linearLayout> 

效果如图:

二、Scroller滑动辅助类

根据我们上面的分析,可知VIEw的scrollTo()、scrollBy()是瞬间完成的,当我们的手指在屏幕上移动时,内容会跟着手指滑动,但是当我们手指一抬起时,滑动就会停止,如果我们想要有一种惯性的滚动过程效果和回d效果,此时就需要使用Scroller辅助类。

但是注意的是,Scroller本身不会去移动VIEw,它只是一个移动计算辅助类,用于跟踪控件滑动的轨迹,只相当于一个滚动轨迹记录工具,最终还是通过VIEw的scrollTo、scrollBy方法完成VIEw的移动的。

在使用Scroller类之前,先了解其重要的两个方法:

(1)startScroll()

public voID startScroll(int startX,int startY,int dx,int dy,int duration)

开始一个动画控制,由(startX,startY)在duration时间内前进(dx,dy)个单位,即到达偏移坐标为(startX+dx,startY+dy)处。

(2)computeScrollOffset()

public boolean computeScrollOffset()

滑动过程中,根据当前已经消逝的时间计算当前偏移的坐标点,保存在mCurrX和mCurrY值中。

上面两个方法的源码如下:

public class Scroller {private int mStartX;//水平方向,滑动时的起点偏移坐标private int mStartY;//垂直方向,滑动时的起点偏移坐标private int mFinalX;//滑动完成后的偏移坐标,水平方向private int mFinalY;//滑动完成后的偏移坐标,垂直方向private int mCurrX;//滑动过程中,根据消耗的时间计算出的当前的滑动偏移距离,水平方向private int mCurrY;//滑动过程中,根据消耗的时间计算出的当前的滑动偏移距离,垂直方向private int mDuration; //本次滑动的动画时间private float mDeltaX;//滑动过程中,在达到mFinalX前还需要滑动的距离,水平方向private float mDeltaY;//滑动过程中,在达到mFinalX前还需要滑动的距离,垂直方向public voID startScroll(int startX,int dy) { startScroll(startX,startY,dx,dy,DEFAulT_DURATION);}/** * 开始一个动画控制,由(startX,startY+dy)处*/public voID startScroll(int startX,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;}/** * 滑动过程中,根据当前已经消逝的时间计算当前偏移的坐标点,保存在mCurrX和mCurrY值中 * @return*/public boolean computeScrollOffset() { if (mFinished) {//已经完成了本次动画控制,直接返回为false 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);//计算出当前的滑动偏移位置,x轴 mCurrY = mStartY + Math.round(x * mDeltaY);//计算出当前的滑动偏移位置,y轴 break; ... } }else { mCurrX = mFinalX; mCurrY = mFinalY; mFinished = true; } return true; } ...}

Scroller类中最重要的两个方法就是startScroll()和computeScrollOffset(),但是Scroller类只是一个滑动计算辅助类,它的startScroll()和computeScrollOffset()方法中也只是对一些轨迹参数进行设置和计算,真正需要进行滑动还是得通过VIEw的scrollTo()、scrollBy()方法。为此,VIEw中提供了computeScroll()方法来控制这个滑动流程。computeScroll()方法会在绘制子视图的时候进行调用。其源码如下:

/**  * 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必须实现方法体  } 

因此Scroller类的基本使用流程可以总结如下:

(1)首先通过Scroller类的startScroll()开始一个滑动动画控制,里面进行了一些轨迹参数的设置和计算;

(2)在调用startScroll()的后面调用invalIDate();引起视图的重绘 *** 作,从而触发VIEwGroup中的computeScroll()被调用;

(3)在computeScroll()方法中,先调用Scroller类中的computeScrollOffset()方法,里面根据当前消耗时间进行轨迹坐标的计算,然后取得计算出的当前滑动的偏移坐标,调用VIEw的scrollTo()方法进行滑动控制,最后也需要调用invalIDate();进行重绘。

如下的一个简单代码示例:  

@OverrIDe  public boolean ontouchEvent(MotionEvent ev) {  initVeLocityTrackerIfNotExists();  mVeLocityTracker.addMovement(ev);  int x = (int) ev.getX();  switch (ev.getAction()){  case MotionEvent.ACTION_DOWN:  if(!mScroller.isFinished()){   mScroller.abortAnimation();  }  mLastX = x;  break;  case MotionEvent.ACTION_MOVE:  int dx = mLastX - x;  int oldScrollX = getScrollX();//原来的偏移量  int preScrollX = oldScrollX + dx;//本次滑动后形成的偏移量  if(preScrollX > (getChildCount() - 1) * getWIDth()){   preScrollX = (getChildCount() - 1) * getWIDth();  }  if(preScrollX < 0){   preScrollX = 0;  }  //开始滑动动画  mScroller.startScroll(mScroller.getFinalX(),mScroller.getFinalY(),0);//第一步  //注意,一定要进行invalIDate刷新界面,触发computeScroll()方法,因为单纯的startScroll()是属于Scroller的,只是一个辅助类,并不会触发界面的绘制  invalIDate();  mLastX = x;  break;  }  return true;  }  @OverrIDe  public voID computeScroll() {  super.computeScroll();  if(mScroller.computeScrollOffset()){//第二步  scrollTo(mScroller.getCurrX(),mScroller.getCurrY());//第三步  invalIDate();  }  } 

下面是一个完整的例子:一个类似VIEwPager的Demo,效果图如下:

代码如下:

public class MyVIEwPager3 extends VIEwGroup {  private int mLastX;  private Scroller mScroller;  private VeLocityTracker mVeLocityTracker;  private int mtouchSlop;  private int mMaxVeLocity;  /**  * 当前显示的是第几个屏幕  */  private int mCurrentPage = 0;  public MyVIEwPager3(Context context) {  super(context);  init(context);  }  public MyVIEwPager3(Context context,attrs);  init(context);  }  public MyVIEwPager3(Context context,defStyleAttr);  init(context);  }  private voID init(Context context) {  mScroller = new Scroller(context);  VIEwConfiguration config = VIEwConfiguration.get(context);  mtouchSlop = config.getScaledPagingtouchSlop();  mMaxVeLocity = config.getScaledMinimumFlingVeLocity();  }  @OverrIDe  protected voID onMeasure(int wIDthMeasureSpec,heightmeasureSpec);  }  }  @OverrIDe  protected voID onLayout(boolean changed,(i + 1) * getWIDth(),b);  }  }  @OverrIDe  public boolean ontouchEvent(MotionEvent ev) {  initVeLocityTrackerIfNotExists();  mVeLocityTracker.addMovement(ev);  int x = (int) ev.getX();  switch (ev.getAction()){  case MotionEvent.ACTION_DOWN:  if(!mScroller.isFinished()){   mScroller.abortAnimation();  }  mLastX = x;  break;  case MotionEvent.ACTION_MOVE:  int dx = mLastX - x;  /* 注释的里面是使用startScroll()来进行滑动的  int oldScrollX = getScrollX();//原来的偏移量  int preScrollX = oldScrollX + dx;//本次滑动后形成的偏移量  if (preScrollX > (getChildCount() - 1) * getWIDth()) {   preScrollX = (getChildCount() - 1) * getWIDth();   dx = preScrollX - oldScrollX;  }  if (preScrollX < 0) {   preScrollX = 0;   dx = preScrollX - oldScrollX;  }  mScroller.startScroll(mScroller.getFinalX(),0);  //注意,使用startScroll后面一定要进行invalIDate刷新界面,触发computeScroll()方法,因为单纯的startScroll()是属于Scroller的,只是一个辅助类,并不会触发界面的绘制  invalIDate();  */  //但是一般在ACTION_MOVE中我们直接使用scrollTo或者scrollBy更加方便  scrollBy(dx,0);  mLastX = x;  break;  case MotionEvent.ACTION_UP:  final VeLocityTracker veLocityTracker = mVeLocityTracker;  veLocityTracker.computeCurrentVeLocity(1000);  int initVeLocity = (int) veLocityTracker.getXVeLocity();  if(initVeLocity > mMaxVeLocity && mCurrentPage > 0){//如果是快速的向右滑,则需要显示上一个屏幕   Log.d("TAG","----------------快速的向右滑--------------------");   scrolltopage(mCurrentPage - 1);  }else if(initVeLocity < -mMaxVeLocity && mCurrentPage < (getChildCount() - 1)){//如果是快速向左滑动,则需要显示下一个屏幕   Log.d("TAG","----------------快速的向左滑--------------------");   scrolltopage(mCurrentPage + 1);  }else{//不是快速滑动的情况,此时需要计算是滑动到   Log.d("TAG","----------------慢慢的滑动--------------------");   slowScrolltopage();  }  recycleVeLocityTracker();  break;  }  return true;  }  /**  * 缓慢滑动抬起手指的情形,需要判断是停留在本Page还是往前、往后滑动  */  private voID slowScrolltopage() {  //当前的偏移位置  int scrollX = getScrollX();  int scrollY = getScrollY();  //判断是停留在本Page还是往前一个page滑动或者是往后一个page滑动  int whichPage = (getScrollX() + getWIDth() / 2 ) / getWIDth() ;  scrolltopage(whichPage);  }  /**  * 滑动到指定屏幕  * @param indexPage  */  private voID scrolltopage(int indexPage) {  mCurrentPage = indexPage;  if(mCurrentPage > getChildCount() - 1){  mCurrentPage = getChildCount() - 1;  }  //计算滑动到指定Page还需要滑动的距离  int dx = mCurrentPage * getWIDth() - getScrollX();  mScroller.startScroll(getScrollX(),Math.abs(dx) * 2);//动画时间设置为Math.abs(dx) * 2 ms  //记住,使用Scroller类需要手动invalIDate  invalIDate();  }  @OverrIDe  public voID computeScroll() {  Log.d("TAG","---------computeScrollcomputeScrollcomputeScroll--------------");  super.computeScroll();  if(mScroller.computeScrollOffset()){  scrollTo(mScroller.getCurrX(),mScroller.getCurrY());  invalIDate();  }  }  private voID recycleVeLocityTracker() {  if (mVeLocityTracker != null) {  mVeLocityTracker.recycle();  mVeLocityTracker = null;  }  }  private voID initVeLocityTrackerIfNotExists() {  if(mVeLocityTracker == null){  mVeLocityTracker = VeLocityTracker.obtain();  }  } } 

布局文件如下:

<?xml version="1.0" enCoding="utf-8"?><linearLayout xmlns:androID="http://schemas.androID.com/apk/res/androID" androID:layout_wIDth="match_parent" androID:layout_height="match_parent" androID:orIEntation="vertical"> <com.lusheep.vIEwtest.vIEw.MyVIEwPager3 androID:layout_wIDth="match_parent" androID:layout_height="200dp" androID:background="#999" > <ImageVIEw androID:layout_wIDth="300dp" androID:layout_height="match_parent" androID:scaleType="fitXY" androID:src="@drawable/test1" /> <ImageVIEw androID:layout_wIDth="300dp" androID:layout_height="match_parent" androID:scaleType="fitXY" androID:src="@drawable/test2" /> <ImageVIEw androID:layout_wIDth="300dp" androID:layout_height="match_parent" androID:scaleType="fitXY" androID:src="@drawable/test3" /> <ImageVIEw androID:layout_wIDth="300dp" androID:layout_height="match_parent" androID:scaleType="fitXY" androID:src="@drawable/test4" /> </com.lusheep.vIEwtest.vIEw.MyVIEwPager3></linearLayout>

点此下载

简单总结:

(1)Scroller类能够帮助我们实现高级的滑动功能,如手指抬起后的惯性滑动功能。使用流程为,首先通过Scroller类的startScroll()+invalIDate()触发VIEw的computeScroll(),在computeScroll()中让Scroller类去计算最新的坐标信息,拿到最新的坐标偏移信息后还是要调用VIEw的scrollTo来实现滑动。可以看到,使用Scroller的整个流程比较简单,关键的是控制滑动的一些逻辑计算,比如上面例子中的计算什么时候该往哪一页滑动...

(2)AndroID后面推出了OverScroller类,OverScroller在整体功能上和Scroller类似,使用也相同。OverScroller类可以完全代替Scroller,相比Scroller,OverScroller主要是增加了对滑动到边界的一些控制,如增加一些回d效果等,功能更加强大。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持编程小技巧!

总结

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

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

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

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

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

发表评论

登录后才能评论

评论列表(0条)

保存