【Android】Lottie - 实现炫酷的Android导航栏动画

【Android】Lottie - 实现炫酷的Android导航栏动画

Lottie是什么?

Lottie 是一个由Airbnb 开发并开源的高级动画解决方案,它能将设计师用 After Effects 制作的动画导出为轻量的 JSON 文件,让我们可以轻松地在移动端(iOS/Android)、Web 以及桌面端渲染高质量的矢量动画。

简单来说,Lottie 就像一个"动画播放器",它能够解析用 After Effects 制作的动画,并完美地还原在各类应用和网站上。

Lottie项目地址:github.com/airbnb/lott...

效果展示

以上是基于Lottie实现的炫酷的移动端动画,而在项目中应用Lottie却非常简单,接下来主要介绍Lottie在Android Studio中的使用,最后会介绍怎么在BottomNavigationView中使用Lottie动画作为图标。

使用步骤

分享几个我经常用的Lottie动画资源网站:

lottiefiles.com/ (动画非常多,不过要付费) lordicon.com/(虽然动画少点,不过质...

icons8.com/icons/set/p...

1.添加Lottie动画依赖

在app下的build.gradle中添加依赖,并点击Syan Now

groovy 复制代码
dependencies {
    implementation "com.airbnb.android:lottie:6.4.0" 
}

最新版本可以访问GitHub项目地址查看

2.下载Lottie JSON文件并导入到Android Studio中

动画Json文件下载后,在AS中的app\src\main目录下创建assets文件夹,将下载好的动画JSON文件复制到这个目录下,这样就可以直接在代码中引用了。

这里还有另一种方式,在res目录下创建raw文件夹,然后把JSON文件复制到这个目录下,两种方式区别不大,也就是在代码中的引用方式不同而已,这个后面会说到。

接下来就可以在xml布局或者代码中设置Lottie了

3.在代码中引用Lottie动画资源

3.1 在xml中引用

在布局文件中添加LottieAnimationView组件

xml 复制代码
<com.airbnb.lottie.LottieAnimationView
    android:id="@+id/lottieAnimationView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:lottie_rawRes="@raw/ic_video"
    app:lottie_autoPlay="true"
    app:lottie_loop="true"
    />

这里通过app:lottie_rawRes来指定为raw目录下的动画资源,另外要是指定assets目录下的JSON,可以使用app:lottie_fileName属性,例如

xml 复制代码
app:lottie_fileName="ic_video.json"

没什么区别,只是app:lottie_fileName可能会存在找不到路径的错误,在代码中设置记得加上异常处理

常用属性:

属性 说明 取值
app:lottie_autoPlay 是否自动播放 true/false(默认false)
app:lottie_loop 是否循环播放 true/false(默认false)
app:lottie_repeatCount 循环次数 整数或 -1(无限循环)
app:lottie_repeatMode 循环模式 restart(重新开始) / reverse(倒放)
app:lottie_speed 播放速度 浮点数,如 1.0(正常), 2.0(2倍速), -1.0(倒放)

另外,在xml中设置动画资源后,直接运行是没有任何效果的,因为这时Lottie动画还没有加载,你需要在代码中加载动画并开始播放动画才能生效。不过在xml中设置app:lottie_autoPlayapp:lottie_loop这两个属性为true后直接运行是可以看到动画的,这是因为设置app:lottie_autoPlay 后,动画会自动加载并开始播放,配合app:lottie_loop属性就能完成自动播放+无限循环的效果。

3.2 在代码中设置

在MainActivity中:

java 复制代码
public class MainActivity extends AppCompatActivity {
    
    private LottieAnimationView lottieAnimationView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        // 初始化View
        lottieAnimationView = findViewById(R.id.lottieAnimationView);
        
        // 从raw资源设置动画
        lottieAnimationView.setAnimation(R.raw.loading_animation);
        
        // 从assets文件夹设置动画
        // lottieAnimationView.setAnimation("ic_video.json");// 使用这个记得加上异常处理
        
        // 开始播放动画
        // lottieAnimationView.playAnimation();
    }
}

