關於Android滑動衝突的解決方案(二)
之前的一遍學習筆記主要就Android滑動衝突中,在不同方向的滑動所造成衝突進行了瞭解,這種衝突很容易理解,當然也很容易解決。今天,就同方向的滑動所造成的衝突進行一下瞭解,這裡就先以垂直方向的滑動衝突為背景,這也是日常開發中最常見的一種情況。
這裡先看一張
由於GIF 圖片大小的限制,效果不是很好
是在購物軟體上常見的上拉查看圖文詳情,關於這中動畫效果的實現,其實實現整體的效果,辦法是有很多的,網上有很多相關的例子,但是對某些細節的處理不是很清晰,比如,下拉之後顯示的部分(例如底部的圖文詳情)又是一個類ScrollView的控制項(比如WebView)的話,又會產生新的問題。這裡就以下拉查看圖文詳情為背景做一下同方向滑動衝突的分析。
整體思路
這裡看
首先,關於這張圖做一些設定:
黑色的框代表手機螢幕
綠色的框代表一個外層的ScrollView
兩個紅色的框代表嵌套在裡面的兩個類ScrollView控制項1,這裡我們就暫時簡稱為 SUp,SDown
好了,接下來就分析一下實現整個流程的過程。
這裡必須明確的一點,無論何時,SUp和SDown可見的部分始終是手機螢幕的高度。知道了這一點,我們就可以按以下步驟展開
首先,我們確保外部的ScrollView不攔截滑動事件,這樣SUp必然獲得此次事件,然後根據其Action_Move事件,當其為向下滑動且自身滑動距離+螢幕高度=其自身高度 時,即可認為SUp滑動到了底部,此時外部ScrollView可攔截滑動事件,從而保證能夠繼續向下滑動,這個時候底部SDown就顯示出來了。
同理,這時候外部ScrollView不允許外部ScrollView攔截滑動事件,由SDown處理,根據其Action_move事件,當其為向上滑動,且自身可滑動距離為0時,就說明SDown已經滑動到了頂部,這時外部ScrollView又可以獲得攔截滑動事件的權利,從而保證整個視圖能夠向上繼續滑動,此時SUp再次顯示,有開始新一輪迴圈攔截。
這樣整體的一個流程就可以實現動圖中的效果。好了,說完原理,還是看代碼。
代碼實現SUp實現
public class UpScrollView extends ScrollView { /** * 螢幕高度 */ private int mScreenHeight; /** * 上一次的座標 */ private float mLastY; /** * 當前View滑動距離 */ private int mScrollY; /** * 當前View內子控制項高度 */ private int mChildH; public UpScrollView(Context context) { super(context); init(context); } public UpScrollView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public UpScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context) { WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); DisplayMetrics dm = new DisplayMetrics(); wm.getDefaultDisplay().getMetrics(dm); mScreenHeight = dm.heightPixels; } @Override public boolean onTouchEvent(MotionEvent ev) { //預設設定頂層View不攔截 getParent().getParent().requestDisallowInterceptTouchEvent(true); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mLastY = (int) ev.getY(); break; case MotionEvent.ACTION_MOVE: float y = ev.getY(); float deltaY = y - mLastY; mChildH = this.getChildAt(0).getMeasuredHeight(); int translateY = mChildH - mScrollY; //向上滑動時,如果translateY等於螢幕高度時,即表明滑動到底部,可又頂層View控制滑動 if (deltaY < 0 && translateY == mScreenHeight) { getParent().getParent().requestDisallowInterceptTouchEvent(false); } break; default: break; } return super.onTouchEvent(ev); } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); mScrollY = t; }}
這裡在ACTION_MOVE裡做了減法,其實道理是一樣的。
onScrollChanged 是在View類中實現,查看其API可以看到其第二個參數t解釋
@param t Current vertical scroll origin.
即為當前View此次滑動的距離
SDown實現
public class MyWebView extends WebView { public float oldY; private int t; public MyWebView(Context context) { super(context); init(); } public MyWebView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public MyWebView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { WebSettings settings = getSettings(); settings.setJavaScriptEnabled(true); setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } }); } @Override public boolean onTouchEvent(MotionEvent ev) { getParent().getParent().requestDisallowInterceptTouchEvent(true); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: oldY = ev.getY(); break; case MotionEvent.ACTION_MOVE: float Y = ev.getY(); float Ys = Y - oldY; if (Ys > 0 && t == 0) { getParent().getParent().requestDisallowInterceptTouchEvent(false); } break; } return super.onTouchEvent(ev); } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { this.t = t; super.onScrollChanged(l, t, oldl, oldt); }}
看以看到,這裡底部的View並沒有繼承ScrollView,而是選擇繼承了WebView,這裡只是為了方便,當然繼承ScrollView也是沒有問題。這裡只是需要按實際情況考慮,因為底部圖文詳情的內容就是一個WebView載入資料。
這個類的實現,按照之前說的原理應該很好理解。
外部ScrollView
public class CustomerScrollViews extends ScrollView { /** * 螢幕高度 */ private int mScreenHeight; private UpScrollView upScrollView; private MyWebView myWebView; private boolean init = false; private float fator = 0.2f; private int factorHeight; private boolean upShow = true; public CustomerScrollViews(Context context) { super(context); init(context); } public CustomerScrollViews(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public CustomerScrollViews(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context) { WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); DisplayMetrics dm = new DisplayMetrics(); wm.getDefaultDisplay().getMetrics(dm); mScreenHeight = dm.heightPixels; factorHeight = (int) (mScreenHeight * fator); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (!init) { LinearLayout parentView = (LinearLayout) getChildAt(0); //獲得內部的兩個子view upScrollView = (UpScrollView) parentView.getChildAt(0); myWebView = (MyWebView) parentView.getChildAt(2);// //並設定其高度為螢幕高度 upScrollView.getLayoutParams().height = mScreenHeight; myWebView.getLayoutParams().height = mScreenHeight; init = true; } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (changed) { scrollTo(0, 0); } } @Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_UP: int scrollY = getScrollY(); if (upShow) { if (scrollY <= factorHeight) { smoothScrollTo(0, 0); } else { smoothScrollTo(0, mScreenHeight); upShow = false; } } else { int scrollpadding = mScreenHeight - scrollY; if (scrollpadding >= factorHeight) { this.smoothScrollTo(0, 0); upShow = true; } else { this.smoothScrollTo(0, mScreenHeight); } } return true; } return super.onTouchEvent(ev); }}
這個類的實現,就很靈活了,在onMeasure方法中初始化完內部的View之後,在OnTouch方法中就可以根據實際需求完成不同的邏輯實現,這裡只是為了仿照查看圖文詳情的效果,對整個視圖通過ScrollView的smoothScrollTo方法進行位移變化,這個邏輯很簡單。
這裡重點說一下一個地方:
upScrollView = (UpScrollView) parentView.getChildAt(0);myWebView = (MyWebView) parentView.getChildAt(2);
你可能會奇怪中間的child(1)去了哪裡?這裡還要從MainActivity的布局檔案說起
dual_scrollview_activity_layout1.xml
整個布局檔案可以看出,我們在CustomerScrollViews這個最外層的自訂ScrollView內部又放置了兩個自訂的ScrollView(就如我們看到的原理圖那樣),只不過在這兩個ScrollView類控制項的中間通過layout又放置一個LinearLayout,裡面的內容就是在動圖中看到的那個中間的寫著qq,baidu字樣的用於切換WebView內容的一個View。這裡就不貼代碼了。
這樣,你就可以理解之前的child(1)為什麼被跳過了吧。
使用
public class DualScrollViewActivity1 extends Activity implements View.OnClickListener { private MyWebView webView; private TextView sinaTv, qqTv, baiduTv; private View line1, line2, line3; private final String BAIDU = "http://www.baidu.com"; private final String QQ = "http://www.qq.com"; private final String SINA = "http://sina.cn"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); InitView(); sinaTv.performClick(); } private void InitView() { setContentView(R.layout.dual_scrollview_activity_layout1); webView = V.f(this, R.id.web); sinaTv = V.f(this, R.id.one); sinaTv.setOnClickListener(this); qqTv = V.f(this, R.id.two); qqTv.setOnClickListener(this); baiduTv = V.f(this, R.id.three); baiduTv.setOnClickListener(this); line1 = V.f(this, R.id.line1); line2 = V.f(this, R.id.line2); line3 = V.f(this, R.id.line3); } @Override public void onClick(View v) { reset(); String url = ""; switch (v.getId()) { case R.id.one: line1.setVisibility(View.VISIBLE); url = SINA; break; case R.id.two: line2.setVisibility(View.VISIBLE); url = QQ; break; case R.id.three: line3.setVisibility(View.VISIBLE); url = BAIDU; break; default: break; } webView.loadUrl(url); } private void reset(){ line1.setVisibility(View.GONE); line2.setVisibility(View.GONE); line3.setVisibility(View.GONE); }}
這裡關於底部View內容更新,WebView 通過載入不同URL實現不同視圖效果,只是作為Demo測試。
這裡對滑動衝突的解決方案,是由內而外的展開,預設使頂層View失去攔截能力,在由底部View的滑動距離,做出不同邏輯判斷控制了頂層的攔截與否;這也是比較容易理解和實現的思路。當然,對於此類滑動衝突,有很多不同思路,這裡只是列舉其一。
實際開發開發中,這種帶有同方向滑動特性的控制項嵌套時,產生的問題不只是滑動衝突,有時候會有內容顯示不全或不顯示的情況。
最常見如ScrollView嵌套ListView,這種情況只需自訂ListView使其高度計算為一個很大的值,某種意義上讓其失去了滑動的特性,但是這樣也讓ListView貌似失去了視圖回收機制,這種時候如果載入很多很多很多圖片,效果就會很不理想。對於這種情況,通過對ListView添加headView及footView也是一種解決的辦法,但也得實際UI情況允許。
ScrollView嵌套RecyclerView時稍微麻煩一點,需要自訂ScrollView,還需要自訂實現LinearLayoutManager。
由此可見,在Android中這種帶有類似特性的控制項嵌套後,問題還是很多的。對此,Google出了一個叫做NestedScrolling的東西,接下來準備研究一下,看看其在解決滑動衝突方面的優勢。
- 這裡說類ScrollView控制項,就是說類似於ScrollView特性的控制項,即內建滾動特性的控制項。