Android 帶你從源碼的角度解析Scroller的滾動實現原理

Android 帶你從源碼的角度解析Scroller的滾動實現原理,今天給大傢講解的是Scroller類的滾動實現原理,可能很多朋友不太瞭解該類是用來幹嘛的,但是研究Launcher的朋友應該對他很熟悉,Scroller類是滾動的一個封裝類,可以實現View的平滑滾動效果,什麼是實現View的平滑滾動效果呢,舉個簡單的例子,一個View從在我們指定的時間內從一個位置滾動到另外一個位置,我們利用Scroller類可以實現勻速滾動,可以先加速後減速,可以先減速後加速等等效果,而不是瞬間的移動的效果,所以Scroller可以幫我們實現很多滑動的效果。

在介紹Scroller類之前,我們先去瞭解View的scrollBy() 和scrollTo()方法的區別,在區分這兩個方法的之前,我們要先理解View 裡面的兩個成員變量mScrollX, mScrollY,X軸方向的偏移量和Y軸方向的偏移量,這個是一個相對距離,相對的不是屏幕的原點,而是View的左邊緣,舉個通俗易懂的例子,一列火車從吉安到深圳,途中經過贛州,那麼原點就是贛州,偏移量就是 負的吉安到贛州的距離,大傢從getScrollX()方法中的註釋中就能看出答案來

/**

*Returnthescrolledleftpositionofthisview.Thisistheleftedgeof

*thedisplayedpartofyourview.Youdonotneedtodrawanypixels

*fartherleft,sincethoseareoutsideoftheframeofyourviewon

*screen.

*

*@returnTheleftedgeofthedisplayedpartofyourview,inpixels.

*/

publicfinalintgetScrollX(){

returnmScrollX;

} 現在我們知道瞭向右滑動 mScrollX就為負數,向左滑動mScrollX為正數,接下來我們先來看看scrollTo()方法的源碼

[java]view plaincopy

/**

*Setthescrolledpositionofyourview.Thiswillcauseacallto

*{@link#onScrollChanged(int,int,int,int)}andtheviewwillbe

*invalidated.

*@paramxthexpositiontoscrollto

*@paramytheypositiontoscrollto

*/

publicvoidscrollTo(intx,inty){

if(mScrollX!=x||mScrollY!=y){

intoldX=mScrollX;

intoldY=mScrollY;

mScrollX=x;

mScrollY=y;

onScrollChanged(mScrollX,mScrollY,oldX,oldY);

if(!awakenScrollBars()){

invalidate();

}

}

} 從該方法中我們可以看出,先判斷傳進來的(x, y)值是否和View的X, Y偏移量相等,如果不相等,就調用onScrollChanged()方法來通知界面發生改變,然後重繪界面,所以這樣子就實現瞭移動效果啦, 現在我們知道瞭scrollTo()方法是滾動到(x, y)這個偏移量的點,他是相對於View的開始位置來滾動的。在看看scrollBy()這個方法的代碼

[java]view plaincopy

/**

*Movethescrolledpositionofyourview.Thiswillcauseacallto

*{@link#onScrollChanged(int,int,int,int)}andtheviewwillbe

*invalidated.

*@paramxtheamountofpixelstoscrollbyhorizontally

*@paramytheamountofpixelstoscrollbyvertically

*/

publicvoidscrollBy(intx,inty){

scrollTo(mScrollX+x,mScrollY+y);

}

原來他裡面調用瞭scrollTo()方法,那就好辦瞭,他就是相對於View上一個位置根據(x, y)來進行滾動,可能大傢腦海中對這兩個方法還有點模糊,沒關系,還是舉個通俗的例子幫大傢理解下,假如一個View,調用兩次scrollTo(-10, 0),第一次向右滾動10,第二次就不滾動瞭,因為mScrollX和x相等瞭,當我們調用兩次scrollBy(-10, 0),第一次向右滾動10,第二次再向右滾動10,他是相對View的上一個位置來滾動的。

