Android进阶 View事件体系(三):典型的滑动冲突情况和解决策略_android 滑动冲突-程序员宅基地

技术标签: Android开发笔记  java  ui  android  

Android进阶 View事件体系(三):典型的滑动冲突情况和解决策略

在这里插入图片描述

内容概要

本篇文章为总结View事件体系的第三篇文章,前两篇文章的在这里:

本篇文章主要是介绍两种基本的滑动冲突情况和对应的解决策略,内容有:

  1. 基本的滑动冲突情况
  2. 解决滑动冲突的基本策略
  3. 解决滑动冲突的具体示例

基本的滑动冲突情况

实际上最基本的滑动冲突情况就两种:

  1. 外部滑动方向和内部滑动方向不一致
  2. 外部滑动方向和内部滑动方向一致

还有一种情况就是上面两种基本情况的嵌套:

  1. 既有滑动方向不一致,又有滑动方向一致的情况

具体的场景可以如下面几幅图片所示:
在这里插入图片描述
场景一显然可以在使用到ViewPager 或者 ViewPager2配合Fragment进行滑动的页面中见到,这种页面中外部可以进行左右滑动进行Fragment切换,Fragment中又往往有RecycleView或者ListView等可以上下滑动的组件,就会产生滑动的冲突。ViewPager系列中已经解决了这种滑动冲突,我们就不关注了,但是如果大家使用过ScrollView,就会发现会产生许多奇奇怪怪的问题,就像需要我们解决滑动冲突。
在这里插入图片描述
场景二是在内外两层都在同一个方向可以滑动时产生的,当我们进行滑动时,系统就不知道到底要滑动哪一层,最终造成的结果就是两层都能滑动或者两层都不能滑动。
在这里插入图片描述
场景三则是前两种情况的嵌套,实际上在我的开发情况中倒是没有见过很多这种情况,一般嵌套过多的话会强制禁止一层进行滑动,不过我们还是得分析一下。它可以拆分为外中层的嵌套和中内层的嵌套。所以我们处理起来就可以分解为之前两种情况的组合。

本质上来说这三种滑动冲突的复杂度是相同的,因为他们的不同仅仅是滑动策略的不同,所以他们也有几种通用的解决策略,下面我们来介绍几种解决策略。

解决滑动冲突的基本策略

基本策略

在 Android 中,处理滑动冲突的策略可以根据具体的需求和布局结构选择不同的方法。一般来说,有以下几种策略:

  1. 内部拦截法(内部消费法):父容器拦截滑动事件,并根据需要将滑动事件传递给子视图处理。这种策略常用于嵌套滑动的情况,例如在一个垂直滚动的列表或滚动视图中包含水平滚动的子视图。父容器可以通过重写onInterceptTouchEvent()方法来判断是否拦截滑动事件。

  2. 外部拦截法(外部消费法):子视图拦截滑动事件,不让其传递给父容器处理。这种策略常用于需要子视图完全接管滑动事件的情况,例如在一个自定义的可拖拽控件中,子视图需要处理拖拽操作并阻止父容器的滑动。

  3. 嵌套滑动:使用嵌套滑动机制,通过NestedScrollingParent和NestedScrollingChild接口实现父子视图之间的协调滑动。父容器和子视图可以协同工作,根据滑动距离和方向进行滑动的分发和处理,以实现平滑的滑动效果。这种策略适用于复杂的滑动场景,例如嵌套的滚动视图或多层级的滑动布局。

  4. 触摸事件优先级处理:通过调整视图的触摸事件分发优先级来处理滑动冲突。可以通过修改视图的事件处理顺序、设置requestDisallowInterceptTouchEvent()方法或使用ViewGroup.setDescendantFocusability()方法等方式来调整触摸事件的分发顺序。

  5. 多指触摸处理:对于多指触摸冲突,可以使用手势识别器(GestureDetector)或处理多指触摸事件的相关方法来判断和处理滑动冲突。可以根据具体的手势类型或触摸点位置等条件来进行冲突解决。

其中,内部拦截法和外部拦截法是最基本的两种策略,这里我们也将详细介绍这两种解决策略。

分析冲突情况

