Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android滑動沖突的完美解決方案

Android滑動沖突的完美解決方案

編輯:關於Android編程

關於滑動沖突

在Android開發中,如果是一些簡單的布局,都很容易搞定,但是一旦涉及到復雜的頁面,特別是為了兼容小屏手機而使用了ScrollView以後,就會出現很多點擊事件的沖突,最經典的就是ScrollView中嵌套了ListView。我想大部分剛開始接觸Android的同學們都踩到過這個坑,下面跟著小編一起來看看解決方案吧。。

同方向滑動沖突

比如ScrollView嵌套ListView,或者是ScrollView嵌套自己

這裡先看一張效果圖

上圖是在購物軟件上常見的上拉查看圖文詳情,關於這中動畫效果的實現,其實實現整體的效果,辦法是有很多的,網上有很多相關的例子,但是對某些細節的處理不是很清晰,比如,下拉之後顯示的部分(例如底部的圖文詳情)又是一個類ScrollView的控件(比如WebView)的話,又會產生新的問題。這裡就以下拉查看圖文詳情為背景做一下同方向滑動沖突的分析。

整體思路
這裡看下圖

多個ScrollView嵌套示意圖

首先,關於這張圖做一些設定:

     1.黑色的框代表手機屏幕

     2.綠色的框代表一個外層的ScrollView

     3.兩個紅色的框代表嵌套在裡面的兩個類ScrollView控件,這裡我們就暫時簡稱為 SUp,SDown

好了,接下來就分析一下實現整個流程的過程。

這裡必須明確的一點,無論何時,SUp和SDown可見的部分始終是手機屏幕的高度。知道了這一點,我們就可以按以下步驟展開

首先,我們確保外部的ScrollView不攔截滑動事件,這樣SUp必然獲得此次事件,然後根據其Action_Move事件,當其為向下滑動且自身滑動距離+屏幕高度=其自身高度 時,即可認為SUp滑動到了底部,此時外部ScrollView可攔截滑動事件,從而保證整個視圖能夠繼續向下滑動,這個時候底部SDown就顯示出來了。

同理,這時候不允許外部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

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:orientation="vertical">

 <com.example.dreamwork.activity.superscrollview.CustomerScrollViews
 android:layout_width="match_parent"
 android:layout_height="match_parent">

 <LinearLayout
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="vertical">

  <com.example.dreamwork.activity.superscrollview.UpScrollView

  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:scrollbars="vertical">

  <LinearLayout
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:orientation="vertical">

   <ImageView
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:scaleType="fitXY"
   android:src="@drawable/taobao" />
   <ImageView
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:scaleType="fitXY"
   android:src="@drawable/taobao" />

   <TextView
   android:textSize="20sp"
   android:padding="10dp"
   android:gravity="center"
   android:layout_marginTop="20dp"
   android:layout_marginBottom="60dp"
   android:text="查看圖文詳情"
   android:layout_width="match_parent"
   android:layout_height="wrap_content" />

  </LinearLayout>
  </com.example.dreamwork.activity.superscrollview.UpScrollView>

  <include layout="@layout/selector_tab_items" />

  <com.example.dreamwork.activity.superscrollview.MyWebView
  android:id="@+id/web"
  android:layout_width="match_parent"
  android:layout_height="match_parent"/>

 </LinearLayout>
 </com.example.dreamwork.activity.superscrollview.CustomerScrollViews>

</LinearLayout>

整個布局文件可以看出,我們在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測試,實際中應考慮通過fragment切換實現。

這裡對滑動沖突的解決方法,是由內而外的展開,默認使頂層View失去攔截能力,在由底部View的滑動距離,做出不同邏輯判斷控制了頂層的攔截與否;這也是比較容易理解和實現的思路。當然,對於此類滑動沖突,有很多不同思路,這裡只是列舉其一。

實際開發開發中,這種帶有同方向滑動特性的控件嵌套時,產生的問題不只是滑動沖突,有時候會有內容顯示不全或不顯示的情況。

最常見如ScrollView嵌套ListView,這種情況只需自定義ListView使其高度計算為一個很大的值,某種意義上讓其失去了滑動的特性,但是這樣也讓ListView貌似失去了視圖回收機制,這種時候如果加載很多很多很多圖片,效果就會很不理想。對於這種情況,通過對ListView添加headView及footView也是一種解決的辦法,但也得實際UI情況允許。

ScrollView嵌套RecyclerView時稍微麻煩一點,需要自定義ScrollView,還需要自定義實現LinearLayoutManager。

不同方向滑動沖突

比如ScrollView嵌套ViewPager,或者是ViewPager嵌套ScrollView,這種情況其實很典型。現在大部分應用最外層都是ViewPager+Fragment 的底部切換(比如微信)結構,這種時候,就很容易出現滑動沖突。不過ViewPager裡面無論是嵌套ListView還是ScrollView,滑動沖突是沒有的,畢竟是官方的東西,可能已經考慮到了這些,所以比較完善。

復雜一點的滑動沖突,基本上就是這兩個沖突結合的結果。

滑動沖突解決思路
滑動沖突,就其本質來說,兩個不同方向(或者是同方向)的View,其中有一個是占主導地位的,每次總是搶著去處理外界的滑動行為,這樣就導致一種很別扭的用戶體驗,明明只是橫向的滑動了一下,縱向的列表卻在垂直方向發生了動作。就是說,這個占主導地位的View,每一次都身不由己的攔截了這個滑動的動作,因此,要解決滑動沖突,就是得明確告訴這個占主導地位的View,什麼時候你該攔截,什麼時候你不應該攔截,應該由下一層的View去處理這個滑動動作。

