手把手教你打造RecyclerView滚动特效

本篇文章已授权微信公众号 code小生 发布 转载请表明出处: http://www.jianshu.com/p/4176c1247eed

前情提要

效果图

最近开发中遇到这样的需求,recyclerview的item随滚动改变大小和透明度。这个效果看起来挺有动感的,似乎实现起来有点复杂,其实不然,接下来将带领大家手把手实现这个效果。

Item动画分析

我们化整为零,将这个效果分解到一个item上来看其实是这样的:

item动画

  • 实现思路 看到这个动画效果时,我首先想到的是,这个动画是可控的,不是通过设置anim.setDuration来实现的,所以要放弃Animation的念头,转而用传入process(动画执行的进度)的思路。
  • 分解动画 继续化整为零,可以将这个动画效果分解为:蒙版透明度(alpha)、宽度(width)、图片缩放(scale)
  • 状态转换 先不考虑动画变化的具体细节,先分清楚状态机。动画的变化状态为: 蒙版:暗->亮->暗 宽度:小->大->小 图片:缩->放->缩
  • 考虑细节 蒙版(黑色蒙版): 1%->50%: 1.0->0.0; 51%->100%: 0.0->1.0; 宽度(通过设置横向外边距): 1%->25%: 16dp->0dp; 26%->75%: 0dp; 76%->100%: 0dp->16dp 图片缩放:

图片缩放 1%->25%: 1.0->(b/a); 26%->50%: (b/a)->(c/a); 51%->75%: (c/a)->(b/a); 76%->100%: (b/a)->1.0;

Item动画代码实现

新建一个CustomAnimation类,定义相应动画控件的id,并初始化:

// 无控件
private static final int NO_VIEW = -999;
// 透明度变化视图
private int mAlphaViewId = NO_VIEW;
// 图片变化视图
private int mImageViewId = NO_VIEW;
// 边距变化视图
private int mMarginViewId = NO_VIEW;

/**
 * 设置透明度变化控件的ID
 * @param resId
 */
public void setAlphaViewId(int resId) {
    Log.i("animm", "setAlphaViewId");
    mAlphaViewId = resId;
}

/**
 * 设置图片变化控件的ID
 * @param resId
 */
public void setImageViewId(int resId) {
    Log.i("animm", "setImageViewId");
    mImageViewId = resId;
}

/**
 * 设置外边距变化控件的ID
 * @param resId
 */
public void setMarginViewId(int resId) {
    Log.i("animm", "setMarginViewId");
    mMarginViewId = resId;
}

定义变量process,并通过传入process的值进行效果实现:

// 动画进度
private int mProcess = 0;

/**
 * 通过进度值控制动画的进度
 * @param viewGroup 父容器
 * @param process 动画变化进度
 */
public void setAnimByProcess(ViewGroup viewGroup, int process) {
    if (viewGroup == null) {
        return;
    }
    mProcess = process;
    /**
     * 蒙版透明度设置
     */
    if (enableAlpha && mAlphaViewId != NO_VIEW) {
        View view = viewGroup.findViewById(mAlphaViewId);
        if (process > 0 && process <= 25) {
            float alpha = (25 - process) / 25.0f;
            view.setAlpha(alpha);
        } else if (process > 75 && process <= 100) {
            float alpha = (process - 75) / 25.0f;
            view.setAlpha(alpha);
        }
    }
   
   /**
     *
     * 设置图片大小
     */    if (enableImage && mImageViewId != NO_VIEW) {
        ImageView imageView = (ImageView) viewGroup.findViewById(mImageViewId);
        float curWidth = 0;
        if (process <= 25) {
            float percent = process / 25.0f;
            float marginHorizontal = mMarginHorizontal * percent;
            curWidth = mImgOrgWidth + 2 * marginHorizontal;
        } else if (process > 25 && process <= 50) {
            float percent = (process - 25) / 25.0f;
            float marginHorizontal = mMarginHorizontal * percent;
            curWidth = mScreenWidth + 2 * marginHorizontal;
        } else if (process > 50 && process <= 75) {
            float percent = (75 - process) / 25.0f;
            float marginHorizontal = mMarginHorizontal * percent;
            curWidth =  mScreenWidth + 2 * marginHorizontal;
        } else {
            float percent = (100 - process) / 25.0f;
            float marginHorizontal = mMarginHorizontal * percent;
            curWidth = mImgOrgWidth + 2 * marginHorizontal;
        }
        float scale = curWidth / mImgOrgWidth ;
        scale *= 1.1f;
        imageView.setScaleX(scale);
        imageView.setScaleY(scale);
    }
    /**
     * 设置外边距(横向)
     */
    if (enableMargin && mMarginViewId != NO_VIEW) {
        View view = viewGroup.findViewById(mMarginViewId);
        RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) view.getLayoutParams();
        if (process > 0 && process <= 25) {
            float percent = (25 - process) / 25.0f;
            float marginHorizontal = mMarginHorizontal * percent;
            lp.setMargins((int)marginHorizontal, (int)mMarginTop, (int)marginHorizontal, (int)mMarginBottom);
            view.setLayoutParams(lp);
        } else if (process > 75 && process <= 100) {
            float percent = (process - 75) / 25.0f;
            float marginHorizontal = mMarginHorizontal * percent;
            lp.setMargins((int)marginHorizontal, (int)mMarginTop, (int)marginHorizontal, (int)mMarginBottom);
            view.setLayoutParams(lp);
        }
    }
}