對於scrollTo()和scrollBy()方法還有一點需要註意,這點也很重要,假如你給一個LinearLayout調用scrollTo()方法,並不是LinearLayout滾動,而是LinearLayout裡面的內容進行滾動,比如你想對一個按鈕進行滾動,直接用Button調用scrollTo()一定達不到你的需求,大傢可以試一試,如果真要對某個按鈕進行scrollTo()滾動的話,我們可以在Button外面包裹一層Layout,然後對Layout調用scrollTo()方法。

瞭解瞭scrollTo()和scrollBy()方法之後我們就瞭解下Scroller類瞭,先看其構造方法

[java]view plaincopy

/**

*CreateaScrollerwiththedefaultdurationandinterpolator.

*/

publicScroller(Contextcontext){

this(context,null);

}

/**

*CreateaScrollerwiththespecifiedinterpolator.Iftheinterpolatoris

*null,thedefault(viscous)interpolatorwillbeused.

*/

publicScroller(Contextcontext,Interpolatorinterpolator){

mFinished=true;

mInterpolator=interpolator;

floatppi=context.getResources().getDisplayMetrics().density*160.0f;

mDeceleration=SensorManager.GRAVITY_EARTH//g(m/s^2)

*39.37f//inch/meter

*ppi//pixelsperinch

*ViewConfiguration.getScrollFriction();

} 隻有兩個構造方法,第一個隻有一個Context參數,第二個構造方法中指定瞭Interpolator,什麼Interpolator呢?中文意思插補器,瞭解Android動畫的朋友都應該熟悉

Interpolator,他指定瞭動畫的變化率,比如說勻速變化,先加速後減速,正弦變化等等,不同的Interpolator可以做出不同的效果出來,第一個使用默認的Interpolator(viscous)

接下來我們就要在Scroller類裡面找滾動的方法,我們從名字上面可以看出startScroll()應該是個滾動的方法,我們來看看其源碼吧

[java]view plaincopy

publicvoidstartScroll(intstartX,intstartY,intdx,intdy,intduration){

mMode=SCROLL_MODE;

mFinished=false;

mDuration=duration;

mStartTime=AnimationUtils.currentAnimationTimeMillis();

mStartX=startX;

mStartY=startY;

mFinalX=startX+dx;

mFinalY=startY+dy;

mDeltaX=dx;

mDeltaY=dy;

mDurationReciprocal=1.0f/(float)mDuration;

//Thiscontrolstheviscousfluideffect(howmuchofit)

mViscousFluidScale=8.0f;

//mustbesetto1.0(usedinviscousFluid())

mViscousFluidNormalize=1.0f;

mViscousFluidNormalize=1.0f/viscousFluid(1.0f);

} 在這個方法中我們隻看到瞭對一些滾動的基本設置動作,比如設置滾動模式,開始時間,持續時間等等,並沒有任何對View的滾動操作,也許你正納悶,不是滾動的方法幹嘛還叫做startScroll(),稍安勿躁,既然叫開始滾動,那就是對滾動的滾動之前的基本設置咯。

[java]view plaincopy

/**

*Callthiswhenyouwanttoknowthenewlocation.Ifitreturnstrue,

*theanimationisnotyetfinished.locwillbealteredtoprovidethe

*newlocation.

*/

publicbooleancomputeScrollOffset(){

if(mFinished){

returnfalse;

}

inttimePassed=(int)(AnimationUtils.currentAnimationTimeMillis()-mStartTime);

if(timePassed switch(mMode){

caseSCROLL_MODE:

floatx=(float)timePassed*mDurationReciprocal;

if(mInterpolator==null)

x=viscousFluid(x);

else

x=mInterpolator.getInterpolation(x);

mCurrX=mStartX+Math.round(x*mDeltaX);

mCurrY=mStartY+Math.round(x*mDeltaY);

break;

caseFLING_MODE:

floattimePassedSeconds=timePassed/1000.0f;

floatdistance=(mVelocity*timePassedSeconds)

-(mDeceleration*timePassedSeconds*timePassedSeconds/2.0f);

mCurrX=mStartX+Math.round(distance*mCoeffX);

//PintomMinX<=mCurrX<=mMaxX

mCurrX=Math.min(mCurrX,mMaxX);

mCurrX=Math.max(mCurrX,mMinX);

mCurrY=mStartY+Math.round(distance*mCoeffY);

//PintomMinY<=mCurrY<=mMaxY

mCurrY=Math.min(mCurrY,mMaxY);

mCurrY=Math.max(mCurrY,mMinY);

break;

}

}

else{

mCurrX=mFinalX;

mCurrY=mFinalY;

mFinished=true;

}

returntrue;

}

