【Android 手势冲突】彻底解决RecyclerView与ScrollView滑动冲突问题,并实现RecyclerView悬停导航栏

介绍

在新一期的需求中,产品要求我们做出和美团某个页面类似的功能,即一个页面包含在scrollView中,上面一个部分放置一些常用的广告banner、宫格tab等,下面放置一个RecyclerView用于展示具体的产品列表。

要想实现上述功能,不可避免地要用到ScrollView嵌套RecyclerView。为什么要用RecyclerView?因为下面的产品列表项非常多,有60条,如果一次性加载到内存里肯定不现实,所以下方一定要用到可复用的RecyclerView。

而RecyclerView和ScrollView怎么嵌套使用呢?在以前,我总是习惯性地把RecyclerView设置为wrap_content,并且把RecyclerView的setNestedScrollingEnbaled设置为false,这样从来没有遇到过滑动冲突的问题,并且我看到团队里的很多大咖也是这么用。

然而,我们的产品有个需求是在滑动RecyclerView的过程中,RecyclerView顶部的悬停导航栏是要跟着滑动的,于是我就想到在RecyclerView的addOnScrollingListener里设置监听,并且利用linearLayouManager的findLastVisibleItemPosition、findFirstVisibleItemPosition、getChildCount这几个方法来判断当前滑动到RecyclerView的什么位置了,然后去对顶部悬停的导航栏进行联动。问题出现了。无论我怎么滑动,firstVisiblePosition永远为0,lastVisiblePosition永远为item总数-1,getChildCount永远为item总数。WTF,这是什么情况?后来查看资料发现,把RecyclerView高度设置为wrap_content居然是把所有的item都一次性加载进来,并没有用到复用和回收!!!!

对于一直强调代码性能的我,这绝对是我无法忍受的。那么,在为RecyclerView设置一个高度,并把setNestedScrollingEnabled(是否允许嵌套滑动)方法设置为true之后,滑动冲突问题出现了。那么,怎么解决呢?

只需要对ScrollView进行简单的修改,就可以实现。实现原理是,在进到页面中默认把滑动事件交给ScrollView,同时屏蔽RecyclerView的滑动事件;在RecyclerView滑动到顶部的时候,把滑动事件交给RecyclerView。

那么,怎么判断RecyclerView是否滑动到了屏幕顶部了呢?实现方法也是非常简单!通过recyclerview的getTop方法得到recyclerview距离顶部的距离,然后通过scrollView的getScrollY方法得到ScrollView滑动的距离。只需要比较这两个值就可以了。这里,我设置了两个接口回调,在Activity里设置ReyclerView的setNestedScrollingEnabled方法。

package com.example.zhshan.hoveringScrollView;
 
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ScrollView;
 
/**
 * @author Zhenhua on 2017/5/24 11:15.
 * @email [email protected]
 */
 
public class MyscrollView extends ScrollView{
    public MyscrollView(Context context) {
        super(context);
    }
 
    public MyscrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
 
    public MyscrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
 
    View view;
 
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if(changed){
            LinearLayout v = (LinearLayout) getChildAt(0);
            if(v != null){
                for(int i=0;i<v.getChildCount();i++){
                    if(v.getChildAt(i).getTag() != null && ((String)v.getChildAt(i).getTag()).equals("aaa")){
                        view = v.getChildAt(i);
                        break;
                    }
                }
            }
        }
    }
 
    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if(getScrollY() >= view.getTop()){
            fixHead();
       


  //此处代码为实现悬停导航栏,如果只是单纯想解决滑动冲突,可删掉
	    canvas.save();
            canvas.translate(0,getScrollY());
            canvas.clipRect(0,0,view.getWidth(),view.getHeight());
            view.draw(canvas);
            canvas.restore();
        }else {
            resetHead();
        }
    }
 
 
 
 
    private OnFixHeadListener listener;
 
    private void fixHead() {
        if (listener != null) {
            listener.onFix();
        }
    }
 
    private void resetHead() {
        if (listener != null) {
            listener.onReset();
        }
    }
 
    public void setFixHeadListener(OnFixHeadListener listener) {
        this.listener = listener;
    }
 
    public interface OnFixHeadListener {
        void onFix();
        void onReset();
    }
}
    canvas.save();
            canvas.translate(0,getScrollY());
            canvas.clipRect(0,0,view.getWidth(),view.getHeight());
            view.draw(canvas);
            canvas.restore();
        }else {
            resetHead();
        }
    }




    private OnFixHeadListener listener;

    private void fixHead() {
        if (listener != null) {
            listener.onFix();
        }
    }

    private void resetHead() {
        if (listener != null) {
            listener.onReset();
        }
    }

    public void setFixHeadListener(OnFixHeadListener listener) {
        this.listener = listener;
    }

    public interface OnFixHeadListener {
        void onFix();
        void onReset();
    }
}

