1.常见的滑动冲突场景
常见的滑动冲突场景可以简单氛围下面三种情况:
2.外部滑动方向和内部滑动方向一致
3.以上两种嵌套
场景1:主要是将ViewPager和Fragment配合使用所组成的页面的滑动效果,很多主流应用基本都会用到这个效果,这个效果可以通过左右滑动来切换页面,而每个页面内部经常又是ListView。但是ViewPager内部处理了这个滑动冲突,因此我们无需关注这个问题,如果外部采用ScrollView代替ViewPager,那么此时就必须手动处理滑动冲突,否则会造成内外两层智能滑动一层的后果。
场景2:当内外两层都在同一个方向可以滑动的时候,系统无法知道用户到底是想滑动哪一层,所以此时当手指滑动的时候就会出现问题,要么只能滑动一层,要么两层滑动的很卡顿。
场景3:场景3是场景1和场景2的嵌套,但是它是几个单一的滑动冲突的叠加,因此只需要分别处理内层和中层、中层和外层之间的滑动冲突即可。
2.滑动冲突的处理规则
对于场景1的处理规则是:当用户左右滑动时,需要让外部的View拦截点击事件,当用户上下滑动时,需要让内部View拦截点击事件,格局滑动时水平滑动还是竖直滑动就可以判断到底该谁拦截事件。具体操作是根据手指滑动的起始坐标就可以判断是水平滑动还是竖直滑动。具体判断方式比如可以根据滑动路径和水平方向的夹角、可以根据水平方向和竖直方向的滑动距离、可以根据水平方向和竖直方向滑动的速率来判断。
对于场景2来说比较特殊,它无法根据滑动的角度、距离来有自己速度差来做判断,这时候就需要在业务上找突破点,比如业务规定当属于某种状态时需要外部View相应用户的滑动,而处于另一种状态时需要内部View来相应View的滑动。场景3跟场景2的处理方式基本一致。
3.滑动冲突的解决方式
1.外部拦截法
外部拦截法就是点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,不需要就不拦截,这样可以解决滑动冲突问题,这个方法比较符合点击事件的分发机制,外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可。 父容器代码如下:
public class MyHorizontalScrollView extends ViewGroup {
private static final String TAG = "MyHorizontalScrollView";
private int mChildSize;
private int mChildWidth;
private int mChildIndex;
//分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
//分别记录上次滑动的坐标(onInterceptTouchEvent)
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;
private Scroller mScroller;
private VelocityTracker mVelocityTracher;
private void init() {
mScroller = new Scroller(getContext());
mVelocityTracher = VelocityTracker.obtain();
}
public MyHorizontalScrollView(Context context) {
this(context, null);
}
public MyHorizontalScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyHorizontalScrollView(Context context, AttributeSet attrs, int style) {
super(context, attrs, style);
init();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
final int childCount = getChildCount();
mChildSize = 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;
}
}
}
@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
public boolean onInterceptHoverEvent(MotionEvent ev) {
//是否拦截事件
boolean intercepted=false;
//记录点击的坐标
int x=(int)ev.getX();
int y=(int)ev.getY();
//判断事件类型,根据类型做出相应的处理
switch (ev.getAction()){
//按下
case MotionEvent.ACTION_DOWN:{
intercepted = false;
if (!mScroller.isFinished()) {//滑动结束
//停止动画
mScroller.abortAnimation();
intercepted = true;
}
break;
}
//移动
case MotionEvent.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.v(TAG,"intercepted"+intercepted);
//记录此次点击事件的坐标
mLastY=y;
mLastX=x;
mLastYIntercept=y;
mLastXIntercept=x;
return intercepted;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracher.addMovement(event);
//获取点击事件坐标
int x=(int)event.getX();
int y=(int)event.getY();
switch (event.getAction()){
//按压
case MotionEvent.ACTION_DOWN:{
if (!mScroller.isFinished()){
mScroller.abortAnimation();
}
break;
}
//移动
case MotionEvent.ACTION_MOVE:{
int deltaX=x-mLastX;
int deltaY=y-mLastY;
scrollBy(-deltaX,0);
}
//抬起 动作结束后处理
case MotionEvent.ACTION_UP:{
int scrollX=getScrollX();
int scrollToChildIndex=scrollX/mChildWidth;
mVelocityTracher.computeCurrentVelocity(1000);
float xVelocity=mVelocityTracher.getXVelocity();
if (Math.abs(xVelocity)>=50){
mChildIndex=xVelocity>0?mChildIndex-1:mChildIndex+1;
}else {
mChildIndex=(scrollX+mChildWidth/2)/mChildWidth;
}
mChildIndex=Math.max(0,Math.min(mChildIndex,mChildSize-1));
int dx=mChildIndex*mChildWidth-scrollX;
smoothScrollBy(dx,0);
mVelocityTracher.clear();
break;
}
default:
break;
}
mLastX=x;
mLastY=y;
return true;
}
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();
}
}
}
Activity代码如下:
public class SlidingConflictActivity extends Activity {
private static final String TAG = "SlidingConflictActivity";
@BindView(R.id.horizontal)
HorizontalScrollViewEx mListContainer;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sliding_conflict);
ButterKnife.bind(this);
initView();
}
private void initView() {
LayoutInflater inflater=getLayoutInflater();
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);
List<String> datas=new ArrayList<>();
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);
}
}
xml代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent">
<com.my.learn.code.viewsystem.HorizontalScrollViewEx
android:id="@+id/horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.my.learn.code.viewsystem.HorizontalScrollViewEx>
</LinearLayout>
上面代码是外部拦截法的典型逻辑,针对不同的滑动冲突,只需要修改父容器需要当前点击事件的这个条件即可,其他不能修改。 在onInterceptTouchEvent方法中,首先是ACTION_DOWN这个事件,父容器必须返回false,因为父容器一旦拦截了ACTION_DOWN,那么后续的ACTION_MOVE和ACTION_UP事件都会直接交由父容器处理,这个时候事件没法再传递给子元素了,其次是ACTION_MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要就返回ture,否则返回false,最后是ACTION_UP事件,这里必须返回false,ACTION_UP没有太多意义。因为父容器一旦开始拦截任何事件,那么后续的事件都会交给它来处理,这时候子元素的onClick时间就无法触发。
2内部拦截法
内部拦截法是指父容器不拦截任何事件,所有的时间都传递给子元素,如果子元素需要事件就直接消耗掉,如果不需要就交给父容器进行处理,这种方法与Android中的时间分发禁止不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常同坐,使用起来较外部拦截稍微复杂,一下是实现代码:
内部ListView代码:
public class ListViewEx extends ListView {
private static final String TAG = "ListViewEx";
//外部父控件
private HorizontalScrollViewEx2 mHorizontalScrollViewEx2;
// 分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
public void setHorizontalScrollViewEx2(
HorizontalScrollViewEx2 horizontalScrollViewEx2) {
mHorizontalScrollViewEx2 = horizontalScrollViewEx2;
}
public ListViewEx(Context context) {
this(context, null);
}
public ListViewEx(Context context, AttributeSet set) {
super(context, set);
}
public ListViewEx(Context context, AttributeSet set, int style) {
super(context, set, style);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
//交给父控件处理
mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
Log.d(TAG, "dx:" + deltaX + " dy:" + deltaY);
//消耗点击事件
if (Math.abs(deltaX) > Math.abs(deltaY)) {
//父控件不处理此事件
mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
}
父控件代码:
public class HorizontalScrollViewEx2 extends ViewGroup{
private static final String TAG = "HorizontalScrollViewEx2";
private int mChildrenSize;
private int mChildWidth;
private int mChildIndex;
// 分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
// 分别记录上次滑动的坐标(onInterceptTouchEvent)
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
public HorizontalScrollViewEx2(Context context) {
super(context);
init();
}
public HorizontalScrollViewEx2(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public HorizontalScrollViewEx2(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) {
int x = (int) event.getX();
int y = (int) event.getY();
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
mLastX = x;
mLastY = y;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
return true;
}
return false;
} else {
return true;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent action:" + event.getAction());
mVelocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
Log.d(TAG, "move, deltaX:" + deltaX + " deltaY:" + deltaY);
scrollBy(-deltaX, 0);
break;
}
case MotionEvent.ACTION_UP: {
int scrollX = getScrollX();
int scrollToChildIndex = scrollX / mChildWidth;
Log.d(TAG, "current index:" + scrollToChildIndex);
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
if (Math.abs(xVelocity) >= 50) {
mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
} else {
mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
}
mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
int dx = mChildIndex * mChildWidth - scrollX;
smoothScrollBy(dx, 0);
mVelocityTracker.clear();
Log.d(TAG, "index:" + scrollToChildIndex + " dx:" + dx);
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) {
Log.d(TAG, "width:" + getWidth());
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();
}
}
Activity代码:
public class SlidingConflictTwoActivity extends Activity {
private static final String TAG = "SlidingConflictTwoActivity";
@BindView(R.id.horizontal)
HorizontalScrollViewEx2 mListContainer;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sliding_conflict2);
ButterKnife.bind(this);
initView();
}
private void initView() {
LayoutInflater inflater = getLayoutInflater();
mListContainer = (HorizontalScrollViewEx2) 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_layout2, 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) {
ListViewEx listView = (ListViewEx) 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.setHorizontalScrollViewEx2(mListContainer);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
Toast.makeText(SlidingConflictTwoActivity.this, "click item",
Toast.LENGTH_SHORT).show();
}
});
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(TAG, "dispatchTouchEvent action:" + ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent action:" + event.getAction());
return super.onTouchEvent(event);
}
}
xml代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent">
<com.my.learn.code.viewsystem.HorizontalScrollViewEx2
android:id="@+id/horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.my.learn.code.viewsystem.HorizontalScrollViewEx2>
</LinearLayout>
content_layout2.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" >
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:text="TextView" />
<com.my.learn.code.viewsystem.ListViewEx
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fff4f7f9"
android:cacheColorHint="#00000000"
android:divider="#dddbdb"
android:dividerHeight="1.0px"
android:listSelector="@android:color/transparent" />
</LinearLayout>