为了解决我们上面所说的三种冲突情况,我们需要分析下这三种情况。对于情况一来说,我们只需要判断用户的意图究竟是横向滑动还是纵向滑动即可,若是横向滑动就交由父容器处理,否则就交由子View处理。那该如何判断呢?实际上通过横向滑动的距离和纵向滑动的距离就可以判断出滑动角度了,若滑动路径与纵向坐标夹角的较小角度大于45°,则判断为纵向滑动,否则判断为横向滑动,这样可能有点抽象,我们看下面图片进行分析:
在这里插入图片描述
此次滑动的水平滑动值为dx,垂直滑动值为dy,只要当dy > dx时则可以满足我们之前所说的对纵向滑动的判断标准,即:当dy > dx 时,判断为纵向滑动,交由子View处理;dx > dy时,判断为横向滑动,交由父ViewGroup容器处理。

对于场景二,它无法通过滑动的角度,距离差以及速度差来进行判断,但这个时候一般都能在业务上找到突破点,比如业务上有规定:当处于某种状态时需要外部View响应用户的滑动,而处于另外一种状态时则需要内部View响应View的滑动,根据这种业务上的需求我们也可以得出响应的处理规则,有了处理规则同样就可以进行下一步的处理了。

而场景三就是场景一二的组合,还是需要从业务上找到突破点。

外部拦截法

所谓外部拦截法就是指点击事件都要先经过父容器的拦截处理,如果父容器需要拦截就进行事件拦截,否则就不需要进行拦截。这样就可以解决滑动冲突的问题,这种方法也符合事件分发机制。

外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部进行相应的拦截,伪代码如下所示:

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:{
    
			intercepted = false;
			break;
		}
		case MotionEvent.ACTION_MOVE:{
    
			if(父容器需要当前点击事件){
    
				intercepted = true;
			}else{
    
				intercepted = false;
			}
			break;
		}
		case MotionEvent.ACTION_UP:{
    
			intercepted = false;
			break;
		}
		default:
			break;
	}
	mLastXIntercepted = x;
	mLastYIntercepted = y;
	return intercepted;
}

要理解这里的逻辑需要我们对之前的事件分发的流程熟记于心,在onInterceptTouchEvent方法中,对ACTION_DOWN事件父容器必须返回false,即必须不拦截,因为一旦父容器拦截了事件,后续的ACTION_MOVE和ACTION_UP事件就不能传递给子View了,而是直接交给父容器处理。

而对于ACTION_MOVE,则需要根据需求决定是否需要拦截。比如说对于我们的情况一来说,当横向滑动距离dx 大于 纵向滑动距离dy,那么就需要父容器进行拦截并处理。

最后是ACTION_UP事件,这里我们一般返回false,即不拦截,这样可以传递给子View,如果有特殊需求的话可以进行调整。

内部拦截法

内部拦截法就是指父容器不拦截任何事件,所有事件都传递给子View,如果子View需要的话就直接消耗掉,否则就交给父容器处理,需要配合requestDisallowInterceptTouchEvent方法进行。我们需要重写子元素的dispatchTouchEvent方法,伪代码如下:

public boolean dispatchTouchEvent(MotionEvent event){
    
	int x = (int) event.getX();
	int y = (int) event.getY();
	swicth(event.getAction()){
    
		case MotionEvent.ACTION_DOWN:{
    
			parent.requestDisallowInterceptTouchEvent(true);//禁止父容器拦截
			break;
		}
		case MotionEvent.ACTION_MOVE:{
    
			int deltaX = x - mLastX;
			int deltaY = y - mLastY;
			if(父容器需要点击事件){
    
				parent.requestDisallowInterceptTouchEvent(false);//允许父容器拦截
			}
			break;
		}
		case MotionEvent.ACTION_UP:{
    
			break;
		}
		default:
			break;
	}

	mLastX = x;
	mLastY = y;
	return super.dispatchTouchEvent(event); //还要调用超类的逻辑是为了不影响其内部的View的事件响应
}

接着,我们还需要修改父容器的onInterceptTouchEvent方法配合:

public boolean onInterceptTouchEvent(MotionEvent event){
    
	int action = event.getAction();
	if(action == MotionEvent.ACTION_DOWN){
    
		return false;
	}else {
    
		return true;
	}
}

这里首先要将父容器的DOWN事件设置为不拦截,因为内部拦截法就是要先将事件交给子View进行处理,一个事件序列又总是从DOWN事件开始的,所以一旦父容器将DOWN事件拦截,那么之后的一系列事件都无法传递给子View处理了;后面的其余事件都拦截是因为,如果子事件设置了父容器的DisallowInterceptTouchEvent为false的话,那就说明这个时候子View允许父容器进行拦截了,所以其他事件都要返回true。

可以看到,内部拦截法是比较复杂且其不符合事件分发的流程,所以我们一般还是建议使用外部拦截法进行处理。

