前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android Notes|BottomNavigationView 爱上 Lottie

Android Notes|BottomNavigationView 爱上 Lottie

作者头像
贺biubiu
发布2020-09-08 15:19:53
3.7K0
发布2020-09-08 15:19:53
举报
文章被收录于专栏:HLQ_StruggleHLQ_StruggleHLQ_Struggle

LZ-Says

鸡老大说:大丈夫岂能久居人下。

前言

好容易解决个问题,赶紧记录一波。

当日事当日毕,践行鸡老大,点滴积累,万一某天优秀了呢?

以前大部分项目底部导航栏关于图片部分的实现,要么两套图 selector 切换,要么通过着色器 tint 进行渲染,总之最后呈现的效果便是在点击时两张图静态切换,说 Low 吧,也还凑合,但是总是没那么高大上。

项目重构时,韩总说了,之前的方式呈现的效果太 Low 了,这次重构要求底部要动。(内心默默来句,你咋不上天。)

先来看个两者间对比效果吧~

效果对比

原有两张静态图切换:

小动画浪起来:

前期介绍

针对目前使用的 BottomNavigationView 以及 Lottie 简单记录下,以便日后遗忘直接查看。

1. BottomNavigationView 简述

简单记录,后续想到随时补充。

一般我用于底部导航栏,最多支持 5 个 item,源码有写,如下:

@RestrictTo(LIBRARY_GROUP)
public final class BottomNavigationMenu extends MenuBuilder {
  public static final int MAX_ITEM_COUNT = 5;

  public BottomNavigationMenu(Context context) {
    super(context);
  }

  @NonNull
  @Override
  public SubMenu addSubMenu(int group, int id, int categoryOrder, CharSequence title) {
    throw new UnsupportedOperationException("BottomNavigationView does not support submenus");
  }

  @Override
  protected MenuItem addInternal(int group, int id, int categoryOrder, CharSequence title) {
    // 超过 5 个则抛出异常
    if (size() + 1 > MAX_ITEM_COUNT) {
      throw new IllegalArgumentException(
          "Maximum number of items supported by BottomNavigationView is "
              + MAX_ITEM_COUNT
              + ". Limit can be checked with BottomNavigationView#getMaxItemCount()");
    }
    // ...
    return item;
  }
}

对于基本的 MenuItem Icon 选中/默认切换,一般配合 menu 来食用,例如。

<menu xmlns:android="http://schemas.android.com/apk/res/android">
  <item
      android:id="@+id/page_1"
      android:icon="@drawable/icon_1"
      android:title="@string/text_label_1"/>
  <!-- ... -->
</menu>

相对比较简单的方式,便是提供一套默认的 Icon,然后根据选中进行 tint 着色,当然,也可以通过 selector 选择器去设置对应选中以及未选中的 Icon,根据个人喜欢以及项目自行选择。

设置字体颜色,尤其默认以及选中,同样可以通过 selector 选择器进行对应设置。

而关于选中状态切换时,对应标题字体大小发生改变以及导航栏高度,都可以通过在 dimens 定义如下解决:

<!-- 处理 BottomNavigationView 点击放大 -->
<dimen name="design_bottom_navigation_active_text_size">@dimen/sp_12</dimen>
<dimen name="design_bottom_navigation_text_size">@dimen/sp_12</dimen>
<!-- 设置导航栏高度 -->
<dimen name="design_bottom_navigation_height">84dp</dimen>

对于设置角标,也就是右上角小圆点或者对应的数字,可通过获取 Badge 进行对应设置,这里简单复制官方例子:

var badge = bottomNavigation.getOrCreateBadge(menuItemId)
badge.isVisible = true
// An icon only badge will be displayed unless a number is set:
badge.number = 99

基本常用属性:

需要单独说明的属性:

app:labelVisibilityMode:item 标签显示模式

  • auto:item 少于等于 3 个时,标题处于显示状态;大于等于 4 个,选中才显示标题;
  • selected:选中才显示标题;
  • labeled:标题一直显示;
  • unlabeled:只显示 icon,不显示标题。

2. Lottie

对于这个东东,不知道说啥。忽略吧。

想起来都是累,韩总让我自己折腾 Lottie json 文件。哭唧唧

安利一个在线编辑 Lottie json 文件的地址:

  • lottiefiles.com/editor

实战部分