通过这样的方法能够非常完美的实现 解决RecyclerView和ScrollView滑动冲突,与RecyclerView悬停导航栏功能。

下面附上demo(点击下载),并贴上两张demo截图。

PS: demo中也完美地实现了ReyclerView指定item置顶功能。

~~~~~~~华丽丽的分割线:问题进一步升级!!5.0以下手机无法解决滑动冲突问题~~~~~~~~~~

1、问题的背景:在RecyclerView需要把滑动事件处理权力交给ScrollView时,调用RecyclerView的setnestedscrollenable(false)方法;在ScrollView需要把滑动事件处理权力交给RecyclerView时,调用RecyclerView的setnestedscrollenable(true)方法。本以为这样就可以完美解决滑动冲突问题,然而测试却在我提测之后第一天就提了bug,我心灰意冷地打开后发现,5.0以下的手机仍然存在滑动冲突问题。去查了下recyclerview的setnestedscrollenable方法的文档才发现,这个方法只有在5.0以上手机有用。擦。。

2、问题如何得到解决?

这个时候就只能按照最原始的方法,根据Android的事件传递原理去一步步解决滑动冲突了。其实,我在2014年的时候就解决过一个滑动冲突问题,并做了一些总结。解决滑动冲突问题其实很简单!!

解决滑动冲突的原理:

(1)刚开始把滑动事件给ScrollView,在ScrollView滑动到某一个位置时,再把滑动事件给RecyclerView。

  (2)在RecyclerView滑动到某一个位置时,再把滑动事件交给ScrollView。

总结一下,解决滑动冲突需要知道(1)什么时候把滑动事件传给内部View;(2)什么时候内部View再把滑动事件传给外部View。

先来看一下,如何把事件传给内部View?只需要在ScrollView滑动到某个位置后,使用接口回调,并且让RecyclerView在接口回调里,getParent().requestdisallowintercept()。具体代码如下:

protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if (getScrollY() >= fixView.getTop()) {
            fix();
        } else {
            dismiss();
        }
    }


在fix回调里,requestdisallowintercept(true)来让ScrollView不拦截。

那么,RecyclerView如何把事件传给外部?

需要给RecyclerView设置ontouchlistener,然后在RecyclerView滑动到第一个item,并且正在向下滑动时,requestdisallowintercept(false)来让ScrollView拦截。

~~~~~~~~~~~~~华丽丽的分割线:滑动冲突完全解析~~~~~~~~~~~~~~~~~~~~~~

1、外部拦截法

所有点击事件都先经过父容器拦截处理

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
 
        switch(ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //必须返回false,否则子控件永远无法拿到焦点
                return false;
            case MotionEvent.ACTION_MOVE:
                if(事件交给子控件的条件) {
                    return false;
                } else {
                    return super.onInterceptTouchEvent(ev);
                }
            case MotionEvent.ACTION_UP:
                //必须返回false,否则子控件永远无法拿到焦点
                return false;
            default:
                return super.onInterceptTouchEvent(ev);
         }
 
    }

2、内部拦截法

所有点击事件都先交给子控件处理

@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;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

在使用内部拦截法的时候,必须在父容器的Touch_down方法里返回false

demo在这里(点击下载)

~~~~~~~~~~~~~华丽丽的分割线:框架进一步升级,使用RecyclerView代替~~~~~~~~~~~~~~~~~~~~~~

时隔几个月后,确实发现之前的框架存在一定问题,ScrollView嵌套RecyclerView的方式着实让人头痛。趁着不忙的时候,我已经对框架进行了升级,抛弃了过去ScrollView嵌套RecyclerView的方式,而采用多类型RecylerView的方式。这种方式能很好地实现悬停!!

具体请看我的下一篇文章《RecyclerView实现悬停导航栏》。

~~~~~~~~~~~~~华丽丽的分割线:框架进一步升级,近期逛github时找到了嵌套滑动的终极解决办法~~~~~~~~~~~~~~~~~~~~~~

