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

介绍

在新一期的需求中,产品要求我们做出和美团某个页面类似的功能,即一个页面包含在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方法。

[java]  view plain  copy
  1. package com.example.zhshan.hoveringScrollView;  
  2.   
  3. import android.content.Context;  
  4. import android.graphics.Canvas;  
  5. import android.util.AttributeSet;  
  6. import android.view.View;  
  7. import android.widget.LinearLayout;  
  8. import android.widget.ScrollView;  
  9.   
  10. /** 
  11.  * @author Zhenhua on 2017/5/24 11:15. 
  12.  * @email [email protected] 
  13.  */  
  14.   
  15. public class MyscrollView extends ScrollView{  
  16.     public MyscrollView(Context context) {  
  17.         super(context);  
  18.     }  
  19.   
  20.     public MyscrollView(Context context, AttributeSet attrs) {  
  21.         super(context, attrs);  
  22.     }  
  23.   
  24.     public MyscrollView(Context context, AttributeSet attrs, int defStyleAttr) {  
  25.         super(context, attrs, defStyleAttr);  
  26.     }  
  27.   
  28.     View view;  
  29.   
  30.     @Override  
  31.     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {  
  32.         super.onLayout(changed, left, top, right, bottom);  
  33.         if(changed){  
  34.             LinearLayout v = (LinearLayout) getChildAt(0);  
  35.             if(v != null){  
  36.                 for(int i=0;i<v.getChildCount();i++){  
  37.                     if(v.getChildAt(i).getTag() != null && ((String)v.getChildAt(i).getTag()).equals("aaa")){  
  38.                         view = v.getChildAt(i);  
  39.                         break;  
  40.                     }  
  41.                 }  
  42.             }  
  43.         }  
  44.     }  
  45.   
  46.     @Override  
  47.     protected void dispatchDraw(Canvas canvas) {  
  48.         super.dispatchDraw(canvas);  
  49.         if(getScrollY() >= view.getTop()){  
  50.             fixHead();  
[java]  view plain  copy
  1. //此处代码为实现悬停导航栏,如果只是单纯想解决滑动冲突,可删掉  
[java]  view plain  copy
  1. <span style="white-space:pre;"> </span>    canvas.save();  
  2.             canvas.translate(0,getScrollY());  
  3.             canvas.clipRect(0,0,view.getWidth(),view.getHeight());  
  4.             view.draw(canvas);  
  5.             canvas.restore();  
  6.         }else {  
  7.             resetHead();  
  8.         }  
  9.     }  
  10.   
  11.   
  12.   
  13.   
  14.     private OnFixHeadListener listener;  
  15.   
  16.     private void fixHead() {  
  17.         if (listener != null) {  
  18.             listener.onFix();  
  19.         }  
  20.     }  
  21.   
  22.     private void resetHead() {  
  23.         if (listener != null) {  
  24.             listener.onReset();  
  25.         }  
  26.     }  
  27.   
  28.     public void setFixHeadListener(OnFixHeadListener listener) {  
  29.         this.listener = listener;  
  30.     }  
  31.   
  32.     public interface OnFixHeadListener {  
  33.         void onFix();  
  34.         void onReset();  
  35.     }  
  36. }  
[java]  view plain  copy
  1.   

通过这样的方法能够非常完美的实现 解决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()。具体代码如下:

[html]  view plain  copy
  1. protected void dispatchDraw(Canvas canvas) {  
  2.         super.dispatchDraw(canvas);  
  3.         if (getScrollY() >= fixView.getTop()) {  
  4.             fix();  
  5.         } else {  
  6.             dismiss();  
  7.         }  
  8.     }  

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

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

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


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


1、外部拦截法

所有点击事件都先经过父容器拦截处理
[java]  view plain  copy
  1. @Override  
  2.     public boolean onInterceptTouchEvent(MotionEvent ev) {  
  3.   
  4.         switch(ev.getAction()) {  
  5.             case MotionEvent.ACTION_DOWN:  
  6.                 //必须返回false,否则子控件永远无法拿到焦点  
  7.                 return false;  
  8.             case MotionEvent.ACTION_MOVE:  
  9.                 if(事件交给子控件的条件) {  
  10.                     return false;  
  11.                 } else {  
  12.                     return super.onInterceptTouchEvent(ev);  
  13.                 }  
  14.             case MotionEvent.ACTION_UP:  
  15.                 //必须返回false,否则子控件永远无法拿到焦点  
  16.                 return false;  
  17.             default:  
  18.                 return super.onInterceptTouchEvent(ev);  
  19.          }  
  20.   
  21.     }  


