仿小米下载热榜,RecyclerView item轮流入场

具体使用方法在这:项目地址java

小米下载热榜

仿

下面说下具体实现: 首先,既然作了,那就多作几种模式,左右两边均可以设置轮流进入或是轮流退出,因此先定义两个枚举类来讲明是哪一种模式:git

public enum ScrollDirection {
        LEFT, // 从右到左
        RIGHT, // 从左到又
        BOTH // 都支持
    }

    public enum Mode {
        IN, // 入场
        OUT, // 出场
        BOTH // 都支持
    }

    public void setSupportScrollDirection(ScrollDirection scrollDirection) {
        mIsSupportLeft = scrollDirection == ScrollDirection.LEFT || scrollDirection == ScrollDirection.BOTH;
        mIsSupportRight = scrollDirection == ScrollDirection.RIGHT || scrollDirection == ScrollDirection.BOTH;
    }

    public void setSupportMode(Mode mode) {
        mIsSupportIn = mode == Mode.IN || mode == Mode.BOTH;
        mIsSupportOut = mode == Mode.OUT || mode == Mode.BOTH;
    }
复制代码

而后说下思路,这是个ViewPager嵌套RecyclerView,随着页面的滑动,item按照RecyclerView进入屏幕的比例设置必定的偏移量,大体就是这样,其中的一些细节后面再讲。 按照上面的思路,首先咱们须要给ViewPager添加滑动监听,在监听中经过获取RecyclerView相对于整个屏幕的坐标位置,来判断它处于什么状态,正在进入屏幕仍是滑出屏幕,从左边进入仍是右边进入,该不应执行入场出场动画。添加监听就不说了,获取RecyclerView的位置能够经过View#getGlobalVisibleRect(Rect r)这个方法来实现,该方法会返回一个boolean值说明目标View是否在屏幕内,若是在屏幕内的话,会将在屏幕内的部分的坐标写入Rect参数中,这里须要注意一下是屏幕内*的部分,以下图,黑色的框为屏幕,蓝色的是RecyclerView,红色的是被赋值的Rectgithub

getGlobalVisibleRect

因为咱们的动画须要在进入或者退出时执行,因此先要判断一下何时是进入,何时是退出,若是以前不在屏幕中那么说明是正在进入,若是以前彻底落在了屏幕中说明是正在退出,因此代码大体是这样的:布局

// 若是被回收了,那么根布局的parent为null,不然为ViewRootImpl
        if (mRecyclerView.getRootView().getParent() == null) {
            return;
        }

        boolean isInScreen = mRecyclerView.getGlobalVisibleRect(mBounds);

        if (!isInScreen) {
            mIsDoingAnimation = false;
            mIsEntering = true;
            return;
        }

        getLeftAndRight();

        // RecyclerView彻底进入屏幕
        if (mLeft >= 0 && mRight <= mWindowSize.x) {
            mIsDoingAnimation = false;
            mIsEntering = false;
            return;
        }
复制代码

首先判断了如下RecyclerView是否被ViewPager回收了,若是被回收了,也就是RecyclerView没有父View了,getGlobalVisibleRect获取到的信息就不是咱们想要的,缘由这里就不说了,具体能够看下getGlobalVisibleRect的源码。这里采用了一个我在Debug的时候发现的方法来判断是否被回收了,若是被回收了,那么根布局的parentnull,不然为ViewRootImpl。 而后经过getGlobalVisibleRect方法来判断RecyclerView是否在屏幕中,若是不在则说明下次出如今屏幕中的时候就是正在进入,将mIsEntering设置为true,这里还有个变量mIsDoingAnimation是用来标记当前是否在执行动画的,具体做用后面再讲。 接着经过getLeftAndRight方法来获取RecyclerView真实的左右坐标,有了真实的坐标才能够判断是否彻底进入屏幕。若是彻底进入屏幕则说明下次滑动时就是正在退出屏幕,将mIsEntering设置为false,同时将mIsDoingAnimation也设置为falsegetLeftAndRight方法实现以下:优化

/** * 计算出左右的坐标,getGlobalVisibleRect获取的是进入屏幕内的左右坐标 */
    private void getLeftAndRight() {
        if (mBounds.left < mWindowSize.x && mBounds.right >= mWindowSize.x) {
            mLeft = mBounds.left;
            mRight = mLeft + mRecyclerViewWidth;
        } else if (mBounds.left <= 0 && mBounds.right > 0) {
            mRight = mBounds.right;
            mLeft = mRight - mRecyclerViewWidth;
        }
    }
复制代码

判断完是正在进入屏幕仍是退出屏幕,接下来就能够根据这个来执行相应的动画了,而这个动画实际上就是在ViewPager横向滑动的时候来设置RecyclerView每一个item的水平偏移量,那么咱们就须要经过LayoutManager来获取在屏幕中展现的每一个item的对象:动画

private void getChildList() {
        mChildList.clear();
        mChildCount = mLayoutManager.getChildCount();
        for (int i = 0; i < mChildCount; i++) {
            mChildList.add(mLayoutManager.getChildAt(i));
        }
    }
复制代码

获取到了这些对象以后,咱们须要对他们进行从新布局,这时还须要item的左右margin值才能布局到正确的位置,这里我只获取了第一item的margin,正常状况下不会给每一个item设置不一样的marginspa

private void getMargin() {
        ViewGroup.LayoutParams params = mChildList.get(0).getLayoutParams();
        if (params instanceof ViewGroup.MarginLayoutParams) {
            mLeftMargin = ((ViewGroup.MarginLayoutParams) params).leftMargin;
            mRightMargin = ((ViewGroup.MarginLayoutParams) params).rightMargin;
        } else {
            mLeftMargin = 0;
            mRightMargin = 0;
        }
    }
