前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >手把手教你打造RecyclerView滚动特效

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

作者头像
代码咖啡
发布2018-08-28 10:04:18
2.6K0
发布2018-08-28 10:04:18
举报
文章被收录于专栏:程序员叨叨叨

本篇文章已授权微信公众号 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,并初始化:

代码语言:javascript
复制
// 无控件
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的值进行效果实现:

代码语言:javascript
复制
// 动画进度
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中:

代码语言:javascript
复制
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进行滚动事件的监听:

代码语言:javascript
复制
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,谢谢!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2016.11.15 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前情提要
  • Item动画分析
  • Item动画代码实现
  • 结合RecyclerView思考
  • 计算滑动块底部的位置
  • 不足及期望
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档