Android Scroller 是如何运行的

前言

Android 中存在很多Scroller,实际上其本身和View的关系并不大,因为很多时候,自定义View你都不会用到Scroller,那么Scroller起到什么作用呢?

关于Scroller

Scroller到底起到什么作用,以及为什么要使用Scroller?

Scroller的作用

无论从构造方法还是其他方法,以及 Scroller 的属性可知,其并不会持有 View,驱动ViewGroup 滑动。

Scroller 只是个计算器,提供插值计算,让滚动过程具有动画属性,但它并不是View必须要有的,也不能驱动View滚动,真正作用是为了View滑动作参考,而参考方法一般是在View#computeScroll()方法中进行,而View#computeScroll()方法的调用仍然是通过View的invalidate -> draw 方法来驱动。

Scroller 计算机制

Scroller计算距离是通过Scroller#computeScrollOffset方法来进行的,而computeScrollOffset方法的调用一般是在View#computeScroll()中进行

如何保证计算结果连续

如何让 Scroller 的计算也是连续的?

这个就问到了什么时候调用 computeScroll 了,如上所说 computeScroll 调用 Scroller#computeScrollOffset(),只要 computeScroll 调用连续,Scroller 也会连续,实质上 computeScroll 的连续性又 invalidate 方法控制,scrollTo,scrollBy 都会调用 invalidate,而 invalidate 回去触发 draw, 从而 computeScroll 被连续调用,综上,Scroller 也会被连续调用,除非 invalidate 停止调用。

这点很像补间动画,在draw的时候触发,下面代码中,scrollTo中会调用到invalidate方法

此外,其内部也维护了时钟

java 复制代码
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);

Scroller 经典案例

通过一个 SlidePanel 的例子,我们来深刻的了解一下 注意:在移动平台中,要明确知道 "滑动" 与 "滚动" 的不同,具体来说,滑动和滚动的方向总是相反的。

案例简介

我们利用Scroller来实现一个SlidingPanel,可以实现左侧和右侧都能侧滑

java 复制代码
public class SlidingPanel extends RelativeLayout{}

当然,我们需要定义三个View,并且加入到布局中

java 复制代码
private FrameLayout leftMenu; //左侧菜单
private FrameLayout middleMenu; //中间内容
private FrameLayout rightMenu; //右侧菜单

// 省略一些代码
 addView(leftMenu);
 addView(middleMenu);
 addView(rightMenu);

接下来我们创建一个Scroller,使其可以匀减速运动

java 复制代码
mScroller = new Scroller(context, new DecelerateInterpolator());

我们按正常方式测量和布局,但是左侧菜单和右侧菜单不能覆盖整个屏幕,这里给其宽度为 0.8f * screenWidth,布局按从左到右布局即可

java 复制代码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    middleMenu.measure(widthMeasureSpec, heightMeasureSpec);
    middleMask.measure(widthMeasureSpec, heightMeasureSpec);
    int realWidth = MeasureSpec.getSize(widthMeasureSpec);
    int tempWidthMeasure = MeasureSpec.makeMeasureSpec(
            (int) (realWidth * 0.8f), MeasureSpec.EXACTLY);
    leftMenu.measure(tempWidthMeasure, heightMeasureSpec);
    rightMenu.measure(tempWidthMeasure, heightMeasureSpec);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
    middleMenu.layout(l, t, r, b);
    middleMask.layout(l, t, r, b);
    leftMenu.layout(l - leftMenu.getMeasuredWidth(), t, r, b);
    rightMenu.layout(
            l + middleMenu.getMeasuredWidth(),
            t,
            l + middleMenu.getMeasuredWidth()
                    + rightMenu.getMeasuredWidth(), b);
}

事件处理

在Android中,一般滑动都是由事件驱动的,这里我们要记住需要在dispatchTouchEvent中处理事件,因为滑动过程中事件可能被拦截,因此在dispatchTouchEvent处理是非常必要的。

