在读完了上一节
android事件分发(三)重要的函数requestDisallowInterceptTouchEvent
之后,可能有点意犹未尽的感觉,这一篇将说明requestDisallowInterceptTouchEvent在解决滑动冲突时的巨大作用。 在有多个滚动控件的时候常常提到滑动冲突,那什么是滑动冲突,又如何解决呢?为什么原生的控件一般都不存在滑动冲突的问题?这就是本文要说的故事滑动冲突概念
什么是滑动冲突,比如我有一个listview,还有一个viewpager,listview可以纵向滑动,viewpager可以横向滑动,我在viewpager上横向滑了一大段,在这个滑动过程中,很可能我们的手不仅仅横向滑了,纵向也滑了,谁也无法保证自己滑的是一条直线,很可能是横向滑了100dp,纵向抖动了3dp,此时理想的情况是只有viewpager滑动,listview不要瞎JB乱动,但是现实往往是残酷的,viewpager动了,listview也动了,这就是滑动冲突。我们想要的效果是如果viewpager开始滑了,那么listview就不准滑,除非手指抬起再按下,也就是说在一个cycle内,listview不准滑,这让我们想起了上节 的requestDisallowInterceptTouchEvent,简直是量身定做的啊。
滑动冲突举例
下面用一个例子来说明如何解决滑动冲突,我们有个listview,listview内的item是可滑动的,左划会出现删除按钮,效果如下,存在滑动冲突
package com.fish.scrollconflict;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.TextView;
import java.util.List;
public class MyBaseAdapter extends BaseAdapter {
private Context mContext;
private List<String> mData;
private LayoutInflater mInflater;
public static final class ViewHolder {
public TextView textView;
public Button btn;
}
public MyBaseAdapter(Context context, List<String> data) {
mContext = context;
mData = data;
this.mInflater = LayoutInflater.from(context);
}
@Override
public int getCount() {
return mData.size();
}
@Override
public Object getItem(int arg0) {
return mData.get(arg0);
}
@Override
public long getItemId(int arg0) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// LogUtil.fish("getview "+position);
ViewHolder holder = null;
if (convertView == null) {
holder = new ViewHolder();
convertView = mInflater.inflate(R.layout.item, parent, false);
holder.textView = (TextView) convertView.findViewById(R.id.tv);
holder.btn = (Button) convertView.findViewById(R.id.btn);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.textView.setText(mData.get(position));
// holder.textView.setText((String) mData.get(position).get("textView"));
//给每一个列表后面的按钮添加响应事件
holder.btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// showInfo();
}
});
return convertView;
}
}
package com.fish.scrollconflict;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
/**
* Created by fish on 16/6/28.
*/
public class ScrollLayout extends LinearLayout {
public ScrollLayout(Context context) {
super(context);
}
public ScrollLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
TextView tv;
Button btn;
float mLastX;
float mLastY;
boolean isScrolling;
@Override
protected void onFinishInflate() {
super.onFinishInflate();
tv = (TextView) findViewById(R.id.tv);
btn = (Button) findViewById(R.id.btn);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = event.getX();
mLastY = event.getY();
isScrolling = false;
return true;
case MotionEvent.ACTION_MOVE:
//左划,应该为负的
float xDiff = event.getX() - mLastX;
mLastX = event.getX();
LinearLayout.LayoutParams params = (LayoutParams) tv.getLayoutParams();
params.leftMargin += xDiff;
if (params.leftMargin > 0) {
params.leftMargin = 0;
}
tv.setLayoutParams(params);
return true;
}
return super.onTouchEvent(event);
}
}
<?xml version="1.0" encoding="utf-8"?>
<com.fish.scrollconflict.ScrollLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="350dp"
android:layout_height="100dp"
android:gravity="center_vertical"
android:background="#44ff0000">
<TextView
android:textSize="20sp"
android:id="@+id/tv"
android:layout_width="350dp"
android:layout_height="wrap_content"
android:text="Hello 我是无辜的!" />
<Button
android:textSize="20sp"
android:id="@+id/btn"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="删除" />
</com.fish.scrollconflict.ScrollLayout>
很明显上图中存在滑动冲突,那么怎么解决呢?
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = event.getX();
mLastY = event.getY();
isScrolling = false;
return true;
case MotionEvent.ACTION_MOVE:
//左划,应该为负的
float xDiff = event.getX() - mLastX;
float yDiff = event.getY() - mLastY;
if (Math.abs(xDiff) > Math.abs(yDiff)) {
isScrolling = true;
getParent().requestDisallowInterceptTouchEvent(true);
}
mLastX = event.getX();
LinearLayout.LayoutParams params = (LayoutParams) tv.getLayoutParams();
params.leftMargin += xDiff;
if (params.leftMargin > 0) {
params.leftMargin = 0;
}
tv.setLayoutParams(params);
return true;
}
return super.onTouchEvent(event);
}
非常简单就解决了滑动冲突,看下图,可以发现,不会有滑动冲突了,listview不会乱滚了
viewpager在listview内会不会滑动冲突
经常看到有人说viewpager放在listview里会产生滑动冲突,还有人给出了若干解决方案,我自己写了个试了一下,发现没有冲突啊?到底是他们错了,还是我错了呢?
原来低版本的时候的确会有冲突,高版本修复了这个问题
这是4.4_r1的Viewpager的onTouchEvent的处理move的代码,在supportv4包内,
case MotionEvent.ACTION_MOVE:
if (!mIsBeingDragged) {
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float xDiff = Math.abs(x - mLastMotionX);
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float yDiff = Math.abs(y - mLastMotionY);
if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
if (xDiff > mTouchSlop && xDiff > yDiff) {
if (DEBUG) Log.v(TAG, "Starting drag!");
mIsBeingDragged = true;
requestParentDisallowInterceptTouchEvent(true);
mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
mInitialMotionX - mTouchSlop;
mLastMotionY = y;
setScrollState(SCROLL_STATE_DRAGGING);
setScrollingCacheEnabled(true);
// Disallow Parent Intercept, just in case
ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
}
// Not else! Note that mIsBeingDragged can be set above.
if (mIsBeingDragged) {
// Scroll to follow the motion event
final int activePointerIndex = MotionEventCompat.findPointerIndex(
ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, activePointerIndex);
needsInvalidate |= performDrag(x);
}
break;
而上一个版本4.3.1_r1,
case MotionEvent.ACTION_MOVE:
if (!mIsBeingDragged) {
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float xDiff = Math.abs(x - mLastMotionX);
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float yDiff = Math.abs(y - mLastMotionY);
if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
if (xDiff > mTouchSlop && xDiff > yDiff) {
if (DEBUG) Log.v(TAG, "Starting drag!");
mIsBeingDragged = true;
mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
mInitialMotionX - mTouchSlop;
mLastMotionY = y;
setScrollState(SCROLL_STATE_DRAGGING);
setScrollingCacheEnabled(true);
}
}
// Not else! Note that mIsBeingDragged can be set above.
if (mIsBeingDragged) {
// Scroll to follow the motion event
final int activePointerIndex = MotionEventCompat.findPointerIndex(
ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, activePointerIndex);
needsInvalidate |= performDrag(x);
}
break;
可以明显看到,高版本调用了 requestParentDisallowInterceptTouchEvent(true);,而requestParentDisallowInterceptTouchEvent代码如下,就是掉parent的requestDisallowInterceptTouchEvent,不许父亲吃我的事件
private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
所以我们使用高版本的v4包或者v7包,就不用考虑滑动冲突的问题,ohlala。
原生控件如何解决滑动冲突
我再看了下ScrollView
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
再看下HorizontalScrollView
if (!mIsBeingDragged && Math.abs(deltaX) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaX > 0) {
deltaX -= mTouchSlop;
} else {
deltaX += mTouchSlop;
}
}
看来原生控件都是类似的,在onTouchEvent的move里判断是否是我的滑动事件,如果是,那就拦截,条件一般就是Math.abs(deltaY) > mTouchSlop
所以大部分原生控件其实都已经解决了滑动冲突的问题,但是我们自己写的控件,或者说github上的一些控件经常有滑动冲突的问题需要解决,如何解决呢?完全可以参考原生控件的方法。