解决滑动冲突的具体示例

接下来,我们在一些具体的场景下进行具体分析,这里都用外部拦截法进行处理了。

场景一

场景一即内部和外部滑动方向不同的情况下,我们来实现一个类似于ViewPager的控件,其实就相当于一个可以左右滑动的LinearLayout,内部横向摆放着若干个ListView,这样LinearLayout是左右滑动的,而ListView本身又可以上下滑动,就造成了滑动冲突。这种情况下,我们可以根据我们之前分析的滑动策略来解决,即判断水平滑动距离大还是竖直滑动距离大,我们先来看自定义LinearLayout类的源码:

public class HorizontalScrollViewEx extends LinearLayout {
    
    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;
    private int mLastX = 0;
    private int mLastY = 0;
    //子元素的相关信息
    private int mChildrenSize = 3;
    private int mChildWidth;
    private int mChildIndex;
    private Scroller mScroller;//弹性滚动对象-仅能滚动内容
    private VelocityTracker mVelocityTracker;
    public HorizontalScrollViewEx(Context context) {
    
        super(context);
        init();
    }

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

    public HorizontalScrollViewEx(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    
        super(context, attrs, defStyleAttr);
        init();
    }

    public HorizontalScrollViewEx(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    public void setInfo(int mChildrenSize,int mChildWidth){
    
        this.mChildWidth = mChildWidth;
        this.mChildrenSize = mChildrenSize;
    }

    private void init(){
    //初始化速度追踪器和弹性滚动对象
        mScroller = new Scroller(getContext());
        mVelocityTracker = VelocityTracker.obtain();

    }


    //先是外部拦截法
    @Override
    public boolean onInterceptTouchEvent(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;
        }
        //更新状态
        mLastX = x;
        mLastY = y;
        mLastYIntercept = y;
        mLastXIntercept = x;
        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:{
    
                if(!mScroller.isFinished()){
    
                    mScroller.abortAnimation();//停止滚动动画
                }
                break;
            }
            case MotionEvent.ACTION_MOVE:{
    
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                scrollBy(-deltaX,0);
                break;
            }
            case MotionEvent.ACTION_UP:{
    
                int scrollX = getScrollX();
                int scrollToChildIndex = scrollX / mChildWidth;
                mVelocityTracker.computeCurrentVelocity(1000);
                float velocityX = mVelocityTracker.getXVelocity();//获取横向滑动速度
                if(Math.abs(velocityX) >= 50){
    
                    mChildIndex = velocityX > 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();
                break;
            }

            default:
                break;
        }
        mLastX = x;
        mLastY = y;
        return true;
    }

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

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

可以看到,在onIntercepTouchEvent方法中通过水平和垂直滑动距离来判断是否要进行父容器的处理。然后我们看在父容器的onTouchEvent方法中是如何处理滑动事件的,首先如果是正常滑动的话,即是MOVE事件的话就用scrollBy方法将这个View的画布进行滑动,实现了滑动的效果。我们接着还需要考虑一些特殊情况,比如说如果滑动的距离超过了画布中内容的最大宽度或者小于最小宽度的话,为了防止越界的话,就要在UP事件中进行特殊的处理:

case MotionEvent.ACTION_UP:{
    
                int scrollX = getScrollX();
                int scrollToChildIndex = scrollX / mChildWidth;
                mVelocityTracker.computeCurrentVelocity(1000);
                float velocityX = mVelocityTracker.getXVelocity();//获取横向滑动速度
                if(Math.abs(velocityX) >= 50){
    
                    mChildIndex = velocityX > 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();
                break;
            }

首先是获得了scrollX的值,这个值的意义就是画布的左边界距离View的边界的距离,这个值为正值的话就是说明此时画布已经左移了。然后我们用速度追踪器追踪了移动事件的速度,这样做的意义就是当手指快速移动的时候即使滑动距离很小也将由父容器进行处理。

接下来的判断逻辑就是用来处理一些特殊情况的,速度超过50就将其视为一次父容器的滑动,且当滑动距离大于子View的二分之一宽度时将其视为一次父布局的滑动,反之将其视为不滑动。最后得到的mChildIndex参数则是计算出的应该滑动到的子View的下标

mChildIndex = Math.max(0,Math.min(mChildIndex,mChildrenSize-1));

这段代码则是为了防止下标越界的问题。

看完了自定义LinearLayout的代码,接着我们还要在Activity中进行相应的初始化操作:

public class MainActivity extends AppCompatActivity {
    

    private static final String TAG = "MainActivity";
    private HorizontalScrollViewEx mListContainer;
    private DisplayMetrics displayMetrics;//用来获得屏幕尺寸的

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

    private void initView(){
    //初始化视图
        displayMetrics = new DisplayMetrics();
        WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
        LayoutInflater inflater = getLayoutInflater();//获取xml加载器--加载到该Activity中
        mListContainer = findViewById(R.id.mListContainer);
        int screenWidth = 0;//屏幕宽度
        int screenHeight = 0;//屏幕高度
        if(windowManager != null){
    
            windowManager.getDefaultDisplay().getMetrics(displayMetrics);
            screenWidth = displayMetrics.widthPixels;
            screenHeight = displayMetrics.heightPixels;
        }
        Log.d(TAG, "width : "+screenWidth);
        Log.d(TAG, "height : "+screenHeight);
        for(int i = 0;i < 3;i++){
    
            ViewGroup layout = (ViewGroup) inflater.inflate(R.layout.content_layout,mListContainer,false);
            layout.getLayoutParams().width = screenWidth;
            TextView textView = layout.findViewById(R.id.mTitle);
            textView.setText("Page "+(i + 1));
            layout.setBackgroundColor(Color.rgb(255/(i+1),255/(i+1),255/(i+1)));//设置背景色
            createList(layout);//为当前页面生成纵向List
            mListContainer.addView(layout);//添加到自定义View中
        }
        mListContainer.setInfo(3,screenWidth);
    }

    private void createList(ViewGroup layout){
    
        ListView listView = layout.findViewById(R.id.mList);
        ArrayList<String> datas = new ArrayList<>();
        for(int i = 0; i < 50;i++){
    
            datas.add("name "+i);
        }
        ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1,datas);
        listView.setAdapter(adapter);
    }
}

首先是通过WindowManager和DisplayMetrics获取到了当前设备屏幕的宽和高,方便我们在初始子View时将其正好填满一个手机屏幕的大小。接着使用布局膨胀器将xml文件加载成View并添加入Activity中,下面是xml文件的源码,使用的是约束布局:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/mTitle"
        android:layout_width="0dp"
        android:layout_height="36dp"
        android:gravity="center"
        android:text="TextView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ListView
        android:id="@+id/mList"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/mTitle" />
</androidx.constraintlayout.widget.ConstraintLayout>

接着再createList方法中,我们将ListView的内容进行了初始化,运行程序,我们就可以得到我们自己写的ViewPager了:
![在这里插入图片描述](https://img-blog.csdnimg.cn/a61b0507f1ee437f8640059350da75b6.jpeg

场景二

要处理场景二的冲突的话比较复杂,这里我们实现一个类似于可折叠toolbar(CollapsingToolbarLayout)结合ListView的效果的冲突,差不多就是这个情况:
在这里插入图片描述

我们在之前写的HorizontalScrollView的基础上进行修改,这里我们首先要总结出处理逻辑(首先默认Header一开始为展开状态):

  • 即当Header为展开状态且ListView已经到顶部时,且滑动方向为向上,这个时候将事件交由父ViewGroup进行处理,将Header进行折叠;
  • 当Header为折叠状态且ListView已经到顶部时,且滑动方向为向下,这个时候将事件交由父ViewGroup进行处理,将Header进行展开;
  • 当Header为折叠状态且ListView已经到顶部时,且滑动方向为向上,这个时候将事件交由子View进行处理,滑动ListView即可;
  • 滑动完成后要更新是否到达顶部和折叠状态的更新。

这里笔者根据这几个简单的逻辑就简单实现了这个功能,不过这里仅做演示,这个自定View实际上还有许多别的问题,先上结果图:
在这里插入图片描述
demo我放在我的github里☞:滑动冲突示例

这里只给出自定义View的源码,完整的一套代码还是放在github中了:

public class StickyLayout extends LinearLayout {
    
    private static final String TAG = "StickyLayout";
    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;
    private int mLastX = 0;
    private int mLastY = 0;
    //子元素的相关信息

    //特有的状态标志位来判断
    private int mHeaderStatue = Statue_EXPAND; //一开始状态为展开
    private boolean isSticky = true; //ListView是否到达顶部 - 一开始状态为到达顶部

    private static final int Statue_EXPAND = 1;//状态为展开
    private static final int Statue_COLLASPLE = 2;//状态为折叠
    private int mTouSlop;
    public TextView mHeader;
    public ListView mListView;
    private Scroller mScroller;//弹性滚动对象-仅能滚动内容
    private VelocityTracker mVelocityTracker;
    public StickyLayout(Context context) {
    
        super(context);
        init();
    }

    public StickyLayout(Context context, @Nullable AttributeSet attrs) {
    
        super(context, attrs);
        init();
    }

    public StickyLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    
        super(context, attrs, defStyleAttr);
        init();
    }

    public StickyLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    
        super(context, attrs, defStyleAttr, defStyleRes);
    }