Step 1:导入提供的 Lottie Json 文件

新建 assets 目录,这里我做了 Android 10 深色兼容,所以需要提供深色(暗黑)模式下 Lottie 文件。

老渣男,给我的素材用不了,害我借用别人家的 App Lottie 素材。

Step 2:定义 Lottie 枚举类并封装基础数据:

enum class LottieAnimation(val value: String) { 
    // 截取「喜马拉雅」App Lottie 素材

    HOME("lottie/bottom_tab_home_page_btn.json"),
    SUBSCRIBE("lottie/bottom_tab_my_listen_btn.json"),
    DISCOVERY("lottie/bottom_tab_finding_btn.json"),
    ACCOUNT("lottie/bottom_tab_mine_btn.json"),

    HOME_NIGHT("lottie-night/bottom_tab_home_page_btn.json"),
    SUBSCRIBE_NIGHT("lottie-night/bottom_tab_my_listen_btn.json"),
    DISCOVERY_NIGHT("lottie-night/bottom_tab_finding_btn.json"),
    ACCOUNT_NIGHT("lottie-night/bottom_tab_mine_btn.json")
}

封装个 BasicData,存放 App 内置的一些基本数据,这里主要针对 Lottie 文件:

val mNavigationAnimationList = arrayListOf(
    LottieAnimation.HOME,
    LottieAnimation.SUBSCRIBE,
    LottieAnimation.DISCOVERY,
    LottieAnimation.ACCOUNT
)

val mNavigationAnimationNightList = arrayListOf(
    LottieAnimation.HOME_NIGHT,
    LottieAnimation.SUBSCRIBE_NIGHT,
    LottieAnimation.DISCOVERY_NIGHT,
    LottieAnimation.ACCOUNT_NIGHT
)

Step 3:导入对应依赖,新增 Lottie Utils

api 'com.google.android.material:material:1.2.0'
api 'com.airbnb.android:lottie:3.4.1'

工具类方法:

/**
 * 获取 Lottie Drawable
 */
fun getLottieDrawable(
    animation: LottieAnimation,
    bottomNavigationView: BottomNavigationView
): LottieDrawable {
    return LottieDrawable().apply {
        val result = LottieCompositionFactory.fromAssetSync(
            bottomNavigationView.context.applicationContext, animation.value
        )
        callback = bottomNavigationView
        composition = result.value
    }
}

/**
 * 获取不同模式下 Lottie json 文件
 */
fun getLottieAnimationList(context: Context): ArrayList<LottieAnimation> {
    return if (isDarkTheme(context)) {
        mNavigationAnimationNightList
    } else {
        mNavigationAnimationList
    }
}

判断是否深色模式我单独提取了一个工具类,Lottie-android 中也有对深色模式的兼容方法:

/**
 * 验证当前是否为深色模式
 */
fun isDarkTheme(context: Context): Boolean {
    val flag = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
    return flag == Configuration.UI_MODE_NIGHT_YES
}

Step 4:设置布局

先添加个 tab 字体选中和非选中的字体颜色 selecor:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/colorMain" android:state_checked="true" />
    <item android:color="@color/colorTitleText" android:state_checked="false" />
</selector>

整一波布局文件:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorBackground"
    tools:context=".module.home.activity.HomeActivity">

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/nav_bottom_bar"
        android:layout_width="@dimen/dp_0"
        android:layout_height="wrap_content"
        android:background="@color/colorBackground"
        app:itemIconSize="@dimen/dp_30"
        app:itemTextColor="@color/selector_menu_state_navigation"
        app:labelVisibilityMode="labeled"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <FrameLayout
        android:layout_width="@dimen/dp_0"
        android:layout_height="@dimen/dp_0"
        app:layout_constraintBottom_toTopOf="@id/nav_bottom_bar"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Step 5:初始化 BottomNavigationView 以及 Menu

private fun initBottomNavigationView() {
    nav_bottom_bar.menu.apply {
        for (i in 0 until mNavigationTitleList.size) {
            add(Menu.NONE, i, Menu.NONE, mNavigationTitleList[i])
        }
        setLottieDrawable(getLottieAnimationList(mSelfActivity))
    }
    initEvent()
}