重写了ScrollView!!只有两个类!!使用起来非常容易!!完美解决了各种滑动冲突问题!!

public class HeaderScrollHelper {
 
    private int sysVersion;         //当前sdk版本,用于判断api版本
    private ScrollableContainer mCurrentScrollableContainer;
 
    public HeaderScrollHelper() {
        sysVersion = Build.VERSION.SDK_INT;
    }
 
    /** 包含有 ScrollView ListView RecyclerView 的组件 */
    public interface ScrollableContainer {
 
        /** @return ScrollView ListView RecyclerView 或者其他的布局的实例 */
        View getScrollableView();
    }
 
    public void setCurrentScrollableContainer(ScrollableContainer scrollableContainer) {
        this.mCurrentScrollableContainer = scrollableContainer;
    }
 
    private View getScrollableView() {
        if (mCurrentScrollableContainer == null) return null;
        return mCurrentScrollableContainer.getScrollableView();
    }
 
    /**
     * 判断是否滑动到顶部方法,ScrollAbleLayout根据此方法来做一些逻辑判断
     * 目前只实现了AdapterView,ScrollView,RecyclerView
     * 需要支持其他view可以自行补充实现
     */
    public boolean isTop() {
        View scrollableView = getScrollableView();
        if (scrollableView == null) {
            throw new NullPointerException("You should call ScrollableHelper.setCurrentScrollableContainer() to set ScrollableContainer.");
        }
        if (scrollableView instanceof AdapterView) {
            return isAdapterViewTop((AdapterView) scrollableView);
        }
        if (scrollableView instanceof ScrollView) {
            return isScrollViewTop((ScrollView) scrollableView);
        }
        if (scrollableView instanceof RecyclerView) {
            return isRecyclerViewTop((RecyclerView) scrollableView);
        }
        if (scrollableView instanceof WebView) {
            return isWebViewTop((WebView) scrollableView);
        }
        throw new IllegalStateException("scrollableView must be a instance of AdapterView|ScrollView|RecyclerView");
    }
 
    private boolean isRecyclerViewTop(RecyclerView recyclerView) {
        if (recyclerView != null) {
            RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
            if (layoutManager instanceof LinearLayoutManager) {
                int firstVisibleItemPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
                View childAt = recyclerView.getChildAt(0);
                if (childAt == null || (firstVisibleItemPosition == 0 && childAt.getTop() == 0)) {
                    return true;
                }
            }
        }
        return false;
    }
 
    private boolean isAdapterViewTop(AdapterView adapterView) {
        if (adapterView != null) {
            int firstVisiblePosition = adapterView.getFirstVisiblePosition();
            View childAt = adapterView.getChildAt(0);
            if (childAt == null || (firstVisiblePosition == 0 && childAt.getTop() == 0)) {
                return true;
            }
        }
        return false;
    }
 
    private boolean isScrollViewTop(ScrollView scrollView) {
        if (scrollView != null) {
            int scrollViewY = scrollView.getScrollY();
            return scrollViewY <= 0;
        }
        return false;
    }
 
    private boolean isWebViewTop(WebView scrollView) {
        if (scrollView != null) {
            int scrollViewY = scrollView.getScrollY();
            return scrollViewY <= 0;
        }
        return false;
    }
 
    /**
     * 将特定的view按照初始条件滚动
     *
     * @param velocityY 初始滚动速度
     * @param distance  需要滚动的距离
     * @param duration  允许滚动的时间
     */
    @SuppressLint("NewApi")
    public void smoothScrollBy(int velocityY, int distance, int duration) {
        View scrollableView = getScrollableView();
        if (scrollableView instanceof AbsListView) {
            AbsListView absListView = (AbsListView) scrollableView;
            if (sysVersion >= 21) {
                absListView.fling(velocityY);
            } else {
                absListView.smoothScrollBy(distance, duration);
            }
        } else if (scrollableView instanceof ScrollView) {
            ((ScrollView) scrollableView).fling(velocityY);
        } else if (scrollableView instanceof RecyclerView) {
            ((RecyclerView) scrollableView).fling(0, velocityY);
        } else if (scrollableView instanceof WebView) {
            ((WebView) scrollableView).flingScroll(0, velocityY);
        }
    }
}
public class HeaderScrollView extends LinearLayout {
 
