美文网首页Android TechAndroid开发MaterialDesign4App
如何利用RecyclerView打造炫酷滑动卡片

如何利用RecyclerView打造炫酷滑动卡片

作者: 半栈工程师 | 来源:发表于2016-11-10 23:14 被阅读9948次

(本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布)

前言

前段时间一直在B站追《黑镜》第三季,相比前几季,这季很良心的拍了六集,😄着实过了一把瘾。由于看的是字幕组贡献的版本,每集开头都插了一个app的广告,叫“人人美剧”,一向喜欢看美剧的我便扫了一下二维码,安装了试一试。我打开app,匆匆滑动了一下首页的美剧列表,然后便随手切换到了订阅页面,然后,我就被订阅页面的动画效果吸引住了。

没错,就是上面这玩意儿,是不是很炫酷,本着发扬一名码农的职业精神,我心里便痒痒的想实现这种效果,当然因为长期的fork compile,第一时间我还是上网搜了搜,有木有哪位好心人已经开源了类似的控件。借助强大的Google,我马上搜到了一个项目 SwipeCards,是仿照探探的老父亲Tinder的app动画效果打造的,果然程序员都一个操行,看到好看的就想动手实现,不过人家的成绩让我可望而不可及~

他实现的效果是这样的:

嗯,还不错,为了进行思想上的碰撞,我就download了一下他的源码,稍稍read了一下_

作为一个有思想,有抱负的程序员,怎么能满足于compile别人的库呢?必须得自己动手,丰衣足食啊!

正式开工

思考

一般这种View都是自定义的,然后重写onLayout,但是有木有更简单的方法呢?由于项目里一直使用RecyclerView,那么能不能用RecyclerView来实现这种效果呢?能,当然能啊!得力于RecyclerView优雅的扩展性,我们完全可以自定义一个LayoutManager来实现嘛。

布局实现

RecyclerView可以通过自定义LayoutManager来实现各种布局,官方自己提供了LinearLayoutManager、GridLayoutManager,相比于ListView,可谓是方便了不少。同样,我们也可以通过自定义LayoutManager,实现这种View一层层叠加的效果。

自定义LayoutManager,最重要的是要重写onLayoutChildren()

@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);
        }
    }
}

这种布局实现起来其实相当简单,因为每个child的left和top都一样,直接设置为0就可以了,这样child就依次叠加在一起了,至于最后两句,主要是为了使顶部Child之下的childs有一种缩放的效果。

动画实现

下面到了最重要的地方了,主要分为以下几个部分。

(1)手势追踪

当手指按下时,我们需要取到RecyclerView的顶部Child,并让其跟随手指滑动。

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);
}

手指按下的时候,记录topChildView的位置,移动的时候,根据偏移量,动态调整topChildView的位置,就实现了基本效果。但是这样还不够,记得我们在实现布局时,对其他子View进行了缩放吗?那时候的缩放是为现在做准备的。当手指在屏幕上滑动时,我们同样会调用updateNextItem(),对topChildView下面的子view进行缩放。

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);
}

这里的factor计算很简单,只要当topChildView滑动到设置的边界时,nextView刚好缩放到原本大小,即factor=1,就可以了。因为nextView一开始缩放为0.8,所以可计算出:

factor=Math.abs(topView.getX() - mTopViewX) * 0.2 / mBorder + 0.8

(2)抬起手指

手指抬起后,我们要进行状态判断

1.滑动未超过边界

此时我们需要对topChildView进行归位。

2.超过边界

此时我们需要根据滑动方向,使topChildView飞离屏幕。

对于这两种情况,我们都是通过计算view的终点坐标,然后利用动画实现的。对于第一种,很简单,targetX和targetY直接就是topChildView的原始坐标。但是对于第二种,需要根据topChildView的原始坐标和目前坐标,计算出线性表达式,然后再根据targetX来计算targetY,至于targetX,往右飞targetX就可以赋为getScreenWidth,而往左就直接为0-view.width,只要终点在屏幕外就可以。具体代码如下。

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);
                    }
                }
            });

}

对于第二种情况,如果直接启动动画,并在动画结束时通知adapter删除item,在连续操作时,会导致数据错乱。但是如果在动画启动时直接移除item,又会失去动画效果。所以我在这里采用了另一种办法,在动画开始前创建一个与topChildView一模一样的镜像View,添加到DecorView上,并隐藏删除掉topChildView,然后利用镜像View来展示动画。添加镜像View的代码如下:

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;
}

因为镜像View是添加在DecorView上的,topChildView父容器是RecyclerVIew,而View的x、y是相对于父容器而言的,所以镜像View的targetX和targetY需要加上一定偏移量。

好了到这里,一切就准备就绪了,下面让我们看看动画效果如何。

总结