结合RecyclerView思考

基于上述代码,我们基本实现动画的细节,接下来我们需要思考的是,如何将RecyclerView与process结合?思考这个问题前,我们来看一下这个效果:

列表滑动效果

这是我用简书的Markdown代码块语法实现的仿RecyclerView列表的效果,基于这个效果我想到将侧边栏的滑块和RecyclerView的Item结合起来,与动画的process变量相关联:

0%

50%

100%

通过右侧小滑块底部与Item顶部之间的距离占两个Item高度的百分比作为process的值:

手机屏幕坐标示意图

process = (turningLine - itemTop) / (2 * itemHeight);

如此,我们将此关系放入新建的类TurnProcess中:

public class TurnProcess {
    /**
     * 返回动画完成的进度
     * @param itemTop
     * @param turningLine
     * @param itemHeight
     * @return
     */
    public static int getProcess(float itemTop, float turningLine, float itemHeight) {
        if (turningLine < itemTop || turningLine > (itemHeight + itemTop)) {
            return 0;
        } else {
            float percent = (turningLine - itemTop) / itemHeight;
            return (int) (percent * 100);
        }
    }
}

计算滑动块底部的位置

得到了上一步滑动与process的关系,接下来我们来计算一下滑块底部到RecyclerView可见范围顶部的距离。

RecyclerView初始情况

我们可以将RecyclerView初始情况设想如上图,此时turningLine的值为0。当RecyclerView滑动时:

RecyclerView滚动高度与turningLine的关系

由上图,我们可得到turniingLine与RecyclerView滑动距离的关系,从而得到turningLine的值: scrollY / totalScroll = turningLine / totalHeight; turningLine = scrollY * totalHeight / totalScroll;

totalScroll的值可以通过RecyclerView总高度(包含不可见部分)与RecyclerView可见部分的高度相差得到;而scrollY则随着RecyclerView的滚动变化,因此需要对RecyclerView进行滚动事件的监听:

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        float scrollY = getScrollDistance(recyclerView);
    }
}

/**
 * 获取滚动的距离
 */
private int getScrollDistance(RecyclerView recyclerView) {
    LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
    View firstVisibleItem = recyclerView.getChildAt(0);
    int firstItemPosition = layoutManager.findFirstVisibleItemPosition();
    int itemHeight = firstVisibleItem.getHeight();
    int firstItemBottom = layoutManager.getDecoratedBottom(firstVisibleItem);
    return (firstItemPosition + 1) * itemHeight - firstItemBottom;}

如此,不断变化的turningLine与RecyclerView的滚动建立了关系;至此,动画与RecyclerView的逻辑关系梳理完毕。按照实现RecyclerView的套路一步步实现最基本的列表效果,然后将动画与滚动监听的关系放入Adapter中。需要强调的是:每一个Item都是随着RecyclerView的滚动进行变化的,所以每一个Item的ViewHolder中都注册RecyclerView的监听事件来监听RecyclerView的滑动。