    private static final int DIRECTION_UP = 1;
    private static final int DIRECTION_DOWN = 2;
 
    private int topOffset = 0;      //滚动的最大偏移量
 
    private Scroller mScroller;
    private int mTouchSlop;         //表示滑动的时候,手的移动要大于这个距离才开始移动控件。
    private int mMinimumVelocity;   //允许执行一个fling手势动作的最小速度值
    private int mMaximumVelocity;   //允许执行一个fling手势动作的最大速度值
    private int sysVersion;         //当前sdk版本,用于判断api版本
    private View mHeadView;         //需要被滑出的头部
    private int mHeadHeight;        //滑出头部的高度
    private int maxY = 0;           //最大滑出的距离,等于 mHeadHeight
    private int minY = 0;           //最小的距离, 头部在最顶部
    private int mCurY;              //当前已经滚动的距离
    private VelocityTracker mVelocityTracker;
    private int mDirection;
    private int mLastScrollerY;
    private boolean mDisallowIntercept;  //是否允许拦截事件
    private boolean isClickHead;         //当前点击区域是否在头部
    private OnScrollListener onScrollListener;   //滚动的监听
    private HeaderScrollHelper mScrollable;
 
    public interface OnScrollListener {
        void onScroll(int currentY, int maxY);
    }
 
    public void setOnScrollListener(OnScrollListener onScrollListener) {
        this.onScrollListener = onScrollListener;
    }
 
    public HeaderScrollView(Context context) {
        this(context, null);
    }
 
    public HeaderScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
 