2、内部拦截法

所有点击事件都先交给子控件处理
[java]  view plain  copy
  1. @Override  
  2.     public boolean dispatchTouchEvent(MotionEvent ev) {  
  3.         switch(ev.getAction()) {  
  4.             case MotionEvent.ACTION_DOWN:  
  5.                 //父容器禁止拦截  
  6.                 getParent().requestDisallowInterceptTouchEvent(true);  
  7.                 break;  
  8.             case MotionEvent.ACTION_MOVE:  
  9.                 if(事件交给父容器的条件) {  
  10.                     getParent().requestDisallowInterceptTouchEvent(false);  
  11.                 }  
  12.                 break;  
  13.             case MotionEvent.ACTION_UP:  
  14.                 break;  
  15.             default:  
  16.                 break;  
  17.         }  
  18.         return super.dispatchTouchEvent(ev);  
  19.     }  
在使用内部拦截法的时候,必须在父容器的Touch_down方法里返回false

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

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

~~~~~~~~~~~~~华丽丽的分割线:框架进一步升级,近期逛github时找到了嵌套滑动的终极解决办法~~~~~~~~~~~~~~~~~~~~~~
重写了ScrollView!!只有两个类!!使用起来非常容易!!完美解决了各种滑动冲突问题!!
[java]  view plain  copy
  1. public class HeaderScrollHelper {  
  2.   
  3.     private int sysVersion;         //当前sdk版本,用于判断api版本  
  4.     private ScrollableContainer mCurrentScrollableContainer;  
  5.   
  6.     public HeaderScrollHelper() {  
  7.         sysVersion = Build.VERSION.SDK_INT;  
  8.     }  
  9.   
  10.     /** 包含有 ScrollView ListView RecyclerView 的组件 */  
  11.     public interface ScrollableContainer {  
  12.   
  13.         /** @return ScrollView ListView RecyclerView 或者其他的布局的实例 */  
  14.         View getScrollableView();  
  15.     }  
  16.   
  17.     public void setCurrentScrollableContainer(ScrollableContainer scrollableContainer) {  
  18.         this.mCurrentScrollableContainer = scrollableContainer;  
  19.     }  
  20.   
  21.     private View getScrollableView() {  
  22.         if (mCurrentScrollableContainer == nullreturn null;  
  23.         return mCurrentScrollableContainer.getScrollableView();  
  24.     }  
  25.   
  26.     /** 
  27.      * 判断是否滑动到顶部方法,ScrollAbleLayout根据此方法来做一些逻辑判断 
  28.      * 目前只实现了AdapterView,ScrollView,RecyclerView 
  29.      * 需要支持其他view可以自行补充实现 
  30.      */  
  31.     public boolean isTop() {  
  32.         View scrollableView = getScrollableView();  
  33.         if (scrollableView == null) {  
  34.             throw new NullPointerException("You should call ScrollableHelper.setCurrentScrollableContainer() to set ScrollableContainer.");  
  35.         }  
  36.         if (scrollableView instanceof AdapterView) {  
  37.             return isAdapterViewTop((AdapterView) scrollableView);  
  38.         }  
  39.         if (scrollableView instanceof ScrollView) {  
  40.             return isScrollViewTop((ScrollView) scrollableView);  
  41.         }  
  42.         if (scrollableView instanceof RecyclerView) {  
  43.             return isRecyclerViewTop((RecyclerView) scrollableView);  
  44.         }  
  45.         if (scrollableView instanceof WebView) {  
  46.             return isWebViewTop((WebView) scrollableView);  
  47.         }  
  48.         throw new IllegalStateException("scrollableView must be a instance of AdapterView|ScrollView|RecyclerView");  
  49.     }  
  50.   
  51.     private boolean isRecyclerViewTop(RecyclerView recyclerView) {  
  52.         if (recyclerView != null) {  
  53.             RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();  
  54.             if (layoutManager instanceof LinearLayoutManager) {  
  55.                 int firstVisibleItemPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();  
  56.                 View childAt = recyclerView.getChildAt(0);  
  57.                 if (childAt == null || (firstVisibleItemPosition == 0 && childAt.getTop() == 0)) {  
  58.                     return true;  
  59.                 }  
  60.             }  
  61.         }  
  62.         return false;  
  63.     }  
  64.   
  65.     private boolean isAdapterViewTop(AdapterView adapterView) {  
  66.         if (adapterView != null) {  
  67.             int firstVisiblePosition = adapterView.getFirstVisiblePosition();  
  68.             View childAt = adapterView.getChildAt(0);  
  69.             if (childAt == null || (firstVisiblePosition == 0 && childAt.getTop() == 0)) {  
  70.                 return true;  
  71.             }  
  72.         }  
  73.         return false;  
  74.     }  
  75.   
  76.     private boolean isScrollViewTop(ScrollView scrollView) {  
  77.         if (scrollView != null) {  
  78.             int scrollViewY = scrollView.getScrollY();  
  79.             return scrollViewY <= 0;  
  80.         }  
  81.         return false;  
  82.     }  
  83.   
  84.     private boolean isWebViewTop(WebView scrollView) {  
  85.         if (scrollView != null) {  
  86.             int scrollViewY = scrollView.getScrollY();  
  87.             return scrollViewY <= 0;  
  88.         }  
  89.         return false;  
  90.     }  
  91.   
  92.     /** 
  93.      * 将特定的view按照初始条件滚动 
  94.      * 
  95.      * @param velocityY 初始滚动速度 
  96.      * @param distance  需要滚动的距离 
  97.      * @param duration  允许滚动的时间 
  98.      */  
  99.     @SuppressLint("NewApi")  
  100.     public void smoothScrollBy(int velocityY, int distance, int duration) {  
  101.         View scrollableView = getScrollableView();  
  102.         if (scrollableView instanceof AbsListView) {  
  103.             AbsListView absListView = (AbsListView) scrollableView;  
  104.             if (sysVersion >= 21) {  
  105.                 absListView.fling(velocityY);  
  106.             } else {  
  107.                 absListView.smoothScrollBy(distance, duration);  
  108.             }  
  109.         } else if (scrollableView instanceof ScrollView) {  
  110.             ((ScrollView) scrollableView).fling(velocityY);  
  111.         } else if (scrollableView instanceof RecyclerView) {  
  112.             ((RecyclerView) scrollableView).fling(0, velocityY);  
  113.         } else if (scrollableView instanceof WebView) {  
  114.             ((WebView) scrollableView).flingScroll(0, velocityY);  
  115.         }  
  116.     }  
  117. }  
[java]  view plain  copy
  1. public class HeaderScrollView extends LinearLayout {  
  2.   
  3.     private static final int DIRECTION_UP = 1;  
  4.     private static final int DIRECTION_DOWN = 2;  
  5.   
  6.     private int topOffset = 0;      //滚动的最大偏移量  
  7.   
  8.     private Scroller mScroller;  
  9.     private int mTouchSlop;         //表示滑动的时候,手的移动要大于这个距离才开始移动控件。  
  10.     private int mMinimumVelocity;   //允许执行一个fling手势动作的最小速度值  
  11.     private int mMaximumVelocity;   //允许执行一个fling手势动作的最大速度值  
  12.     private int sysVersion;         //当前sdk版本,用于判断api版本  
  13.     private View mHeadView;         //需要被滑出的头部  
  14.     private int mHeadHeight;        //滑出头部的高度  
  15.     private int maxY = 0;           //最大滑出的距离,等于 mHeadHeight  
  16.     private int minY = 0;           //最小的距离, 头部在最顶部  
  17.     private int mCurY;              //当前已经滚动的距离  
  18.     private VelocityTracker mVelocityTracker;  
  19.     private int mDirection;  
  20.     private int mLastScrollerY;  
  21.     private boolean mDisallowIntercept;  //是否允许拦截事件  
  22.     private boolean isClickHead;         //当前点击区域是否在头部  
  23.     private OnScrollListener onScrollListener;   //滚动的监听  
  24.     private HeaderScrollHelper mScrollable;  
  25.   
  26.     public interface OnScrollListener {  
  27.         void onScroll(int currentY, int maxY);  
  28.     }  
  29.   
  30.     public void setOnScrollListener(OnScrollListener onScrollListener) {  
  31.         this.onScrollListener = onScrollListener;  
  32.     }  
  33.   
  34.     public HeaderScrollView(Context context) {  
  35.         this(context, null);  
  36.     }  
  37.   
  38.     public HeaderScrollView(Context context, AttributeSet attrs) {  
  39.         this(context, attrs, 0);  
  40.     }  
  41.   
  42.     public HeaderScrollView(Context context, AttributeSet attrs, int defStyleAttr) {  
  43.         super(context, attrs, defStyleAttr);  
  44.   
  45.         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CTTourHeaderScrollView);  
  46.         topOffset = a.getDimensionPixelSize(R.styleable.CTTourHeaderScrollView_top_offset, topOffset);  
  47.         a.recycle();  
  48.   
  49.         mScroller = new Scroller(context);  
  50.         mScrollable = new HeaderScrollHelper();  
  51.         ViewConfiguration configuration = ViewConfiguration.get(context);  
  52.         mTouchSlop = configuration.getScaledTouchSlop();   //表示滑动的时候,手的移动要大于这个距离才开始移动控件。  
  53.         mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); //允许执行一个fling手势动作的最小速度值  
  54.         mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); //允许执行一个fling手势动作的最大速度值  
  55.         sysVersion = Build.VERSION.SDK_INT;  
  56.     }  
  57.   
  58.     @Override  
  59.     protected void onFinishInflate() {  
  60.         super.onFinishInflate();  
  61.         if (mHeadView != null && !mHeadView.isClickable()) {  
  62.             mHeadView.setClickable(true);  
  63.         }  
  64.     }  
  65.   
  66.     @Override  
  67.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  68.         mHeadView = getChildAt(0);  
  69.         measureChildWithMargins(mHeadView, widthMeasureSpec, 0, MeasureSpec.UNSPECIFIED, 0);  
  70.         mHeadHeight = mHeadView.getMeasuredHeight();  
  71.         maxY = mHeadHeight - topOffset;  
  72.         //让测量高度加上头部的高度  
  73.         super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec) + maxY, MeasureSpec.EXACTLY));  
  74.     }  
  75.   
  76.     /** @param disallowIntercept 作用同 requestDisallowInterceptTouchEvent */  
  77.     public void requestHeaderViewPagerDisallowInterceptTouchEvent(boolean disallowIntercept) {  
  78.         super.requestDisallowInterceptTouchEvent(disallowIntercept);  
  79.         mDisallowIntercept = disallowIntercept;  
  80.     }  
  81.   
  82.     private float mDownX;  //第一次按下的x坐标  
  83.     private float mDownY;  //第一次按下的y坐标  
  84.     private float mLastY;  //最后一次移动的Y坐标  
  85.     private boolean verticalScrollFlag = false;   //是否允许垂直滚动  
  86.   
  87.     @Override  
  88.     public boolean dispatchTouchEvent(MotionEvent ev) {  
  89.         float currentX = ev.getX();                   //当前手指相对于当前view的X坐标  
  90.         float currentY = ev.getY();                   //当前手指相对于当前view的Y坐标  
  91.         float shiftX = Math.abs(currentX - mDownX);   //当前触摸位置与第一次按下位置的X偏移量  
  92.         float shiftY = Math.abs(currentY - mDownY);   //当前触摸位置与第一次按下位置的Y偏移量  
  93.         float deltaY;                                 //滑动的偏移量,即连续两次进入Move的偏移量  
  94.         obtainVelocityTracker(ev);                    //初始化速度追踪器  
  95.         switch (ev.getAction()) {  
  96.             //Down事件主要初始化变量  
  97.             case MotionEvent.ACTION_DOWN:  
  98.                 mDisallowIntercept = false;  
  99.                 verticalScrollFlag = false;  
  100.                 mDownX = currentX;  
  101.                 mDownY = currentY;  
  102.                 mLastY = currentY;  
  103.                 checkIsClickHead((int) currentY, mHeadHeight, getScrollY());  
  104.                 mScroller.abortAnimation();  
  105.                 break;  
  106.             case MotionEvent.ACTION_MOVE:  
  107.                 if (mDisallowIntercept) break;  
  108.                 deltaY = mLastY - currentY; //连续两次进入move的偏移量  
  109.                 mLastY = currentY;  
  110.                 if (shiftX > mTouchSlop && shiftX > shiftY) {  
  111.                     //水平滑动  
  112.                     verticalScrollFlag = false;  
  113.                 } else if (shiftY > mTouchSlop && shiftY > shiftX) {  
  114.                     //垂直滑动  
  115.                     verticalScrollFlag = true;  
  116.                 }  
  117.                 /** 
  118.                  * 这里要注意,对于垂直滑动来说,给出以下三个条件 
  119.                  * 头部没有固定,允许滑动的View处于第一条可见,当前按下的点在头部区域 
  120.                  * 三个条件满足一个即表示需要滚动当前布局,否者不处理,将事件交给子View去处理 
  121.                  */  
  122.                 if (verticalScrollFlag && (!isStickied() || mScrollable.isTop() || isClickHead)) {  
  123.                     //如果是向下滑,则deltaY小于0,对于scrollBy来说  
  124.                     //正值为向上和向左滑,负值为向下和向右滑,这里要注意  
  125.                     scrollBy(0, (int) (deltaY + 0.5));  
  126.                     invalidate();  
  127.                 }  
  128.                 break;  
  129.             case MotionEvent.ACTION_UP:  
  130.                 if (verticalScrollFlag) {  
  131.                     mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); //1000表示单位,每1000毫秒允许滑过的最大距离是mMaximumVelocity  
  132.                     float yVelocity = mVelocityTracker.getYVelocity();  //获取当前的滑动速度  
  133.                     mDirection = yVelocity > 0 ? DIRECTION_DOWN : DIRECTION_UP;  //下滑速度大于0,上滑速度小于0  
  134.                     //根据当前的速度和初始化参数,将滑动的惯性初始化到当前View,至于是否滑动当前View,取决于computeScroll中计算的值  
  135.                     //这里不判断最小速度,确保computeScroll一定至少执行一次  
  136.                     mScroller.fling(0, getScrollY(), 0, -(int) yVelocity, 00, -Integer.MAX_VALUE, Integer.MAX_VALUE);  
  137.                     mLastScrollerY = getScrollY();  
  138.                     invalidate();  //更新界面,该行代码会导致computeScroll中的代码执行  
  139.                     //阻止快读滑动的时候点击事件的发生,滑动的时候,将Up事件改为Cancel就不会发生点击了  
  140.                     if ((shiftX > mTouchSlop || shiftY > mTouchSlop)) {  
  141.                         if (isClickHead || !isStickied()) {  
  142.                             int action = ev.getAction();  
  143.                             ev.setAction(MotionEvent.ACTION_CANCEL);  
  144.                             boolean dd = super.dispatchTouchEvent(ev);  
  145.                             ev.setAction(action);  
  146.                             return dd;  
  147.                         }  
  148.                     }  
  149.                 }  
  150.                 recycleVelocityTracker();  
  151.                 break;  
  152.             case MotionEvent.ACTION_CANCEL:  
  153.                 recycleVelocityTracker();  
  154.                 break;  
  155.             default:  
  156.                 break;  
  157.         }  
  158.         //手动将事件传递给子View,让子View自己去处理事件  
  159.         super.dispatchTouchEvent(ev);  
  160.         //消费事件,返回True表示当前View需要消费事件,就是事件的TargetView  
  161.         return true;  
  162.     }  
  163.   
  164.     private void checkIsClickHead(int downY, int headHeight, int scrollY) {  
  165.         isClickHead = ((downY + scrollY) <= headHeight);  
  166.     }  
  167.   
  168.     private void obtainVelocityTracker(MotionEvent event) {  
  169.         if (mVelocityTracker == null) {  
  170.             mVelocityTracker = VelocityTracker.obtain();  
  171.         }  
  172.         mVelocityTracker.addMovement(event);  
  173.     }  
  174.   
  175.     private void recycleVelocityTracker() {  
  176.         if (mVelocityTracker != null) {  
  177.             mVelocityTracker.recycle();  
  178.             mVelocityTracker = null;  
  179.         }  
  180.     }  
  181.   
  182.     @Override  
  183.     public void computeScroll() {  
  184.         if (mScroller.computeScrollOffset()) {  
  185.             final int currY = mScroller.getCurrY();  
  186.             if (mDirection == DIRECTION_UP) {  
  187.                 // 手势向上划  
  188.                 if (isStickied()) {  
  189.                     //这里主要是将快速滚动时的速度对接起来,让布局看起来滚动连贯  
  190.                     int distance = mScroller.getFinalY() - currY;    //除去布局滚动消耗的时间后,剩余的时间  
  191.                     int duration = calcDuration(mScroller.getDuration(), mScroller.timePassed()); //除去布局滚动的距离后,剩余的距离  
  192.                     mScrollable.smoothScrollBy(getScrollerVelocity(distance, duration), distance, duration);  
  193.                     //外层布局已经滚动到指定位置,不需要继续滚动了  
  194.                     mScroller.abortAnimation();  
  195.                     return;  
  196.                 } else {  
  197.                     scrollTo(0, currY);  //将外层布局滚动到指定位置  
  198.                     invalidate();        //移动完后刷新界面  
  199.                 }  
  200.             } else {  
  201.                 // 手势向下划,内部View已经滚动到顶了,需要滚动外层的View  
  202.                 if (mScrollable.isTop() || isClickHead) {  
  203.                     int deltaY = (currY - mLastScrollerY);  
  204.                     int toY = getScrollY() + deltaY;  
  205.                     scrollTo(0, toY);  
  206.                     if (mCurY <= minY) {  
  207.                         mScroller.abortAnimation();  
  208.                         return;  
  209.                     }  
  210.                 }  
  211.                 //向下滑动时,初始状态可能不在顶部,所以要一直重绘,让computeScroll一直调用  
  212.                 //确保代码能进入上面的if判断  
  213.                 invalidate();  
  214.             }  
  215.             mLastScrollerY = currY;  
  216.         }  
  217.     }  
  218.   
  219.     @SuppressLint("NewApi")  
  220.     private int getScrollerVelocity(int distance, int duration) {  
  221.         if (mScroller == null) {  
  222.             return 0;  
  223.         } else if (sysVersion >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {  
  224.             return (int) mScroller.getCurrVelocity();  
  225.         } else {  
  226.             return distance / duration;  
  227.         }  
  228.     }  
  229.   
  230.     /** 对滑动范围做限制 */  
  231.     @Override  
  232.     public void scrollBy(int x, int y) {  
  233.         int scrollY = getScrollY();  
  234.         int toY = scrollY + y;  
  235.         if (toY >= maxY) {  
  236.             toY = maxY;  
  237.         } else if (toY <= minY) {  
  238.             toY = minY;  
  239.         }  
  240.         y = toY - scrollY;  
  241.         super.scrollBy(x, y);  
  242.     }  
  243.   
  244.     /** 对滑动范围做限制 */  
  245.     @Override  
  246.     public void scrollTo(int x, int y) {  
  247.         if (y >= maxY) {  
  248.             y = maxY;  
  249.         } else if (y <= minY) {  
  250.             y = minY;  
  251.         }  
  252.         mCurY = y;  
  253.         if (onScrollListener != null) {  
  254.             onScrollListener.onScroll(y, maxY);  
  255.         }  
  256.         super.scrollTo(x, y);  
  257.     }  
  258.   
  259.     /** 头部是否已经固定 */  
  260.     public boolean isStickied() {  
  261.         return mCurY == maxY;  
  262.     }  
  263.   
  264.     private int calcDuration(int duration, int timepass) {  
  265.         return duration - timepass;  
  266.     }  
  267.   
  268.     public int getMaxY() {  
  269.         return maxY;  
  270.     }  
  271.   
  272.     public boolean isHeadTop() {  
  273.         return mCurY == minY;  
  274.     }  
  275.   
  276.     /** 是否允许下拉,与PTR结合使用 */  
  277.     public boolean canPtr() {  
  278.         return verticalScrollFlag && mCurY == minY && mScrollable.isTop();  
  279.     }  
  280.   
  281.     public void setTopOffset(int topOffset) {  
  282.         this.topOffset = topOffset;  
  283.     }  
  284.   
  285.     public void setCurrentScrollableContainer(HeaderScrollHelper.ScrollableContainer scrollableContainer) {  
  286.         mScrollable.setCurrentScrollableContainer(scrollableContainer);  
  287.     }  
  288. }  
福利!!福利!!此代码已经应用在我们的产品里,并且已经上线,且稳定运行了三个大版本。可直接拿去用!如有不理解,可直接留言提问,博主每天都会查看!

~~~~~~~~~~~~~华丽丽的分割线:解答朋友们关心的几个问题~~~~~~~~~~~~~~~~~~~~~~
首先感谢各位朋友的支持,看到你们能给我的github一个star或者fork我写的demo,我的内心充满了感恩。
由于最近工作比较忙,迟迟没有把最终解决方案上传到github上,只是在本博文里贴上了代码。

最近有很多朋友通过留言或邮件的方式联系我,希望我能进行更细致的讲解。

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

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

猜你喜欢

转载自blog.csdn.net/system_err/article/details/80008916
今日推荐