前言:在日常的开发中,我们可能遇到各种各样的需求,今天我们主要来一起探究RecycleView+下拉刷新+上拉加载+粘性头部,同时避免滑动冲突的联合实现过程。看到这里,你可能心中暗想,没图说个JB!!!客官别急,下面我们就来看一下最终实现的效果:

上面就是我们最终要实现的效果,现在我们先来对它进行拆分和分析,如下图所示:

从上图可以看出,这是最原始的状态。大致可分为导航栏、广告栏、Indicator、Recycleview和下拉刷新。当向上滑动的时候,先由外部ViewGroup拦截滑动事件,同时Banner条向上移动。当Banner条完全隐藏的时候,Indicator固定在头部,同时由RecycleView接管滑动事件,此时向下拉动的时候,下拉刷新是失效的,只有当Banner完全现实的时候,下下滑动才会出现下拉刷新效果,(这里不得不说,如果把下拉刷新加到Indicator下方,实现要简便得多)。以上大致就是我们需要实现效果的完整流程,下面我们将从代码的角度为您一一剖析。

流程分析:

使用组件:

  1. 下拉刷新:使用Google自带SwipeRefreshLayout。
  2. 列表:使用Google自带RecyclerView。
  3. 自定义StickyNavLayout实现滑动事件的分发和处理。
  4. 广告栏:第三方Banner。
  5. 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 ,下载源码跑一跑,可能更有助于你的理解。望大家支持!!!

更多相关文章

  1. android高仿微信视频编辑页-视频多张图片提取
  2. Android分析View的scrollBy()和scrollTo()的参数正负问题原理分
  3. 【Android】android镜像翻转
  4. Android实现控件滑动的几种方法
  5. Android之内存缓存——LruCache的使用及原理
  6. android自定义布局中的平滑移动
  7. Android学习07-----事件处理(1)单击事件_改变屏幕方向和密码明文
  8. Android(安卓)PowerImageView实现,可以播放动画的强大ImageView
  9. android 性能分析(优化)-利用AOP技术,字节码方法插桩,实现 android

随机推荐

  1. Android帮助文档翻译——开发指南(十五)获
  2. 在Android上使用Facebook Concel对文件加
  3. Android(安卓)BOOT Time 优化
  4. Android初始化语言 (init.*.rc、init.con
  5. [教程]Android(安卓)killer简单去除 APP
  6. Android中的权限机制
  7. android学习五---OpenCV for android环境
  8. 一文带你了解各种方式实现Android的点击
  9. AIDL-- Android中的远程接口
  10. Android之Adapter用法总结