背景
这是一个滑动帮助类,并不可以使View真正的滑动,而是根据时间的流逝,获取插值器中的数据,传递给我们,让我们去配合scrollTo/scrollBy去让view产生缓慢滑动,产生动画的效果,其实是和属性动画同一个原理。下面是官方文档对于这个类所给的解释:
This class encapsulates scrolling. You can use scrollers (Scroller or OverScroller) to collect the data you need to produce a scrolling animation—for example, in response to a fling gesture. Scrollers track scroll offsets for you over time, but they don’t automatically apply those positions to your view. It’s your responsibility to get and apply new coordinates at a rate that will make the scrolling animation look smooth.
一.scroller的绘制过程:
调用public void startScroll(int startX, int startY, int dx, int dy)
该方法为scroll做一些准备工作.
比如设置了移动的起始坐标,滑动的距离和方向以及持续时间等.
该方法并不是真正的滑动scroll的开始,感觉叫prepareScroll()更贴切些.调用invalidate()或者postInvalidate()使View(ViewGroup)树重绘
重绘会调用View的draw()方法
draw()一共有六步:绘制背景
保存画布
调用onDraw()绘制内容
去调用dispatchDraw()绘制子View
If necessary, draw the fading edges and restore layers
Draw decorations (scrollbars for instance)
其中最重要的是第三步和第四步,重绘分两种情况:
2.1 . ViewGroup的重绘
在完成第三步onDraw()以后,进入第四步ViewGroup重写了
父类View的dispatchDraw()绘制子View,于是这样继续调用:
dispatchDraw()-->drawChild()-->child.computeScroll();
2.2 .View的重绘
当View调用invalidate()方法时,会导致整个View树进行从上至下的一次重绘.比如从最外层的Layout到里层的Layout,直到每个子View.在重绘View树时ViewGroup和View时按理都会经过onMeasure()和onLayout()以及onDraw()方法。
当然系统会判断这三个方法是否都必须执行,如果没有必要就不会调用.看到这里就明白了:当这个子View的父容器重绘时,也会调用上面提到的线路:onDraw()-->dispatchDraw()-->drawChild()-->child.computeScroll();
于是子View(比如此处举例的ButtonSubClass类)中重写的computeScroll()方法就会被调用到.
3.** View树的重绘会调用到View中的computeScroll()方法**
4.** 在computeScroll()方法中,在View的源码中可以看到public void computeScroll(){}是一个空方法. 具体的实现需要自己来写.在该方法中我们可调用scrollTo()或scrollBy()来实现移动.该方法才是实现移动的核心.**
4.1 利用Scroller的mScroller.computeScrollOffset()判断移动过程是否完成
注意:该方法是Scroller中的方法而不是View中的
public boolean computeScrollOffset(){
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.
}
返回true时表示还移动还没有完成.
4.2 若动画没有结束,则调用:scrollTo(By)();使其滑动scrolling
5.再次调用invalidate()
调用invalidate()方法那么又会重绘View树.
从而跳转到第3步,如此循环,直到computeScrollOffset返回false
二.onMeasure、onLayout、draw 关系
onMeasure()方法
onMeasure(int widthMeasureSpec,int heightMeasureSpec)
1、调用时间:当控件的父元素放置该控件时,用于告诉父元素该控件需要的大小。
2、传入参数:widthMeasureSpec,heightMeasureSpec。这两个传入参数由高32位和低16位组成,高32位保存的值叫specMode,可以通过MeasureSpec.getMode()获取;低16位为specSize可以由MeasureSpec.getSize()获取。这两个值是由ViewGroup中的layout_width,layout_height和padding以及View自身的layout_margin共同决定。权值weight也是尤其需要考虑的因素,有它的存在情况可能会稍微复杂点。
specMode可以取三个值:MeasureSpec.EXACTLY ,MeasureSpec.AT_MOST,MeasureSpec.UNSPECIFIED;specMode与layout_的对应关系如下:
match_parent - MeasureSpec.EXACTLY:当layout_为match_parent或者为某一具体值的时候specMode为EXACTLY代表精确的值;
wrap_content - MeasureSpec.AT_MOST:表示能获得的最大尺寸;
当无法确定尺寸的时候则是 MeasureSpec.UNSPECIFIED,这时候specSize会为最小值(即0);
3、可以在onMeasure()中来计算控件的尺寸,然后根据setMeasuredDimension(mWidth,mHeight);方法来告诉父控件此控件需要的尺寸,onMeasure()方法中必须调用此方法。
4、值得注意的是:
1)specSize和传入setMeasuredDimension()方法中的值的单位都是px(dp*density就是px)。2)match_parent并不是填充整个父容器,而是在不覆盖已经加入父容器的控件的情况下填充父容器。
onLayout()方法
onLayout(boolean changed, int left, int top,int right,int bottom);
父容器的onLayout()调用子类的onLayout()来确定子view在viewGroup中的位置,如:onLayout(10,10,100,100)表示子容器在父容器中(10,10)位置显示,长、宽都是90。结合onMeasure()方法使用可以确定子view的布局。
onDraw()方法
onDraw(Canvas canvas)
自定义view的关键方法,用于绘制界面,可以重写此方法以绘制自定义View。
onMeasure 属于View的方法,用来测量自己和内容的来确定宽度和高度 ,view的measure方法体中会调用onMeasure。
onLayout属于ViewGroup的方法,用来为当前ViewGroup的子元素分配位置和大小 View的layout方法体中会调用onLayout。
onMeasure和onLayout, onMeasure在onLayout之前调用。
设置background后,会重新调用onMeasure和onLayout,onMeasure测量子VIEW大小后调用LAYOUT布局 所以初始化的时候会多次调用onlayout方法
实例:
ublic class MultiViewGroup extends ViewGroup {
private VelocityTracker mVelocityTracker; // 用于判断甩动手势
private static final int SNAP_VELOCITY = 600; // X轴速度基值,大于该值时进行切换
private Scroller mScroller;// 滑动控制
private int mCurScreen; // 当前页面为第几屏
private int mDefaultScreen = 0;
private float mLastMotionX;// 记住上次触摸屏的位置
private int deltaX;
private OnViewChangeListener mOnViewChangeListener;
public MultiViewGroup(Context context) {
this(context, null);
}
public MultiViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
init(getContext());
}
private void init(Context context) {
mScroller = new Scroller(context);
mCurScreen = mDefaultScreen;
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {// 会更新Scroller中的当前x,y位置
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int count = getChildCount();
for (int i = 0; i < count; i++) {
measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
}
scrollTo(mCurScreen * width, 0);// 移动到第一页位置
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int margeLeft = 0;
int size = getChildCount();
for (int i = 0; i < size; i++) {
View view = getChildAt(i);
if (view.getVisibility() != View.GONE) {
int childWidth = view.getMeasuredWidth();
// 将内部子孩子横排排列
view.layout(margeLeft, 0, margeLeft + childWidth,
view.getMeasuredHeight());
margeLeft += childWidth;
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
float x = event.getX();
switch (action) {
case MotionEvent.ACTION_DOWN:
obtainVelocityTracker(event);
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
mLastMotionX = x;
break;
case MotionEvent.ACTION_MOVE:
deltaX = (int) (mLastMotionX - x);
if (canMoveDis(deltaX)) {
obtainVelocityTracker(event);
mLastMotionX = x;
// 正向或者负向移动,屏幕跟随手指移动
scrollBy(deltaX, 0);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// 当手指离开屏幕时,记录下mVelocityTracker的记录,并取得X轴滑动速度
obtainVelocityTracker(event);
mVelocityTracker.computeCurrentVelocity(1000);
float velocityX = mVelocityTracker.getXVelocity();
// 当X轴滑动速度大于SNAP_VELOCITY
// velocityX为正值说明手指向右滑动,为负值说明手指向左滑动
if (velocityX > SNAP_VELOCITY && mCurScreen > 0) {
// Fling enough to move left
snapToScreen(mCurScreen - 1);
} else if (velocityX < -SNAP_VELOCITY
&& mCurScreen < getChildCount() - 1) {
// Fling enough to move right
snapToScreen(mCurScreen + 1);
} else {
snapToDestination();
}
releaseVelocityTracker();
break;
}
// super.onTouchEvent(event);
return true;// 这里一定要返回true,不然只接受down
}
/**
* 边界检测
*
* @param deltaX
* @return
*/
private boolean canMoveDis(int deltaX) {
int scrollX = getScrollX();
// deltaX<0说明手指向右划
if (deltaX < 0) {
if (scrollX <= 0) {
return false;
} else if (deltaX + scrollX < 0) {
scrollTo(0, 0);
return false;
}
}
// deltaX>0说明手指向左划
int leftX = (getChildCount() - 1) * getWidth();
if (deltaX > 0) {
if (scrollX >= leftX) {
return false;
} else if (scrollX + deltaX > leftX) {
scrollTo(leftX, 0);
return false;
}
}
return true;
}
/**
* 使屏幕移动到第whichScreen+1屏
*
* @param whichScreen
*/
public void snapToScreen(int whichScreen) {
int scrollX = getScrollX();
if (scrollX != (whichScreen * getWidth())) {
int delta = whichScreen * getWidth() - scrollX;
mScroller.startScroll(scrollX, 0, delta, 0, Math.abs(delta) * 2);
mCurScreen = whichScreen;
invalidate();
if (mOnViewChangeListener != null) {
mOnViewChangeListener.OnViewChange(mCurScreen);
}
}
}
/**
* 当不需要滑动时,会调用该方法
*/
private void snapToDestination() {
int screenWidth = getWidth();
int whichScreen = (getScrollX() + (screenWidth / 2)) / screenWidth;
snapToScreen(whichScreen);
}
private void obtainVelocityTracker(MotionEvent event) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
}
private void releaseVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
public void SetOnViewChangeListener(OnViewChangeListener listener) {
mOnViewChangeListener = listener;
}
public interface OnViewChangeListener {
public void OnViewChange(int page);
}
}
总结:
Scroller执行流程里面的三个核心方法
mScroller.startScroll()
mScroller.computeScrollOffset()
view.computeScroll()
在
mScroller.startScroll()
中为滑动做了一些初始化准备.
比如:起始坐标,滑动的距离和方向以及持续时间(有默认值)等.
其实除了这些,在该方法内还做了些其他事情:
比较重要的一点是设置了动画开始时间.computeScrollOffset()
方法主要是根据当前已经消逝的时间
来计算当前的坐标点并且保存在mCurrX和mCurrY值中。
因为在mScroller.startScroll()中设置了动画时间,那么在computeScrollOffset()方法中依据已经消逝的时间就很容易得到当前时刻应该所处的位置并将其保存在变量mCurrX和mCurrY中。除此之外该方法还可判断动画是否已经结束。
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), 0);
invalidate();
}
}
先执行mScroller.computeScrollOffset()判断了滑动是否结束
2.1 返回false,滑动已经结束.
2.2 返回true,滑动还没有结束.
并且在该方法内部也计算了最新的坐标值mCurrX和mCurrY.
就是说在当前时刻应该滑动到哪里了.
既然computeScrollOffset()如此贴心,盛情难却啊!
于是我们就覆写View的computeScroll()方法,
调用scrollTo(By)滑动到那里