Scroller是一個(gè)專門用于處理滾動(dòng)效果的工具類,可能在大多數(shù)情況下,我們直接使用Scroller的場(chǎng)景并不多慕爬,但是很多大家所熟知的控件在內(nèi)部都是使用Scroller來(lái)實(shí)現(xiàn)的台囱,如ViewPager、ListView等兵琳。而如果能夠把Scroller的用法熟練掌握的話狂秘,我們自己也可以輕松實(shí)現(xiàn)出類似于ViewPager這樣的功能。那么首先新建一個(gè)ScrollerTest項(xiàng)目躯肌,今天就讓我們?通過例子來(lái)學(xué)習(xí)一下吧者春。
先撇開Scroller類不談,其實(shí)任何一個(gè)控件都是可以滾動(dòng)的清女,因?yàn)樵赩iew類當(dāng)中有scrollTo()和scrollBy()這兩個(gè)方法钱烟,如下圖所示:
這兩個(gè)方法都是用于對(duì)View進(jìn)行滾動(dòng)的,那么它們之間有什么區(qū)別呢嫡丙?簡(jiǎn)單點(diǎn)講拴袭,scrollBy()方法是讓View相對(duì)于當(dāng)前的位置滾動(dòng)某段距離,而scrollTo()方法則是讓View相對(duì)于初始的位置滾動(dòng)某段距離曙博。這樣講大家理解起來(lái)可能有點(diǎn)費(fèi)勁拥刻,我們來(lái)通過例子實(shí)驗(yàn)一下就知道了。
修改activity_main.xml中的布局文件父泳,代碼如下所示:
外層我們使用了一個(gè)LinearLayout般哼,然后在里面包含了兩個(gè)按鈕,一個(gè)用于觸發(fā)scrollTo邏輯惠窄,一個(gè)用于觸發(fā)scrollBy邏輯蒸眠。
接著修改MainActivity中的代碼,如下所示:
publicclassMainActivityextendsAppCompatActivity{privateLinearLayout layout;privateButton scrollToBtn;privateButton scrollByBtn;@OverrideprotectedvoidonCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);? ? ? ? setContentView(R.layout.activity_main);? ? ? ? layout = (LinearLayout) findViewById(R.id.layout);? ? ? ? scrollToBtn = (Button) findViewById(R.id.scroll_to_btn);? ? ? ? scrollByBtn = (Button) findViewById(R.id.scroll_by_btn);? ? ? ? scrollToBtn.setOnClickListener(newView.OnClickListener() {@OverridepublicvoidonClick(View v) {? ? ? ? ? ? ? ? layout.scrollTo(-60, -100);? ? ? ? ? ? }? ? ? ? });? ? ? ? scrollByBtn.setOnClickListener(newView.OnClickListener() {@OverridepublicvoidonClick(View v) {? ? ? ? ? ? ? ? layout.scrollBy(-60, -100);? ? ? ? ? ? }? ? ? ? });? ? }}
沒錯(cuò)睬捶,代碼就是這么簡(jiǎn)單黔宛。當(dāng)點(diǎn)擊了scrollTo按鈕時(shí),我們調(diào)用了LinearLayout的scrollTo()方法擒贸,當(dāng)點(diǎn)擊了scrollBy按鈕時(shí)臀晃,調(diào)用了LinearLayout的scrollBy()方法。那有的朋友可能會(huì)問了介劫,為什么都是調(diào)用的LinearLayout中的scroll方法徽惋?這里一定要注意,不管是scrollTo()還是scrollBy()方法座韵,滾動(dòng)的都是該View內(nèi)部的內(nèi)容险绘,而LinearLayout中的內(nèi)容就是我們的兩個(gè)Button,如果你直接調(diào)用button的scroll方法的話誉碴,那結(jié)果一定不是你想看到的宦棺。
另外還有一點(diǎn)需要注意,就是兩個(gè)scroll方法中傳入的參數(shù)黔帕,第一個(gè)參數(shù)x表示相對(duì)于當(dāng)前位置橫向移動(dòng)的距離代咸,正值向左移動(dòng),負(fù)值向右移動(dòng)成黄,單位是像素呐芥。第二個(gè)參數(shù)y表示相對(duì)于當(dāng)前位置縱向移動(dòng)的距離逻杖,正值向上移動(dòng),負(fù)值向下移動(dòng)思瘟,單位是像素荸百。
那說(shuō)了這么多,scrollTo()和scrollBy()這兩個(gè)方法到底有什么區(qū)別呢滨攻?其實(shí)運(yùn)行一下代碼我們就能立刻知道了:
可以看到够话,當(dāng)我們點(diǎn)擊scrollTo按鈕時(shí),兩個(gè)按鈕會(huì)一起向右下方滾動(dòng)光绕,因?yàn)槲覀儌魅氲膮?shù)是-60和-100更鲁,因此向右下方移動(dòng)是正確的。但是你會(huì)發(fā)現(xiàn)奇钞,之后再點(diǎn)擊scrollTo按鈕就沒有任何作用了,界面不會(huì)再繼續(xù)滾動(dòng)漂坏,只有點(diǎn)擊scrollBy按鈕界面才會(huì)繼續(xù)滾動(dòng)景埃,并且不停點(diǎn)擊scrollBy按鈕界面會(huì)一起滾動(dòng)下去。
現(xiàn)在我們?cè)賮?lái)回頭看一下這兩個(gè)方法的區(qū)別顶别,scrollTo()方法是讓View相對(duì)于初始的位置滾動(dòng)某段距離谷徙,由于View的初始位置是不變的,因此不管我們點(diǎn)擊多少次scrollTo按鈕滾動(dòng)到的都將是同一個(gè)位置驯绎。而scrollBy()方法則是讓View相對(duì)于當(dāng)前的位置滾動(dòng)某段距離完慧,那每當(dāng)我們點(diǎn)擊一次scrollBy按鈕,View的當(dāng)前位置都進(jìn)行了變動(dòng)剩失,因此不停點(diǎn)擊會(huì)一直向右下方移動(dòng)屈尼。
通過這個(gè)例子來(lái)理解,相信大家已經(jīng)把scrollTo()和scrollBy()這兩個(gè)方法的區(qū)別搞清楚了拴孤,但是現(xiàn)在還有一個(gè)問題脾歧,從上圖中大家也能看得出來(lái),目前使用這兩個(gè)方法完成的滾動(dòng)效果是跳躍式的演熟,沒有任何平滑滾動(dòng)的效果鞭执。沒錯(cuò),只靠scrollTo()和scrollBy()這兩個(gè)方法是很難完成ViewPager這樣的效果的芒粹,因此我們還需要借助另外一個(gè)關(guān)鍵性的工具兄纺,也就我們今天的主角Scroller。
Scroller的基本用法其實(shí)還是比較簡(jiǎn)單的化漆,主要可以分為以下幾個(gè)步驟:
1. 創(chuàng)建Scroller的實(shí)例
2. 調(diào)用startScroll()方法來(lái)初始化滾動(dòng)數(shù)據(jù)并刷新界面
3. 重寫computeScroll()方法估脆,并在其內(nèi)部完成平滑滾動(dòng)的邏輯
那么下面我們就按照上述的步驟,通過一個(gè)模仿ViewPager的簡(jiǎn)易例子來(lái)學(xué)習(xí)和理解一下Scroller的用法获三。
新建一個(gè)ScrollerLayout并讓它繼承自ViewGroup來(lái)作為我們的簡(jiǎn)易ViewPager布局旁蔼,代碼如下所示:
/**
* Created by guolin on 16/1/12.
*/publicclassScrollerLayoutextendsViewGroup{/**
* 用于完成滾動(dòng)操作的實(shí)例
*/privateScroller mScroller;/**
* 判定為拖動(dòng)的最小移動(dòng)像素?cái)?shù)
*/privateintmTouchSlop;/**
* 手機(jī)按下時(shí)的屏幕坐標(biāo)
*/privatefloatmXDown;/**
* 手機(jī)當(dāng)時(shí)所處的屏幕坐標(biāo)
*/privatefloatmXMove;/**
* 上次觸發(fā)ACTION_MOVE事件時(shí)的屏幕坐標(biāo)
*/privatefloatmXLastMove;/**
* 界面可滾動(dòng)的左邊界
*/privateintleftBorder;/**
* 界面可滾動(dòng)的右邊界
*/privateintrightBorder;publicScrollerLayout(Context context, AttributeSet attrs) {super(context, attrs);// 第一步锨苏,創(chuàng)建Scroller的實(shí)例mScroller =newScroller(context);? ? ? ? ViewConfiguration configuration = ViewConfiguration.get(context);// 獲取TouchSlop值mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);? ? }@OverrideprotectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);intchildCount = getChildCount();for(inti =0; i < childCount; i++) {? ? ? ? ? ? View childView = getChildAt(i);// 為ScrollerLayout中的每一個(gè)子控件測(cè)量大小measureChild(childView, widthMeasureSpec, heightMeasureSpec);? ? ? ? }? ? }@OverrideprotectedvoidonLayout(booleanchanged,intl,intt,intr,intb) {if(changed) {intchildCount = getChildCount();for(inti =0; i < childCount; i++) {? ? ? ? ? ? ? ? View childView = getChildAt(i);// 為ScrollerLayout中的每一個(gè)子控件在水平方向上進(jìn)行布局childView.layout(i * childView.getMeasuredWidth(),0, (i +1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());? ? ? ? ? ? }// 初始化左右邊界值leftBorder = getChildAt(0).getLeft();? ? ? ? ? ? rightBorder = getChildAt(getChildCount() -1).getRight();? ? ? ? }? ? }@OverridepublicbooleanonInterceptTouchEvent(MotionEvent ev) {switch(ev.getAction()) {caseMotionEvent.ACTION_DOWN:? ? ? ? ? ? ? ? mXDown = ev.getRawX();? ? ? ? ? ? ? ? mXLastMove = mXDown;break;caseMotionEvent.ACTION_MOVE:? ? ? ? ? ? ? ? mXMove = ev.getRawX();floatdiff = Math.abs(mXMove - mXDown);? ? ? ? ? ? ? ? mXLastMove = mXMove;// 當(dāng)手指拖動(dòng)值大于TouchSlop值時(shí),認(rèn)為應(yīng)該進(jìn)行滾動(dòng)棺聊,攔截子控件的事件if(diff > mTouchSlop) {returntrue;? ? ? ? ? ? ? ? }break;? ? ? ? }returnsuper.onInterceptTouchEvent(ev);? ? }@OverridepublicbooleanonTouchEvent(MotionEvent event) {switch(event.getAction()) {caseMotionEvent.ACTION_MOVE:? ? ? ? ? ? ? ? mXMove = event.getRawX();intscrolledX = (int) (mXLastMove - mXMove);if(getScrollX() + scrolledX < leftBorder) {? ? ? ? ? ? ? ? ? ? scrollTo(leftBorder,0);returntrue;? ? ? ? ? ? ? ? }elseif(getScrollX() + getWidth() + scrolledX > rightBorder) {? ? ? ? ? ? ? ? ? ? scrollTo(rightBorder - getWidth(),0);returntrue;? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? scrollBy(scrolledX,0);? ? ? ? ? ? ? ? mXLastMove = mXMove;break;caseMotionEvent.ACTION_UP:// 當(dāng)手指抬起時(shí)伞租,根據(jù)當(dāng)前的滾動(dòng)值來(lái)判定應(yīng)該滾動(dòng)到哪個(gè)子控件的界面inttargetIndex = (getScrollX() + getWidth() /2) / getWidth();intdx = targetIndex * getWidth() - getScrollX();// 第二步,調(diào)用startScroll()方法來(lái)初始化滾動(dòng)數(shù)據(jù)并刷新界面mScroller.startScroll(getScrollX(),0, dx,0);? ? ? ? ? ? ? ? invalidate();break;? ? ? ? }returnsuper.onTouchEvent(event);? ? }@OverridepublicvoidcomputeScroll() {// 第三步限佩,重寫computeScroll()方法葵诈,并在其內(nèi)部完成平滑滾動(dòng)的邏輯if(mScroller.computeScrollOffset()) {? ? ? ? ? ? scrollTo(mScroller.getCurrX(), mScroller.getCurrY());? ? ? ? ? ? invalidate();? ? ? ? }? ? }}
整個(gè)Scroller用法的代碼都在這里了,代碼并不長(zhǎng)祟同,一共才100多行作喘,我們一點(diǎn)點(diǎn)來(lái)看。
首先在ScrollerLayout的構(gòu)造函數(shù)里面我們進(jìn)行了上述步驟中的第一步操作晕城,即創(chuàng)建Scroller的實(shí)例泞坦,由于Scroller的實(shí)例只需創(chuàng)建一次,因此我們把它放到構(gòu)造函數(shù)里面執(zhí)行砖顷。另外在構(gòu)建函數(shù)中我們還初始化的TouchSlop的值贰锁,這個(gè)值在后面將用于判斷當(dāng)前用戶的操作是否是拖動(dòng)。
接著重寫onMeasure()方法和onLayout()方法滤蝠,在onMeasure()方法中測(cè)量ScrollerLayout里的每一個(gè)子控件的大小豌熄,在onLayout()方法中為ScrollerLayout里的每一個(gè)子控件在水平方向上進(jìn)行布局。如果有朋友對(duì)這兩個(gè)方法的作用還不理解物咳,可以參照我之前寫的一篇文章Android視圖繪制流程完全解析锣险,帶你一步步深入了解View(二)。
接著重寫onInterceptTouchEvent()方法览闰, 在這個(gè)方法中我們記錄了用戶手指按下時(shí)的X坐標(biāo)位置芯肤,以及用戶手指在屏幕上拖動(dòng)時(shí)的X坐標(biāo)位置,當(dāng)兩者之間的距離大于TouchSlop值時(shí)压鉴,就認(rèn)為用戶正在拖動(dòng)布局纷妆,然后我們就將事件在這里攔截掉,阻止事件傳遞到子控件當(dāng)中晴弃。
那么當(dāng)我們把事件攔截掉之后掩幢,就會(huì)將事件交給ScrollerLayout的onTouchEvent()方法來(lái)處理。如果當(dāng)前事件是ACTION_MOVE上鞠,說(shuō)明用戶正在拖動(dòng)布局际邻,那么我們就應(yīng)該對(duì)布局內(nèi)容進(jìn)行滾動(dòng)從而影響拖動(dòng)事件,實(shí)現(xiàn)的方式就是使用我們剛剛所學(xué)的scrollBy()方法芍阎,用戶拖動(dòng)了多少這里就scrollBy多少世曾。另外為了防止用戶拖出邊界這里還專門做了邊界保護(hù),當(dāng)拖出邊界時(shí)就調(diào)用scrollTo()方法來(lái)回到邊界位置谴咸。
如果當(dāng)前事件是ACTION_UP時(shí)轮听,說(shuō)明用戶手指抬起來(lái)了骗露,但是目前很有可能用戶只是將布局拖動(dòng)到了中間,我們不可能讓布局就這么停留在中間的位置血巍,因此接下來(lái)就需要借助Scroller來(lái)完成后續(xù)的滾動(dòng)操作萧锉。首先這里我們先根據(jù)當(dāng)前的滾動(dòng)位置來(lái)計(jì)算布局應(yīng)該繼續(xù)滾動(dòng)到哪一個(gè)子控件的頁(yè)面,然后計(jì)算出距離該頁(yè)面還需滾動(dòng)多少距離述寡。接下來(lái)我們就該進(jìn)行上述步驟中的第二步操作柿隙,調(diào)用startScroll()方法來(lái)初始化滾動(dòng)數(shù)據(jù)并刷新界面。startScroll()方法接收四個(gè)參數(shù)鲫凶,第一個(gè)參數(shù)是滾動(dòng)開始時(shí)X的坐標(biāo)禀崖,第二個(gè)參數(shù)是滾動(dòng)開始時(shí)Y的坐標(biāo),第三個(gè)參數(shù)是橫向滾動(dòng)的距離螟炫,正值表示向左滾動(dòng)波附,第四個(gè)參數(shù)是縱向滾動(dòng)的距離,正值表示向上滾動(dòng)昼钻。緊接著調(diào)用invalidate()方法來(lái)刷新界面叶雹。
現(xiàn)在前兩步都已經(jīng)完成了,最后我們還需要進(jìn)行第三步操作换吧,即重寫computeScroll()方法,并在其內(nèi)部完成平滑滾動(dòng)的邏輯 钥星。在整個(gè)后續(xù)的平滑滾動(dòng)過程中沾瓦,computeScroll()方法是會(huì)一直被調(diào)用的,因此我們需要不斷調(diào)用Scroller的computeScrollOffset()方法來(lái)進(jìn)行判斷滾動(dòng)操作是否已經(jīng)完成了谦炒,如果還沒完成的話贯莺,那就繼續(xù)調(diào)用scrollTo()方法,并把Scroller的curX和curY坐標(biāo)傳入宁改,然后刷新界面從而完成平滑滾動(dòng)的操作缕探。
現(xiàn)在ScrollerLayout已經(jīng)準(zhǔn)備好了,接下來(lái)我們修改activity_main.xml布局中的內(nèi)容还蹲,如下所示:
可以看到爹耗,這里我們?cè)赟crollerLayout中放置了三個(gè)按鈕用來(lái)進(jìn)行測(cè)試,其實(shí)這里不僅可以放置按鈕谜喊,放置任何控件都是沒問題的潭兽。
最后MainActivity當(dāng)中刪除掉之前測(cè)試的代碼:
publicclassMainActivityextendsAppCompatActivity{@OverrideprotectedvoidonCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);? ? ? ? setContentView(R.layout.activity_main);? ? }}
好的,所有代碼都在這里了斗遏,現(xiàn)在我們可以運(yùn)行一下程序來(lái)看一看效果了山卦,如下圖所示:
http://blog.csdn.net/guolin_blog/article/details/48719871