初始化LottieAnimationView,并设置动画源,同样有两种方式。如果没有在代码中设置自动播放,在代码中记得开始播放动画,由于已经带xml中设置自动播放和循环,这里就会自动开始一直循环播放动画。

播放设置:

java 复制代码
// 播放动画
lottieAnimationView.playAnimation();

// 暂停动画(保留当前进度)
lottieAnimationView.pauseAnimation();

// 取消动画(重置进度)
lottieAnimationView.cancelAnimation();

// 恢复播放(从暂停处继续)
lottieAnimationView.resumeAnimation();

// 判断是否正在播放
boolean isPlaying = lottieAnimationView.isAnimating();

// 获取动画时长(毫秒)
long duration = lottieAnimationView.getDuration();

进度控制:

java 复制代码
// 设置播放进度(0.0f - 1.0f)
lottieAnimationView.setProgress(0.5f);

// 获取当前进度
float currentProgress = lottieAnimationView.getProgress();

// 从指定进度开始播放
lottieAnimationView.setProgress(0.3f);
lottieAnimationView.playAnimation();

// 播放特定区间(从30%到70%)
lottieAnimationView.setMinProgress(0.3f);
lottieAnimationView.setMaxProgress(0.7f);
lottieAnimationView.playAnimation();

在Java代码中,你可以通过以上方法轻松实现对Lottiie动画的播放控制以及进度设置,如果想要配合点击事件实现更复杂的交互效果,你可以添加一个动画监听器。

3.3添加动画监听器
java 复制代码
// 添加动画监听器
lottieAnimationView.addAnimatorListener(new Animator.AnimatorListener() {
    @Override
    public void onAnimationStart(Animator animation) {
        // 动画开始时的回调
        Log.d("Lottie", "动画开始播放");
    }

    @Override
    public void onAnimationEnd(Animator animation) {
        // 动画结束时的回调
        Log.d("Lottie", "动画播放结束");
    }

    @Override
    public void onAnimationCancel(Animator animation) {
        // 动画被取消时的回调
        Log.d("Lottie", "动画被取消");
    }

    @Override
    public void onAnimationRepeat(Animator animation) {
        // 每次循环重复时的回调
        Log.d("Lottie", "动画开始新一轮循环");
    }
});

通过动画监听器,你可以监听动画播放过程中的各种状态变化,在对应实际执行自定义逻辑,实现更复杂的交互逻辑,比如你可以在onAnimationEnd方法,也就是动画结束方法回调中执行页面跳转的逻辑,效果就是在动画播完后再跳转。再比如在onAnimationStart方法中可以添加一个文字提示,点击播放加载中动画,提高用户体验。

怎么样,使用起来是不是非常容易,接下来将介绍怎么自定义BottomNavigationView将其中的ImageView替换成LottieAnimationView,实现炫酷的底部导航栏动画。

4.自定义BottomNavigationView实现炫酷的底部导航栏点击动画

4.1 使用效果

首先,还是先看看具体效果:

定义LottieBottomNavigationView类继承自BottomNavigationViewupdateMenuItemIcon方法中调用replaceImageViewWithLottie方法将BottomNavigationView中原有的ImageView替换成LottieAnimationView,并在init方法中设置LottieAnimationView的点击事件,点击播放Lottie动画,效果就是点击播放,如果再次点击时动画还没有播放完毕,那么就从头开始再次播放动画。

4.2 自定义LottieBottomNavigationView继承自BottomNavigationView

LottieBottomNavigationView:

java 复制代码
import android.content.Context;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.Gravity;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import android.widget.ImageView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.airbnb.lottie.LottieAnimationView;
import com.google.android.material.bottomnavigation.BottomNavigationView;

public class LottieBottomNavigationView extends BottomNavigationView {
    private SparseArray<String> animationMap = new SparseArray<>();
    private boolean iconsSet = false;

    public LottieBottomNavigationView(@NonNull Context context) {
        super(context);
        init();
    }

