View的滑動衝突

介紹
相信開發Android的人都會有這種體會:從網上下載的demo運行的好好的,但是隻要出現了滑動衝突,Demo就無法正常工作了。但是不用擔心,解決滑動衝突有固定的模式,常見的有內部攔截和外部攔截兩種,只要按照這個模式來就可以順利解決。本文會涉及到View事件分發的相關知識,關於事件分發請參考http://sparkyuan.me/2016/03/11/View%E4%BA%8B%E4%BB%B6%E5%88%86%E5%8F%91%E6%9C%BA%E5%88%B6/

常見的滑動衝突場景

圖片

示例

處理規則

對於場景1,處理規則爲:當用戶左右滑動時,讓外部的View攔截點擊事件,當用戶上下滑動時,讓內部的View攔截點擊事件。當產生滑動時,根據滑動的起始點與終點座標位置,如果垂直方向滑動距離大,就判斷爲垂直滑動,否則判斷爲水平滑動。其他兩種情況處理方法相似,都是從業務需求上得出相應的規則。

解決方法

外部攔截髮

所有的點擊事件都先經過父容器攔截處理,如果父容器需要攔截就攔截,不需要就傳給內部的View。僞代碼如下

public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;

  switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN: {
        intercepted = false;
          break;
      }
      case MotionEvent.ACTION_MOVE: {
          if (滿足父容器的攔截要求) {
              intercepted = true;
          } else {
              intercepted = false;
          }
          break;
      }
      case MotionEvent.ACTION_UP: {
          intercepted = false;
          break;
      }
      default:
          break;
  }
  mLastXIntercept = x;
  mLastYIntercept = y;
  return intercepted;

}
注:
ACTION_DOWN這個事件是不能攔截的,因爲一旦攔截後續的事件都會由父容器處理了。

內部攔截法

父容器不攔截任何事件,所有事件都傳給子元素。如果子元素需要此事件就直接消耗,否則就交給父容器進行處理。完成這個功能需要配合requestDisallowInterceptTouchEvent()方法纔可。這個方法表示是否讓父容器攔截事件。僞代碼如下:
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();

   switch (event.getAction()) {
   case MotionEvent.ACTION_DOWN: {
       parent.requestDisallowInterceptTouchEvent(true);
       break;
   }
   case MotionEvent.ACTION_MOVE: {
       if (滿足父容器的攔截要求) {
           parent.requestDisallowInterceptTouchEvent(false);
       }
       break;
   }
   case MotionEvent.ACTION_UP: {
       break;
   }
   default:
       break;
   }
   mLastX = x;
   mLastY = y;
   return super.dispatchTouchEvent(event);

}

父容器默認攔截除了ACTION_DOWN以外的其他事件,這樣子當元素調用parent.requestDisallowInterceptTouchEvent(false)時,父元素才能攔截所需的事件。

總結

解決滑動衝突有兩種方法,推薦外部攔截法,實現起來簡單。
本文以場景1爲例做了講解,場景2,3的做法與1類似,都是根據業務需要制定處理規則。

示例效果的源碼

Activity

public class DemoActivity_1 extends Activity {
private static final String TAG = “DemoActivity_1”;

private HorizontalScrollViewEx mListContainer;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.demo_1);
    Log.d(TAG, "onCreate");
    initView();
}

private void initView() {
    LayoutInflater inflater = getLayoutInflater();
    mListContainer = (HorizontalScrollViewEx) findViewById(R.id.container);
    final int screenWidth = MyUtils.getScreenMetrics(this).widthPixels;
    final int screenHeight = MyUtils.getScreenMetrics(this).heightPixels;
    for (int i = 0; i < 3; i++) {
        ViewGroup layout = (ViewGroup) inflater.inflate(
                R.layout.content_layout, mListContainer, false);
        layout.getLayoutParams().width = screenWidth;
        TextView textView = (TextView) layout.findViewById(R.id.title);
        textView.setText("page " + (i + 1));
        layout.setBackgroundColor(Color.rgb(255 / (i + 1), 255 / (i + 1), 0));
        createList(layout);
        mListContainer.addView(layout);
    }
}

