联动滑动效果及不同Item效果的RecyclerView

这几天项目中做了一个页面滑动效果,今天总结了一个小demo来分享一下,先来看一下效果哈:::


1. 页面滑动时 联动效果介绍

        这种效果总结起来可以分为两部分,一个是包裹在最外层的layout,滑动时先由layout拦截滑动事件,当顶部view缩小为0后,RecyclerView再接收滑动事件,在RecyclerView中支持多种不同item布局。

        先看一下最外层的Layout事件拦截逻辑,这里我自定义了一UniteSlideLayout继承RelativeLayout,在UniteSlideLayout的事件拦截方法onInterceptTouchEvent)中,对MotionEvent.ACTION_MOVE做判断,当符合Layout滑动条件时,该方法反回true,即拦截了MOVE事件,当拦截了MOVE事件后,之后的MOVE都由Layout消耗,即调用UniteSlideLayout的onTouchEvent(MotionEvent event)消耗MOVE事件,MOVE事件被拦截后,在一个事件序列中,针对MOVE事件,onInterceptTouchEvent方法都不会再被调用。下面看一下Layout事件拦截方法onInterceptTouchEvent的实现:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    int y = (int) ev.getY();
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mInterceptLastMotionY = y;
            if (!isCanSlide) {
                return true;
            }
            break;
        case MotionEvent.ACTION_MOVE:
            int dy = y - mInterceptLastMotionY;
            if ((Math.abs(dy) >= mProtectDistance)
                    && ((mIsShrink && mCardListView.getScrollToTop() && dy > 0) || (!mIsShrink))) {
                mOnTouchLastMotionY = y;
                mCardListView.setOpListActionUp(false);
                return true;
            }
            break;
        case MotionEvent.ACTION_UP:
            if (!mCardListView.getOpListActionUp()) {
                mCardListView.setOpListActionUp(true);
                return true;
            }
            break;
        default:
            break;
    }
    return false;
}

        在DOWN事件中有个mCardListView.setOpListActionUp方法用于设置opListActionUp参数,主要用于控制当正在滑动时点击事件不再起作用,之所以有这个参数控制是因为当时项目中出现了滑动事件由Layout交付给RecyclerView时,会重新下发一个DOWN事件,这是手指马上台阶起,出发UP事件,这种情况下会发生误点击,所以用isCanSlide参数去避免这种情况。
        isCanSlide参数适用于页面是否可以滑动的控制,用于当页面在做滑动回弹动画时,这是手指滑动是不生效的。
        上面MOVE事件的判断中,主要判断逻辑是:当滑动距离大于最小保护距离,且向下滑动、RecyclerView滑动到顶部Item、顶部View已完全压缩或顶部View没有完全压缩,则由Layout拦截MOVE时间,否则将事件下发到RecyclerView处理。
        下面看一下UniteSlideLayout的事件处理方法onTouchEvent(MotionEvent event):
@Override
public boolean onTouchEvent(MotionEvent event) {

    int y = (int) event.getY();
    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(event);
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mOnTouchLastMotionY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            if (mIsFirstTouch) {
                mOnTouchLastMotionY = y;
                mIsFirstTouch = false;
            }

            int dy = y - mOnTouchLastMotionY;
            if (mCardListView.getVisibility() == View.VISIBLE) {
                doListAnimByMove(mListContent, mListContentLayoutParams, dy);
                if (mScaleSize == 0) {
                    event.setAction(MotionEvent.ACTION_DOWN);
                    dispatchTouchEvent(event);
                }
            }
            mOnTouchLastMotionY = y;
            break;
        case MotionEvent.ACTION_UP:
            if (!mCardListView.getOpListActionUp()) {
                mCardListView.setOpListActionUp(true);
            }
            mIsFirstTouch = true;
            handleUpAnim();
            releaseVelocityTracker();
            break;

        case MotionEvent.ACTION_CANCEL:
            releaseVelocityTracker();
            break;

    }

    return super.onTouchEvent(event);
}
        这个方法比较容易看懂,就是各种事件的处理,对MOVE事件的处理就是让RecyclerView上滑,顶部View做相应的缩小,处理方法在doListAnimByMove()中,在MOVE事件中,如果监听到顶部View压缩为0后,重新下发一个DOWN事件,让事件整个流程重新开始。在UP事件触发到时,调用handleUpAnim()方法做后面的回弹效果,当顶部View压缩大于一半时,RecyclerView上滑倒最顶部,否则回原位。回弹的代码就不列出来了,感兴趣的可以看代码。

        下面再看一下RecyclerView的事件处理机制,我们只是重写Layout事件处理还是不够的,在RecyclerView也需要监听MOVE事件,在合适的事件将事件重新交付给父View,也就是UniteSlideLayout去处理。
        下面看一下RecyclerView重写的事件拦截方法dispatchTouchEvent:
public boolean dispatchTouchEvent(MotionEvent event) {
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mInterceptLastMotionY = y;
            if (!opListActionUp) {
                return true;
            }
            break;
        case MotionEvent.ACTION_MOVE:
            int dy = y - mInterceptLastMotionY;
            if (dy > mProtectDistance) {
                if (mIsScrollToTop) {
                    float offsetX = 0;
                    float offsetY = 0;
                    for (View v = this; v != null && v != parent; v = (View) v.getParent()) {
                        offsetX += v.getLeft() - v.getScrollX();
                        offsetY += v.getTop() - v.getScrollY();
                    }
                    final MotionEvent eventParent = MotionEvent.obtainNoHistory(event);
                    eventParent.offsetLocation(offsetX, offsetY);


                    eventParent.setAction(MotionEvent.ACTION_DOWN);
                    if (parent != null) {
                        post(new Runnable() {
                            @Override
                            public void run() {
                                parent.dispatchTouchEvent(eventParent);
                            }
                        });
                        return false;
                    }
                }
            }

            break;
        case MotionEvent.ACTION_UP:
            if (!opListActionUp) {
                opListActionUp = true;
            }
            break;
        default:
            break;
    }
    return super.dispatchTouchEvent(event);
}

        看上面的代码,最主要做的还是MOVE事件的处理,判断逻辑也相对比较简单,当滑动式判断出RecyclerView已经滑动到最顶部的Item,并且是向下滑动,那么就重新给父View下发DOWN事件,让父View重新执行事件判断逻辑就ok了。监听是否滑动到顶部Item,利用RecyclerView.OnScrollListener()监听就可以。

        上面是最主要的事件活动处理逻辑,还有些其他跟随滑动以及滑动动画的代码就不贴出来了,感兴趣的朋友可下载源码

2. 支持不同Item布局的RecyclerView

        上面介绍了多控件滑动效果的实现,不知道大家有没有注意到图片上的RecyclerView是支持不同Item布局的,下面看一下RecyclerView的效果:
联动滑动效果及不同Item效果的RecyclerView_第1张图片

        图中的宫格、大图列表、图片预览这些都是RecyclerView中的Item,不过她们布局不同,这些布局随着滑动,相同类型的布局可以复用。关于RecyclerView不同Item支持的实现网上很多,可是写的都过于简单,更别提代码的逻辑结构了。下面简单介绍一下RecyclerView不同Item支持的实现,该结构有较好的代码扩展性。

        首先自定义一个BaseCardViewHolder继承RecyclerView.ViewHolder,之后所用到的每种布局的ViewHolder都继承BaseCardViewHolder
然后以工厂模式真对不同的Item分发ViewHolder,具体见OpViewHolderFactory类,该类中定义了一个HashMap:linkCardAndHolderMap,用该Map将具体Item的View与ViewHolder相关联,然后在getViewHolderByKey()方法中根据Item的对应的ViewHolder的Key反回对应的ViewHolder。OpViewHolderFactory代码如下:
public class OpViewHolderFactory {

    public static final int GRID_VIEWHOLDER = 1;
    public static final int DEFAULT_VIEWHOLDER = 2;
    public static final int IMAGE_VIEWHOLDER = 3;

    public static final Map linkCardAndHolderMap = new HashMap();

    static {
        linkCardAndHolderMap.put(OneCardView.CARDVIEW_KEY, OpDefaultViewHolder.HOLDER_KEY);
        linkCardAndHolderMap.put(TwoCardView.CARDVIEW_KEY, OpDefaultViewHolder.HOLDER_KEY);
        linkCardAndHolderMap.put(ThreeCardView.CARDVIEW_KEY, OpDefaultViewHolder.HOLDER_KEY);
        linkCardAndHolderMap.put(GridCardView.CARDVIEW_KEY, GridViewHolder.HOLDER_KEY);
        linkCardAndHolderMap.put(ImageCardView.CARDVIEW_KEY, ImageViewHolder.HOLDER_KEY);
    }

    public static BaseCardViewHolder getViewHolderByKey(ViewGroup viewGroup, int holderKey) {
        LayoutInflater inflater = LayoutInflater.from(MyApplication.getInstance());
        switch (holderKey) {
            case DEFAULT_VIEWHOLDER:
                return new OpDefaultViewHolder(inflater.inflate(R.layout.big_icon_item_view, viewGroup, false));

            case GRID_VIEWHOLDER:
                return new GridViewHolder(inflater.inflate(R.layout.grid_item_view, viewGroup, false));

            case IMAGE_VIEWHOLDER:
                return new ImageViewHolder(inflater.inflate(R.layout.image_item_view, viewGroup, false));
            default:
                break;

        }
        return null;
    }
}
        下面就是具体的Item了,每个Item继承OpBaseCardItem这个抽象类,代码如下:
public abstract class OpBaseCardItem {
    private boolean mFirstCreate = true;
    /**
     * 返回列表展示的UI样式类型
     */
    public abstract int getCardViewType();
    /**
     * 返回列表展示的UI样式类型
     */
    public abstract String getCardPageType();
    /**
     * 判断列表是否可以展示,各列表自行控制
     */
    public boolean needShow() {
        return false;
    }
    /**
     * 等同于Adapter的getView,具体列表控制UI展示
     */
    public abstract void bindItemView(Context context, RecyclerView.ViewHolder viewHolder, CardListAdapter
            cardAdapter, int position);

    public void reportShowAndbindView(Context context, RecyclerView.ViewHolder viewHolder, CardListAdapter
            cardAdapter, int position) {
        // 默认实现,上报功能卡片的创建展示
        if (mFirstCreate) {
            mFirstCreate = false;
        }
        bindItemView(context, viewHolder, cardAdapter, position);
    }

}

        每一个具体的Item主要实现bindItemView方法,该方法中拿到自己的viewHolder,然后对布局进行相应的处理。

        Item的管理也是利用工厂模式的,见OpCardViewFactory类,该类中有一个Map保存Item的Key与Item实体类的映射,方便创建Item实体对象,代码如下:
public class OpCardViewFactory {

    public static final String TAG = "OpCardViewFactory";

    public static final String GRID_CARD_KEY = "grid_item";
    public static final String BIG_ONE_CARD_KEY = "one_big_icon_item";
    public static final String BIG_TWO_CARD_KEY = "two_big_icon_item";
    public static final String BIG_THREE_CARD_KEY = "ohree_big_icon_item";
    public static final String IMAGE_CARD_KEY = "image_card_item";
    /**
     * 卡片key - 实例映射表
     */
    private static final Map> keyCardInstanceMap =
            new HashMap>();

    static {
        keyCardInstanceMap.put(GRID_CARD_KEY, GridCardView.class);
        keyCardInstanceMap.put(BIG_ONE_CARD_KEY, OneCardView.class);
        keyCardInstanceMap.put(BIG_TWO_CARD_KEY, TwoCardView.class);
        keyCardInstanceMap.put(BIG_THREE_CARD_KEY, ThreeCardView.class);
        keyCardInstanceMap.put(IMAGE_CARD_KEY, ImageCardView.class);
    }

    public static OpBaseCardItem getCardItem(String cardKey) {
        Class itemClass = keyCardInstanceMap.get(cardKey);
        if (itemClass != null) {
            try {
                return itemClass.newInstance();
            } catch (Exception e) {
            }
        }
        return null;
    }
}
        到这里,Item和ViewHolder之间的关联就写好了,以后想要添加一个Item,只需要添加相应的Key在工厂类里面即可。
下面看一下Adapter的实现,RecyclerView中没有addHeader方法,如果我们有这方面需求其实很简单,就把Header当成一个Item就好了,这里要注意getItemCount()方法,如果添加了Header,返回值需要数据列表长度+1,在getItemViewType中,我们只需要反回每一个Item对应的viewHolder的Key即可。
        至于列表中的Item项的展示顺序可以通过一个类独立控制(OptimizerCardDataSet),在创建Adapter时,只需要在该类中获取展示的顺序等相关信息即可。当然也可以加入云端顺序可控逻辑,根据云端下发的Key以及顺序调整列表Item展示。

        大致就写到这里了,当然有关RecyclerView支持多列表逻辑实现肯定有更好的方法,我这里只是其中一种,仅供参考。
        关于滑动的联动效果,大家也可以看一下coordinatorLayout,这是官方提供的支持多种布局协调联动效果的控件,使用起来也是比较方便的,只需要实现一些监听方法即可,其内部原理还是上面的实现方式,感兴趣的同学可以看看,这里提供几个学习链接:
http://www.jianshu.com/p/f418bf95db2d
http://blog.csdn.net/xyz_lmn/article/details/48055919
http://blog.csdn.net/qq_31340657/article/details/51918773
http://www.jianshu.com/p/97206f5973c5

        对本Demo感兴趣的同学可以下载代码。。。。。。。。。。
        github: 点击打开GitHub链接






你可能感兴趣的:(android学习)