前言
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方法
![](https://img.haomeiwen.com/i27820348/b7ed7827290857b3.png)
此外,其内部也维护了时钟
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
Scroller 经典案例
通过一个 SlidePanel 的例子,我们来深刻的了解一下 注意:在移动平台中,要明确知道 “滑动” 与 “滚动” 的不同,具体来说,滑动和滚动的方向总是相反的。
案例简介
我们利用Scroller来实现一个SlidingPanel,可以实现左侧和右侧都能侧滑
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);
}
事件处理
在Android中,一般滑动都是由事件驱动的,这里我们要记住需要在dispatchTouchEvent中处理事件,因为滑动过程中事件可能被拦截,因此在dispatchTouchEvent处理是非常必要的。
@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
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滑动的参考值。
/**
* 通过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都没有统一的规范。
总结
本篇到这里就结束了,通过本篇我们可以了解到Scroller与View的关系,其本身并不是完全依赖的,Scroller也不存在任何规范,仅仅提供运动差值计算而已。
网友评论