具体使用方法在这:项目地址
小米下载热榜 仿下面说下具体实现:
首先,既然做了,那就多做几种模式,左右两边都可以设置轮流进入或是轮流退出,所以先定义两个枚举类来说明是哪种模式:
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
,红色的是被赋值的Rect
。
由于我们的动画需要在进入或者退出时执行,所以先要判断一下什么时候是进入,什么时候是退出,如果之前不在屏幕中那么说明是正在进入,如果之前完全落在了屏幕中说明是正在退出,所以代码大致是这样的:
// 如果被回收了,那么根布局的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的时候发现的方法来判断是否被回收了,如果被回收了,那么根布局的parent
为null
,否则为ViewRootImpl
。
然后通过getGlobalVisibleRect
方法来判断RecyclerView
是否在屏幕中,如果不在则说明下次出现在屏幕中的时候就是正在进入,将mIsEntering
设置为true
,这里还有个变量mIsDoingAnimation
是用来标记当前是否在执行动画的,具体作用后面再讲。
接着通过getLeftAndRight
方法来获取RecyclerView
真实的左右坐标,有了真实的坐标才可以判断是否完全进入屏幕。如果完全进入屏幕则说明下次滑动时就是正在退出屏幕,将mIsEntering
设置为false
,同时将mIsDoingAnimation
也设置为false
。
getLeftAndRight
方法实现如下:
/**
* 计算出左右的坐标,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设置不同的margin
:
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个数来计算,如下:
mUnitOffset = (mRecyclerViewWidth - mLeftMargin - mRightMargin) / mChildCount;
这样随着RecyclerView
逐渐进入屏幕,只需将最初的偏移量减去进入屏幕部分的宽度就可以得到想要的偏移量。
下面是设置偏移量的代码:
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
销毁时需要调用,在PagerAdapter
的destroyItem
中也要调用:
public void onDestroy() {
mParent.removeOnPageChangeListener(mViewPagerListener);
mParent = null;
mRecyclerView = null;
mLayoutManager = null;
mChildList.clear();
}
总结
整体来讲比较简单,目前实现是一个TakeTurnHelper
对应一个RecyclerView
,可以优化一下,改成可以对应多个RecyclerView
,以后想起来再搞,现在懒得改了。
网友评论