java 复制代码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (!isSlideCompete) {
        handleSlideEvent(ev);
        return true;
    }
    if (isHorizontalScroll) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_MOVE:
                int curScrollX = getScrollX();
                int dis_x = (int) (ev.getX() - point.x);
                //滑动方向和滚动滚动条方向相反,因此dis_x必须取负值
                int expectX = -dis_x + curScrollX;

                if (dis_x > 0) {
                    Log.d("I", "Right-Slide,Left-Scroll");//向右滑动,向左滚动
                } else {
                    Log.d("I", "Left-Slide,Right-Scroll");
                }

                Log.e("I", "ScrollX=" + curScrollX + " , X=" + ev.getX() + " , dis_x=" + dis_x);
                //规定expectX的变化范围
                int finalX = Math.max(-leftMenu.getMeasuredWidth(), Math.min(expectX, rightMenu.getMeasuredWidth()));

                scrollTo(finalX, 0);
                point.x = (int) ev.getX();//更新,保证滑动平滑
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                curScrollX = getScrollX();
                if (Math.abs(curScrollX) > leftMenu.getMeasuredWidth() >> 1) {
                    if (curScrollX < 0) {
                        mScroller.startScroll(curScrollX, 0,
                                -leftMenu.getMeasuredWidth() - curScrollX, 0,
                                200);
                    } else {
                        mScroller.startScroll(curScrollX, 0,
                                leftMenu.getMeasuredWidth() - curScrollX, 0,
                                200);
                    }

                } else {
                    mScroller.startScroll(curScrollX, 0, -curScrollX, 0, 200);
                }
                invalidate();
                isHorizontalScroll = false;
                isSlideCompete = false;
                break;
        }
    } else {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_UP:
                isHorizontalScroll = false;
                isSlideCompete = false;
                break;

            default:
                break;
        }
    }

    return super.dispatchTouchEvent(ev);
}

从上面的代码中我们可以看到,Scroller一般使用在事件CANCEL或者UP时,这也是Scroller一般的用法,用于滑动速度测量和差值计算

同时我们不要忘了,mScroller.startScroll()调用之后,需要触发View#draw方法,当然可以使用invalidate

java 复制代码
if (Math.abs(curScrollX) > leftMenu.getMeasuredWidth() >> 1) {
    if (curScrollX < 0) {
        mScroller.startScroll(curScrollX, 0,
                -leftMenu.getMeasuredWidth() - curScrollX, 0,
                200);
    } else {
        mScroller.startScroll(curScrollX, 0,
                leftMenu.getMeasuredWidth() - curScrollX, 0,
                200);
    }

} else {
    mScroller.startScroll(curScrollX, 0, -curScrollX, 0, 200);
}
invalidate();

循环调用

我们开头说过,Scroller不会驱动View的滑动,所有的滑动都需要通过View自身来驱动,而在Vsync信号执行期间,我们需要通过computeScroll来获取Scroller滑动的参考值。

java 复制代码
/**
 * 通过invalidate操纵,此方法通过draw方法调用
 */
@Override
public void computeScroll() {
    super.computeScroll();
    if (!mScroller.computeScrollOffset()) {
        //计算currX,currY,并检测是否已完成"滚动"
        return;
    }
    int tempX = mScroller.getCurrX();
    scrollTo(tempX, 0); //会重复调用invalidate
}

通过上述代码我们就实现了策划菜单,这里就不贴图了。

完整代码

java 复制代码
public class SlidingPanel extends RelativeLayout {
   private Context context;
   private FrameLayout leftMenu;
   private FrameLayout middleMenu;
   private FrameLayout rightMenu;
   private FrameLayout middleMask;
   private Scroller mScroller;
   public  final int LEFT_ID = 0xaabbcc;
   public  final int MIDEELE_ID = 0xaaccbb;
   public  final int RIGHT_ID = 0xccbbaa;

   private boolean isSlideCompete;
   private boolean isHorizontalScroll;