private void createList(ViewGroup layout) {
    ListView listView = (ListView) layout.findViewById(R.id.list);
    ArrayList<String> datas = new ArrayList<String>();
    for (int i = 0; i < 50; i++) {
        datas.add("name " + i);
    }

    ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
            R.layout.content_list_item, R.id.name, datas);
    listView.setAdapter(adapter);
    listView.setOnItemClickListener(new OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> parent, View view,
                int position, long id) {
            Toast.makeText(DemoActivity_1.this, "click item",
                    Toast.LENGTH_SHORT).show();

        }
    });
}

}

水平滑動的View

private int mLastY = 0;
// 分別記錄上次滑動的座標(onInterceptTouchEvent)
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;

private Scroller mScroller;
private VelocityTracker mVelocityTracker;

public HorizontalScrollViewEx(Context context) {
    super(context);
    init();
}

public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
}

public HorizontalScrollViewEx(Context context, AttributeSet attrs,
                              int defStyle) {
    super(context, attrs, defStyle);
    init();
}

private void init() {
    mScroller = new Scroller(getContext());
    mVelocityTracker = VelocityTracker.obtain();
}

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercepted = false;
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            Log.d(TAG, "onInterceptTouchEvent: ACTION_DOWN");
            intercepted = false;
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
                intercepted = true;
            }
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            Log.d(TAG, "onInterceptTouchEvent: ACTION_MOVE");
            int deltaX = x - mLastXIntercept;
            int deltaY = y - mLastYIntercept;
            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            intercepted = false;
            break;
        }
        default:
            break;
    }

    Log.d(TAG, "intercepted=" + intercepted);
    mLastX = x;
    mLastY = y;
    mLastXIntercept = x;
    mLastYIntercept = y;

    return intercepted;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    mVelocityTracker.addMovement(event);
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            Log.d(TAG, "onTouchEvent: ACTION_DOWN");
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
            }
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            Log.d(TAG, "onTouchEvent: ACTION_MOVE");
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            Log.d(TAG, "onTouchEvent: deltaX" + deltaX);
            scrollBy(-deltaX, 0);
            break;
        }
        case MotionEvent.ACTION_UP: {

            int scrollX = getScrollX();
            int scrollToChildIndex = scrollX / mChildWidth;
            mVelocityTracker.computeCurrentVelocity(1000);
            float xVelocity = mVelocityTracker.getXVelocity();

            //滑的速度到達閾值就認爲需要進入下一頁
            if (Math.abs(xVelocity) >= 100) {
                mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
            } else {
                //滑動的距離超過一半,就進入下一頁
                mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
            }
            //保證在0頁和最後一頁滑動時不會越界
            mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
            //沒有達到進入下一頁的要求,恢復原樣
            int dx = mChildIndex * mChildWidth - scrollX;
            smoothScrollBy(dx, 0);
            Log.d(TAG, "onTouchEvent: dx = " + dx);
            mVelocityTracker.clear();
            break;
        }
        default:
            break;
    }

    mLastX = x;
    mLastY = y;
    return true;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int measuredWidth = 0;
    int measuredHeight = 0;
    final int childCount = getChildCount();
    measureChildren(widthMeasureSpec, heightMeasureSpec);

    int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    if (childCount == 0) {
        setMeasuredDimension(0, 0);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
        final View childView = getChildAt(0);
        measuredHeight = childView.getMeasuredHeight();
        setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
        final View childView = getChildAt(0);
        measuredWidth = childView.getMeasuredWidth() * childCount;
        setMeasuredDimension(measuredWidth, heightSpaceSize);
    } else {
        final View childView = getChildAt(0);
        measuredWidth = childView.getMeasuredWidth() * childCount;
        measuredHeight = childView.getMeasuredHeight();
        setMeasuredDimension(measuredWidth, measuredHeight);
    }
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childLeft = 0;
    final int childCount = getChildCount();
    mChildrenSize = childCount;

    for (int i = 0; i < childCount; i++) {
        final View childView = getChildAt(i);
        if (childView.getVisibility() != View.GONE) {
            final int childWidth = childView.getMeasuredWidth();
            mChildWidth = childWidth;
            childView.layout(childLeft, 0, childLeft + childWidth,
                    childView.getMeasuredHeight());
            childLeft += childWidth;
        }
    }
}

private void smoothScrollBy(int dx, int dy) {
    mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
    invalidate();
}

@Override
public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
        postInvalidate();
    }
}

@Override
protected void onDetachedFromWindow() {
    mVelocityTracker.recycle();
    super.onDetachedFromWindow();
}

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章