    public HeaderScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
 
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CTTourHeaderScrollView);
        topOffset = a.getDimensionPixelSize(R.styleable.CTTourHeaderScrollView_top_offset, topOffset);
        a.recycle();
 
        mScroller = new Scroller(context);
        mScrollable = new HeaderScrollHelper();
        ViewConfiguration configuration = ViewConfiguration.get(context);
        mTouchSlop = configuration.getScaledTouchSlop();   //表示滑动的时候,手的移动要大于这个距离才开始移动控件。
        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); //允许执行一个fling手势动作的最小速度值
        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); //允许执行一个fling手势动作的最大速度值
        sysVersion = Build.VERSION.SDK_INT;
    }
 
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        if (mHeadView != null && !mHeadView.isClickable()) {
            mHeadView.setClickable(true);
        }
    }
 
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        mHeadView = getChildAt(0);
        measureChildWithMargins(mHeadView, widthMeasureSpec, 0, MeasureSpec.UNSPECIFIED, 0);
        mHeadHeight = mHeadView.getMeasuredHeight();
        maxY = mHeadHeight - topOffset;
        //让测量高度加上头部的高度
        super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec) + maxY, MeasureSpec.EXACTLY));
    }
 
    /** @param disallowIntercept 作用同 requestDisallowInterceptTouchEvent */
    public void requestHeaderViewPagerDisallowInterceptTouchEvent(boolean disallowIntercept) {
        super.requestDisallowInterceptTouchEvent(disallowIntercept);
        mDisallowIntercept = disallowIntercept;
    }
 
    private float mDownX;  //第一次按下的x坐标
    private float mDownY;  //第一次按下的y坐标
    private float mLastY;  //最后一次移动的Y坐标
    private boolean verticalScrollFlag = false;   //是否允许垂直滚动
 
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        float currentX = ev.getX();                   //当前手指相对于当前view的X坐标
        float currentY = ev.getY();                   //当前手指相对于当前view的Y坐标
        float shiftX = Math.abs(currentX - mDownX);   //当前触摸位置与第一次按下位置的X偏移量
        float shiftY = Math.abs(currentY - mDownY);   //当前触摸位置与第一次按下位置的Y偏移量
        float deltaY;                                 //滑动的偏移量,即连续两次进入Move的偏移量
        obtainVelocityTracker(ev);                    //初始化速度追踪器
        switch (ev.getAction()) {
            //Down事件主要初始化变量
            case MotionEvent.ACTION_DOWN:
                mDisallowIntercept = false;
                verticalScrollFlag = false;
                mDownX = currentX;
                mDownY = currentY;
                mLastY = currentY;
                checkIsClickHead((int) currentY, mHeadHeight, getScrollY());
                mScroller.abortAnimation();
                break;
            case MotionEvent.ACTION_MOVE:
                if (mDisallowIntercept) break;
                deltaY = mLastY - currentY; //连续两次进入move的偏移量
                mLastY = currentY;
                if (shiftX > mTouchSlop && shiftX > shiftY) {
                    //水平滑动
                    verticalScrollFlag = false;
                } else if (shiftY > mTouchSlop && shiftY > shiftX) {
                    //垂直滑动
                    verticalScrollFlag = true;
                }
                /**
                 * 这里要注意,对于垂直滑动来说,给出以下三个条件
                 * 头部没有固定,允许滑动的View处于第一条可见,当前按下的点在头部区域
                 * 三个条件满足一个即表示需要滚动当前布局,否者不处理,将事件交给子View去处理
                 */
                if (verticalScrollFlag && (!isStickied() || mScrollable.isTop() || isClickHead)) {
                    //如果是向下滑,则deltaY小于0,对于scrollBy来说
                    //正值为向上和向左滑,负值为向下和向右滑,这里要注意
                    scrollBy(0, (int) (deltaY + 0.5));
                    invalidate();
                }
                break;
            case MotionEvent.ACTION_UP:
                if (verticalScrollFlag) {
                    mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); //1000表示单位,每1000毫秒允许滑过的最大距离是mMaximumVelocity
                    float yVelocity = mVelocityTracker.getYVelocity();  //获取当前的滑动速度
                    mDirection = yVelocity > 0 ? DIRECTION_DOWN : DIRECTION_UP;  //下滑速度大于0,上滑速度小于0
                    //根据当前的速度和初始化参数,将滑动的惯性初始化到当前View,至于是否滑动当前View,取决于computeScroll中计算的值
                    //这里不判断最小速度,确保computeScroll一定至少执行一次
                    mScroller.fling(0, getScrollY(), 0, -(int) yVelocity, 0, 0, -Integer.MAX_VALUE, Integer.MAX_VALUE);
                    mLastScrollerY = getScrollY();
                    invalidate();  //更新界面,该行代码会导致computeScroll中的代码执行
                    //阻止快读滑动的时候点击事件的发生,滑动的时候,将Up事件改为Cancel就不会发生点击了
                    if ((shiftX > mTouchSlop || shiftY > mTouchSlop)) {
                        if (isClickHead || !isStickied()) {
                            int action = ev.getAction();
                            ev.setAction(MotionEvent.ACTION_CANCEL);
                            boolean dd = super.dispatchTouchEvent(ev);
                            ev.setAction(action);
                            return dd;
                        }
                    }
                }
                recycleVelocityTracker();
                break;
            case MotionEvent.ACTION_CANCEL:
                recycleVelocityTracker();
                break;
            default:
                break;
        }
        //手动将事件传递给子View,让子View自己去处理事件
        super.dispatchTouchEvent(ev);
        //消费事件,返回True表示当前View需要消费事件,就是事件的TargetView
        return true;
    }
 
    private void checkIsClickHead(int downY, int headHeight, int scrollY) {
        isClickHead = ((downY + scrollY) <= headHeight);
    }
 
    private void obtainVelocityTracker(MotionEvent event) {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
    }
 
    private void recycleVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }
 
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            final int currY = mScroller.getCurrY();
            if (mDirection == DIRECTION_UP) {
                // 手势向上划
                if (isStickied()) {
                    //这里主要是将快速滚动时的速度对接起来,让布局看起来滚动连贯
                    int distance = mScroller.getFinalY() - currY;    //除去布局滚动消耗的时间后,剩余的时间
                    int duration = calcDuration(mScroller.getDuration(), mScroller.timePassed()); //除去布局滚动的距离后,剩余的距离
                    mScrollable.smoothScrollBy(getScrollerVelocity(distance, duration), distance, duration);
                    //外层布局已经滚动到指定位置,不需要继续滚动了
                    mScroller.abortAnimation();
                    return;
                } else {
                    scrollTo(0, currY);  //将外层布局滚动到指定位置
                    invalidate();        //移动完后刷新界面
                }
            } else {
                // 手势向下划,内部View已经滚动到顶了,需要滚动外层的View
                if (mScrollable.isTop() || isClickHead) {
                    int deltaY = (currY - mLastScrollerY);
                    int toY = getScrollY() + deltaY;
                    scrollTo(0, toY);
                    if (mCurY <= minY) {
                        mScroller.abortAnimation();
                        return;
                    }
                }
                //向下滑动时,初始状态可能不在顶部,所以要一直重绘,让computeScroll一直调用
                //确保代码能进入上面的if判断
                invalidate();
            }
            mLastScrollerY = currY;
        }
    }
 
    @SuppressLint("NewApi")
    private int getScrollerVelocity(int distance, int duration) {
        if (mScroller == null) {
            return 0;
        } else if (sysVersion >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            return (int) mScroller.getCurrVelocity();
        } else {
            return distance / duration;
        }
    }
 
    /** 对滑动范围做限制 */
    @Override
    public void scrollBy(int x, int y) {
        int scrollY = getScrollY();
        int toY = scrollY + y;
        if (toY >= maxY) {
            toY = maxY;
        } else if (toY <= minY) {
            toY = minY;
        }
        y = toY - scrollY;
        super.scrollBy(x, y);
    }
 
    /** 对滑动范围做限制 */
    @Override
    public void scrollTo(int x, int y) {
        if (y >= maxY) {
            y = maxY;
        } else if (y <= minY) {
            y = minY;
        }
        mCurY = y;
        if (onScrollListener != null) {
            onScrollListener.onScroll(y, maxY);
        }
        super.scrollTo(x, y);
    }
 
    /** 头部是否已经固定 */
    public boolean isStickied() {
        return mCurY == maxY;
    }
 
    private int calcDuration(int duration, int timepass) {
        return duration - timepass;
    }
 
    public int getMaxY() {
        return maxY;
    }
 
    public boolean isHeadTop() {
        return mCurY == minY;
    }
 
    /** 是否允许下拉,与PTR结合使用 */
    public boolean canPtr() {
        return verticalScrollFlag && mCurY == minY && mScrollable.isTop();
    }
 
    public void setTopOffset(int topOffset) {
        this.topOffset = topOffset;
    }
 
    public void setCurrentScrollableContainer(HeaderScrollHelper.ScrollableContainer scrollableContainer) {
        mScrollable.setCurrentScrollableContainer(scrollableContainer);
    }
}