   private Point point = new Point();
   private static final int SLIDE_SLOP = 20;

   public SlidingPanel(Context context) {
      super(context);
      initView(context);
   }

   public SlidingPanel(Context context, AttributeSet attrs) {
      super(context, attrs);
      initView(context);
   }

   private void initView(Context context) {

      this.context = context;
      mScroller = new Scroller(context, new DecelerateInterpolator());
      leftMenu = new FrameLayout(context);
      middleMenu = new FrameLayout(context);
      rightMenu = new FrameLayout(context);
      middleMask = new FrameLayout(context);
      leftMenu.setBackgroundColor(Color.RED);
      middleMenu.setBackgroundColor(Color.GREEN);
      rightMenu.setBackgroundColor(Color.RED);
      middleMask.setBackgroundColor(0x88000000);

      addView(leftMenu);
      addView(middleMenu);
      addView(rightMenu);
      addView(middleMask);

      middleMask.setAlpha(0);
   }
   public float onMiddleMask(){
      return middleMask.getAlpha();
   }
   
   @Override
   public void scrollTo(int x, int y) {
      super.scrollTo(x, y);
      onMiddleMask();
   // Log.e("getScrollX","getScrollX="+getScrollX());//可以是负值
      int curX = Math.abs(getScrollX());
      float scale = curX/(float)leftMenu.getMeasuredWidth();
      middleMask.setAlpha(scale);
      
   }

   @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      middleMenu.measure(widthMeasureSpec, heightMeasureSpec);
      middleMask.measure(widthMeasureSpec, heightMeasureSpec);
      int realWidth = MeasureSpec.getSize(widthMeasureSpec);
      int tempWidthMeasure = MeasureSpec.makeMeasureSpec(
            (int) (realWidth * 0.8f), MeasureSpec.EXACTLY);
      leftMenu.measure(tempWidthMeasure, heightMeasureSpec);
      rightMenu.measure(tempWidthMeasure, heightMeasureSpec);
   }

   @Override
   protected void onLayout(boolean changed, int l, int t, int r, int b) {
      super.onLayout(changed, l, t, r, b);
      middleMenu.layout(l, t, r, b);
      middleMask.layout(l, t, r, b);
      leftMenu.layout(l - leftMenu.getMeasuredWidth(), t, r, b);
      rightMenu.layout(
            l + middleMenu.getMeasuredWidth(),
            t,
            l + middleMenu.getMeasuredWidth()
                  + rightMenu.getMeasuredWidth(), b);
   }



   @Override
   public boolean dispatchTouchEvent(MotionEvent ev) {
      if (!isSlideCompete) {
         handleSlideEvent(ev);
         return true;
      }
      if (isHorizontalScroll) {
         switch (ev.getActionMasked()) {
         case MotionEvent.ACTION_MOVE:
            int curScrollX = getScrollX();
            int dis_x = (int) (ev.getX() - point.x);
            //滑动方向和滚动滚动条方向相反,因此dis_x必须取负值
            int expectX = -dis_x + curScrollX;

            if(dis_x>0)
            {
               Log.d("I","Right-Slide,Left-Scroll");//向右滑动,向左滚动
            }else{
               Log.d("I","Left-Slide,Right-Scroll");
            }

            Log.e("I","ScrollX="+curScrollX+" , X="+ev.getX()+" , dis_x="+dis_x);
            //规定expectX的变化范围
             int    finalX = Math.max(-leftMenu.getMeasuredWidth(),Math.min(expectX, rightMenu.getMeasuredWidth()));

            scrollTo(finalX, 0);
            point.x = (int) ev.getX();//更新,保证滑动平滑
            break;

         case MotionEvent.ACTION_UP:
         case MotionEvent.ACTION_CANCEL:
            curScrollX = getScrollX();
            if (Math.abs(curScrollX) > leftMenu.getMeasuredWidth() >> 1) {
               if (curScrollX < 0) {
                  mScroller.startScroll(curScrollX, 0,
                        -leftMenu.getMeasuredWidth() - curScrollX, 0,
                        200);
               } else {
                  mScroller.startScroll(curScrollX, 0,
                        leftMenu.getMeasuredWidth() - curScrollX, 0,
                        200);
               }

            } else {
               mScroller.startScroll(curScrollX, 0, -curScrollX, 0, 200);
            }
            invalidate();
            isHorizontalScroll = false;
            isSlideCompete = false;
            break;
         }
      } else {
         switch (ev.getActionMasked()) {
         case MotionEvent.ACTION_UP:
            isHorizontalScroll = false;
            isSlideCompete = false;
            break;

         default:
            break;
         }
      }

      return super.dispatchTouchEvent(ev);
   }

   /**
    * 通过invalidate操纵,此方法通过draw方法调用
    */
   @Override
   public void computeScroll() {
      super.computeScroll();
      if (!mScroller.computeScrollOffset()) {
         //计算currX,currY,并检测是否已完成"滚动"
         return;
      }
      int tempX = mScroller.getCurrX();
      scrollTo(tempX, 0); //会重复调用invalidate
   }


   private void handleSlideEvent(MotionEvent ev) {
      switch (ev.getAction()&MotionEvent.ACTION_MASK) {
      case MotionEvent.ACTION_DOWN:
         point.x = (int) ev.getX();
         point.y = (int) ev.getY();
         super.dispatchTouchEvent(ev);
         break;

      case MotionEvent.ACTION_MOVE:
         int dX = Math.abs((int) ev.getX() - point.x);
         int dY = Math.abs((int) ev.getY() - point.y);
         if (dX >= SLIDE_SLOP && dX > dY) { // 左右滑动
            isHorizontalScroll = true;
            isSlideCompete = true;
            point.x = (int) ev.getX();
            point.y = (int) ev.getY();
         } else if (dY >= SLIDE_SLOP && dY > dX) { // 上下滑动
            isHorizontalScroll = false;
            isSlideCompete = true;
            point.x = (int) ev.getX();
            point.y = (int) ev.getY();
         }
         break;
      case MotionEvent.ACTION_UP:
      case MotionEvent.ACTION_OUTSIDE:
      case MotionEvent.ACTION_CANCEL:
         super.dispatchTouchEvent(ev);
         isHorizontalScroll = false;
         isSlideCompete = false;
         break;
      }
   }

}