    public LottieBottomNavigationView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public LottieBottomNavigationView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        setLabelVisibilityMode(LABEL_VISIBILITY_LABELED);
        getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (!iconsSet && getChildCount() > 0) {
                    getViewTreeObserver().removeOnGlobalLayoutListener(this);
                    updateAllMenuIcons();
                    iconsSet = true;
                    // forceRemovePadding();
                    setPadding(0, 0, 0, 0);
                }
            }
        });

        setOnNavigationItemSelectedListener(new OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                int itemId = item.getItemId();
                playLottieAnimation(itemId); // 点击时播放相应动画
                return true; // 返回true表示菜单项已被处理
            }
        });
    }
    private void forceRemovePadding() {
        // 强制设置高度为更小的值
        ViewGroup.LayoutParams params = getLayoutParams();
        params.height = dipToPx(48); // 设置为较小的值

        // 设置垂直偏移
        setTranslationY(-dipToPx(4));

        if (getChildCount() > 0) {
            ViewGroup menuView = (ViewGroup) getChildAt(0);
            ViewGroup.LayoutParams menuParams = menuView.getLayoutParams();
            menuParams.height = dipToPx(40);
            menuView.setLayoutParams(menuParams);
        }
    }

    public void setLottieIcon(int menuItemId, String animationAssetName) {
        animationMap.put(menuItemId, animationAssetName);
        if (iconsSet) {
            updateMenuItemIcon(menuItemId);
        }
    }

    private void updateAllMenuIcons() {
        for (int i = 0; i < animationMap.size(); i++) {
            updateMenuItemIcon(animationMap.keyAt(i));
        }
    }

    private void updateMenuItemIcon(int menuItemId) {
        View menuItemView = findMenuItemView(menuItemId);
        if (menuItemView == null) return;

        String assetName = animationMap.get(menuItemId);
        FrameLayout iconContainer = menuItemView.findViewById(android.R.id.icon);

        if (iconContainer != null) {
            setupLottieInContainer(iconContainer, assetName);
            return;
        }

        ImageView iconView = findIconImageView(menuItemView);
        if (iconView != null) {
            replaceImageViewWithLottie(iconView, assetName);
            return;
        }

        if (menuItemView instanceof ViewGroup) {
            setupLottieDirectly((ViewGroup) menuItemView, assetName);
        }
    }

    private View findMenuItemView(int menuItemId) {
        View view = findViewById(menuItemId);
        if (view != null) return view;

        if (getChildCount() > 0) {
            ViewGroup menuView = (ViewGroup) getChildAt(0);
            for (int i = 0; i < menuView.getChildCount(); i++) {
                View child = menuView.getChildAt(i);
                if (child.getId() == menuItemId) return child;
            }
        }
        return null;
    }

    private ImageView findIconImageView(View menuItemView) {
        if (menuItemView instanceof ImageView) {
            return (ImageView) menuItemView;
        }
        if (menuItemView instanceof ViewGroup) {
            ViewGroup group = (ViewGroup) menuItemView;
            for (int i = 0; i < group.getChildCount(); i++) {
                ImageView result = findIconImageView(group.getChildAt(i));
                if (result != null) return result;
            }
        }
        return null;
    }

    private void setupLottieInContainer(FrameLayout container, String assetName) {
        container.removeAllViews();
        LottieAnimationView lottieIcon = createLottieView(assetName);
        container.addView(lottieIcon);
    }

    private void replaceImageViewWithLottie(ImageView oldIcon, String assetName) {
        ViewGroup parent = (ViewGroup) oldIcon.getParent();
        int index = parent.indexOfChild(oldIcon);

        LottieAnimationView lottieIcon = createLottieView(assetName);
        lottieIcon.setProgress(1f);
        lottieIcon.setLayoutParams(oldIcon.getLayoutParams());

        parent.removeViewAt(index);
        parent.addView(lottieIcon, index);
    }

    private void setupLottieDirectly(ViewGroup container, String assetName) {
        LottieAnimationView lottieIcon = createLottieView(assetName);
        container.addView(lottieIcon, 0);
    }

    private LottieAnimationView createLottieView(String assetName) {
        LottieAnimationView lottieIcon = new LottieAnimationView(getContext());
        lottieIcon.setAnimation(assetName);
        lottieIcon.setRepeatCount(0);
        lottieIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);

        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
                dipToPx(30), dipToPx(30), Gravity.CENTER
        );
        lottieIcon.setLayoutParams(params);

        return lottieIcon;
    }

    public void playLottieAnimation(int menuItemId) {
        View menuItemView = findMenuItemView(menuItemId);
        if (menuItemView != null) {
            LottieAnimationView lottieView = findLottieView(menuItemView);
            if (lottieView != null) lottieView.playAnimation();
        }
    }

    private LottieAnimationView findLottieView(View view) {
        if (view instanceof LottieAnimationView) {
            return (LottieAnimationView) view;
        }
        if (view instanceof ViewGroup) {
            ViewGroup group = (ViewGroup) view;
            for (int i = 0; i < group.getChildCount(); i++) {
                LottieAnimationView result = findLottieView(group.getChildAt(i));
                if (result != null) return result;
            }
        }
        return null;
    }

    public int dipToPx(float dipValue) {
        float scale = getContext().getResources().getDisplayMetrics().density;
        return (int) (dipValue * scale + 0.5f);
    }
}
4.3 在xml中使用LottieBottomNavigationView

