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

news2024/12/23 18:14:57

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

在这里插入图片描述

内容概要

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

  • Android进阶 View事件体系(一):概要介绍和实现View的滑动
  • 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:{
                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;//状态为折叠

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/579828.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Oracle——数据操纵DML(一)

STU1 1、不指定字段的整行插入 在STU1中新增一名同学的基本信息&#xff0c;SQL如下&#xff1a; INSERT INTO test.stu1 VALUES(0001,牛牛,男,24,to_date(1988-05-25,YYYY-MM-DD),12外语)格式如下&#xff1a; INSERT INTO 表名 VALUES(值1,值2,...,值n)对于CHAR或VARCHAR等…

sql-labs SQL注入平台——第二关Less-2 GET - Error based - Intiger based (基于错误的GET整型注入)

Less-2 GET - Error based - Intiger based (基于错误的GET整型注入) 一、先确认漏洞是否存在 &#xff08;1&#xff09;查询 id1返回查询结果正常 &#xff08;2&#xff09;查询 id1’返回查询结果报错&#xff0c;可能存在SQL注入 &#xff08;3&#xff09;查询 id1 …

路径规划算法:基于帝国主义竞争优化的路径规划算法- 附代码

路径规划算法&#xff1a;基于帝国主义竞争优化的路径规划算法- 附代码 文章目录 路径规划算法&#xff1a;基于帝国主义竞争优化的路径规划算法- 附代码1.算法原理1.1 环境设定1.2 约束条件1.3 适应度函数 2.算法结果3.MATLAB代码4.参考文献 摘要&#xff1a;本文主要介绍利用…

Dubbo环境搭建

1.搭建zookeeper注册中心环境 zookeeper下载地址 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6ptMw7rb-1685261782669)(b894c0cbb6501ca97145d3b09685ae8f.png)] 在bin文件下&#xff0c;启动zkServer.cmd会有报错&#xff0c;处理需要在condi…

你所不知道的 数据在内存中储存 的来龙去脉

那么好了好了&#xff0c;宝子们&#xff0c;今天给大家介绍一下 “数据在内存中储存” 的来龙去脉&#xff0c;来吧&#xff0c;开始整活&#xff01;⛳️ 一、数据类型的介绍 &#xff08;1&#xff09;整型和浮点型&#xff1a; &#xff08;2&#xff09;其他类型…

Linux——使用命令行参数管理环境变量

目录 使用命令行参数获取用户在DOS命令行输入的指令&#xff1a; 方法&#xff1a;代码如下&#xff1a; 使用命令行参数获取并打印部分或者整体环境变量的方法&#xff1a; 方法1&#xff1a; 运行结果&#xff1a; 方法2&#xff1a;使用外部链接environ: 使用命令行参数…

如何开发背包系统?

UE5 插件开发指南 前言0 背包数据结构1 背包管理组件2 背包UI显示前言 相信大家对于背包系统并不陌生,谁还没有玩过几款游戏呢?游戏中的背包都是大同小异的,朴素的功能就是存放我们获取到的物品,高级一点就是要有物品分类,便于玩家刷选背包中的物品,能够显示玩家拥有的货…

2023 牛津大学博士后申请指南

牛津大学是全球著名的高等教育机构&#xff0c;其博士后项目备受瞩目。为了帮助申请者更好地了解牛津大学博士后申请流程&#xff0c;本文将介绍该校博士后申请指南的相关内容。一、申请条件首先&#xff0c;申请者必须已经获得博士学位或即将完成博士学位&#xff0c;并具有相…

《数据库应用系统实践》------ 报刊销售系统

系列文章 《数据库应用系统实践》------ 报刊销售系统 文章目录 系列文章一、需求分析1、系统背景2、 系统功能结构&#xff08;需包含功能结构框图和模块说明&#xff09;3&#xff0e;系统功能简介 二、概念模型设计1&#xff0e;基本要素&#xff08;符号介绍说明&#xff…

k8s 对已完成job自动清理

job在处理完一个任务以后&#xff0c;状态会变成Completed&#xff0c;job在状态为Completed的时候默认不会自动清理的&#xff0c;还会继续占用系统资源。 TTL-after-finished控制器 kubernetes中有专门的控制器可以自动清理已完成的job,就是TTL-after-finished控制器。 TTL…

