背景
這是一個滑動幫助類,并不可以使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)滑動到那里