標籤:android scrollview 滑動衝突
最近在解決一個比較棘手的問題,就是ScrollView與父視圖之間滑動衝突的問題。
這裡所說的滑動衝突,是指包裹ScrollView的父視圖本身就是一個可以滑動的組件,比如說ScrollView嵌套ScrollView的情況(當然,這在實際應用中是沒有意義的,因為ScrollView只能包含一個子組件,這裡只是舉一個栗子)。
問題定義
說要解決這樣一個問題,肯定會有點摸不到頭腦,但我們可以用分解法,來“大事化小”。
遇到這種滑動事件衝突的嵌套首先要明白一個孰先孰後的問題,就是先讓父組件滑動還是先讓子組件滑動。按照一般的情境,我們一般會選擇讓子組件先滑動,因為這樣會比較好處理一點。
那麼問題就來了,為了先讓子組件滑動,我們需要做哪些事情呢?
- 監聽子組件滑動到頂部
- 監聽子組件滑動到底部
- 在子組件滑動完成後將滑動事件傳遞迴父組件
- 可能的卡頓及最佳化
好了,根據這幾個問題,讓我們一個個來解決他們。
解決步驟1. 子組件滑動到頂部與底部的監聽
其實上面的問題1和問題2是可以合并為一個監聽子組件滑動靠岸的問題,因為無論是靠頂部還是靠底部其實都是滑動已經完成的訊號。
這裡我們需要自訂一個自己的ScrollView:
import android.content.Context;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import android.widget.ScrollView;/** * Created by lemondoor on 15/8/8. */public class MyScrollView extends ScrollView { private View contentView; //ScrollView包含子群組件 private OnBorderListener onBorderListener; public MyScrollView(Context context) { super(context); } public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); } public MyScrollView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } /* 在滑動發生時監聽滑動靠岸 */ @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); doOnBorderListener(); } /** * 關鍵函數,判斷什麼時候滑動靠岸 * 因為ScrollView只包含一個子組件,因此子組件的原始測量高度就是ScrollView的高度, * 當縱向滑動值(滑動出螢幕的高度)加上螢幕上顯示的高度大於或等於ScrollView的高度時,就說明到底了, * 當縱向滑動值為0的時候,是從頂部開始顯示的,因此說明處於頂部位置 */ private void doOnBorderListener() { if (contentView != null && contentView.getMeasuredHeight() <= getScrollY() + getHeight()) { if (onBorderListener != null) { onBorderListener.onBottom(); } } else if (getScrollY() == 0) { if (onBorderListener != null) { onBorderListener.onTop(); } } } public void setOnBorderListener(final OnBorderListener onBorderListener) { if(onBorderListener == null) { this.onBorderTouchListener = null; return; } else { this.onBorderListener = onBorderListener; } if(contentView == null) { contentView = getChildAt(0); //因為ScrollView只能包含一個子組件,因此可以直接通過索引‘0’擷取子組件 } } /** * OnborderListener, called when scroll to top or bottom */ public static interface OnBorderListener { /** * Called when scroll to bottom */ public void onBottom(); /** * Called when scroll to top */ public void onTop(); }}
好了,通過上面的代碼,我們已經可以檢測到子組件滑動完成的狀態了。
2. 在檢測到子組件滑動完成後將滑動事件傳遞給父組件
前面拿到了子組件滑動靠岸的資訊,這個時候就需要將事件還給父組件了,畢竟人家已經眼巴巴地讓小輩先玩了那麼久。
這一步,我們需要在執行個體化的MyScrollView的類中進行相應地響應處理。
這就用到了requestDisallowInterceptTouchEvent方法:
mScrollView.setOnBorderListener(new MyScrollView.OnBorderListener() { @Override public void onBottom() { //設為false表示已經不關心該事件了,允許父組件進行事件攔截 mScrollView.getParent().requestDisallowInterceptTouchEvent(false); } @Override public void onTop() { //設為false表示已經不關心該事件了,允許父組件進行事件攔截 mScrollView.getParent().requestDisallowInterceptTouchEvent(false); } });
3. 最佳化
上面的兩步其實已經將問題解決了,但如果試著做一下就會發現,在剛進入頁面的時候,子組件處於頂部的判斷是沒有進行處理的,而且有的時候必須回拉一下才能響應到靠岸的事件。
根據上面的問題,我們制定了兩步的最佳化處理步驟:
3.1 處理子組件初始靠岸狀態
其實滑動還是需要手指去與螢幕互動的,我們沒有必要在看到頁面的狀態就判斷子組件是否靠岸,只需要在滑動的時候進行判斷就好了。
回到我們的MyScrollView類,進行如下修改:
private OnTouchListener onBorderTouchListener;public void setOnBorderListener(final OnBorderListener onBorderListener) { if(onBorderListener == null) { this.onBorderTouchListener = null; return; } else { this.onBorderListener = onBorderListener; } if(contentView == null) { contentView = getChildAt(0); //因為ScrollView只能包含一個子組件,因此可以直接通過索引‘0’擷取子組件 } /* 其實這是另一種處理監聽滑動靠岸的方法,是在touch事件發生時進行滑動靠岸監聽 */ this.onBorderTouchListener = new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { //這裡面試了幾個動作,最終正明MOVE動作的效果是最好,最及時的 case MotionEvent.ACTION_MOVE : doOnBorderListener(); break; } return false; } }; super.setOnTouchListener(onBorderTouchListener); } /** * 重寫setOnTouchListener方法,防止在MyScrollView執行個體中調用該方法的時候,覆蓋掉我們上面做的處理 */ @Override public void setOnTouchListener(final OnTouchListener l) { OnTouchListener onTouchListener = new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (onBorderTouchListener != null) { onBorderTouchListener.onTouch(v, event); } return l.onTouch(v, event); } }; super.setOnTouchListener(onTouchListener); }
這裡其實已經處理了上面提到的初始化和偶爾需要會拉才會觸發靠岸事件的問題,但又會產生新的父組件會跟著子組件一起滑動的問題,這是因為我們只是給出了允許父組件攔截事件的通道,卻沒有給出不允許父組件攔截的通道。
3.2 繼續最佳化,設定不允許父組件攔截的情況
其實原理很簡單,就是子組件沒有滑動靠岸的時候就不允許父組件攔截事件,言下之意就是我還沒玩夠,你在等等。
還是要修改MyScrollView類:
private void doOnBorderListener() { if (contentView != null && contentView.getMeasuredHeight() <= getScrollY() + getHeight()) { if (onBorderListener != null) { onBorderListener.onBottom(); } } else if (getScrollY() == 0) { if (onBorderListener != null) { onBorderListener.onTop(); } } else { //沒靠岸的時候調用onMiddle方法 if (onBorderListener != null) { onBorderListener.onMiddle(); } } }//在介面中加一個onMiddle方法,來處理沒靠岸的情況public static interface OnBorderListener { /** * Called when scroll to bottom */ public void onBottom(); /** * Called when scroll to top */ public void onTop(); /** * Called when scroll in middle */ public void onMiddle(); }
繼續在執行個體化的mScrollView中進行如下修改
mScrollView.setOnBorderListener(new MyScrollView.OnBorderListener() { @Override public void onBottom() { mScrollView.getParent().requestDisallowInterceptTouchEvent(false); } @Override public void onTop() { mScrollView.getParent().requestDisallowInterceptTouchEvent(false); } @Override public void onMiddle() { //在沒靠岸的時候不允許父組件攔截事件 mScrollView.getParent().requestDisallowInterceptTouchEvent(true); } });
引用聲明
- android 事件處理機制之requestDisallowInterceptTouchEvent
- 滑動到底部或頂部響應的ScrollView實現
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
Android 解決ScrollView與父視圖滑動衝突問題