    private void init(){
    //初始化速度追踪器和弹性滚动对象
        mScroller = new Scroller(getContext());
        mVelocityTracker = VelocityTracker.obtain();
        mTouSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
    }


    //先是外部拦截法
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
    
        boolean intercept = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()){
    
            case MotionEvent.ACTION_DOWN:{
    
				if(!mScroller.isFinished()){
    
                    mScroller.abortAnimation();
                }
                intercept = false;
                break;
            }
            case MotionEvent.ACTION_MOVE:{
    
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;
                if(mHeaderStatue == Statue_EXPAND  && isSticky == true){
    
                    intercept = true;
                }else if(mHeaderStatue == Statue_COLLASPLE && deltaY > 0 && isSticky == true){
    
                    intercept = true;
                }else if(mHeaderStatue == Statue_COLLASPLE && deltaY < 0 && isSticky == true){
    
                    intercept = false;
                }

                //判断ListView是否到达顶部了
                if(mListView.getFirstVisiblePosition() == 0){
    
                    View view  = mListView.getChildAt(0);
                    if(view != null && view.getTop() >= 0){
    
                        isSticky = true;
                    }else{
    
                        isSticky = false;
                    }
                }else if(mListView.getFirstVisiblePosition() != 0){
    
                    isSticky = false;
                }

                break;
            }
            case MotionEvent.ACTION_UP:{
    
                intercept = false;
                break;
            }
            default:
                break;
        }
        mLastYIntercept = y;
        mLastXIntercept = x;
        mLastY = y;
        mLastX = x;
        Log.d(TAG, "finish ScrollY: "+getScrollY());
        Log.d(TAG, "isColl?: "+ mHeaderStatue);
        Log.d(TAG, "isSticky?: "+isSticky);
        return intercept;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
    
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()){
    
            case MotionEvent.ACTION_DOWN:{
    
                break;
            }
            case MotionEvent.ACTION_MOVE:{
    
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                scrollBy(0,-deltaY);
                break;
            }
            case MotionEvent.ACTION_UP:{
    
                if(getScrollY() < 0){
    
                    smoothScrollBy(0,-getScrollY());
                }else if(getScrollY() >= 300){
    
                    smoothScrollBy(0,200-getScrollY());//剩余200dp就判断为折叠了
                    mHeaderStatue = Statue_COLLASPLE;//如果滑动到了一定程度,就判定为折叠了
                }
                if(getScrollY() <= 0){
    
                    mHeaderStatue = Statue_EXPAND;
                }
                break;
            }
            default:
                break;
        }
        mLastX = x;
        mLastY = y;

        return true;
    }

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

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

