背景
這是一個(gè)滑動(dòng)幫助類台谍,并不可以使View真正的滑動(dòng),而是根據(jù)時(shí)間的流逝吁断,獲取插值器中的數(shù)據(jù)趁蕊,傳遞給我們,讓我們?nèi)ヅ浜蟬crollTo/scrollBy去讓view產(chǎn)生緩慢滑動(dòng)仔役,產(chǎn)生動(dòng)畫(huà)的效果掷伙,其實(shí)是和屬性動(dòng)畫(huà)同一個(gè)原理。下面是官方文檔對(duì)于這個(gè)類所給的解釋:
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的繪制過(guò)程:
調(diào)用public void startScroll(int startX, int startY, int dx, int dy)
該方法為scroll做一些準(zhǔn)備工作.
比如設(shè)置了移動(dòng)的起始坐標(biāo),滑動(dòng)的距離和方向以及持續(xù)時(shí)間等.
該方法并不是真正的滑動(dòng)scroll的開(kāi)始,感覺(jué)叫prepareScroll()更貼切些.調(diào)用invalidate()或者postInvalidate()使View(ViewGroup)樹(shù)重繪
重繪會(huì)調(diào)用View的draw()方法
draw()一共有六步:繪制背景
保存畫(huà)布
調(diào)用onDraw()繪制內(nèi)容
去調(diào)用dispatchDraw()繪制子View
If necessary, draw the fading edges and restore layers
Draw decorations (scrollbars for instance)
其中最重要的是第三步和第四步骂因,重繪分兩種情況:
2.1 . ViewGroup的重繪
在完成第三步onDraw()以后,進(jìn)入第四步ViewGroup重寫(xiě)了
父類View的dispatchDraw()繪制子View,于是這樣繼續(xù)調(diào)用:
dispatchDraw()-->drawChild()-->child.computeScroll();
2.2 .View的重繪
當(dāng)View調(diào)用invalidate()方法時(shí),會(huì)導(dǎo)致整個(gè)View樹(shù)進(jìn)行從上至下的一次重繪.比如從最外層的Layout到里層的Layout,直到每個(gè)子View.在重繪View樹(shù)時(shí)ViewGroup和View時(shí)按理都會(huì)經(jīng)過(guò)onMeasure()和onLayout()以及onDraw()方法。
當(dāng)然系統(tǒng)會(huì)判斷這三個(gè)方法是否都必須執(zhí)行,如果沒(méi)有必要就不會(huì)調(diào)用.看到這里就明白了:當(dāng)這個(gè)子View的父容器重繪時(shí),也會(huì)調(diào)用上面提到的線路:onDraw()-->dispatchDraw()-->drawChild()-->child.computeScroll();
于是子View(比如此處舉例的ButtonSubClass類)中重寫(xiě)的computeScroll()方法就會(huì)被調(diào)用到.
3.** View樹(shù)的重繪會(huì)調(diào)用到View中的computeScroll()方法**
4.** 在computeScroll()方法中赃泡,在View的源碼中可以看到public void computeScroll(){}是一個(gè)空方法. 具體的實(shí)現(xiàn)需要自己來(lái)寫(xiě).在該方法中我們可調(diào)用scrollTo()或scrollBy()來(lái)實(shí)現(xiàn)移動(dòng).該方法才是實(shí)現(xiàn)移動(dòng)的核心.**
4.1 利用Scroller的mScroller.computeScrollOffset()判斷移動(dòng)過(guò)程是否完成
注意:該方法是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時(shí)表示還移動(dòng)還沒(méi)有完成.
4.2 若動(dòng)畫(huà)沒(méi)有結(jié)束,則調(diào)用:scrollTo(By)();使其滑動(dòng)scrolling
5.再次調(diào)用invalidate()
調(diào)用invalidate()方法那么又會(huì)重繪View樹(shù).
從而跳轉(zhuǎn)到第3步,如此循環(huán),直到computeScrollOffset返回false
二.onMeasure寒波、onLayout、draw 關(guān)系
onMeasure()方法
onMeasure(int widthMeasureSpec,int heightMeasureSpec)
1升熊、調(diào)用時(shí)間:當(dāng)控件的父元素放置該控件時(shí)俄烁,用于告訴父元素該控件需要的大小。
2级野、傳入?yún)?shù):widthMeasureSpec页屠,heightMeasureSpec。這兩個(gè)傳入?yún)?shù)由高32位和低16位組成,高32位保存的值叫specMode辰企,可以通過(guò)MeasureSpec.getMode()獲确缇馈;低16位為specSize可以由MeasureSpec.getSize()獲取牢贸。這兩個(gè)值是由ViewGroup中的layout_width竹观,layout_height和padding以及View自身的layout_margin共同決定。權(quán)值weight也是尤其需要考慮的因素潜索,有它的存在情況可能會(huì)稍微復(fù)雜點(diǎn)臭增。
specMode可以取三個(gè)值:MeasureSpec.EXACTLY ,MeasureSpec.AT_MOST竹习,MeasureSpec.UNSPECIFIED誊抛;specMode與layout_的對(duì)應(yīng)關(guān)系如下:
match_parent - MeasureSpec.EXACTLY:當(dāng)layout_為match_parent或者為某一具體值的時(shí)候specMode為EXACTLY代表精確的值;
wrap_content - MeasureSpec.AT_MOST:表示能獲得的最大尺寸整陌;
當(dāng)無(wú)法確定尺寸的時(shí)候則是 MeasureSpec.UNSPECIFIED拗窃,這時(shí)候specSize會(huì)為最小值(即0);
3蔓榄、可以在onMeasure()中來(lái)計(jì)算控件的尺寸并炮,然后根據(jù)setMeasuredDimension(mWidth,mHeight);方法來(lái)告訴父控件此控件需要的尺寸,onMeasure()方法中必須調(diào)用此方法甥郑。
4逃魄、值得注意的是:
1)specSize和傳入setMeasuredDimension()方法中的值的單位都是px(dp*density就是px)。2)match_parent并不是填充整個(gè)父容器澜搅,而是在不覆蓋已經(jīng)加入父容器的控件的情況下填充父容器伍俘。
onLayout()方法
onLayout(boolean changed, int left, int top,int right,int bottom);
父容器的onLayout()調(diào)用子類的onLayout()來(lái)確定子view在viewGroup中的位置,如:onLayout(10勉躺,10癌瘾,100,100)表示子容器在父容器中(10饵溅,10)位置顯示妨退,長(zhǎng)、寬都是90蜕企。結(jié)合onMeasure()方法使用可以確定子view的布局咬荷。
onDraw()方法
onDraw(Canvas canvas)
自定義view的關(guān)鍵方法,用于繪制界面轻掩,可以重寫(xiě)此方法以繪制自定義View幸乒。
onMeasure 屬于View的方法,用來(lái)測(cè)量自己和內(nèi)容的來(lái)確定寬度和高度 唇牧,view的measure方法體中會(huì)調(diào)用onMeasure罕扎。
onLayout屬于ViewGroup的方法聚唐,用來(lái)為當(dāng)前ViewGroup的子元素分配位置和大小 View的layout方法體中會(huì)調(diào)用onLayout。
onMeasure和onLayout腔召, onMeasure在onLayout之前調(diào)用杆查。
設(shè)置background后,會(huì)重新調(diào)用onMeasure和onLayout宴咧,onMeasure測(cè)量子VIEW大小后調(diào)用LAYOUT布局 所以初始化的時(shí)候會(huì)多次調(diào)用onlayout方法
實(shí)例:
ublic class MultiViewGroup extends ViewGroup {
private VelocityTracker mVelocityTracker; // 用于判斷甩動(dòng)手勢(shì)
private static final int SNAP_VELOCITY = 600; // X軸速度基值根灯,大于該值時(shí)進(jìn)行切換
private Scroller mScroller;// 滑動(dòng)控制
private int mCurScreen; // 當(dāng)前頁(yè)面為第幾屏
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()) {// 會(huì)更新Scroller中的當(dāng)前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);// 移動(dòng)到第一頁(yè)位置
}
@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();
// 將內(nèi)部子孩子橫排排列
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;
// 正向或者負(fù)向移動(dòng),屏幕跟隨手指移動(dòng)
scrollBy(deltaX, 0);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// 當(dāng)手指離開(kāi)屏幕時(shí)掺栅,記錄下mVelocityTracker的記錄烙肺,并取得X軸滑動(dòng)速度
obtainVelocityTracker(event);
mVelocityTracker.computeCurrentVelocity(1000);
float velocityX = mVelocityTracker.getXVelocity();
// 當(dāng)X軸滑動(dòng)速度大于SNAP_VELOCITY
// velocityX為正值說(shuō)明手指向右滑動(dòng),為負(fù)值說(shuō)明手指向左滑動(dòng)
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
}
/**
* 邊界檢測(cè)
*
* @param deltaX
* @return
*/
private boolean canMoveDis(int deltaX) {
int scrollX = getScrollX();
// deltaX<0說(shuō)明手指向右劃
if (deltaX < 0) {
if (scrollX <= 0) {
return false;
} else if (deltaX + scrollX < 0) {
scrollTo(0, 0);
return false;
}
}
// deltaX>0說(shuō)明手指向左劃
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;
}
/**
* 使屏幕移動(dòng)到第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);
}
}
}
/**
* 當(dāng)不需要滑動(dòng)時(shí)氧卧,會(huì)調(diào)用該方法
*/
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);
}
}
總結(jié):
Scroller執(zhí)行流程里面的三個(gè)核心方法
mScroller.startScroll()
mScroller.computeScrollOffset()
view.computeScroll()
在
mScroller.startScroll()
中為滑動(dòng)做了一些初始化準(zhǔn)備.
比如:起始坐標(biāo),滑動(dòng)的距離和方向以及持續(xù)時(shí)間(有默認(rèn)值)等.
其實(shí)除了這些,在該方法內(nèi)還做了些其他事情:
比較重要的一點(diǎn)是設(shè)置了動(dòng)畫(huà)開(kāi)始時(shí)間.computeScrollOffset()
方法主要是根據(jù)當(dāng)前已經(jīng)消逝的時(shí)間
來(lái)計(jì)算當(dāng)前的坐標(biāo)點(diǎn)并且保存在mCurrX和mCurrY值中桃笙。
因?yàn)樵趍Scroller.startScroll()中設(shè)置了動(dòng)畫(huà)時(shí)間,那么在computeScrollOffset()方法中依據(jù)已經(jīng)消逝的時(shí)間就很容易得到當(dāng)前時(shí)刻應(yīng)該所處的位置并將其保存在變量mCurrX和mCurrY中。除此之外該方法還可判斷動(dòng)畫(huà)是否已經(jīng)結(jié)束沙绝。
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), 0);
invalidate();
}
}
先執(zhí)行mScroller.computeScrollOffset()判斷了滑動(dòng)是否結(jié)束
2.1 返回false,滑動(dòng)已經(jīng)結(jié)束.
2.2 返回true,滑動(dòng)還沒(méi)有結(jié)束.
并且在該方法內(nèi)部也計(jì)算了最新的坐標(biāo)值mCurrX和mCurrY.
就是說(shuō)在當(dāng)前時(shí)刻應(yīng)該滑動(dòng)到哪里了.
既然computeScrollOffset()如此貼心,盛情難卻啊!
于是我們就覆寫(xiě)View的computeScroll()方法,
調(diào)用scrollTo(By)滑動(dòng)到那里