private fun initEvent() {
    nav_bottom_bar.setOnNavigationItemSelectedListener(this)
    nav_bottom_bar.setOnNavigationItemReselectedListener(this)
    // 默认选中第一个
    nav_bottom_bar.selectedItemId = 0
    // 处理长按 MenuItem 提示 TooltipText
    nav_bottom_bar.menu.forEach {
        val menuItemView = mSelfActivity.findViewById(it.itemId) as BottomNavigationItemView
        menuItemView.setOnLongClickListener {
            true
        }
    }
}

private fun Menu.setLottieDrawable(lottieAnimationList: ArrayList<LottieAnimation>) {
    for (i in 0 until mNavigationTitleList.size) {
        findItem(i)?.icon =
            getLottieDrawable(lottieAnimationList[i], nav_bottom_bar)
    }
}

override fun onNavigationItemSelected(item: MenuItem): Boolean {
    handleNavigationItem(item)
    return true
}

override fun onNavigationItemReselected(item: MenuItem) {
    handleNavigationItem(item)
}

private fun handleNavigationItem(item: MenuItem) {
    handlePlayLottieAnimation(item)
    mPreClickPosition = item.itemId
}

private fun handlePlayLottieAnimation(item: MenuItem) {
    val currentIcon = item.icon as? LottieDrawable
    currentIcon?.apply {
        playAnimation()
    }
    // 处理 tab 切换,icon 对应调整
    if (item.itemId != mPreClickPosition) {
        nav_bottom_bar.menu.findItem(mPreClickPosition).icon =
            getLottieDrawable(
                getLottieAnimationList(mSelfActivity)[mPreClickPosition],
                nav_bottom_bar
            )
    }
}

问题汇总

鸡老大说:

  • 遇到问题是好事儿,多总结,多积累,掌握一个循循渐进的过程。

1、BottomNavigationView 切换对应的 Lottie 不改变,怎么玩?

这个问题是我从一开始就陷入了固有思维循环中。

下面是我陷入误区的思路:

  • 我想着因为是通过 playAnimation 开始执行动画从而过渡到最后的颜色,那么对应的 endAnimation 应该是直接能回到初始状态。那么我直接缓存上一此点击 MenuItem 然后修改状态不就好了嘛。
  • tint 着色器修改?

整整折腾了好久,折腾到韩总说,不行咱就放弃吧。

想想鸡老大,怎能轻易放弃?

昨天突然想到,为什么我不重新给设置一次 Drawable 呢?反正初始的 Drawable 就是灰色,当然也是未选中的状态,随后赶紧实战测试了一波,附上关键代码:

override fun onNavigationItemReselected(item: MenuItem) {
    handleNavigationItem(item)
}

private fun handleNavigationItem(item: MenuItem) {
    handlePlayLottieAnimation(item)
    mPreClickPosition = item.itemId
}

private fun handlePlayLottieAnimation(item: MenuItem) {
    val currentIcon = item.icon as? LottieDrawable
    currentIcon?.apply {
        playAnimation()
    }
    // 这里判断如果当前点击的和上一次点击索引不同,则将上一次点击索引位置的 MenuItem Icon 替换
    if (item.itemId != mPreClickPosition) {
        // 获取到上一个 MenuItem 并修改对应的 icon drawable
        nav_bottom_bar.menu.findItem(mPreClickPosition).icon =
            getLottieDrawable(
                getLottieAnimationList(mSelfActivity)[mPreClickPosition],
                nav_bottom_bar
            )
    }
}

具体代码参考文章实战部分。

小教训(心得):

真的是有时候不得不换种思维方式,首要的便是实现,随后才是优化。基本雏形都没有,何谈优化?

  • 身为猿猿,面对实际开发中遇到的问题,一定要采取多方案,首要保证内容、结果的输出,其次才是合理的循循渐进的优化。

2、BottomNavigationView Item 长按提示怎么搞掉?

先来看个效果图:

ummm。不对呀。首次进来两个 Tab 长按符合预期,后续呢?

ummm,或者,我直接断了丫的念想?直接拦截长按事件一波?

nav_bottom_bar.menu.forEach {
    val menuItemView = mSelfActivity.findViewById(it.itemId) as BottomNavigationItemView
    menuItemView.setOnLongClickListener {
        true
    }
}

运行一波看看?

ummm,好扎心。

参考资料

  • BottomNavigationView
  • Material design - Bottom Navigation
  • lottie
  • lottie-android
  • 提示

欢迎各位关注

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-09-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 贺biubiu 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档