PullScrollView详解(三)——PullScrollView实现
眼看下周就要休婚假了,感覺(jué)真是棒極了,嘿嘿哈哈吼吼,休假前把這個(gè)系列寫(xiě)完給大家
相關(guān)文章:
1、《PullScrollView詳解(一)——自定義控件屬性》
2、《PullScrollView詳解(二)——Animation、Layout與下拉回彈》
3、《PullScrollView詳解(三)——PullScrollView實(shí)現(xiàn)》
4、PullScrollView詳解(四)——完全使用listview實(shí)現(xiàn)下拉回彈(方法一)
5、《PullScrollView詳解(五)——完全使用listview實(shí)現(xiàn)下拉回彈(方法二)》
6、《PullScrollView詳解(六)——延伸拓展(listview中g(shù)etScrollY()一直等于0、ScrollView中的overScrollBy)》
前面鋪墊的已經(jīng)很多了,這篇就要進(jìn)入正式環(huán)節(jié)了,下面就是這個(gè)系列最終的效果圖:
下面我們就一步步來(lái)做,這篇就先跟大家一起來(lái)實(shí)現(xiàn)一個(gè)雛形,先把最基本的功能實(shí)現(xiàn)。下篇再對(duì)一些問(wèn)題進(jìn)行優(yōu)化。
###一、框架搭建
先看看我們要搭建的框架:
大家可以看不明白,它是怎么搭的,我們還是直接上XML代碼吧:
####1、XML
這里要注意兩個(gè)地方:
1、ImageView就是我們底部的小狗圖片,它利用android:layout_marginTop="-100dp"來(lái)將圖片向頂部縮進(jìn)100dp,為我們下拉做準(zhǔn)備
2、PullScrollView,派生自ScrollView,它是通過(guò)將子控件的android:layout_marginTop="150dp"來(lái)將底部的圖片顯示出來(lái)的。又由于,PullScrollView的android:layout_height是match_parent,所以我們可以向上滾動(dòng),覆蓋住底部的圖片。千萬(wàn)不要把a(bǔ)ndroid:layout_marginTop="150dp"放在PullScrollView里,比如這樣:
這樣就會(huì)有下面的效果:
這就是因?yàn)槲覀儗ndroid:layout_marginTop="150dp"放在了PullScrollView中;此時(shí)的PullScrollView的布局范圍并不是整個(gè)屏幕了,而是從屏幕以下150dp的地方開(kāi)始。
####2、PullScrollView
在這里,我們只是搭框架,并不會(huì)做什么下拉和回彈的操作,所以,我們?cè)谶@一環(huán)節(jié),我們僅僅將PullScrollView派生自ScrollView就好了
代碼如下:
####3、MainActivity
在這里其實(shí)也沒(méi)什么要做的,就是把TableLayout給填充起來(lái):
核心代碼就在showTable()函數(shù)中,先構(gòu)造一個(gè)TableRow,在TableRow中添加一個(gè)TextView,然后將TableRow添加到TableLayout中;這里不是講解的重點(diǎn),難度也不大,所以就不再細(xì)講了。我們用TableLayout 的主要目的就是把PullScrolLView撐開(kāi),讓它可以上下滾動(dòng)。
###二、下拉隨手指移動(dòng)
這段我們就要看看怎么讓頂部的圖片和上面的Content 一塊隨手指移動(dòng)。像下面的效果:
在上篇我們講過(guò),讓布局跟隨手指移動(dòng),就是計(jì)算出手指的移動(dòng)距離,然后利用layout()函數(shù)來(lái)移動(dòng)布局就好了。
####1、首先有關(guān)變量的設(shè)定:
這里主要分為三個(gè)部分:
mHeaderView:表示頭部的圖片的VIEW
mContentView:表示PullScrollView的子控件,這里是TableLayout控件對(duì)應(yīng)的VIEW
另外是三個(gè)初始化的位置:
mTouchPoint:表示用戶(hù)在滑動(dòng)手指的初始點(diǎn)擊位置。用來(lái)計(jì)算手指的移動(dòng)距離的
mHeadInitRect:用來(lái)保存頭部View初始化位置。用來(lái)找到回彈的位置用。
mContentInitRect:保存ContentView的初始化位置。跟mHeadInitRect一樣,也是回彈用。
####2、變量初始化
首先是mHeaderView和mContentView的初始化
mContentView的初始化非常容易,與第二篇一樣,我們直接在onFinishInflate()函數(shù)中,就可以獲得解析后的變量實(shí)例:
但mHeaderView確是需要使用PullScrollView的用戶(hù)自己傳進(jìn)去的,所以我們需要寫(xiě)一個(gè)接口來(lái)讓使用PullScrollView的用戶(hù)來(lái)設(shè)置它要操作的headView
public void setmHeaderView(View view){mHeaderView = view;}同樣,有關(guān)mHeadInitRect,mContentInitRect的初始化,都必須在圖像顯示出來(lái)以后,才能獲取到頭部和Content的位置,所以我們將它們的初始化全都放在當(dāng)用戶(hù)點(diǎn)擊屏幕的時(shí)候,與mTouchPoint一起初始化:
public boolean onTouchEvent(MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_DOWN: {mTouchPoint.set((int) event.getX(), (int) event.getY());mHeadInitRect.set(mHeaderView.getLeft(), mHeaderView.getTop(), mHeaderView.getRight(), mHeaderView.getBottom());mContentInitRect.set(mContentView.getLeft(), mContentView.getTop(), mContentView.getRight(), mContentView.getBottom());}break;…………} }這段代碼沒(méi)什么難度,就是在用戶(hù)點(diǎn)擊屏幕的時(shí)候,將headView和contentView的坐標(biāo)保存起來(lái);
####3、布局跟隨手指移動(dòng)
到這一步,就是要讓布局跟隨手指移動(dòng)了。當(dāng)然跟上篇一樣,首先計(jì)算移動(dòng)距離,然后使用layout()函數(shù)來(lái)移動(dòng)布局。
計(jì)算距離:
計(jì)算距離的代碼如下:
用當(dāng)前的手指所在的Y軸位置減去點(diǎn)擊時(shí)的位置。就得到了手指的移動(dòng)距離:
int deltaY =(int)event.getY() - mTouchPoint.y;但手指的移動(dòng)距離并不代表是headView和contentView允許移動(dòng)的距離,凡事都有一個(gè)最大值,這里假設(shè)最大值就是headview的高度。所以當(dāng)手指的移動(dòng)距離超過(guò)headView的高度時(shí),我們就不再增大移動(dòng)距離,而是將headView直接賦值給deltaY:
deltaY = deltaY > mHeaderView.getHeight() ? mHeaderView.getHeight() : deltaY;然后就是計(jì)算headView和contentView的應(yīng)該所在位置了,并移動(dòng)他們了,寫(xiě)出來(lái)完整的ACTION_MOVE的代碼吧:
case MotionEvent.ACTION_MOVE:{int deltaY =(int)event.getY() - mTouchPoint.y;deltaY = deltaY > mHeaderView.getHeight() ? mHeaderView.getHeight() : deltaY;if (deltaY > 0 && deltaY >= getScrollY()) {float headerMoveHeight = deltaY * 0.5f * SCROLL_RATIO;mHeaderCurTop = (int) (mHeadInitRect.top + headerMoveHeight);mHeaderCurBottom = (int) (mHeadInitRect.bottom + headerMoveHeight);float contentMoveHeight = deltaY * SCROLL_RATIO;mContentTop = (int) (mContentInitRect.top + contentMoveHeight);mContentBottom = (int) (mContentInitRect.bottom + contentMoveHeight);if (mContentTop <= mHeaderCurBottom) {mHeaderView.layout(mHeadInitRect.left, mHeaderCurTop, mHeadInitRect.right, mHeaderCurBottom);mContentView.layout(mContentInitRect.left, mContentTop, mContentInitRect.right, mContentBottom);}} } break;先看判斷語(yǔ)名:
if (deltaY > 0 && deltaY >= getScrollY()) { }deltaY > 0很容易理解,表示手指的移動(dòng)距離是正值,即手指是向下移動(dòng)的;
那另外一個(gè)deltaY >= getScrollY()是個(gè)什么鬼?
我們先看一下,把這個(gè)判斷語(yǔ)句去掉,效果會(huì)怎樣:(即只保留deltaY > 0)
看到了沒(méi),當(dāng)先向上滑一段之后,再下滑,會(huì)發(fā)現(xiàn),headView也會(huì)跟著下滑;這明顯是不對(duì)的,因?yàn)閔eadview應(yīng)該在我們超過(guò)原始位置的時(shí)候再下滑才是正確的。所以要判斷當(dāng)前PullScrollView是不是已經(jīng)滾動(dòng)。如果手指的移動(dòng)距離大于滾動(dòng)距離,這才說(shuō)明,我們已經(jīng)超過(guò)了原始位置在下拉。這時(shí)候再移動(dòng)headView;
計(jì)算高度:
使用layout移動(dòng)View前,先計(jì)算當(dāng)前headview和contentView應(yīng)該移動(dòng)的距離。
明顯,headview要比contentview移動(dòng)的慢,因?yàn)槲覀冊(cè)谟?jì)算headerMoveHeight 時(shí)多乘以了一個(gè)0.5;這其實(shí)也是為了增加一個(gè)效果:讓用戶(hù)覺(jué)得下拉比較困難,移動(dòng)的速度慢。當(dāng)然你也可以去掉,也可以改成其它值。
float headerMoveHeight = deltaY * 0.5f * SCROLL_RATIO; float contentMoveHeight = deltaY * SCROLL_RATIO;在計(jì)算好移動(dòng)高度以后,就是計(jì)算要移動(dòng)的位置了,有關(guān)mHeaderCurTop、mHeaderCurBottom和mContentTop、mContentBottom的計(jì)算就不講了,沒(méi)什么意思,就是在原始高度的基礎(chǔ)上,加上移動(dòng)的高度。
移動(dòng)view:
最后是使用layout移動(dòng)View:
這里做了一個(gè)判斷,即contentView的上邊沿不能低于headView的底邊。如果低于headView的底邊,那就是contentView和headView分離了,這怎么能行。不能分離!
符合條件以后,就移動(dòng)headview和contentview。
###三、松手回彈
在上面的那段代碼中,大家可以發(fā)現(xiàn),在移動(dòng)headview和contentview時(shí),設(shè)置了一個(gè)變量mIsMoving = true;這個(gè)變量是用來(lái)標(biāo)識(shí),headview和contentview是否已經(jīng)被移動(dòng)了位置。如果移動(dòng)了位置則需要反彈;
我們上篇也講了有關(guān)反彈的動(dòng)畫(huà)代碼寫(xiě)法:先利用layout()將布局還原,然后再做動(dòng)畫(huà),讓它從跟隨手移動(dòng)的位置移動(dòng)到初始位置。
先來(lái)看看代碼:
首先判斷當(dāng)前view是否移動(dòng),如果移動(dòng)了,則返回;有關(guān)返回部分的代碼,我就不再細(xì)了,如果有不理解的同學(xué),可以參考上篇文章《PullScrollView詳解(二)——Animation、Layout與下拉回彈》;
這時(shí)候的效果基本上就完成了,效果圖如下:
###四、BUG修復(fù)及優(yōu)化
####1、攔截點(diǎn)擊事件
首先,有關(guān)事件攔截的問(wèn)題,大家可以先看這篇文章:《Android-onInterceptTouchEvent()和onTouchEvent()總結(jié)》,看完這篇文章以后,大家可以會(huì)了解到,有關(guān)Touch攔截與分發(fā)消費(fèi)的問(wèn)題。
假如,我們?cè)贛ainActivity中,添加了如下代碼:
即,當(dāng)用戶(hù)點(diǎn)擊TableLayout的某一個(gè)ITEM時(shí),彈出一個(gè)TOAST。換句話(huà)說(shuō),就是在PullScrollView的子控件中消費(fèi)了點(diǎn)擊事件。
那現(xiàn)在我們?cè)龠\(yùn)行代碼,這時(shí)候就出現(xiàn)了問(wèn)題:
在錄像上可能看不出來(lái),大家仔細(xì)看,在headview上點(diǎn)擊向下拖動(dòng)是沒(méi)有問(wèn)題的;但當(dāng)用手指放在contentview上向下拖動(dòng)的時(shí)候,contentview會(huì)突然下滑一下,然后再向下拖動(dòng)。
剛開(kāi)始,我也沒(méi)理解這是怎么回事,后來(lái)經(jīng)過(guò)打LOG發(fā)現(xiàn),,MotionEvent.ACTION_DOWN里的代碼根本沒(méi)有運(yùn)行,這是為什么?
想必大家看過(guò)上面的《Android-onInterceptTouchEvent()和onTouchEvent()總結(jié)》之后,應(yīng)該就能理解出來(lái),OnTouchEvent()走的是事件的消費(fèi)階段,只有它的所有子控件不消費(fèi)的事件才會(huì)傳到它這來(lái)。也就是說(shuō),只有PullScrollView中的所有子控件都不消費(fèi)的事件,才會(huì)傳遞到PullScrollView的OnTouchEvent()中來(lái)處理。而我們?cè)赥ableLayout中已經(jīng)為每個(gè)ITEM添加了onClick()事件的響應(yīng),也就是已經(jīng)消費(fèi)了點(diǎn)擊事件,所以,PullScrollView肯定就不會(huì)再收到點(diǎn)擊相關(guān)的事件了,因?yàn)橐呀?jīng)被它的子控件消費(fèi)掉了。
所以,我們要在事件分發(fā)階段攔截點(diǎn)擊事件,以保證肯定被運(yùn)行。要在分發(fā)階段攔截,就需要重寫(xiě)onInterceptTouchEvent()函數(shù),在其中做處理:
####2、攔截MOVE事件
同理,我們不能祈禱PullScrollView的所有子控件都不處理MotionEvent.ACTION_MOVE事件,我們必須攔截MOVE事件,以保證我們的代碼一定能夠運(yùn)行,同樣,我們要重寫(xiě)onInterceptTouchEvent()函數(shù),而且在要保證我們?cè)谝苿?dòng)的時(shí)候,不能把ACTION_MOVE事件傳遞給子控件;也就是我們?cè)谝苿?dòng)的時(shí)候禁止所有子控件的對(duì)MOVE事件的捕捉,所以要返回true,所以完整的onInterceptTouchEvent()代碼如下:
通過(guò)返回true,來(lái)禁止事件繼續(xù)傳遞。
####3、拖動(dòng)時(shí)禁止ScollView本身的滾動(dòng)行為
我們要做到,我們移動(dòng)布局時(shí)不受任何的干擾,在上面我們?cè)谕ㄟ^(guò)攔截ACTION_MOVE事件來(lái)禁止PullScrollView所有子控件的移動(dòng);由于我們的PullScrollView派生自ScrollView,除了禁止子控件的移動(dòng)以外,還要在移動(dòng)時(shí)禁止ScrollView自身的滾動(dòng)行為。
首先,我們定義一個(gè)變量來(lái)標(biāo)識(shí)當(dāng)前是否要禁止ScrollView自身的默認(rèn)行為:
然后當(dāng)我們移動(dòng)布局時(shí)設(shè)置為T(mén)rue,其它時(shí)候都設(shè)置為FALSE:
public boolean onTouchEvent(MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_MOVE: {…………if (mContentTop <= mHeaderCurBottom) {mHeaderView.layout(mHeadInitRect.left, mHeaderCurTop, mHeadInitRect.right, mHeaderCurBottom);mContentView.layout(mContentInitRect.left, mContentTop, mContentInitRect.right, mContentBottom);mEnableMoving = true;}}}break;case MotionEvent.ACTION_UP: {mEnable = false;}…………}return mEnableMoving || super.onTouchEvent(event); }最后在return時(shí):
return mEnableMoving || super.onTouchEvent(event);這句就厲害了,由于mEnableMoving和super.onTouchEvent(event);用的是或運(yùn)算符,所以當(dāng)mEnableMoving為true時(shí),就不會(huì)執(zhí)行super.onTouchEvent(event),也就不會(huì)執(zhí)行ScrollView的默認(rèn)操作;相反,當(dāng)mEnableMoving為false時(shí),就會(huì)執(zhí)行super.onTouchEvent(event),就會(huì)執(zhí)行ScrollView默認(rèn)操作。
到這里,基本所有的問(wèn)題都解決了。下面我們就再優(yōu)化一下操作。
####4、只允許布局在初始狀態(tài)時(shí),才允許用戶(hù)滾動(dòng)
由于我們?cè)谟脩?hù)點(diǎn)擊時(shí)獲取當(dāng)前布局的位置的,但當(dāng)用戶(hù)先將布局向上滾,然后再點(diǎn)擊向下拉。這時(shí)我們獲取到的初始值就會(huì)出錯(cuò)。所以反彈就肯定會(huì)出問(wèn)題。所以我們要想辦法,只讓在布局在初始狀態(tài)時(shí),才允許用戶(hù)拖拽。效果如下:
可以看到,當(dāng)向上滾動(dòng)后再向下拉是拉不動(dòng)的。只有在初始位置時(shí)才能拖動(dòng)。
在ScrollView中,正好有一個(gè)值來(lái)判斷當(dāng)前的ScrollView是不是在初始化狀態(tài),即getScrollY()是不是等于0,當(dāng)?shù)扔?時(shí),肯定是初始化狀態(tài)。
所以,首先,定義一個(gè)變量來(lái)標(biāo)識(shí)當(dāng)前布局是不是初始化狀態(tài):
在點(diǎn)擊的時(shí)候,判斷當(dāng)前是不是初始狀態(tài):
public boolean onInterceptTouchEvent(MotionEvent event) {if (event.getAction() == MotionEvent.ACTION_DOWN) {…………//如果當(dāng)前不是從初始化位置開(kāi)始滾動(dòng)的話(huà),就不讓用戶(hù)拖拽if (getScrollY() == 0){mIsLayout = true;}} else if (event.getAction() == MotionEvent.ACTION_MOVE) {…………}return super.onInterceptTouchEvent(event); }在A(yíng)CTION_MOVE的時(shí)候,判斷當(dāng)前是否能夠移動(dòng):
public boolean onTouchEvent(MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_MOVE: {int deltaY = (int) event.getY() - mTouchPoint.y;deltaY = deltaY < 0 ? 0 : (deltaY > mHeaderHeight ? mHeaderHeight : deltaY);if (deltaY > 0 && deltaY >= getScrollY() && mIsLayout) {…………}}}break;………… }####5、使用declare-styleable來(lái)自定義屬性
在代碼中,我們的拖動(dòng)的最大高度,直接是通過(guò)headview.getHeight()來(lái)獲取的:
在第一篇中,我們講述了如何通過(guò)declare-styleable來(lái)自定義控件屬性,這里我們就通過(guò)自定義的屬性來(lái)預(yù)定義PullScrollView的這項(xiàng)參數(shù),當(dāng)然,大家也可以發(fā)揮想象,把其它的可變參數(shù),也可以由用戶(hù)通過(guò)參數(shù)來(lái)自定義。
首先,在values文件夾下新建一個(gè)attrs.xml文件:
我這里除了headerHeight(頭部高度)變量,還額外申請(qǐng)了幾個(gè),以便大家進(jìn)一步認(rèn)識(shí)declare-styleable的用法,雖然這里也用不到……
然后,在activity_main.xml中使用:
在XML中使用declare-styleable自定義的屬性,前面我們講過(guò),首先要在根布局添加:
xmlns:app="http://schemas.android.com/apk/res-auto"然后在使用時(shí),直接使用app:XXXX即可;具體可以參考本系列第一篇。
然后在PullScrollView中得到定義的值并使用:
先是獲取定義的值:
然后在用到mHeaderView.getHeight()的地方改成mHeaderHeight;
即把下面的:
改成:
deltaY = deltaY < 0 ? 0 : (deltaY > mHeaderHeight ? mHeaderHeight : deltaY);好了,到這里基本就全部完成了,源碼在文章底部給出
如果本文有幫到你,記得加關(guān)注哦
####源碼下載地址:http://download.csdn.net/detail/harvic880925/8862541
####請(qǐng)大家尊重原創(chuàng)者版權(quán),轉(zhuǎn)載請(qǐng)標(biāo)明出處:http://blog.csdn.net/harvic880925/article/details/46728247 謝謝
如果你喜歡我的文章,你可能更喜歡我的公眾號(hào)
參考文章:
1、PullScrollView
2、Android仿IOS回彈效果 ScrollView回彈 總結(jié)
3、Android ScrollView回彈效果(二)
4、高仿QQ的上下回彈效果之自定義的ScrollView
5、自定義上拉下拉反彈ScrollView
6、android坐標(biāo)
7、Android-onInterceptTouchEvent()和onTouchEvent()總結(jié)
8、scrollView的fillviewport
9、Android 自定義UI View - 04 圓形圖片控件之自定義屬性
總結(jié)
以上是生活随笔為你收集整理的PullScrollView详解(三)——PullScrollView实现的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 嘿嘿,我的读者拿到阿里offer,复盘他
- 下一篇: 下拉搜索词api接口、淘宝搜索下拉框选词