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 标签显示模式
2. Lottie
对于这个东东,不知道说啥。忽略吧。
想起来都是累,韩总让我自己折腾 Lottie json 文件。哭唧唧
安利一个在线编辑 Lottie json 文件的地址:
实战部分
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 不改变,怎么玩?
这个问题是我从一开始就陷入了固有思维循环中。
下面是我陷入误区的思路:
整整折腾了好久,折腾到韩总说,不行咱就放弃吧。
想想鸡老大,怎能轻易放弃?
昨天突然想到,为什么我不重新给设置一次 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,好扎心。
参考资料
欢迎各位关注