如何利用RecyclerView打造炫酷滑动卡片
DorMcMurray
8年前
<p>前段时间一直在B站追《黑镜》第三季,相比前几季,这季很良心的拍了六集,:smile:着实过了一把瘾。由于看的是字幕组贡献的版本,每集开头都插了一个app的广告,叫“人人美剧”,一向喜欢看美剧的我便扫了一下二维码,安装了试一试。我打开app,匆匆滑动了一下首页的美剧列表,然后便随手切换到了订阅页面,然后,我就被订阅页面的动画效果吸引住了。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/ca96a4327119020d962a27fe5a3f4265.gif"></p> <p>没错,就是上面这玩意儿,是不是很炫酷,本着发扬一名码农的职业精神,我心里便痒痒的想实现这种效果,当然因为长期的fork compile,第一时间我还是上网搜了搜,有木有哪位好心人已经开源了类似的控件。借助强大的Google,我马上搜到了一个项目 SwipeCards ,是仿照探探的老父亲Tinder的app动画效果打造的,果然程序员都一个操行,看到好看的就想动手实现,不过人家的成绩让我可望而不可及~</p> <p><img src="https://simg.open-open.com/show/0c11ee032601fa9449e618865d4970d4.png"></p> <p>他实现的效果是这样的:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/063c8fb681113a6d4e90e123183ac1c3.gif"></p> <p>嗯,还不错,为了进行思想上的碰撞,我就download了一下他的源码,稍稍read了一下~_~</p> <p>作为一个有思想,有抱负的程序员,怎么能满足于compile别人的库呢?必须得自己动手,丰衣足食啊!</p> <h2><strong>开工</strong></h2> <p>###思考</p> <p>一般这种View都是自定义的,然后重写onLayout,但是有木有更简单的方法呢?由于项目里一直使用RecyclerView,那么能不能用RecyclerView来实现这种效果呢?能,当然能啊!得力于RecyclerView优雅的扩展性,我们完全可以自定义一个LayoutManager来实现嘛。</p> <h3><strong>布局实现</strong></h3> <p>RecyclerView可以通过自定义LayoutManager来实现各种布局,官方自己提供了LinearLayoutManager、GridLayoutManager,相比于ListView,可谓是方便了不少。同样,我们也可以通过自定义LayoutManager,实现这种View一层层叠加的效果。</p> <p>自定义LayoutManager,最重要的是要重写onLayoutChildren()</p> <pre> <code class="language-java">@Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { detachAndScrapAttachedViews(recycler); for (int i = 0; i < getItemCount(); i++) { View child = recycler.getViewForPosition(i); measureChildWithMargins(child, 0, 0); addView(child); int width = getDecoratedMeasuredWidth(child); int height = getDecoratedMeasuredHeight(child); layoutDecorated(child, 0, 0, width, height); if (i < getItemCount() - 1) { child.setScaleX(0.8f); child.setScaleY(0.8f); } } }</code></pre> <p>这种布局实现起来其实相当简单,因为每个child的left和top都一样,直接设置为0就可以了,这样child就依次叠加在一起了,至于最后两句,主要是为了使顶部Child之下的childs有一种缩放的效果。</p> <h3><strong>动画实现</strong></h3> <p>下面到了最重要的地方了,主要分为以下几个部分。</p> <p><strong>(1)手势追踪</strong></p> <p>当手指按下时,我们需要取到RecyclerView的顶部Child,并让其跟随手指滑动。</p> <pre> <code class="language-java">public boolean onTouchEvent(MotionEvent e) { if (getChildCount() == 0) { return super.onTouchEvent(e); } View topView = getChildAt(getChildCount() - 1); float touchX = e.getX(); float touchY = e.getY(); switch (e.getAction()) { case MotionEvent.ACTION_DOWN: mTopViewX = topView.getX(); mTopViewY = topView.getY(); mTouchDownX = touchX; mTouchDownY = touchY; break; case MotionEvent.ACTION_MOVE: float dx = touchX - mTouchDownX; float dy = touchY - mTouchDownY; topView.setX(mTopViewX + dx); topView.setY(mTopViewY + dy); updateNextItem(Math.abs(topView.getX() - mTopViewX) * 0.2 / mBorder + 0.8); break; case MotionEvent.ACTION_UP: mTouchDownX = 0; mTouchDownY = 0; touchUp(topView); break; } return super.onTouchEvent(e); }</code></pre> <p>手指按下的时候,记录topChildView的位置,移动的时候,根据偏移量,动态调整topChildView的位置,就实现了基本效果。但是这样还不够,记得我们在实现布局时,对其他子View进行了缩放吗?那时候的缩放是为现在做准备的。当手指在屏幕上滑动时,我们同样会调用updateNextItem(),对topChildView下面的子view进行缩放。</p> <pre> <code class="language-java">private void updateNextItem(double factor) { if (getChildCount() < 2) { return; } if (factor > 1) { factor = 1; } View nextView = getChildAt(getChildCount() - 2); nextView.setScaleX((float) factor); nextView.setScaleY((float) factor); }</code></pre> <p>这里的factor计算很简单,只要当topChildView滑动到设置的边界时,nextView刚好缩放到原本大小,即factor=1,就可以了。因为nextView一开始缩放为0.8,所以可计算出:</p> <pre> <code class="language-java">factor=Math.abs(topView.getX() - mTopViewX) * 0.2 / mBorder + 0.8</code></pre> <p><strong>(2)抬起手指</strong></p> <p>手指抬起后,我们要进行状态判断</p> <p><strong>1.滑动未超过边界</strong></p> <p>此时我们需要对topChildView进行归位。</p> <p><strong>2.超过边界</strong></p> <p>此时我们需要根据滑动方向,使topChildView飞离屏幕。</p> <p>对于这两种情况,我们都是通过计算view的终点坐标,然后利用动画实现的。对于第一种,很简单,targetX和targetY直接就是topChildView的原始坐标。但是对于第二种,需要根据topChildView的原始坐标和目前坐标,计算出线性表达式,然后再根据targetX来计算targetY,至于targetX,往右飞targetX就可以赋为getScreenWidth,而往左就直接为0-view.width,只要终点在屏幕外就可以。具体代码如下。</p> <pre> <code class="language-java">private void touchUp(final View view) { float targetX = 0; float targetY = 0; boolean del = false; if (Math.abs(view.getX() - mTopViewX) < mBorder) { targetX = mTopViewX; targetY = mTopViewY; } else if (view.getX() - mTopViewX > mBorder) { del = true; targetX = getScreenWidth()*2; mRemovedListener.onRightRemoved(); } else { del = true; targetX = -view.getWidth()-getScreenWidth(); mRemovedListener.onLeftRemoved(); } View animView = view; TimeInterpolator interpolator = null; if (del) { animView = getMirrorView(view); float offsetX = getX() - mDecorView.getX(); float offsetY = getY() - mDecorView.getY(); targetY = caculateExitY(mTopViewX + offsetX, mTopViewY + offsetY, animView.getX(), animView.getY(), targetX); interpolator = new LinearInterpolator(); } else { interpolator = new OvershootInterpolator(); } final boolean finalDel = del; animView.animate() .setDuration(500) .x(targetX) .y(targetY) .setInterpolator(interpolator) .setUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { if (!finalDel) { updateNextItem(Math.abs(view.getX() - mTopViewX) * 0.2 / mBorder + 0.8); } } }); }</code></pre> <p>对于第二种情况,如果直接启动动画,并在动画结束时通知adapter删除item,在连续操作时,会导致数据错乱。但是如果在动画启动时直接移除item,又会失去动画效果。所以我在这里采用了另一种办法,在动画开始前创建一个与topChildView一模一样的镜像View,添加到DecorView上,并隐藏删除掉topChildView,然后利用镜像View来展示动画。添加镜像View的代码如下:</p> <pre> <code class="language-java">private ImageView getMirrorView(View view) { view.destroyDrawingCache(); view.setDrawingCacheEnabled(true); final ImageView mirrorView = new ImageView(getContext()); Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache()); mirrorView.setImageBitmap(bitmap); view.setDrawingCacheEnabled(false); FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(bitmap.getWidth(), bitmap.getHeight()); int[] locations = new int[2]; view.getLocationOnScreen(locations); mirrorView.setAlpha(view.getAlpha()); view.setVisibility(GONE); ((SwipeCardAdapter) getAdapter()).delTopItem(); mirrorView.setX(locations[0] - mDecorViewLocation[0]); mirrorView.setY(locations[1] - mDecorViewLocation[1]); mDecorView.addView(mirrorView, params); return mirrorView; }</code></pre> <p>因为镜像View是添加在DecorView上的,topChildView父容器是RecyclerVIew,而View的x、y是相对于父容器而言的,所以镜像View的targetX和targetY需要加上一定偏移量。</p> <p>好了到这里,一切就准备就绪了,下面让我们看看动画效果如何。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/6eed901f746dafd505904273f2041adf.gif"></p> <p> </p> <p>来自:https://halfstackdeveloper.github.io/2016/11/09/如何利用RecyclerView打造炫酷滑动卡片/</p> <p> </p>