数据中心产业如何变革?中国数字能源生态大会这些观点值得一读

对于数据中心产业&#xff0c;这是一个最好的时代。数字经济的蓬勃发展&#xff0c;推动产业数字化、企业数字化转型步入纵深阶段&#xff0c;大幅增加数据中心等基础设施的需求&#xff0c;让数据中心产业迎来前所未有的市场良机。 与此同时&#xff0c;对于数据中心产业&…

【备战秋招】每日一题:4月1日美团春招(二批)第一题:题面+题目思路 + C++/python/js/Go/java带注释

2023大厂笔试模拟练习网站&#xff08;含题解&#xff09; www.codefun2000.com 最近我们一直在将收集到的各种大厂笔试的解题思路还原成题目并制作数据&#xff0c;挂载到我们的OJ上&#xff0c;供大家学习交流&#xff0c;体会笔试难度。现已录入200道互联网大厂模拟练习题&…

使用render平台免费部署自己的ChatGPT

&#x1f4cb; 个人简介 &#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是阿牛&#xff0c;全栈领域优质创作者。&#x1f61c;&#x1f4dd; 个人主页&#xff1a;馆主阿牛&#x1f525;&#x1f389; 支持我&#xff1a;点赞&#x1f44d;收藏⭐️留言&#x1f4d…

解决websocket在部署到服务器https下无法使用的问题

目录 一、问题 1.1 问题描述 1.2 问题详细描述 二、解决 2.1 https下的链接类型 2.2 修改Nginx的配置 一、问题 1.1 问题描述 一个小项目中使用到了websocket&#xff0c;这个websocket在本地完全是完全正常运行的&#xff0c;不管是前后台的信息通讯 还是 异常报错接收…

JavaScript教程(高级)

面向对象编程介绍 两大编程思想 &#xff08;1&#xff09;、 面向过程编程&#xff1a; &#xff08;缩写 POP&#xff09;&#xff08; Process-oriented programming&#xff09;面向过程就是分析出解决问题所需要的步骤&#xff0c;然后用函数把这些步骤一步一步实现&am…

【PowerQuery】M语言的使用产品和使用场景

当然PowerQuery的M语言应用场景不只是引用在PowerBI和Excel中,它具有广泛的应用场景。目前我们可以在以下产品的使用场景中应用到M语言。 Excel PowerQuery应用Excel通过M语言可以实现整体数据的清洗和重构。  PowerBI 的PowerQuery应用 PowerBI也是通过M语言来实现数据…

VSCODE 插件推荐

文章目录 项目管理Project Manager markdownmarkdown-pdfPaste ImageMarkdown Preview Enhanced 笔记Notes 思维导图vscode-mindmap 开发Visual Studio IntelliCode GitHub Repositories 项目管理 Project Manager 以下是项目管理器提供的一些功能&#xff1a; 将任何文件夹…

JS中this的指向

JS中this的指向 本文目录 JS中this的指向全局上下文&#xff08;Global Context&#xff09;函数上下文&#xff08;Function Context&#xff09;普通函数调用作为对象的方法调用构造函数调用箭头函数回调函数 事件处理器上下文&#xff08;Event Handler Context&#xff09;…

find_package深度解析及实例应用

1. 检索模式 1.1 module模式 在这个模式下会查找一个名为find.cmake的文件&#xff0c;首先去CMAKE_MODULE_PATH指定的路径下去查找&#xff0c;然后去cmake安装提供的查找模块中查找&#xff08;安装cmake时生成的一些cmake文件&#xff09;。找到之后会检查版本&#xff0c;…

大模型全情投入,低代码也越来越清晰

众所周知&#xff0c;不少互联网企业在大模型领域全情投入。那么在这阵阵浪潮中&#xff0c;我们可以观察到什么样的“众生相”&#xff1f; 今年3月以来&#xff0c;国内已有超过20家企业入局大模型赛道。从百度“文心一言”、阿里“通义千问”的发布&#xff0c;华为“盘古”…