【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/(虽然动画少点,不过质...
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_autoPlay,app: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类继承自BottomNavigationView在updateMenuItemIcon方法中调用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学习之余看到的有趣的小知识,希望大家喜欢~