想经济上支持我 or 想经过视频看我是怎么实现的:javascript
前几天看有人实现了仿人人美剧的订阅界面,不过在细节之处以及实现方式我我的认为都不是最佳的姿式。
因而我也动手撸了一个,还顺带撸了个探探的界面,先看GIF:
git
这里吐个槽,探探这种设计真的像皇帝翻牌子的感受,不喜欢左滑,喜欢右滑。github
人人影视版特色(需求):canvas
除上述动画特色,探探版特色(需求):app
咱们的效果,基本上和原版一致了,写起来怎么样呢?
我不是标题党,如标题所说:ide
LayoutManager
只会加载显示屏幕上可见的数量的View。ItemTouchHelper
处理拖拽&滑动删除逻辑,核心代码不超过50行。且通过封装,四行代码就能够用。若是懒得看这么多文字只想用,直接移步gayhub,gradle导入相关文件or复制。而后以下,搞定。函数
mRv.setLayoutManager(new OverLayCardLayoutManager());
CardConfig.initConfig(this);
ItemTouchHelper.Callback callback = new RenRenCallback(mRv, mAdapter, mDatas);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
itemTouchHelper.attachToRecyclerView(mRv);复制代码
并且我将一些参
数都以变量形式计算,这样就作到了可配置,假如老板让你一开始多显示几层卡片,例如6层,你只须要修改一个参数便可,效果如图:
工具
正确的姿式就是:布局
LayoutManager
实现卡片层叠布局,值得注意的是,只layout出界面上可能会看见的那些View。ItemTouchHelper
,它自己实现了拖拽&滑动删除逻辑,咱们只须要在onChildDraw()
中绘制动画和onSwiped()
中处理数据集(循环or删除)。因此本文也算是填了LayoutManger系列的坑,实现了一个酷炫效果的布局。
Let's Go!
转载请标明出处: gold.xitu.io/post/585682…
gold.xitu.io/post/585682…
本文出自:【张旭童的稀土掘金】(gold.xitu.io/user/56de21…)
代码传送门:喜欢的话,随手点个star。多谢
github.com/mcxtzhang/Z…
其实本例中的LayoutManager
十分简单,由于ItemTouchHelper
的存在,LayoutManager
根本不须要处理它的滑动事件,而LayoutManager
中最难写的就是在滑动时的View
回收和复用,以及layout
新View
的处理。
关于LayoutManager的基础知识和铺垫,我就再也不赘述,可参考我之前的文章:LayoutManager实现流式布局
可是即使如此,仍是有一个惟一的注意事项。咱们只layout
出界面上可能会看见的那些View
便可。
由于考虑到动画,因此是可能会看见。
咱们看人人美剧的界面:
View
,咱们分别起名:
TopView,Top-1View,Top-2View
。其中
TopView
彻底可见,
Top-1View,Top-2View
只有下边缘可见。
如文首GIF,滑动TopView
时,Top-1View,Top-2View
开始慢慢放大,而且向上位移,直至填充至它们各自上层的View。这时候露出了Top-3View
。
因此咱们在书写LayoutManager
的onLayoutChildren()
方法时,只要layout
出当前数据集最后四个View便可。
前文提到的参数配置以下:
包括一些配置
每一级View之间的Scale差别、translationY等等
public class CardConfig {
//屏幕上最多同时显示几个Item
public static int MAX_SHOW_COUNT;
//每一级Scale相差0.05f,translationY相差7dp左右
public static float SCALE_GAP;
public static int TRANS_Y_GAP;
public static void initConfig(Context context) {
MAX_SHOW_COUNT = 6;
SCALE_GAP = 0.05f;
TRANS_Y_GAP = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, context.getResources().getDisplayMetrics());
}
}复制代码
LayoutManager所有代码以下,布满注释,若是看不懂,建议阅读前置文章LayoutManger系列:
public class OverLayCardLayoutManager extends RecyclerView.LayoutManager {
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
int itemCount = getItemCount();
if (itemCount >= MAX_SHOW_COUNT) {
//从可见的最底层View开始layout,依次层叠上去
for (int position = itemCount - MAX_SHOW_COUNT; position < itemCount; position++) {
View view = recycler.getViewForPosition(position);
addView(view);
measureChildWithMargins(view, 0, 0);
int widthSpace = getWidth() - getDecoratedMeasuredWidth(view);
int heightSpace = getHeight() - getDecoratedMeasuredHeight(view);
//咱们在布局时,将childView居中处理,这里也能够改成只水平居中
layoutDecoratedWithMargins(view, widthSpace / 2, heightSpace / 2,
widthSpace / 2 + getDecoratedMeasuredWidth(view),
heightSpace / 2 + getDecoratedMeasuredHeight(view));
/** * TopView的Scale 为1,translationY 0 * 每一级Scale相差0.05f,translationY相差7dp左右 * * 观察人人影视的UI,拖动时,topView被拖动,Scale不变,一直为1. * top-1View 的Scale慢慢变化至1,translation也慢慢恢复0 * top-2View的Scale慢慢变化至 top-1View的Scale,translation 也慢慢变化只top-1View的translation * top-3View的Scale要变化,translation岿然不动 */
//第几层,举例子,count =7, 最后一个TopView(6)是第0层,
int level = itemCount - position - 1;
//除了顶层不须要缩小和位移
if (level > 0 /*&& level < mShowCount - 1*/) {
//每一层都须要X方向的缩小
view.setScaleX(1 - SCALE_GAP * level);
//前N层,依次向下位移和Y方向的缩小
if (level < MAX_SHOW_COUNT - 1) {
view.setTranslationY(TRANS_Y_GAP * level);
view.setScaleY(1 - SCALE_GAP * level);
} else {//第N层在 向下位移和Y方向的缩小的成都与 N-1层保持一致
view.setTranslationY(TRANS_Y_GAP * (level - 1));
view.setScaleY(1 - SCALE_GAP * (level - 1));
}
}
}
}
}
}复制代码
撸到这里,咱们的静态界面已经成型,下面让咱们动起来:
ItemTouchHelper
的基础知识,建议你们自行学习,网上文章不少,我简单介绍一下,
This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView.
It works with a RecyclerView and a Callback class, which configures what type of interactions
are enabled and also receives events when user performs these actions.
Depending on which functionality you support, you should override
{@link Callback#onMove(RecyclerView, ViewHolder, ViewHolder)} and / or
{@link Callback#onSwiped(ViewHolder, int)}.
翻译 + 总结:
这货是一个工具类,为RecyclerView扩展滑动消失(删除)和drag & drop效果的。
它须要和RecyclerView、Callback 一块儿工做。Callback 类里定义了 容许哪些交互,而且会接收到对应的交互事件
根据你须要哪一种功能(滑动消失(删除)和drag & drop),你须要重写
Callback#onMove(RecyclerView, ViewHolder, ViewHolder)-----drag & drop
Callback#onSwiped(ViewHolder, int) 方法。 -----滑动消失(删除)
总结一下入门级用法以下,三个步骤:
定义一个Callback:ItemTouchHelper.Callback callback = new ItemTouchHelper.SimpleCallback(int,int)
,这两个int分别表明要 监听哪几个方向上的拖拽、滑动事件。 经常使用:ItemTouchHelper.DOWN | ItemTouchHelper.UP | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT
将Callback传给ItemTouchHelper:ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
关联ItemTouchHelper和RecyclerView:itemTouchHelper.attachToRecyclerView(mRv)
这三个步骤作完后,ItemTouchHelper就会自动帮咱们完成 滑动消失(删除)和drag & drop 的功能。
咱们本例中,须要的是滑动消失(删除) ,因此咱们的Callback
不须要关注onMove()
方法。
且咱们须要上下左右滑动均可以删除的效果。
则以下构造Callback,传入上下左右:
ItemTouchHelper.Callback callback = new ItemTouchHelper.SimpleCallback(0,
ItemTouchHelper.DOWN | ItemTouchHelper.UP | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT)复制代码
onSwiped()
方法,是滑动删除动做已经发生后回调的,即,咱们先滑动卡片,而后松手,此时ItemTouchHelper
判断咱们的手势是删除手势,会自动对这个卡片执行丢出屏幕外的动画,同时回调onSwiped()
方法。
因此咱们须要在其中以下写:
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
//★实现循环的要点
SwipeCardBean remove = mDatas.remove(viewHolder.getLayoutPosition());
mDatas.add(0, remove);
mAdapter.notifyDataSetChanged();
}复制代码
在这里咱们完成了循环的操做:
View
的ViewHolder
拿到Position
Position
的数据源notifyDataSetChanged()
,通知列表刷新这里提一下为何咱们要调用notifyDataSetChanged()
。
看官方文档:
ItemTouchHelper moves the items' translateX/Y properties to reposition them
即ItemTouchHelper实现的滑动删除,其实只是隐藏了这个滑动的View。并非真的删除了。
在LayoutManager实现流式布局一文第五节中,咱们已经提到,notifyDataSetChanged()
会回调onLayoutChildren()
这个函数,而在这个函数中,咱们会从新布局,即真正的移除(再也不layout)滑动掉的View,同时会补充进新的最底层的View。
嗯,JavaBean也看一眼吧,没亮点:
public class SwipeCardBean {
private int postition;//位置
private String url;
private String name;
}复制代码
咱们写到这里已经完成了滑动删除的功能,其实咱们什么都没有写是吧,复杂的判断都由ItemTouchHelper帮咱们处理掉了,例如速度、滑动距离是否到达删除阈值,删除成功移除的动画、取消删除复位的动画等等。
因此我说利用ItemTouchHelper才是正确的姿式,由于很简单&快速。
下面咱们来实现滑动时的动画。
咱们须要重写Callback
的onChildDraw()
方法,这个方法参数较多:
* @param c The canvas which RecyclerView is drawing its children
* @param recyclerView The RecyclerView to which ItemTouchHelper is attached to
* @param viewHolder The ViewHolder which is being interacted by the User or it was
interacted and simply animating to its original position
* @param dX The amount of horizontal displacement caused by user's action
* @param dY The amount of vertical displacement caused by user's action
* @param actionState 是拖拽仍是滑动事件 The type of interaction on the View. Is either {@link #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}.
* @param isCurrentlyActive 事件是用户产生仍是动画产生的 True if this view is currently being controlled by the user or false it is simply animating back to its original state. 复制代码
对咱们比较有用的有dX dX
,能够判断滑动方向,以及计算滑动的比例,从而控制缩放、位移动画的程度。
本文以下编写,对View的缩放、位移,实际上是对LayoutManager里的操做的逆操做,值得注意的是最后一层,即top-3View
在Y轴上是保持不变的:
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
//先根据滑动的dxdy 算出如今动画的比例系数fraction
double swipValue = Math.sqrt(dX * dX + dY * dY);
double fraction = swipValue / getThreshold(viewHolder);
//边界修正 最大为1
if (fraction > 1) {
fraction = 1;
}
//对每一个ChildView进行缩放 位移
int childCount = recyclerView.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = recyclerView.getChildAt(i);
//第几层,举例子,count =7, 最后一个TopView(6)是第0层,
int level = childCount - i - 1;
if (level > 0) {
child.setScaleX((float) (1 - SCALE_GAP * level + fraction * SCALE_GAP));
if (level < MAX_SHOW_COUNT - 1) {
child.setScaleY((float) (1 - SCALE_GAP * level + fraction * SCALE_GAP));
child.setTranslationY((float) (TRANS_Y_GAP * level - fraction * TRANS_Y_GAP));
}
}
}
}复制代码
getThreshold(viewHolder)
函数,返回是否能够被回收掉的阈值,关于它为何这么写,我是从源码里找到的,本末会讲解:
//水平方向是否能够被回收掉的阈值
public float getThreshold(RecyclerView.ViewHolder viewHolder) {
return mRv.getWidth() * getSwipeThreshold(viewHolder);
}复制代码
一开始文章撸到这里应该结束了,群里出来一个马小跳,告诉我探探和这略有不一样,但愿我一并实现。
嗯,好吧。表示没据说过探探,那我先去下载一个看看吧。
loading-install-open........
哎哟呵,十分钟过去了,我还在滑动看美女 忘记了要干什么,被女票看到胖揍了我一顿。
好的,我捂着脸继续分析。
探探和人人影视有两点不一样:
感受也是炒鸡简单,来吧。五分钟撸完吃外卖。修改点:
onChildDraw()
里,按比例修改TopView的Rotate & Alpha还有一点小不一样,上滑下滑再也不能删除,因此咱们构造时只传入左右便可:
ItemTouchHelper.Callback callback = new ItemTouchHelper.SimpleCallback(0,
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT)复制代码
略
在上文人人影视的基础上扩展,上文的效果,对TopView
是不作任何操做的。这里只须要再对TopView
作额外操做便可:
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
...
for (int i = 0; i < childCount; i++) {
View child = recyclerView.getChildAt(i);
//第几层,举例子,count =7, 最后一个TopView(6)是第0层,
int level = childCount - i - 1;
if (level > 0) {
...
} else {
//探探只是第一层加了rotate & alpha的操做
//不过他区分左右
float xFraction = dX / getThreshold(viewHolder);
//边界修正 最大为1
if (xFraction > 1) {
xFraction = 1;
} else if (xFraction < -1) {
xFraction = -1;
}
//rotate
child.setRotation(xFraction * MAX_ROTATION);
//本身感觉一下吧 Alpha
if (viewHolder instanceof ViewHolder) {
ViewHolder holder = (ViewHolder) viewHolder;
if (dX > 0) {
//露出左边,比心
holder.setAlpha(R.id.iv_love, xFraction);
} else {
//露出右边,滚犊子
holder.setAlpha(R.id.iv_del, -xFraction);
}
}
}
}
}复制代码
实现完后,我觉得结束了,结果比咱们想象的还要复杂一丢丢。由于此时删除后,notifyDataSetChanged()
刷新界面,而TopView
仍是倾斜的,爱心、删除图标也是出现的。这显然与预期不符。因此咱们须要在onSwiped()
里将其复位:
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
...
//探探只是第一层加了rotate & alpha的操做
//对rotate进行复位
viewHolder.itemView.setRotation(0);
//本身感觉一下吧 Alpha
if (viewHolder instanceof ViewHolder) {
ViewHolder holder = (ViewHolder) viewHolder;
holder.setAlpha(R.id.iv_love, 0);
holder.setAlpha(R.id.iv_del, 0);
}
}复制代码
Ok,大功告成。效果和文首同样,尽情去跟产品UI嘚瑟吧。
阈值的寻找,花费了我一些时间,由于我想作到和系统的行为保持一致。
即,当删除、喜欢图标全显,当Top-1View
显示完毕时,松手 TopView
会回收。
这就决定了咱们的缩放、位移的阈值不能随便定,因此咱们必须去源代码里找答案。
//水平方向是否能够被回收掉的阈值
public float getThreshold(RecyclerView.ViewHolder viewHolder) {
return mRv.getWidth() * getSwipeThreshold(viewHolder);
}复制代码
由于滑动删除操做是touch事件致使的,且应该是ACTION_UP时,触发的,
因此在ItemTouchHelper
源码里,搜索onTouch字样:
定位到:mOnItemTouchListener
,->
继续定位其中的onTouchEvent()
,->case MotionEvent.ACTION_UP:
,->void select(ViewHolder selected, int actionState)
->
在这里我注意到有一句代码:animationType = ANIMATION_TYPE_SWIPE_SUCCESS;
这说明删除成功,它的触发条件是:if (swipeDir > 0)
->swipeDir
的值: final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0 : swipeIfNecessary(prevSelected);
->int swipeIfNecessary(ViewHolder viewHolder)
->
if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) {
return swipeDir;
}复制代码
如此返回1的话,则->checkHorizontalSwipe(viewHolder, flags)
->
在其中终于找到源码里阈值的获取之处:
final float threshold = mRecyclerView.getWidth() * mCallback
.getSwipeThreshold(viewHolder);复制代码
因而我就直接复制出来。
代码传送门:喜欢的话,随手点个star。多谢
github.com/mcxtzhang/Z…
想经济上支持我 or 想经过视频看我是怎么实现的:
edu.csdn.net/course/deta…
本文利用LayoutManager
加载显示屏幕上可见的数量的View,搭配ItemTouchHelper
处理拖拽&滑动删除逻辑,核心代码不超过50行。且通过封装,四行代码就能够用。
记住LayoutManager
,咱们写,只layout出界面上可能会看见的那些View便可。
关于ItemTouchHelper
,它自己实现了拖拽&滑动删除逻辑,咱们只须要在onChildDraw()
中绘制动画和onSwiped()
中处理数据集(循环or删除)便可。
之后老板让你作这种效果,你只须要:
CardConfig.initConfig(this);
ItemTouchHelper.Callback callback = new RenRenCallback(mRv, mAdapter, mDatas);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
itemTouchHelper.attachToRecyclerView(mRv);复制代码
若是须要定制特殊的参数,例如显示6层:
CardConfig.MAX_SHOW_COUNT = 6;复制代码
转载请标明出处:
gold.xitu.io/post/585682…
本文出自:【张旭童的稀土掘金】(gold.xitu.io/user/56de21…)
代码传送门:喜欢的话,随手点个star。多谢
github.com/mcxtzhang/Z…
刚建了个QQ搞基交流群:557266366里面如今没有人。嗯,就这样吧。