这样做动画交互,一点都不费力!

作者: a3aac2d1b674 | 来源:发表于2019-01-09 12:01 被阅读7次

    本文由云+社区发表

    作者:paulzeng

    导语:Lottie是Airbnb开源的一个面向 iOS、Android、React Native 的动画库,可实现非常复杂的动画,使用也及其简单,极大释放人力,值得一试。

    一、简介

    Lottie 是Airbnb开源的一个面向 iOS、Android、React Native 的动画库,能分析 Adobe After Effects 导出的动画,并且能让原生 App 像使用静态素材一样使用这些动画,完美实现动画效果。

    现在使用各平台的 native 代码实现一套复杂的动画是一件很困难并且耗时的事,我们需要为不同尺寸的屏幕加载不同的素材资源,还需要写大量难维护的代码,而Lottie可以做到同一个动画文件在不同平台上实现相同的效果,极大减少开发时间,实现不同的动画,只需要设置不同的动画文件即可,极大减少开发和维护成本。

    官方效果图:

    img img

    二、如何使用

    Lottie支持多平台,使用同一个JSON动画文件,可在不同平台实现相同的效果。

    Android 通过Airbnb的开源项目lottie-android实现,最低支持 API 16;

    IOS 通过Airbnb的开源项目lottie-ios实现,最低支持 IOS 7;

    React Native,通过Airbnb的开源项目lottie-react-native实现;

    img

    这是React logo的动画,以下以Android平台为例如何使用Lottie

    1.下载Lottie

    在项目的 build.gradle 文件添加依赖

    dependencies {  
      compile 'com.airbnb.android:lottie:2.1.0'
    }
    

    2.添加 Adobe After Effects 导出的动画文件

    Lottie默认读取Assets中的文件,我们需要把动画文件react.json 保存在app/src/main/assets文件里。(文件比较大,只展示了部分内容,文件链接

    {
        "v": "4.6.0", 
        "fr": 29.9700012207031, 
        "ip": 0, 
        "op": 141.000005743048, 
        "w": 800, 
        "h": 800, 
        "ddd": 0, 
        "assets": [ ], 
        "layers": [
            {
                "ddd": 0, 
                "ind": 0, 
                "ty": 4, 
                "nm": "center_circle", 
                "ks": {...}, 
                "ao": 0, 
                "shapes": [...], 
                "ip": 0, 
                "op": 900.000036657751, 
                "st": 0, 
                "bm": 0, 
                "sr": 1
            }, 
            {...}, 
            {...}, 
            {...}
        ]
    }
    

    3.使用Lottie

    在布局文件中直接添加Lottie的LottieAnimationView控件,即可在界面显示React logo动画效果

    <com.airbnb.lottie.LottieAnimationView
            android:id="@+id/animation_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:lottie_fileName="react.json"
            app:lottie_loop="true"
            app:lottie_autoPlay="true" />
    

    4.引入Lottie影响

    (1)兼容性

    Lottie 最低支持版本API 16,低版本系统需要做降级动画或者不展示动画

    (2)安装包

    影响项 使用前 使用后 结论
    方法数 144807 145891 增加1084个方法
    安装包大小 41969KB 42037KB 增大68KB

    这是用全民K歌release包的测试数据,lottie本身方法数不小,有方法数超标和安装包过大的风险,业务可自行评估

    注:LottieAnimationView继承于V7的AppCompatImageView,需要引入V7兼容包,根据业务需要,可以源码引入Lottie,让LottieAnimationView继承与ImageView,就不用引入V7兼容包,可减小安装大小。

    三、使用小技巧

    1.加载SDCard动画文件

    StringBuilder stringBuilder = new StringBuilder();
    BufferedReader bufferedReader = new BufferedReader(new FileReader(new File(JSON_PATH + "react.json")));
    String content = null;
    while ((content = bufferedReader.readLine()) != null){
        stringBuilder.append(content);
    }
    JSONObject jsonObject = new JSONObject(stringBuilder.toString());
    animationView.setAnimation(jsonObject);
    animationView.loop(true);
    animationView.playAnimation();
    

    2.加载SDCard图片

    animationView.setImageAssetDelegate(new ImageAssetDelegate() {
        @Override
        public Bitmap fetchBitmap(LottieImageAsset asset) {
            try {
                FileInputStream fileInputStream = new FileInputStream(IMAGE_PATH + asset.getFileName());
                return BitmapFactory.decodeStream(fileInputStream);  ///把流转化为Bitmap图片
            } catch (Exception e) {
                Log.e(TAG, "", e);
            }
            return null;
        }
    });
    

    3.加载SDCard字体

    animationView.setFontAssetDelegate(new FontAssetDelegate(){
        public Typeface fetchFont(String fontFamily) {
            Typeface customFont = Typeface.createFromFile(FONT_PATH + fontFamily);
            return customFont;
        }
    });
    

    4.缓存动画

    /*
    * Lottie内部有两个缓存map(强引用缓存,弱引用缓存),在动画文件加载完成后会根据设置的缓存策略缓存动画,方便下次使用。
    */
    animationView.setAnimation(animation, LottieAnimationView.CacheStrategy.Strong);    //强缓存
    
    animationView.setAnimation(animation, LottieAnimationView.CacheStrategy.Weak);      //弱缓存
    

    四、Lottie实现原理

    img

    设计师把一张复杂的图片使用多个图层来表示,每个图层展示一部分内容,图层中的内容也可以拆分为多个元素。拆分元素之后,根据动画需求,可以单独对图层或者图层中的元素做平移、旋转、收缩等动画。

    Lottie的使用的资源是需要先通过bodymovin( bodymovin 插件本身是用于网页上呈现各种AE效果的一个开源库)将 Adobe After Effects (AE)生成的aep动画工程文件转换为通用的json格式描述文件。Lottie则负责解析动画的数据,计算每个动画在某个时间点的状态,准确地绘制到屏幕上。

    导出的json动画描述文件:

    {
        "v": "4.6.0", 
        "fr": 29.9700012207031, 
        "ip": 0, 
        "op": 141.000005743048, 
        "w": 800, 
        "h": 800, 
        "ddd": 0, 
        "assets": [ ], 
        "layers": [
            {...}, 
        ]
    }
    

    Lottie主要类图:

    img

    图:lottie_class

    Lottie对外通过控件LottieAnimationView暴露接口,控制动画。

    LottieAnimationView继承自ImageView,通过当前时间绘制canvas显示到界面上。这里有两个关键类:LottieComposition 负责解析json描述文件,把json内容转成Java数据对象;LottieDrawable负责绘制,把LottieComposition转成的数据对象绘制成drawable显示到View上。顺序如下:

    img

    1.json文件解析

    LottieComposition负责解析json文件,建立数据到java对象的映射关系。

    (1)解析json外部结构

    LottieComposition封装整个动画的信息,包括动画大小,动画时长,帧率,用到的图片,字体,图层等等。

    json外部结构

    {
        "v": "4.6.0",               //bodymovin的版本
        "fr": 29.9700012207031,     //帧率
        "ip": 0,                    //起始关键帧
        "op": 141.000005743048,     //结束关键帧
        "w": 800,                   //动画宽度
        "h": 800,                   //动画高度
        "ddd": 0, 
        "assets": [...]             //资源信息
        "layers": [...]             //图层信息
    }
    //解析json的源码
    static LottieComposition fromJsonSync(Resources res, JSONObject json) {
          Rect bounds = null;
          float scale = res.getDisplayMetrics().density;
          int width = json.optInt("w", -1);
          int height = json.optInt("h", -1);
    
          if (width != -1 && height != -1) {
            int scaledWidth = (int) (width * scale);
            int scaledHeight = (int) (height * scale);
            bounds = new Rect(0, 0, scaledWidth, scaledHeight);
          }
    
          long startFrame = json.optLong("ip", 0);
          long endFrame = json.optLong("op", 0);
          float frameRate = (float) json.optDouble("fr", 0);
          String version = json.optString("v");
          String[] versions = version.split("[.]");
          int major = Integer.parseInt(versions[0]);
          int minor = Integer.parseInt(versions[1]);
          int patch = Integer.parseInt(versions[2]);
          LottieComposition composition = new LottieComposition(
              bounds, startFrame, endFrame, frameRate, scale, major, minor, patch);
          JSONArray assetsJson = json.optJSONArray("assets");
          parseImages(assetsJson, composition); //解析图片
          parsePrecomps(assetsJson, composition);
          parseFonts(json.optJSONObject("fonts"), composition); //解析字体
          parseChars(json.optJSONArray("chars"), composition);  //解析字符
          parseLayers(json, composition);   //解析图层
          return composition;
        }
    

    (2)解析图片资源

    LottieImageAsset类封装图片信息
    
    "assets": [                 //资源信息
        {                       //第一张图片
            "id": "image_0",    //图片id
            "w": 58,            //图片宽度
            "h": 31,            //图片高度
            "u": "images/",     //图片路径
            "p": "img_0.png"    //图片名称
        },
        {...}                   //第n张图片
    ]
    static LottieImageAsset newInstance(JSONObject imageJson) {
        return new LottieImageAsset(imageJson.optInt("w"), imageJson.optInt("h"), imageJson.optString("id"),
              imageJson.optString("p"));
    }
    

    (3)解析图层

    Layer封装图层信息,现在lottie只支持PreComp,Solid,Image,Null,Shape,Text这6中图层。

    "layers": [                 //图层信息
        {                       //第一层动画
            "ddd": 0, 
            "ind": 0,           //layer id 图层 id
            "ty": 4,            //图层类型
            "nm": "center_circle", 
            "ks": {...},        //动画
            "ao": 0, 
            "shapes": [...], 
            "ip": 0,            //inFrame 该图层起始关键帧
            "op": 90,           //outFrame 该图层结束关键帧
            "st": 0,            //startFrame 开始
            "bm": 0, 
            "sr": 1
        }, 
        {...}                   //第n层动画
    ]
    

    2.如何动起来

    Lottie时序图:

    img

    利用属性动画控制进度,每次进度改变通知到每一层,触发LottieAnimationView重绘。

    (1)利用属性动画计算进度

    这里用到了属性动画来产生一个0~1的插值,根据不同的插值来设置当前动画进度。

    代码如下:

    public LottieDrawable() {
        animator.setRepeatCount(0);
        animator.setInterpolator(new LinearInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                if (systemAnimationsAreDisabled) {
                    animator.cancel();
                    setProgress(1f);
                } else {
                    setProgress((float) animation.getAnimatedValue());
                }
            }
        });
    }
    

    (2)通过CompositionLayer把进度传递到各个图层

    @Override
    public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
        super.setProgress(progress);
        if (timeRemapping != null) {
            long duration = lottieDrawable.getComposition().getDuration();
            long remappedTime = (long) (timeRemapping.getValue() * 1000);
            progress = remappedTime / (float) duration;
        }
        if (layerModel.getTimeStretch() != 0) {
            progress /= layerModel.getTimeStretch();
        }
        progress -= layerModel.getStartProgress();
        for (int i = layers.size() - 1; i >= 0; i--) {
            layers.get(i).setProgress(progress);
        }
    }
    

    (3)通知进度改变

      void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
        if (progress < getStartDelayProgress()) {
          progress = 0f;
        } else if (progress > getEndProgress()) {
          progress = 1f;
        }
    
        if (progress == this.progress) {
          return;
        }
        this.progress = progress;
    
        for (int i = 0; i < listeners.size(); i++) {
          listeners.get(i).onValueChanged();
        }
      }
    

    (4)最终回调到LottieAnimationView的invalidateDrawable

    @Override
    public void invalidateDrawable(@NonNull Drawable dr) {
        if (getDrawable() == lottieDrawable) {
          // We always want to invalidate the root drawable so it redraws the whole drawable.
          // Eventually it would be great to be able to invalidate just the changed region.
            super.invalidateDrawable(lottieDrawable);
        } else {
          // Otherwise work as regular ImageView
            super.invalidateDrawable(dr);
        }
    }
    

    (5)最后触发LottieDrawable重绘

    @Override
    public void draw(@NonNull Canvas canvas) {
        ...
        matrix.reset();
        matrix.preScale(scale, scale);
        compositionLayer.draw(canvas, matrix, alpha);   //这里会调用所有layer的绘制方法
        if (hasExtraScale) {
            canvas.restore();
        }
    }
    

    五、性能

    1.官方说明

    如果没有mask和mattes,那么性能和内存非常好,没有bitmap创建,大部分操作都是简单的cavas绘制。

    如果存在mattes,将会创建2~3个bitmap。bitmap在动画加载到window时被创建,被window删除时回收。所以不宜在RecyclerView中使用包涵mattes或者mask的动画,否则会引起bitmap抖动。除了内存抖动,mattes和mask中必要的bitmap.eraseColor()和canvas.drawBitmap()也会降低动画性能。对于简单的动画,在实际使用时性能不太明显。

    如果在列表中使用动画,推荐使用缓存LottieAnimationView.setAnimation(String, CacheStrategy) 。

    2.属性动画和Lottie动画对比

    以下性能对比是以K歌内单个礼物动画效果

    属性动画 lottie使用硬件加速 lottie未使用硬件加速
    帧率 img img img
    内容 img img img
    CPU img img img

    Lottie动画在未开启硬件加速的情况下,帧率、内存,CPU都比属性动画差,开启硬件加速后,性能差不多。

    3、未开启硬件加速,Lottie动画大小帧率对比

    0.12倍 1倍
    img img

    主要耗时在draw方法,绘制区域越小,耗时越小

    六、K歌可用的场景

    1.特性引导视频

    全民K歌每个大版本的首次启动都会有视频引导动画,每次都会在清晰度和文件大小平衡,最终导出一个大概有3-4M的引导视频,使用lottie可提高动画清晰度和减小安装包大小

    2.loading动画

    img img

    3.礼物动画

    img

    这是全民K歌的礼物面板,内部有大量礼物动画,每个礼物动画都不相同,动画过程中有大量的旋转,透明度,大小的变化,需要用属性动画实现,非常麻烦,代码可维护性也比较差。对比使用lottie后,有几大优势:

    1、100%实现设计效果

    2、客户端代码量极少,易维护

    3、每个动画可动态配置动画样式(加载不同的json)

    4、所有动画都可动态配置,动画配置文件,素材都可从网上加载

    4.说唱

    img

    K歌的说唱功能需要歌词按照特定的动画展示出来,这里就涉及歌词放大,缩小,旋转等等特效。实现时,根据当前时间,在canvas上歌词绘制出来,最终再和声音融合在一起生成一个MV视频,这里就导致动画不能特别复杂,并且有一定的规律。

    如果使用lottie后,可以把效果导出到json动画文件里,客户端加载动画文件,循环设置进度,读取每帧画面,再和声音融合生成MV。

    优势:

    1.动画丰富

    2.代码量少

    3.可使用设计导出的字体

    代码

    animationView.setProgress(progress);        //设置当前进度
    animationView.buildDrawingCache();          //强制缓存绘制数据
    Bitmap image = animationView.getDrawingCache(); //获取当前绘制数据
    

    七、总结

    1.劣势

    (1)性能不够好—某些动画特效,内存和性能不够好;相对于属性动画,在展示大动画时,帧率较低

    2.优势

    (1)开发效率高—代码实现简单,更换动画方便,易于调试和维护。

    (2)数据源多样性—可从assets,sdcard,网络加载动画资源,能做到不发版本,动态更新

    (3)跨平台—设计稿导出一份动画描述文件,android,ios,react native通用

    Lottie使用简单,易于上手,非常值得尝试。

    八、参考资料

    1.GitHub - airbnb/lottie-android: Render After Effects animations natively on Android and iOS

    2.Lottie的使用及原理浅析 - 彩笔学长 - CSDN博客

    3.从json文件到炫酷动画-Lottie实现思路和源码分析 - 简书

    4.Most Popular - LottieFiles

    此文已由作者授权腾讯云+社区在各渠道发布

    获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号

    相关文章

      网友评论

        本文标题:这样做动画交互,一点都不费力!

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