《註:》這裡想補充一句個人理解:isFinish()方法返回的就是該方法裡面的mFinished。當時間截止或者路程走完都會使mFinished =true,但是

computeScrollOffset()的返回值是依賴於mFinished的

我們在startScroll()方法的時候獲取瞭當前的動畫毫秒賦值給瞭mStartTime,在computeScrollOffset()中再一次調用AnimationUtils.currentAnimationTimeMillis()來獲取動畫

毫秒減去mStartTime就是持續時間瞭,然後進去if判斷,如果動畫持續時間小於我們設置的滾動持續時間mDuration,進去switch的SCROLL_MODE,然後根據Interpolator來計算出在該時間段裡面移動的距離,賦值給mCurrX, mCurrY, 所以該方法的作用是,計算在0到mDuration時間段內滾動的偏移量,並且判斷滾動是否結束,true代表還沒結束,false則表示滾動介紹瞭,Scroller類的其他的方法我就不提瞭,大都是一些get(), set()方法。

看瞭這麼多,到底要怎麼才能觸發滾動,你心裡肯定有很多疑惑,在說滾動之前我要先提另外一個方法computeScroll(),該方法是滑動的控制方法,在繪制View時,會在draw()過程調用該方法。我們先看看computeScroll()的源碼

[java]view plaincopy

/**

*CalledbyaparenttorequestthatachildupdateitsvaluesformScrollX

*andmScrollYifnecessary.Thiswilltypicallybedoneifthechildis

*animatingascrollusinga{@linkandroid.widget.ScrollerScroller}

*object.

*/

publicvoidcomputeScroll(){

} 沒錯,他是一個空的方法,需要子類去重寫該方法來實現邏輯,到底該方法在哪裡被觸發呢。我們繼續看看View的繪制方法draw()

[java]view plaincopy

