View的滑动冲突场景
常见的滑动冲突可以简单分为如下三种
- 场景1:外部滑动方向和内部滑动方向不一致
- 场景2:外部滑动方向和内部滑动方向一致
-
场景3:上面两种情况的嵌套
View的滑动冲突解决方式
- 外部拦截法:在onInterceptTouchEvvent方法中,首先在ACTION_DOWN这个事件,父容器必须放回false,即不拦截ACTION_DOWN事件,这是因为一旦父容器拦截了ACTION_DOWN,那么后续的ACTION_MOVE和ACTION_UP事件直接传递给父容器处理,没法传递给子元素。
- 内部拦截法:父容器不拦截任何事件,所有的事情都传递给子元素。如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理。如下代码是内部拦截法的典型代码,除了子元素需要做处理以外,父元素也要默认拦截除了ACTION_DOWN以外的其他事件。
场景1的冲突,利用外部拦截法解决
1. 外部拦截法常规解决思路,重写onInterceptTouchEvent方法。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
intercept = false;
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
intercept = false;
break;
case MotionEvent.ACTION_MOVE:
if(父容器需要当前点击事件){
intercept = true;
}else{
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
Log.i(TAG, "onInterceptTouchEvent: ACTION_UP");
break;
}
return intercept;
}
2. 场景1冲突实例
一个可以水平滑动的HorizontalScrollView和一个垂直滑动的ListView就会产生滑动冲突。其实在ViewPager+ListView是不会有滑动冲突的,因为ViewPager内部已经解决。
思路:判断水平滑动还是垂直滑动,可以通过在水平方向上deltaX和垂直方向上deltaY的绝对比较就可以明白用户想要水平还是垂直滑动。
自定义MyViewPager代码
public class MyViewPager extends HorizontalScrollView {
private boolean intercept;
private float lastX,lastY,x,y;
private float deltaX,deltaY;
private float totalX = 0;
private static final String TAG = "MyViewPager";
private LinearLayout mContentLL;
public MyViewPager(Context context) {
this(context,null);
}
public MyViewPager(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public MyViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContentLL = findViewById(R.id.contentLayout);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i(TAG, "dispatchTouchEvent: ");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
intercept = false;
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
intercept = false;
lastX = ev.getX();
lastY = ev.getY();
Log.i(TAG, "onInterceptTouchEvent: ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
x = ev.getX();
y = ev.getY();
deltaX = x - lastX;
deltaY = y - lastY;
Log.i(TAG, "onInterceptTouchEvent: ACTION_MOVE: deltX = " + deltaX + " , deltY = " + deltaY);
if(Math.abs(deltaX) > Math.abs(deltaY)){//水平滑动 , 当前父ViewGroup拦截后交给onTouchEvent去处理具体的操作
intercept = true;
}else{
intercept = false;
}
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_UP:
intercept = false;
Log.i(TAG, "onInterceptTouchEvent: ACTION_UP");
break;
}
return intercept;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = ev.getX();
break;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
x = ev.getX();
deltaX = x - lastX;
totalX += deltaX;
Log.i(TAG, "onTouchEvent: totalX = " + totalX + " , deltaX = " + deltaX);
if(mContentLL != null){
mContentLL.scrollBy((int) -deltaX,0);
}
lastX = x;
break;
}
return super.onTouchEvent(ev);
}
}
布局文件
完美解决冲突
场景2的冲突,利用内部拦截法解决
1. 内部拦截法解决思路,重写子元素的dispatchTouchEvent方法,和父元素的onInterceptTouchEvent方法。
父元素的onInterceptTouchEvent。ACTION_DOWN事件不受FLAG_DISALLOW_INTERCEPT这个标记位的控制,所以一旦父容器拦截ACTION_DOWN事件,那么所有的事件都无法传递到子元素中去,这样内部拦截就无法起作用。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if(action == MotionEvent.ACTION_DOWN){
return false;
}else {
return true;
}
}
子元素的onInterceptTouchEvent。内部拦截法的典型代码,面对不同的滑动策略时只需要修改里面的条件即可。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if(父容器需要此类事件){//事件处理交个父容器
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(ev);
}
2. 场景2冲突实例
一个可以垂直滑动的MyVerticalViewGroup,内部包含一个可以垂直滑动的MyRecyclerView,两者都可以垂直滑动,需要根据自己需求判断垂直滑动时到底滑动哪一个控件。
自定义MyVerticalViewGroup
public class MyVerticalViewGroup extends LinearLayout {
private LinearLayout mContentll;
private float y,lastY,deltaY;
private int mImageViewHeight;
private static final String TAG = "SlidingView03";
public MyVerticalViewGroup(Context context) {
this(context,null);
}
public MyVerticalViewGroup(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public MyVerticalViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContentll = findViewById(R.id.contentLayout);
mImageViewHeight = (int) (200 * getResources().getDisplayMetrics().density+0.5);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if(action == MotionEvent.ACTION_DOWN){
lastY = ev.getY();
return false;
}else {
lastY = ev.getY();
return true;
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {//父容器的事件处理
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
lastY = ev.getY();
Log.i(TAG, "parent onTouchEvent: ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
y = ev.getY();
deltaY = y - lastY;
if(mContentll != null){
mContentll.scrollBy(0, -(int) deltaY);
}
lastY = y;
Log.i(TAG, "parent onTouchEvent: ACTION_MOVE ACTION_UP");
break;
}
return super.onTouchEvent(ev);
}
@Override
public void scrollTo(int x, int y) {
//边界控制
if(y > 0 ){
y = 0;
}
if( y < 0 && Math.abs(y) > mImageViewHeight){
y = -mImageViewHeight;
}
super.scrollTo(x, y);
}
}
自定义MyRecyclerView
public class MyRecyclerView extends RecyclerView {
private static final String TAG = "SlidingView03";
private float y,lastY,deltaY;
public MyRecyclerView(Context context) {
this(context,null);
}
public MyRecyclerView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public MyRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
lastY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
y = ev.getY();
deltaY = y - lastY;//deltaY > 0 表示向下滑动 ; deltaY < 0 表示向上滑动
lastY = y;
boolean isRecyclerViewOnTop = isTop();
Log.i(TAG, "child dispatchTouchEvent: deltaY = " + deltaY + " ,isTop() = " + isRecyclerViewOnTop + ", ViewCompat.canScrollVertically(this, 1) = " + ViewCompat.canScrollVertically(this, 1));
if(( isRecyclerViewOnTop && deltaY > 0 ) || ( deltaY < 0 && !ViewCompat.canScrollVertically(this, 1))){//事件处理交个父容器 -1判读是否可以下滑 1判断是否可以上滑
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
// Log.i(TAG, "onTouchEvent: ACTION_DOWN");
lastY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
y = ev.getY();
deltaY = y - lastY;
lastY = y;
// Log.i(TAG, "onTouchEvent: ACTION_MOVE totalY = " + totalY + " , y = " + y + " ,lastY = " + lastY);
break;
case MotionEvent.ACTION_UP:
// Log.i(TAG, "onTouchEvent: ACTION_UP");
lastY = ev.getY();
break;
}
return super.onTouchEvent(ev);
}
public boolean isTop(){
LinearLayoutManager layoutManager = (LinearLayoutManager) this.getLayoutManager();
int position = layoutManager.findFirstVisibleItemPosition();
View firstVisibleChildView = layoutManager.findViewByPosition(position);
Log.i(TAG, "isTop: position = 0" +" , firstVisibleChildView.getTop() = " + firstVisibleChildView.getTop() + ",RecyclerView " + getScrollY() );
if(position == 0 && firstVisibleChildView.getTop() == 0){
return true;
}else{
return false;
}
}
}
实例演示
总结
- 场景3的情况就是场景1和场景2的结合,方式一样就是复杂一些。
- 个人喜欢用外部拦截法,感觉方便一些
- 场景2中会有一个问题,就是一旦事件交个子元素或者父容器处理,他们会一管到底。知道ACTION_UP为止。利用Android自带嵌套滑动控件解决滑动冲突(NestingScroll,CoordinatorLayout与Behavior)可以很好的解决。
如何成为自定义高手(一)绘制
如何成为自定义高手(二)动画
如何成为自定义高手(三)布局
如何成为自定义高手(四)触摸反馈,事件分发机制
如何成为自定义高手(五)多点触摸
如何成为自定义高手(六)滑动和拖拽
如何成为自定义高手(七)滑动冲突
利用Android自带嵌套滑动控件解决滑动冲突(NestingScroll,CoordinatorLayout与Behavior)