不足及期望

这样的动画效果固然有趣,但是其仍存在很多不足,就自己发现的问题,列不足如下:

  • 每一个Item都监听RecyclerView的滑动事件非常耗时,在低端机上可能存在滑动不流畅的现象,尚未测试,但在红米 Not 3联发科版系统(不得不说这个系统真的很渣,亲测体验)上运行未出现异常。
  • 当RecyclerView滑动太快时,单位滚动距离内,滚动监听事件的触发频率较低,导致有些Item的动画进度未达到100%便从屏幕中消失,从而存在重新滚动到那个Item时,Item的动画停留在1%~99%之间的某一帧,影响RecyclerView的展示效果。
  • 因ImageView设置的ScaleType为CenterCrop,所以图片右侧变化在放大过程中会有类似于金属拉丝的效果,因此图片缩放的scale最好在原来的基础上乘以1.1,在单个Item的动画中此问题已解决,但在RecyclerView中,此问题仍然存在。

在此,期望有耐心将本文看完的小伙伴们在文章下方的评论里留下宝贵意见,一起来完善这个效果。另,若有小伙伴在Github上看到有这样效果的稳定的第三方库,希望可以在文章下方评论中留下链接。

代码已上传Github,欢迎访问Follow。

花两天写了本篇文章,原创不易,转载请注明链接:http://www.jianshu.com/p/4176c1247eed,谢谢!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏GIS讲堂

CSS+JS实现tab标签切换

循环将所有的内容标签隐藏,并将tab标题栏的active样式清除,完了之后设置选中标签的内容显示,并给tab标题栏添加active样式。

4773
来自专栏web开发

自实现PC端jQuery版轮播图

最近其他项目不是很忙,被安排给公司的官网项目做一个新的页面(之前没接触公司官网项目),其中有一个用到轮播图的地方,最开始想直接用swiper.js插件实现就好了...

2752
来自专栏数据小魔方

裁图、抠图、换背景,PPT也可以

今天跟大家聊一聊PPT的基本图片处理功能! ▽ 每次做PPT的时候 总想弄几个感觉还不错的图片 插入PPT里来装装bigger 可是能找到的图片 不是背景不搭 ...

38110
来自专栏吴老师移动开发

【iOS开发】iOS 动画详解

在移动开发中,为了提高用户体验,会用到一些动画来提高应用的视觉效果。让人有眼前一亮的感觉。同时有动画的过渡过程,会让应用看起来不是那么的生硬,更吸引用户。

2256
来自专栏AndroidTv

【Android】5.x炫酷标题栏动画使用理解

Android5.0+推出的新控件感觉特别酷,最近想模仿大神做个看图App出来,所以先把这些新控件用熟悉了。 新控件的介绍、使用等等网上相应的文章已经特别多了...

4456
来自专栏守候书阁

canvas入门实战--邀请卡生成与下载

写了很多的javascript和css3的文章,是时候写一篇canvas的了。canvas是html5提供的一个新的功能!至于作用,就是一个画布。然后画笔就是j...

1113
来自专栏葡萄城控件技术团队

Web页面中5种超酷的Hover效果

想在自己的网站中应用超酷的hover效果吗?也许你可以从如下的这些实例中获得一些灵感,如果你喜欢这些效果,也可以直接拷贝代码并应用到你的站点。 给平淡的站点带来...

2559
来自专栏IMWeb前端团队

CSS StickyFooter——当内容不足一屏时footer紧贴底部

一般来说我们的footer是跟着内容走的,所以当内容较少不足一屏的时候,footer也会跟着内容往上走,导致下面一段空白。所以这里我们来探讨下当内容不足一屏时f...

2177
来自专栏AhDung

【C#】让工具栏ToolStrip能触发焦点控件的Leave、Validating、DataError等事件以验证数据

----------------更新:2014-04-21---------------

942
来自专栏前端

自己实现PC端jQuery版轮播图

最近其他项目不是很忙,被安排给公司的官网项目做一个新的页面(之前没接触公司官网项目),其中有一个用到轮播图的地方,最开始想直接用swiper.js插件实现就好了...

5569

扫码关注云+社区

领取腾讯云代金券