Android 中存在很多Scroller,实际上其本身和View的关系并不大,因为很多时候,自定义View你都不会用到Scroller,那么Scroller起到什么作用呢?
Scroller到底起到什么作用,以及为什么要使用Scroller?
无论从构造方法还是其他方法,以及 Scroller 的属性可知,其并不会持有 View,驱动ViewGroup 滑动。Scroller 只是个计算器,提供插值计算,让滚动过程具有动画属性,但它并不是View必须要有的,也不能驱动View滚动,真正作用是为了View滑动作参考,而参考方法一般是在View#computeScroll()方法中进行,而View#computeScroll()方法的调用仍然是通过View的invalidate -> draw 方法来驱动。
Scroller计算距离是通过Scroller#computeScrollOffset方法来进行的,而computeScrollOffset方法的调用一般是在View#computeScroll()中进行。
这个就问到了什么时候调用 computeScroll 了,如上所说 computeScroll 调用 Scroller#computeScrollOffset(),只要 computeScroll 调用连续,Scroller 也会连续,实质上 computeScroll 的连续性又 invalidate 方法控制,scrollTo,scrollBy 都会调用 invalidate,而 invalidate 回去触发 draw, 从而 computeScroll 被连续调用,综上,Scroller 也会被连续调用,除非 invalidate 停止调用。
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);Scroller 经典案例
注意:在移动平台中,要明确知道 “滑动” 与 “滚动” 的不同,具体来说,滑动和滚动的方向总是相反的。
public class SlidingPanel extends RelativeLayout{}当然,我们需要定义三个View,并且加入到布局中
private FrameLayout leftMenu; //左侧菜单 private FrameLayout middleMenu; //中间内容 private FrameLayout rightMenu; //右侧菜单 // 省略一些代码 addView(leftMenu); addView(middleMenu); addView(rightMenu);接下来我们创建一个Scroller,使其可以匀减速运动
mScroller = new Scroller(context, new DecelerateInterpolator());我们按正常方式测量和布局,但是左侧菜单和右侧菜单不能覆盖整个屏幕,这里给其宽度为 0.8f * screenWidth,布局按从左到右布局即可
@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; // 堆代码 duidaima.com 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一般的用法,用于滑动速度测量和差值计算。
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();循环调用
/** * 通过invalidate操纵,此方法通过draw方法调用 */ @Override public void computeScroll() { super.computeScroll(); if (!mScroller.computeScrollOffset()) { //计算currX,currY,并检测是否已完成“滚动” return; } int tempX = mScroller.getCurrX(); scrollTo(tempX, 0); //会重复调用invalidate }通过上述代码我们就实现了策划菜单,这里就不贴图了。
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都没有统一的规范。