复制代码

有了上面的东西就能够根据进入屏幕的比例来计算出每一个item应该偏移的距离了。以入场为例,在RecyclerView刚进入屏幕的一瞬间每一个item从上到下都比前一个多偏移一个单位距离,假设单位距离为x,那么从上到下依次偏移x,2x,3x,·····,(n-1)x,nx随着进入屏幕的部分愈来愈多,每一个item的偏移量逐渐减少直到回到原来的位置。单位距离采用RecyclerView减去左右margin再除以当前展现的item个数来计算,以下:code

mUnitOffset = (mRecyclerViewWidth - mLeftMargin - mRightMargin) / mChildCount;
复制代码

这样随着RecyclerView逐渐进入屏幕,只需将最初的偏移量减去进入屏幕部分的宽度就能够获得想要的偏移量。 下面是设置偏移量的代码:cdn

private void relayoutChild(int dx, int direction) {
        float offset;

        // 让index为0的子View也有偏移量
        for (int i = 1; i <= mChildCount; i++) {
            offset = (mUnitOffset * i - dx) * 1.25f;
            if (offset < 0) {
                offset = 0;
            }
            // 左为1 右为-1 在右边要反向偏移
            mChildList.get(i - 1).setX(offset * direction + mLeftMargin);
        }
        mRecyclerView.requestLayout();
    }
复制代码

for循环中,从1开始循环是为了让第一个item也有偏移量,还将计算出的偏移量稍微放大了一点,否则感受偏移的有点少了,很差看。其中direction传入1表示右侧,-1表示左侧。右侧的偏移量只须要将算出来的偏移量取反就好了,同时都要加上marginLeft确保能布局到正确的位置。 有了上面这个方法后就能够去布局了,只须要再判断一下是在左边仍是右边就好了:对象

// 状态改变,从新获取子view
        if (!mIsDoingAnimation) {
            getChildList();
            if (mChildCount == 0) {
                return;
            }
            getMargin();
            mUnitOffset = (mRecyclerViewWidth - mLeftMargin - mRightMargin) / mChildCount;
            mIsDoingAnimation = true;
        }

        // 这里判断的是去除左右margin后 item进入或退出屏幕才会执行动画
        if (mLeft + mLeftMargin <= mWindowSize.x && mRight >= mWindowSize.x && mIsSupportRight) {
            relayoutChild(mWindowSize.x - mLeft - mLeftMargin, RIGHT);
        } else if (mLeft <= 0 && mRight + mRightMargin >= 0 && mIsSupportLeft) {
            relayoutChild(mRight - mRightMargin, LEFT);
        }
复制代码

这里若是以前不在执行动画,那么须要从新获取子View,这样RecyclerView竖向无论滑动到哪里均可以获取到对应的一个子View列表。后面判断在左侧仍是右侧须要算上左右margin值,也就是当item滑动到屏幕边缘时才会执行动画,而不是RecyclerView滑动到屏幕边缘就执行动画,否则可能部分item还没进入屏幕就已经回到原来的位置或是还没开始偏移就已经到屏幕外面去了,具体效果能够本身试试。 最终在ViewPager滑动时的监听以下:

if (mRecyclerView == null) {
            return;
        }

        // 若是被回收了,那么根布局的parent为null,不然为ViewRootImpl
        if (mRecyclerView.getRootView().getParent() == null) {
            return;
        }

        boolean isInScreen = mRecyclerView.getGlobalVisibleRect(mBounds);

        if (!isInScreen) {
            // 仅支持out动画时,彻底移出屏幕须要重置位置
            if (mIsSupportOut && !mIsSupportIn && !mIsEntering) {
                resetChild();
            }
            mIsDoingAnimation = false;
            mIsEntering = true;
            return;
        }

        getLeftAndRight();

        // RecyclerView彻底进入屏幕
        if (mLeft >= 0 && mRight <= mWindowSize.x) {
            mIsDoingAnimation = false;
            mIsEntering = false;
            return;
        }

        if (!shouldDoAnimation()) {
            return;
        }

        // 状态改变,从新获取子view
        if (!mIsDoingAnimation) {
            getChildList();
            if (mChildCount == 0) {
                return;
            }
            getMargin();
            mUnitOffset = (mRecyclerViewWidth - mLeftMargin - mRightMargin) / mChildCount;
            mIsDoingAnimation = true;
        }

        // 这里判断的是去除左右margin后 item进入或退出屏幕才会执行动画
        if (mLeft + mLeftMargin <= mWindowSize.x && mRight >= mWindowSize.x && mIsSupportRight) {
            relayoutChild(mWindowSize.x - mLeft - mLeftMargin, RIGHT);
        } else if (mLeft <= 0 && mRight + mRightMargin >= 0 && mIsSupportLeft) {
            relayoutChild(mRight - mRightMargin, LEFT);
        }
复制代码

原理大体就是这样了,最后还要注意释放资源,防止内存泄漏,不仅是在ViewPager销毁时须要调用,在PagerAdapterdestroyItem中也要调用:

public void onDestroy() {
        mParent.removeOnPageChangeListener(mViewPagerListener);
        mParent = null;
        mRecyclerView = null;
        mLayoutManager = null;
        mChildList.clear();
    }
复制代码

总结

总体来说比较简单,目前实现是一个TakeTurnHelper对应一个RecyclerView,能够优化一下,改为能够对应多个RecyclerView,之后想起来再搞,如今懒得改了。

相关文章
相关标签/搜索