补充点

在Android 中,Scroller并没有统一的用法,也没有统一的规范,实际上Scroller仅仅是一个普通的类,但是Scroller 也未必一定需要按照现有模式运行。我们以ViewFlinger为例,实际上它本身就按照自己模式运行,总体上来说,无论是Scroller还是ViewFlinger都没有统一的规范。

总结

本篇到这里就结束了,通过本篇我们可以了解到Scroller与View的关系,其本身并不是完全依赖的,Scroller也不存在任何规范,仅仅提供运动差值计算而已。

相关推荐
萧雾宇4 分钟前
Android Compose打造仿现实逼真的烟花特效
android·flutter·kotlin
喜欢你,还有大家23 分钟前
FTP文件传输服务
linux·运维·服务器·前端
该用户已不存在27 分钟前
你没有听说过的7个Windows开发必备工具
前端·windows·后端
翻滚丷大头鱼31 分钟前
android 性能优化—ANR
android·性能优化
翻滚丷大头鱼36 分钟前
android 性能优化—内存泄漏,内存溢出OOM
android·性能优化
Bi38 分钟前
Dokploy安装和部署项目流程
运维·前端
普通网友40 分钟前
前端安全攻防:XSS, CSRF 等防范与检测
前端·安全·xss
携欢42 分钟前
PortSwigger靶场之Reflected XSS into attribute with angle brackets HTML-encoded通关秘籍
前端·xss
小爱同学_1 小时前
React知识:useState和useRef的使用
前端·react.js
拜无忧1 小时前
【教程】flutter常用知识点总结-针对小白
android·flutter·android studio