其中的

private int mHeaderStatue = Statue_EXPAND; //一开始状态为展开
private boolean isSticky = true; //ListView是否到达顶部 - 一开始状态为到达顶部

private static final int Statue_EXPAND = 1;//状态为展开
private static final int Statue_COLLASPLE = 2;//状态为折叠

这几个变量就是用来保持状态以便后续判断的。这里还防止了过度滑动的问题,如果滑动超过边界则会调用弹性调用方法移动到边界处。

到此为止,前面两种基本的滑动冲突的解决方法就介绍完成了,至于第三种情况,只要把它拆分为前两种情况的组合就可以了。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Tai_Monster/article/details/130906395

智能推荐

c# 调用c++ lib静态库_c#调用lib-程序员宅基地

文章浏览阅读2w次,点赞7次,收藏51次。四个步骤1.创建C++ Win32项目动态库dll 2.在Win32项目动态库中添加 外部依赖项 lib头文件和lib库3.导出C接口4.c#调用c++动态库开始你的表演...①创建一个空白的解决方案,在解决方案中添加 Visual C++ , Win32 项目空白解决方案的创建:添加Visual C++ , Win32 项目这......_c#调用lib

deepin/ubuntu安装苹方字体-程序员宅基地

文章浏览阅读4.6k次。苹方字体是苹果系统上的黑体,挺好看的。注重颜值的网站都会使用,例如知乎:font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, W..._ubuntu pingfang

