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

前言

好容易解决个问题,感觉记录一波。

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

以前大部分我的项目底部导航栏对于图片局部的实现,要么两套图 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

根本罕用属性:

属性/名称作用默认值
android:background背景色
app:itemBackgrounditem 背景色,@null 代表禁用水波纹成果
app:menu底部对应显示的布局文件
app:itemIconSizeitem icon 大小24dp
app:itemIconTintitem icon 着色器?attr/colorOnSurface at 60%
app:itemTextColoritem text 色彩?attr/colorOnSurface at 60%
app:labelVisibilityModeitem 标签显示模式LABEL_VISIBILITY_AUTO(auto)

须要独自阐明的属性:

  • app:labelVisibilityMode: item 标签显示模式

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

2. Lottie

对于这个东东,不晓得说啥。疏忽吧。

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

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

  • https://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 长按提醒怎么搞掉?

先来看个效果图:

比拟难堪的是,用了很久了,头一次某天闲来无事长按发现的这个货色,过后还好奇,没事弹个 Toast 干啥玩意?怀揣着满满的自信,去找源码对于我现实中认为的这个 Toast,没找到。

来,正好一起翻翻源码,看看是否从源码的角度去思考并解决这个问题。

进行一波简要剖析:

从上图以及理论编码过程中,咱们能够得悉,所谓的底部 item,其实只是一个数量不能超过 5 个的 Menu,而理论触发这个提醒必定是 ItemView,那么一起来看下针对 itemView 它做了什么操作?

在初始化 ItemView 时,最初有这么一段设置,一起来看下:

  @Override  public void initialize(@NonNull MenuItemImpl itemData, int menuType) {    // ...     CharSequence tooltipText = !TextUtils.isEmpty(itemData.getTooltipText())        ? itemData.getTooltipText()        : itemData.getTitle();    TooltipCompat.setTooltipText(this, tooltipText);    setVisibility(itemData.isVisible() ? View.VISIBLE : View.GONE);  }

tooltipText,工具提醒文本?有点茫然,仿佛有点意思。这里有个验证,如果 tooltipText 为空则应用 title 值,反正应用本身值,最初将最终的判断后果进行 setTooltipText,咱们一起来看看这个 set 外面干了什么?

/** * Helper class used to emulate the behavior of {@link View#setTooltipText(CharSequence)} prior * to API level 26. * */public class TooltipCompat  {    /**     * Sets the tooltip text for the view.     * <p> Prior to API 26 this method sets or clears (when tooltip is null) the view's     * OnLongClickListener and OnHoverListener. A toast-like subpanel will be created on long click     * or mouse hover.     *     * @param view the view to set the tooltip text on     * @param tooltipText the tooltip text     */    public static void setTooltipText(@NonNull View view, @Nullable CharSequence tooltipText) {        if (Build.VERSION.SDK_INT >= 26) {            view.setTooltipText(tooltipText);        } else {            TooltipCompatHandler.setTooltipText(view, tooltipText);        }    }    private TooltipCompat() {}}

正文写的倒是蛮分明的,在 Api 26 增加的一个工具提醒文本,次要用于长按或者鼠标悬停的一个提醒,相似 Toast。

持续往下看,set 到底干了啥?

    /**     * Sets the tooltip text which will be displayed in a small popup next to the view.     * // ...     * // 重点是上面参数形容,如果不须要设置 null     * @param tooltipText the tooltip text, or null if no tooltip is required     * @see #getTooltipText()     * @attr ref android.R.styleable#View_tooltipText     */    public void setTooltipText(@Nullable CharSequence tooltipText) {        if (TextUtils.isEmpty(tooltipText)) {            setFlags(0, TOOLTIP);            // hide 暗藏?鸡老大万岁            hideTooltip();            mTooltipInfo = null;        } else {            // ...         }    }

能够很明确的看到,如果不想显示这个所谓的 tooltipText 只须要将其设置为空即可。看到这里大略有个思路了,然而还是好奇弹出的这个货色是啥?持续往下找。

    @UnsupportedAppUsage    void hideTooltip() {        // ...        if (mTooltipInfo.mTooltipPopup == null) {            return;        }        mTooltipInfo.mTooltipPopup.hide();        mTooltipInfo.mTooltipPopup = null;        // ...    }

ummm,原来是个 PopupWindow。

好吧,回归正题,勾销 BottomNavigationView 长按时的 tooltipText 提醒。

循环遍历 Menu 并将 tooltipText 设置为 null。

    nav_bottom_bar.menu.forEach {        TooltipCompat.setTooltipText(mSelfActivity.findViewById(it.itemId),null)     }

来看下效果图:

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
  • 提醒