效果是不是还不错,项目地址在这里: https://github.com/HalfStackDeveloper/SwipeCardRecyclerView,欢迎大家fork AND star!也希望大家在使用app,看到一些酷炫效果的时候,也自己去动手实现,谁让我们是有着职业精神的码农呢!

(转载请标明ID:半栈工程师,个人博客:https://halfstackdeveloper.github.io)

欢迎关注我的知乎专栏:https://zhuanlan.zhihu.com/halfstack

相关文章

网友评论

  • 5305eccb3ce4:我想知道 指定item该怎么做
  • d0e1ce288d00:向上层叠效果怎么实现呢?
  • Bottleli:大神 ,我在加入了当删除最后一个item 的时候从新add新数据进入 然后就每次删除到第五个的时候页面就不显示了 如果继续删除 到新的数据加入又会显示了 然后到第五个的时候又不显示了 求商讨解决
  • itlong:你好,mDecorView 不是顶层view么?有两个地方不懂
    第一在 mDecorView.getX() 和 getY()
    第二在 mDecorView.getLocationOnScreen(mDecorViewLocation);
    既然是顶层view 获取的坐标应该都是0才对吧 不知道这样写的意义是什么
    半栈工程师:@itlong 是0,主要是当时以为DecorView没有包含通知栏才这样写的,后来查了一下才发现自己一直理解错了。
  • 曹半斤:Error:(32, 13) Failed to resolve: com.github.HalfStackDeveloper:SwipeCardRecyclerView:v1.0.1
    <a href="openFile:E:/AndroidStudioProject/MyAndroidStudy/app/build.gradle">Show in File</a><br><a href="open.dependency.in.project.structure">Show in Project Structure dialog</a>
    半栈工程师:@曹半斤 根目录的build.gradle配了maven仓库地址吗
  • EllforS:大神 数据量一旦过多就会卡顿~~怎么办
  • 5dfdfb8edea7:好久之前用过一个类似的,不过没你的好,看来需要和你来一次思想上的碰撞了
  • 虎嗅蔷薇zhang:请教下,类似上面效果图的那种卡片重叠效果是如何实现的呢。
    半栈工程师:@OutOfMemery 你想要实现的是下一层没有被完全覆盖的效果吧,那你在layoutDecorated()时自己去计算一下每个View的top就行了
    虎嗅蔷薇zhang:@半栈工程师 可否指导下具体需要修改哪里,我把
    if (i < getItemCount() - 1) {
    child.setScaleX(0.8f);
    child.setScaleY(0.8f);
    }
    里面的child.setScaleX(0.8f);
    child.setScaleY(0.8f); 两句话注释了,添加了一句setTranslationY(2f),发现并没有出现层叠效果。
    半栈工程师: @OutOfMemery 文章里面已经说了啊,利用LayoutManager
  • AArman:博客很不错 请问楼主 你这个gif是用什么软件制作的
    AArman: @半栈工程师 谢谢
    半栈工程师:@五菱老司机 ffmpeg
  • uncochen:思路清晰,很不错.
  • 依然范特稀西:思路不错
  • 老年追梦人:厉害了word哥啊
  • 027f63d16800:厉害👍
  • 我叫小明哥:嗯咯我
  • Exception_Cui:http://www.jianshu.com/p/edb798104dd2/comments/5553117#comment-5553117 楼主可以看看这个 写的也挺好的
    半栈工程师:@Exception_Cui 刚刚看了,看来别人都喜欢用ViewDragHelper啊 :sweat_smile:
  • f722e4a81011:厉害了我的哥
    半栈工程师:@chuspee :grin:
  • 心里有颗小星星:赞~ 顺手star~
    半栈工程师:@心里有颗小星星 顺手star,手有余香
  • Alex_Cin:star 12TH
    半栈工程师:@Alex_Cin thank you :smiley:
  • _飞翔的荷兰豆:感谢楼主的思路,我也有我自己超简单的实现,你可以看一下我的实现 https://github.com/wuapnjie/SwipePostcard。楼主自定义了一个LayoutManager,感觉没有利用RecyclerView的缓存机制,不过还是为楼主的思路点个赞~
    saigou:你好,请问一下,你的这个如何实现无限循环呢?比如滑动到最后一个后,从第一个开始
    半栈工程师:@飞翔的荷兰豆 哇,没想到你用了ViewDragHelper,给你一个大写的赞👍
  • _deadline:给你star了
    wmd看海: @_deadline 大家是说1111啊!
    _deadline: @半栈工程师 没钱啊,所以双11跟我没半毛钱关系!
    半栈工程师: @_deadline 双十一来给star的都是真爱

本文标题:如何利用RecyclerView打造炫酷滑动卡片

本文链接:https://www.haomeiwen.com/subject/qcoxpttx.html