這裡不明白的同學,可以去了解一下Android Touch事件的分發機制,這也是解決滑動沖突的核心知識。

滑動沖突

這裡,說一下背景情況。之前做下拉刷新、上拉加載更多時一直使用的是PullToRefreshView這個控件,因為很方便,不用導入三方工程。在其內部可以放置ListView,GridView及ScrollView,非常方便,用起來可謂是屢試不爽。但是直到有一天,因項目需要,在ListView頂部加了一個輪播圖控件BannerView(這個可以參考之前寫的一篇學習筆記)。結果發現輪播圖滑動的時候,和縱向的下拉刷新組件沖突了。

如之前所說,解決滑動沖突的關鍵,就是明確告知接收到Touch的View,是否需要攔截此次事件。

解決方法

解決方案1,從外部攔截機制考慮

這裡,相當於是PullToRefreshView嵌套了ViewPager,那麼每次優先接收到Touch事件的必然是PullToRefreshView。這樣就清楚了,看代碼:

在PullToRefreshView中:

 @Override
 public boolean onInterceptTouchEvent(MotionEvent e) {
 int y = (int) e.getRawY();
 int x = (int) e.getRawX();
 boolean resume = false;
 switch (e.getAction()) {
  case MotionEvent.ACTION_DOWN:
  // 發生down事件時,記錄y坐標
  mLastMotionY = y;
  mLastMotionX = x;
  resume = false;
  break;
  case MotionEvent.ACTION_MOVE:
  // deltaY > 0 是向下運動,< 0是向上運動
  int deltaY = y - mLastMotionY;
  int deleaX = x - mLastMotionX;

  if (Math.abs(deleaX) > Math.abs(deltaY)) {
   resume = false;
  } else {
  //當前正處於滑動
   if (isRefreshViewScroll(deltaY)) {
   resume = true;
   }
  }
  break;
  case MotionEvent.ACTION_UP:
  case MotionEvent.ACTION_CANCEL:
  break;
 }
 return resume;
 }

這裡最關鍵的代碼就是這行

if (Math.abs(deleaX) > Math.abs(deltaY)) {
   resume = false;
  }

橫向滑動距離大於縱向時,無須攔截這次滑動事件。其實,就是這麼簡單,但前提是你必須明確了解Android Touch事件的傳遞機制,期間各個方法執行的順序及意義。

解決方案2,從內容逆向思維分析

有時候,我們不想去修改引入的第三方控件,或者說是無法修改時。就必須考慮從當前從Touch傳遞事件中最後的那個View逆向考慮。首先,由Android中View的Touch事件傳遞機制,我們知道Touch事件,首先必然由最外層View攔截,如果無法更改這個最外層View,那麼是不是就沒轍了呢?其實不然,Android這麼高大上的系統必然考慮到了這個問題,好了廢話不說,先看代碼

 private BannerView carouselView;
 private Context mContext;

 private PullToRefreshView refreshView;

 .........

 refreshView.setOnTouchListener(new View.OnTouchListener() {
  @Override
  public boolean onTouch(View v, MotionEvent event) {
  carouselView.getParent().requestDisallowInterceptTouchEvent(false);
  return false;
  }
 });

 carouselView.setOnTouchListener(new View.OnTouchListener() {
  @Override
  public boolean onTouch(View v, MotionEvent event) {
  carouselView.getParent().requestDisallowInterceptTouchEvent(true);
  int x = (int) event.getRawX();
  int y = (int) event.getRawY();

  switch (event.getAction()) {
   case MotionEvent.ACTION_DOWN:
   lastX = x;
   lastY = y;
   break;
   case MotionEvent.ACTION_MOVE:
   int deltaY = y - lastY;
   int deltaX = x - lastX;
   if (Math.abs(deltaX) < Math.abs(deltaY)) {
    carouselView.getParent().requestDisallowInterceptTouchEvent(false);
   } else {
    carouselView.getParent().requestDisallowInterceptTouchEvent(true);
   }
   default:
   break;
  }
  return false;
  }
 });

首先說一下這個方法

public abstract void requestDisallowInterceptTouchEvent (boolean disallowIntercept)

API裡的意思很明確,子View如果不希望其父View攔截Touch事件時,可調用此方法。當disallowIntercept這個參數為true時,父View將不攔截。

PS:這個方法的命名和其參數的使用邏輯,讓我想到了一句很有意思的話,敵人的敵人就是朋友,真不知道Google的大神們怎麼想的,非要搞一個反邏輯。

言歸正傳。這裡攔截直接也很明確,在carouselView的onTouch方法中每次進入就設定父View不攔截此次事件,然後在MOTION_MOVE時候,根據滑動的距離判斷再決定是父View是否有權利攔截Touch事件(即滑動行為)。

總結

好了,本文內容到這基本就結束了,本篇文章只是提供一種解決方法的思路,在具體的場景下,交互往往是貼合具體業務需求的。但是不管怎麼樣,找出點擊事件截斷和處理的時機是最重要的,圍繞這個關鍵點,總能找出相應的解決方法。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved