前言:在日常的开发中,我们可能遇到各种各样的需求,今天我们主要来一起探究RecycleView+下拉刷新+上拉加载+粘性头部,同时避免滑动冲突的联合实现过程。看到这里,你可能心中暗想,没图说个JB!!!客官别急,下面我们就来看一下最终实现的效果:
上面就是我们最终要实现的效果,现在我们先来对它进行拆分和分析,如下图所示:
从上图可以看出,这是最原始的状态。大致可分为导航栏、广告栏、Indicator、Recycleview和下拉刷新。当向上滑动的时候,先由外部ViewGroup拦截滑动事件,同时Banner条向上移动。当Banner条完全隐藏的时候,Indicator固定在头部,同时由RecycleView接管滑动事件,此时向下拉动的时候,下拉刷新是失效的,只有当Banner完全现实的时候,下下滑动才会出现下拉刷新效果,(这里不得不说,如果把下拉刷新加到Indicator下方,实现要简便得多)。以上大致就是我们需要实现效果的完整流程,下面我们将从代码的角度为您一一剖析。
流程分析:
使用组件:
自定义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 ,下载源码跑一跑,可能更有助于你的理解。望大家支持!!!