html表单常见操作汇总_html表单的处理程序有那些-程序员宅基地

文章浏览阅读159次。表单表单概述表单标签表单域按钮控件demo表单标签表单标签基本语法结构<form action="处理数据程序的url地址“ method=”get|post“ name="表单名称”></form><!--action,当提交表单时,向何处发送表单中的数据,地址可以是相对地址也可以是绝对地址--><!--method将表单中的数据传送给服务器处理,get方式直接显示在url地址中,数据可以被缓存,且长度有限制;而post方式数据隐藏传输,_html表单的处理程序有那些

PHP设置谷歌验证器(Google Authenticator)实现操作二步验证_php otp 验证器-程序员宅基地

文章浏览阅读1.2k次。使用说明:开启Google的登陆二步验证(即Google Authenticator服务)后用户登陆时需要输入额外由手机客户端生成的一次性密码。实现Google Authenticator功能需要服务器端和客户端的支持。服务器端负责密钥的生成、验证一次性密码是否正确。客户端记录密钥后生成一次性密码。下载谷歌验证类库文件放到项目合适位置(我这边放在项目Vender下面)https://github.com/PHPGangsta/GoogleAuthenticatorPHP代码示例://引入谷_php otp 验证器

【Python】matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距-程序员宅基地

文章浏览阅读4.3k次,点赞5次,收藏11次。matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距

docker — 容器存储_docker 保存容器-程序员宅基地

文章浏览阅读2.2k次。①Storage driver 处理各镜像层及容器层的处理细节,实现了多层数据的堆叠,为用户 提供了多层数据合并后的统一视图②所有 Storage driver 都使用可堆叠图像层和写时复制(CoW)策略③docker info 命令可查看当系统上的 storage driver主要用于测试目的,不建议用于生成环境。_docker 保存容器

随便推点

网络拓扑结构_网络拓扑csdn-程序员宅基地

文章浏览阅读834次,点赞27次,收藏13次。网络拓扑结构是指计算机网络中各组件(如计算机、服务器、打印机、路由器、交换机等设备)及其连接线路在物理布局或逻辑构型上的排列形式。这种布局不仅描述了设备间的实际物理连接方式,也决定了数据在网络中流动的路径和方式。不同的网络拓扑结构影响着网络的性能、可靠性、可扩展性及管理维护的难易程度。_网络拓扑csdn

JS重写Date函数,兼容IOS系统_date.prototype 将所有 ios-程序员宅基地

文章浏览阅读1.8k次,点赞5次,收藏8次。IOS系统Date的坑要创建一个指定时间的new Date对象时,通常的做法是:new Date("2020-09-21 11:11:00")这行代码在 PC 端和安卓端都是正常的,而在 iOS 端则会提示 Invalid Date 无效日期。在IOS年月日中间的横岗许换成斜杠,也就是new Date("2020/09/21 11:11:00")通常为了兼容IOS的这个坑,需要做一些额外的特殊处理,笔者在开发的时候经常会忘了兼容IOS系统。所以就想试着重写Date函数,一劳永逸,避免每次ne_date.prototype 将所有 ios

如何将EXCEL表导入plsql数据库中-程序员宅基地

文章浏览阅读5.3k次。方法一:用PLSQL Developer工具。 1 在PLSQL Developer的sql window里输入select * from test for update; 2 按F8执行 3 打开锁, 再按一下加号. 鼠标点到第一列的列头,使全列成选中状态,然后粘贴,最后commit提交即可。(前提..._excel导入pl/sql

Git常用命令速查手册-程序员宅基地

文章浏览阅读83次。Git常用命令速查手册1、初始化仓库git init2、将文件添加到仓库git add 文件名 # 将工作区的某个文件添加到暂存区 git add -u # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,不处理untracked的文件git add -A # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,包括untracked的文件...

分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120-程序员宅基地

文章浏览阅读202次。分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120

【C++缺省函数】 空类默认产生的6个类成员函数_空类默认产生哪些类成员函数-程序员宅基地

文章浏览阅读1.8k次。版权声明:转载请注明出处 http://blog.csdn.net/irean_lau。目录(?)[+]1、缺省构造函数。2、缺省拷贝构造函数。3、 缺省析构函数。4、缺省赋值运算符。5、缺省取址运算符。6、 缺省取址运算符 const。[cpp] view plain copy_空类默认产生哪些类成员函数

推荐文章

热门文章

相关标签