在xml中使用:

xml 复制代码
<com.example.musicapp.Heapler.LottieBottomNavigationView
        android:id="@+id/bottom_navigation"
        android:layout_width="match_parent"
        android:layout_height="90dp"
        android:background="@color/white"
        app:labelVisibilityMode="labeled"
        app:itemActiveIndicatorStyle="@null"
        app:itemRippleColor="@null"
        app:itemGravity="center"
        android:padding="0dp"
        app:itemPaddingTop="13dp"
        app:itemPaddingBottom="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:menu="@menu/bottom_menu" />

com.example.musicapp.Heapler这部分换成你自己的包名,基本用法和BottomNavigationView基本一致,自定义menu菜单并引用,另外,在menu菜单中正常设置一个随意的占位图,纯色背景也行,运行时会自动替换成Lottie动画。如果不想要BottomNavigationView的点击水波纹效果,可以设置app:itemActiveIndicatorStyle="@null"app:itemRippleColor="@null"

4.4 在代码中设置Lottie图标
java 复制代码
// 获取自定义的 BottomNavigationView
LottieBottomNavigationView lottieBottomNavigationView = findViewById(R.id.bottom_navigation);

// 设置每个菜单项的 Lottie 动画
lottieBottomNavigationView.setLottieIcon(R.id.opt_home, "ic_home.json");
lottieBottomNavigationView.setLottieIcon(R.id.opt_browse, "ic_video.json");
lottieBottomNavigationView.setLottieIcon(R.id.opt_message, "ic_message.json");
lottieBottomNavigationView.setLottieIcon(R.id.opt_shopping_car, "ic_shopping_car.json");
lottieBottomNavigationView.setLottieIcon(R.id.opt_mine, "ic_mine.json");

使用起来非常简单,初始化LottieBottomNavigationView,并为每一item项设置动画Json文件,就可以实现非常炫酷的导航栏点击动画。

分享一些Android学习之余看到的有趣的小知识,希望大家喜欢~

相关推荐
崎岖Qiu8 小时前
【设计模式笔记10】:简单工厂模式示例
java·笔记·设计模式·简单工厂模式
cj6341181508 小时前
网卡驱动架构以及源码分析
java·后端
Sincerelyplz8 小时前
【JDK新特性】分代ZGC到底做了哪些优化?
java·jvm·后端
玛卡巴卡019 小时前
Maven 从入门到实战:搞定依赖管理与 Spring Boot 项目构建
java·spring boot·maven
vortex59 小时前
用 Scoop 快速部署 JeecgBoot 开发环境:从依赖安装到服务管理
java·windows·springboot·web·开发·jeecg-boot
جيون داد ناالام ميづ9 小时前
Spring Boot 核心原理(一):基础认知篇
java·spring boot·后端
fantasy5_510 小时前
手撕vector:从零实现一个C++动态数组
java·开发语言·c++
十八旬10 小时前
RuoYi-Vue3项目定制修改全攻略
java·windows
任风雨10 小时前
3.1.1.Java基础知识
java·开发语言