福利!!福利!!此代码已经应用在我们的产品里,并且已经上线,且稳定运行了三个大版本。可直接拿去用!如有不理解,可直接留言提问,博主每天都会查看!


~~~~~~~~~~~~~华丽丽的分割线:解答朋友们关心的几个问题~~~~~~~~~~~~~~~~~~~~~~
首先感谢各位朋友的支持,看到你们能给我的github一个star或者fork我写的demo,我的内心充满了感恩。

为了能给更大家的开发带来更大的便捷,我决定还是更新一下github,并且把最新的代码合到了demo里,欢迎下载。

看到有一些朋友问为什么不采取多类型recyclerview的方式,我这里试着解答一下。问这个问题的朋友,我相信你肯定只是没有遇到这样必须要scrollview嵌套另外一个可滑动layout的需求。在我们的产品详情页里需要把webview置顶,如果你来实现能有什么更好的方式吗?只能用ScrollView嵌套一个webview吧。另外,我在博文里确实也已经提到过,如果只是实现吸顶功能,确实使用recyclerview就可以实现了,我也已经实现过并且代码也已经上线几个月了。我的一篇博文《【Android 编程架构 程序设计】多Item类型的RecyclerView替代scrollView(附demo)》介绍了一种多类型RecyclerView的编程框架,该代码已经上线并且稳定运行几个月了,如有需要可以去查看。

~~~~~~~~~~~~~华丽丽的分割线:新增高效便捷实现悬停的代码~~~~~~~~~~~~~~~~~~~~~~

以上代码其实不光实现了悬停,更解决了滑动冲突。那么,如果不需要解决滑动冲突,我只希望能将ScrollView中的一个View实现悬停,并没有在ScrollView嵌套RecyclerView的场景中的话,我的代码该怎么写呢?我在demo中新增了单纯实现悬停的方法,这里只有一个view,代码看起来逻辑清晰,并且很清爽,并没有采用两个View隐藏展示的方法。如有需要,欢迎下载demo查看。
--------------------- 
 

感谢这位作者的分享,在此表示感谢。

作者:Colin_Mindset 
来源:CSDN 
原文:https://blog.csdn.net/colinandroid/article/details/72770863 
 

猜你喜欢

转载自blog.csdn.net/Maiduoudo/article/details/88050243
今日推荐