publicvoiddraw(Canvascanvas){

finalintprivateFlags=mPrivateFlags;

finalbooleandirtyOpaque=(privateFlags&PFLAG_DIRTY_MASK)==PFLAG_DIRTY_OPAQUE&&

(mAttachInfo==null||!mAttachInfo.mIgnoreDirtyState);

mPrivateFlags=(privateFlags&~PFLAG_DIRTY_MASK)|PFLAG_DRAWN;

/*

*Drawtraversalperformsseveraldrawingstepswhichmustbeexecuted

*intheappropriateorder:

*

*1.Drawthebackground

*2.Ifnecessary,savethecanvas'layerstoprepareforfading

*3.Drawview'scontent

*4.Drawchildren

*5.Ifnecessary,drawthefadingedgesandrestorelayers

*6.Drawdecorations(scrollbarsforinstance)

*/

//Step1,drawthebackground,ifneeded

intsaveCount;

if(!dirtyOpaque){

finalDrawablebackground=mBackground;

if(background!=null){

finalintscrollX=mScrollX;

finalintscrollY=mScrollY;

if(mBackgroundSizeChanged){

background.setBounds(0,0,mRight-mLeft,mBottom-mTop);

mBackgroundSizeChanged=false;

}

if((scrollX|scrollY)==0){

background.draw(canvas);

}else{

canvas.translate(scrollX,scrollY);

background.draw(canvas);

canvas.translate(-scrollX,-scrollY);

}

}

}

//skipstep2&5ifpossible(commoncase)

finalintviewFlags=mViewFlags;

booleanhorizontalEdges=(viewFlags&FADING_EDGE_HORIZONTAL)!=0;

booleanverticalEdges=(viewFlags&FADING_EDGE_VERTICAL)!=0;

if(!verticalEdges&&!horizontalEdges){

//Step3,drawthecontent

if(!dirtyOpaque)onDraw(canvas);

//Step4,drawthechildren

dispatchDraw(canvas);

//Step6,drawdecorations(scrollbars)

onDrawScrollBars(canvas);

//we'redone…

return;

}

……

……

……

我們隻截取瞭draw()的部分代碼,這上面11-16行為我們寫出瞭繪制一個View的幾個步驟,我們看看第四步繪制孩子的時候會觸發dispatchDraw()這個方法,來看看源碼是什麼內容

[java]view plaincopy

/**

*Calledbydrawtodrawthechildviews.Thismaybeoverridden

*byderivedclassestogaincontroljustbeforeitschildrenaredrawn

*(butafteritsownviewhasbeendrawn).

*@paramcanvasthecanvasonwhichtodrawtheview

*/

protectedvoiddispatchDraw(Canvascanvas){

} 好吧,又是定義的一個空方法,給子類來重寫的方法,所以我們找到View的子類ViewGroup來看看該方法的具體實現邏輯

[java]view plaincopy

@Override

protectedvoiddispatchDraw(Canvascanvas){

finalintcount=mChildrenCount;

finalView[]children=mChildren;

intflags=mGroupFlags;

if((flags&FLAG_RUN_ANIMATION)!=0&&canAnimate()){

finalbooleancache=(mGroupFlags&FLAG_ANIMATION_CACHE)==FLAG_ANIMATION_CACHE;

finalbooleanbuildCache=!isHardwareAccelerated();

for(inti=0;ifinalViewchild=children[i];

if((child.mViewFlags&VISIBILITY_MASK)==VISIBLE){

finalLayoutParamsparams=child.getLayoutParams();

attachLayoutAnimationParameters(child,params,i,count);

bindLayoutAnimation(child);

if(cache){

child.setDrawingCacheEnabled(true);

if(buildCache){

child.buildDrawingCache(true);

}

}

}

}

finalLayoutAnimationControllercontroller=mLayoutAnimationController;

if(controller.willOverlap()){

mGroupFlags|=FLAG_OPTIMIZE_INVALIDATE;

}

controller.start();

mGroupFlags&=~FLAG_RUN_ANIMATION;

mGroupFlags&=~FLAG_ANIMATION_DONE;

if(cache){

mGroupFlags|=FLAG_CHILDREN_DRAWN_WITH_CACHE;

}

if(mAnimationListener!=null){

mAnimationListener.onAnimationStart(controller.getAnimation());

}

}

intsaveCount=0;

finalbooleanclipToPadding=(flags&CLIP_TO_PADDING_MASK)==CLIP_TO_PADDING_MASK;

if(clipToPadding){

saveCount=canvas.save();

canvas.clipRect(mScrollX+mPaddingLeft,mScrollY+mPaddingTop,

mScrollX+mRight-mLeft-mPaddingRight,

mScrollY+mBottom-mTop-mPaddingBottom);

}

//Wewilldrawourchild'sanimation,let'sresettheflag

mPrivateFlags&=~PFLAG_DRAW_ANIMATION;

mGroupFlags&=~FLAG_INVALIDATE_REQUIRED;

booleanmore=false;

finallongdrawingTime=getDrawingTime();

if((flags&FLAG_USE_CHILD_DRAWING_ORDER)==0){

for(inti=0;ifinalViewchild=children[i];

if((child.mViewFlags&VISIBILITY_MASK)==VISIBLE||child.getAnimation()!=null){

more|=drawChild(canvas,child,drawingTime);

}

}

}else{

for(inti=0;ifinalViewchild=children[getChildDrawingOrder(count,i)];

if((child.mViewFlags&VISIBILITY_MASK)==VISIBLE||child.getAnimation()!=null){

more|=drawChild(canvas,child,drawingTime);

}

}

}

//Drawanydisappearingviewsthathaveanimations

if(mDisappearingChildren!=null){

finalArrayListdisappearingChildren=mDisappearingChildren;

finalintdisappearingCount=disappearingChildren.size()-1;

//Gobackwards–wemaydeleteasanimationsfinish

for(inti=disappearingCount;i>=0;i–){

finalViewchild=disappearingChildren.get(i);

more|=drawChild(canvas,child,drawingTime);

}

}

if(debugDraw()){

onDebugDraw(canvas);

}

if(clipToPadding){

canvas.restoreToCount(saveCount);

}

//mGroupFlagsmighthavebeenupdatedbydrawChild()

flags=mGroupFlags;

if((flags&FLAG_INVALIDATE_REQUIRED)==FLAG_INVALIDATE_REQUIRED){

invalidate(true);

}

if((flags&FLAG_ANIMATION_DONE)==0&&(flags&FLAG_NOTIFY_ANIMATION_LISTENER)==0&&

mLayoutAnimationController.isDone()&&!more){

//Wewanttoerasethedrawingcacheandnotifythelistenerafterthe

//nextframeisdrawnbecauseoneextrainvalidate()iscausedby

//drawChild()aftertheanimationisover

mGroupFlags|=FLAG_NOTIFY_ANIMATION_LISTENER;

finalRunnableend=newRunnable(){

publicvoidrun(){

notifyAnimationListener();

}

};

post(end);

}

} 這個方法代碼有點多,但是我們還是挑重點看吧,從65-79行可以看出 在dispatchDraw()裡面會對ViewGroup裡面的子View調用drawChild()來進行繪制,接下來我們來看看drawChild()方法的代碼

[java]view plaincopy

protectedbooleandrawChild(Canvascanvas,Viewchild,longdrawingTime){

……

……

if(!concatMatrix&&canvas.quickReject(cl,ct,cr,cb,Canvas.EdgeType.BW)&&

(child.mPrivateFlags&DRAW_ANIMATION)==0){

returnmore;

}

child.computeScroll();

finalintsx=child.mScrollX;

finalintsy=child.mScrollY;

booleanscalingRequired=false;

Bitmapcache=null;

……

……

} 隻截取瞭部分代碼,看到child.computeScroll()你大概明白什麼瞭吧,轉瞭老半天終於找到瞭computeScroll()方法被觸發,就是ViewGroup在分發繪制自己的孩子的時候,會對其子View調用computeScroll()方法

整理下思路,來看看View滾動的實現原理,我們先調用Scroller的startScroll()方法來進行一些滾動的初始化設置,然後迫使View進行繪制,我們調用View的invalidate()或postInvalidate()就可以重新繪制View,繪制View的時候會觸發computeScroll()方法,我們重寫computeScroll(),在computeScroll()裡面先調用Scroller的computeScrollOffset()方法來判斷滾動有沒有結束,如果滾動沒有結束我們就調用scrollTo()方法來進行滾動,該scrollTo()方法雖然會重新繪制View,但是我們還是要手動調用下invalidate()或者postInvalidate()來觸發界面重繪,重新繪制View又觸發computeScroll(),所以就進入一個循環階段,這樣子就實現瞭在某個時間段裡面滾動某段距離的一個平滑的滾動效果

也許有人會問,幹嘛還要調用來調用去最後在調用scrollTo()方法,還不如直接調用scrollTo()方法來實現滾動,其實直接調用是可以,隻不過scrollTo()是瞬間滾動的,給人的用戶體驗不太好,所以Android提供瞭Scroller類實現平滑滾動的效果。為瞭方面大傢理解,我畫瞭一個簡單的調用示意圖

好瞭,講到這裡就已經講完瞭Scroller類的滾動實現原理啦,不知道大傢理解瞭沒有。

You May Also Like