Android滑动冲突之完美实现RecycleView+下拉刷新+上拉加载+粘性Header
前言:在日常的开发中,我们可能遇到各种各样的需求,今天我们主要来一起探究RecycleView+下拉刷新+上拉加载+粘性头部,同时避免滑动冲突的联合实现过程。看到这里,你可能心中暗想,没图说个JB!!!客官别急,下面我们就来看一下最终实现的效果:
上面就是我们最终要实现的效果,现在我们先来对它进行拆分和分析,如下图所示:
从上图可以看出,这是最原始的状态。大致可分为导航栏、广告栏、Indicator、Recycleview和下拉刷新。当向上滑动的时候,先由外部ViewGroup拦截滑动事件,同时Banner条向上移动。当Banner条完全隐藏的时候,Indicator固定在头部,同时由RecycleView接管滑动事件,此时向下拉动的时候,下拉刷新是失效的,只有当Banner完全现实的时候,下下滑动才会出现下拉刷新效果,(这里不得不说,如果把下拉刷新加到Indicator下方,实现要简便得多)。以上大致就是我们需要实现效果的完整流程,下面我们将从代码的角度为您一一剖析。
流程分析:
使用组件:
- 下拉刷新:使用Google自带SwipeRefreshLayout。
- 列表:使用Google自带RecyclerView。
- 自定义StickyNavLayout实现滑动事件的分发和处理。
- 广告栏:第三方Banner。
- Indicator:使用横向的RecyclerView。
自定义StickyNavLayout实现原理:
public class StickyNavLayout extends LinearLayout { private static final String TAG = "StickyNavLayout"; //Banner条 private View mTop; //导航的Indicator private View mNav; //Banner条的高度 private int mTopViewHeight; // private ViewGroup mInnerScrollView; //判断Banner条是否隐藏的标志位 private boolean isTopHidden = false; private OverScroller mScroller; //显示内容的列表组件 private RecyclerView mRecycleView; //和滑动相关的参数 private VelocityTracker mVelocityTracker; private int mTouchSlop; private int mMaximumVelocity, mMinimumVelocity; private float mLastY; private boolean mDragging; //Indicator是否置顶的标志位 private boolean isStickNav; private boolean isInControl = false; private int stickOffset; //内容组件的宽度和高度 private int mViewPagerMaxHeight; private int mTopViewMaxHeight; private boolean isScroll = true; public StickyNavLayout(Context context) { this(context, null); } public StickyNavLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public StickyNavLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setOrientation(LinearLayout.VERTICAL); //取出xml文件中设置的参数 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StickNavLayout); isStickNav = a.getBoolean(R.styleable.StickNavLayout_isStickNav, false); stickOffset = a.getDimensionPixelSize(R.styleable.StickNavLayout_stickOffset, 0); a.recycle(); //初始化滑动相关的数据 mScroller = new OverScroller(context); mVelocityTracker = VelocityTracker.obtain(); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mMaximumVelocity = ViewConfiguration.get(context) .getScaledMaximumFlingVelocity(); mMinimumVelocity = ViewConfiguration.get(context) .getScaledMinimumFlingVelocity(); } public void setIsStickNav(boolean isStickNav) { this.isStickNav = isStickNav; } /** * 设置悬浮,并自动滚动到悬浮位置(即把top区域滚动上去) */ public void setStickNavAndScrollToNav() { this.isStickNav = true; scrollTo(0, mTopViewHeight); } /**** * 设置顶部区域的高度 * * @param height height */ public void setTopViewHeight(int height) { mTopViewHeight = height; if (isStickNav) scrollTo(0, mTopViewHeight); } /**** * 设置顶部区域的高度 * * @param height height * @param offset offset */ public void setTopViewHeight(int height, int offset) { mTopViewHeight = height; if (isStickNav) scrollTo(0, mTopViewHeight - offset); } @Override protected void onFinishInflate() { super.onFinishInflate(); //在布局加载完成的时候初始化各个View mTop = findViewById(R.id.header); mNav = findViewById(R.id.snlIindicator); mRecycleView = (RecyclerView) findViewById(R.id.rv_content); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); ViewGroup.LayoutParams params = mRecycleView.getLayoutParams();// //修复键盘弹出后键盘关闭布局高度不对问题 int height = getMeasuredHeight() - mNav.getMeasuredHeight() - 20; mViewPagerMaxHeight = (height >= mViewPagerMaxHeight ? height : mViewPagerMaxHeight); params.height = /*mViewPagerMaxHeight - stickOffset*/height; mRecycleView.setLayoutParams(params); //修复键盘弹出后Top高度不对问题 int topHeight = mTop.getMeasuredHeight(); ViewGroup.LayoutParams topParams = mTop.getLayoutParams(); mTopViewMaxHeight = (topHeight >= mTopViewMaxHeight ? topHeight : mTopViewMaxHeight); topParams.height = /*mTopViewMaxHeight*/topHeight; mTop.setLayoutParams(topParams); //设置mTopViewHeight mTopViewHeight = topParams.height; } /** * 更新top区域的视图,如果是处于悬浮状态,隐藏top区域的控件是不起作用的!! */ public void updateTopViews() { if (isTopHidden) { return; } final ViewGroup.LayoutParams params = mTop.getLayoutParams(); mTop.post(new Runnable() { @Override public void run() { if (mTop instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) mTop; int height = viewGroup.getChildAt(0).getHeight(); mTopViewHeight = height - stickOffset; params.height = height; mTop.setLayoutParams(params); params.height = ViewGroup.LayoutParams.WRAP_CONTENT; } else { mTopViewHeight = mTop.getMeasuredHeight() - stickOffset; } } }); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); //在尺寸发生变化的时候重新初始化数据 final ViewGroup.LayoutParams params = mTop.getLayoutParams(); Log.d(TAG, "onSizeChanged-mTopViewHeight:" + mTopViewHeight); mTop.post(new Runnable() { @Override public void run() { if (mTop instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) mTop; int height = viewGroup.getChildAt(0).getHeight(); mTopViewHeight = height - stickOffset; params.height = height; mTop.setLayoutParams(params); mTop.requestLayout(); } else { mTopViewHeight = mTop.getMeasuredHeight() - stickOffset; } } }); } /* *接下来是三个重要的方法,主要是对滑动事件的处理,避免冲突 */ @Override public boolean dispatchTouchEvent(MotionEvent ev) { int action = ev.getAction(); float y = ev.getY(); switch (action) { case MotionEvent.ACTION_DOWN: mLastY = y; break; case MotionEvent.ACTION_MOVE: float dy = y - mLastY; //header隐藏并且向上滑动 if (!isInControl && android.support.v4.view.ViewCompat.canScrollVertically(mRecycleView, -1) && isTopHidden && dy > 0) { isInControl = true; ev.setAction(MotionEvent.ACTION_CANCEL); MotionEvent ev2 = MotionEvent.obtain(ev); dispatchTouchEvent(ev); ev2.setAction(MotionEvent.ACTION_DOWN); isSticky = true; return dispatchTouchEvent(ev2); } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP://处理悬停后立刻抬起的处理 float distance = y - mLastY; if (isSticky && /*distance==0.0f*/Math.abs(distance) <= mTouchSlop) { isSticky = false; return true; } else { isSticky = false; return super.dispatchTouchEvent(ev); } } return super.dispatchTouchEvent(ev); } private boolean isSticky;//mNav-view 是否悬停的标志 @Override public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); float y = ev.getY(); switch (action) { case MotionEvent.ACTION_DOWN: mLastY = y; break; case MotionEvent.ACTION_MOVE: float dy = y - mLastY; if (Math.abs(dy) > mTouchSlop) { mDragging = true; //header没有隐藏或者header隐藏并且向下滑动,拦截滑动事件,并且调用接下来的onTouc方法处理接下来的事件 if (!isTopHidden || (!android.support.v4.view.ViewCompat.canScrollVertically(mRecycleView, -1) && isTopHidden && dy > 0)) { initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement(ev); mLastY = y; return true; } } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mDragging = false; recycleVelocityTracker(); break; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement(event); int action = event.getAction(); float y = event.getY(); switch (action) { case MotionEvent.ACTION_DOWN: if (!mScroller.isFinished()) mScroller.abortAnimation(); mLastY = y; return true; case MotionEvent.ACTION_MOVE: if (isScroll) { float dy = y - mLastY; if (!mDragging && Math.abs(dy) > mTouchSlop) { mDragging = true; } if (mDragging) { //在这里才是真正的滑动,这个方法又会调用接下来的scrollTo方法实现滑动 scrollBy(0, (int) -dy); //如果topView隐藏,且上滑动时,则改变当前事件为ACTION_DOWN if (getScrollY() == mTopViewHeight && dy < 0) { event.setAction(MotionEvent.ACTION_DOWN); dispatchTouchEvent(event); isInControl = false; return true; } else { isSticky = false; } } mLastY = y; } break; case MotionEvent.ACTION_CANCEL: mDragging = false; recycleVelocityTracker(); if (!mScroller.isFinished()) { mScroller.abortAnimation(); } break; case MotionEvent.ACTION_UP: mDragging = false; mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int velocityY = (int) mVelocityTracker.getYVelocity(); if (Math.abs(velocityY) > mMinimumVelocity) { fling(-velocityY); } //up事件的时候回收资源 recycleVelocityTracker(); break; } return super.onTouchEvent(event); } public void fling(int velocityY) { mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight); invalidate(); } //scrollTo方法很重要,主要做了三件事 @Override public void scrollTo(int x, int y) { if (y < 0) { y = 0; } //1.实现ViewGroup的滑动 //2.处理滑动误差,并且根据滑动距离,初始化isTopHidden参数 if (y > mTopViewHeight) { y = mTopViewHeight; } if (y != getScrollY()) { super.scrollTo(x, y); } isTopHidden = getScrollY() == mTopViewHeight; //3.set listener 设置悬浮监听回调,通过回调处理Indicator根据滑动位置的颜色渐变和SwipeRefreshLayout对滑动事件的拦截 if (listener != null) {// if(lastIsTopHidden!=isTopHidden){// lastIsTopHidden=isTopHidden; listener.isStick(isTopHidden);// } listener.scrollPercent((float) getScrollY() / (float) mTopViewHeight); } }// private boolean lastIsTopHidden;//记录上次是否悬浮//实现滑动 @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(0, mScroller.getCurrY()); postInvalidate(); } } private void initVelocityTrackerIfNotExists() { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } }//回收资源 private void recycleVelocityTracker() { if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } } private OnStickStateChangeListener listener; /** * 悬浮状态回调 */ public interface OnStickStateChangeListener { /** * 是否悬浮的回调 * * @param isStick true 悬浮 ,false 没有悬浮 */ void isStick(boolean isStick); /** * 距离悬浮的距离的百分比 * * @param percent 0~1(向上) or 1~0(向下) 的浮点数 */ void scrollPercent(float percent); } public void setOnStickStateChangeListener(OnStickStateChangeListener listener) { this.listener = listener; } public boolean isScroll() { return isScroll; } public void setScroll(boolean scroll) { isScroll = scroll; }}
上面对StickyNavLayout类进行了详尽的分析,它的主要功能是在dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent方法中队滑动事件进行判断、拦截和处理,避免滑动冲突。
View层的处理:仅仅依靠StickyNavLayout还是不能实现想要的效果,还需要在Avtivity中设置上面提到的滑动监听事件,在Header没有彻底展开的时候,禁用SwipeRefreshLayout的下拉刷新事件。同时根据滑动比例,计算当前Indicator的背景颜色,实现良好的用户体验。项目中的处理方式如下所示:
@Override public void scrollPercent(float percent) { //根据滑动比例动态改变颜色 snlIindicator.setBackgroundColor(Color.parseColor((String) CommonUtil.getInstance().evaluate(percent, "#e18e36", "#3F51B5"))); if (percent == 0) { 设置下拉刷新事件 swipeLayout.setEnabled(true); swipeLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { mContentView.getAdapter().notifyDataSetChanged(); swipeLayout.setRefreshing(false); } }); } else { //禁止下拉刷新 swipeLayout.setEnabled(false); swipeLayout.setOnRefreshListener(null); } }
总结:上面是实现原理和关键代码的详细讲解,基本上关键部分都说到了,如果还有不清楚的同学,可以移步:完整StickLayoutDemo ,下载源码跑一跑,可能更有助于你的理解。望大家支持!!!
更多相关文章
- android高仿微信视频编辑页-视频多张图片提取
- Android分析View的scrollBy()和scrollTo()的参数正负问题原理分
- 【Android】android镜像翻转
- Android实现控件滑动的几种方法
- Android之内存缓存——LruCache的使用及原理
- android自定义布局中的平滑移动
- Android学习07-----事件处理(1)单击事件_改变屏幕方向和密码明文
- Android(安卓)PowerImageView实现,可以播放动画的强大ImageView
- android 性能分析(优化)-利用AOP技术,字节码方法插桩,实现 android