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