BottomNavigationView分析

首先先打个广告,自己写了一个AndroidBottomNavigation,扩展官方BottomNavigationView的功能,而且实现起来更加简便。
Bottom-Navigation是谷歌官方发布的android底部状态栏,它的动画效果非常的漂亮,看起来非常的让人赏心悦目。为了能够拥有相同的用户体验,google对它有着严格的设计标准,具体的要求和实例请看:官方文档。同时,谷歌还推出了BottomNavigationView来实现这种设计。那下面就来看看BottomNavigationView是如何实现的。

简单使用

通过BottomNavigationView的官方文档,我们可以看到,BottomNavigationView是在version 25.0.0以后被添加进来的,所以在此之前的版本,要使用就需要添加的包:compile 'com.android.support:design:25.0.0'。同时,官方还给出了简单的使用实例,这里就不在介绍了。

<android.support.design.widget.BottomNavigationView
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/navigation"
     android:layout_width="match_parent"
     android:layout_height="56dp"
     android:layout_gravity="start"
     app:menu="@menu/my_navigation_items" />

 res/menu/my_navigation_items.xml:
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
     <item android:id="@+id/action_search"
          android:title="@string/menu_search"
          android:icon="@drawable/ic_search" />
     <item android:id="@+id/action_settings"
          android:title="@string/menu_settings"
          android:icon="@drawable/ic_add" />
     <item android:id="@+id/action_navigation"
          android:title="@string/menu_navigation"
          android:icon="@drawable/ic_action_navigation_menu" />
 </menu>

这里我们看到,BottomNavigationView的高度被限定在56dp,这个值是在官方的设计文档中明确要求的,因此当你使用大于56dp的高度时,就会有一部分空白区域流出来,同时也完全不建议使用过小的高度,这样,内部的图标或文字都可能会被裁剪掉部分。

实现原理

BottomNavigationView分析

通过阅读BottomNavigationView源码,我们看到BottomNavigationView直接通过继承FrameLayout实现。它里面最重要的有3个对象:MenuBuilderBottomNavigationMenuViewBottomNavigationPresenter。从他们的命名上,我们就可以知道,MenuBuilder主要是创建一个menu,通过xml文件,创建menu后,再将其中的item的title、icon、id等信息传递给BottomNavigationMenuView去创建最终我们看到的view,同时,也将view的点击事件通过menu的回调传回到BottomNavigationMenuViewBottomNavigationPresenter则主要是进行一些逻辑的操作,比如初始化BottomNavigationMenuView,更新BottomNavigationMenuView等;BottomNavigationMenuView则是具体我们所看到的view,它通过MenuBuilder来创建item,同时根据click来进行样式的变化。
除了这三个之外,BottomNavigationView其他部分都是一些参数的设置和初始化,这边就不再介绍了。

BottomNavigationMenuView分析

通过上面我们可以看到,所有的一切都是围绕BottomNavigationMenuView所展开,所以我们重点通过BottomNavigationMenuView来了解整个流程。

初始化

BottomNavigationView的构造方法里,程序在创建完这3个对象后,首先对MenuBuilder进行初始化:

public void inflateMenu(int resId) {
        mPresenter.setUpdateSuspended(true);
        getMenuInflater().inflate(resId, mMenu);//初始化menu
        mPresenter.initForMenu(getContext(), mMenu);
        mPresenter.setUpdateSuspended(false);
        mPresenter.updateMenuView(true);
    }

在初始化menu前,先对BottomNavigationPresenter进行暂停,同样的事情还出现在BottomNavigationMenuView初始化各个item和每次进行动画时。这样做可以避免在初始化和动画时同时在进行更新动画而冲突。
初始化MenuBuilder后,再通过BottomNavigationPresenterBottomNavigationMenuView进行初始化:

@Override
  public void initForMenu(Context context, MenuBuilder menu) {
      mMenuView.initialize(mMenu);
      mMenu = menu;
  }

同时进行界面创建:

@Override
 public void updateMenuView(boolean cleared) {
     if (mUpdateSuspended) return;
     if (cleared) {
         mMenuView.buildMenuView();
     } else {
         mMenuView.updateMenuView();
     }
 }

具体界面创建的方法:

public void buildMenuView() {
       if (mButtons != null) {
           for (BottomNavigationItemView item : mButtons) {
               sItemPool.release(item);
           }
       }
       removeAllViews();
       mButtons = new BottomNavigationItemView[mMenu.size()];
       mShiftingMode = mMenu.size() > 3;
       for (int i = 0; i < mMenu.size(); i++) {
           mPresenter.setUpdateSuspended(true);
           mMenu.getItem(i).setCheckable(true);
           mPresenter.setUpdateSuspended(false);
           BottomNavigationItemView child = getNewItem();
           mButtons[i] = child;
           child.setIconTintList(mItemIconTint);
           child.setTextColor(mItemTextColor);
           child.setItemBackground(mItemBackgroundRes);
           child.setShiftingMode(mShiftingMode);
           child.initialize((MenuItemImpl) mMenu.getItem(i), 0);
           child.setItemPosition(i);
           child.setOnClickListener(mOnClickListener);
           addView(child);
       }
   }

这里我们看到有一个池sItemPool,当界面重构时,会把原来已有的BottomNavigationItemView放到池中,再次创建新界面时又从池中取出,这样做可以减少对象的创建数量。同时,程序会根据menu的item数量创建BottomNavigationItemView数组,而BottomNavigationItemView就是显示的每一个菜单按钮。里面有3个控件:

LayoutInflater.from(context).inflate(R.layout.design_bottom_navigation_item, this, true);
        setBackgroundResource(R.drawable.design_bottom_navigation_item_background);
        mIcon = (ImageView) findViewById(R.id.icon);
        mSmallLabel = (TextView) findViewById(R.id.smallLabel);
        mLargeLabel = (TextView) findViewById(R.id.largeLabel);

这些就是一个item所显示的内容。

onMeasure和onLayout

BottomNavigationMenuView初始化完成之后,就要对里面的控件进行测量和排列。
在onMeasure方法中,做的主要是两件是:1是对里面每一个BottomNavigationItemView都进行宽高的测量;2是设置整个BottomNavigationMenuView的宽高。
第一步的测量还分两种情况,当item的数量大于3个时,mShiftingMode=true。在这种情况下,选中的item和其他的items的宽度是不一样的,所以程序要先计算出选中的item的宽度,然后根据它计算其他items的宽度;第二种情况是当items的数量<=3个时,每个item的宽度是一样的,所以只需要根据总宽度/items的数量就可以计算出item的宽度。
第二步在测量整个view的宽度时,程序将先前的所有可见的items的宽度加起来作为整个BottomNavigationMenuView的宽度(目前也没有发现有什么可能会使item不可见)。
onLayout方法就比较简单,它根据之前计算好的每一个item的宽高,从左往右或从右往左放置每一个item的位置。

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        final int count = getChildCount();
        final int width = right - left;
        final int height = bottom - top;
        int used = 0;
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() == GONE) {
                continue;
            }
            if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL) {
                child.layout(width - used - child.getMeasuredWidth(), 0, width - used, height);
            } else {
                child.layout(used, 0, child.getMeasuredWidth() + used, height);
            }
            used += child.getMeasuredWidth();
        }
    }

点击动画和回调

在初始化BottomNavigationMenuView时,每一个BottomNavigationItemView都会添加onClickListener:

mOnClickListener = new OnClickListener() {
           @Override
           public void onClick(View v) {
               final BottomNavigationItemView itemView = (BottomNavigationItemView) v;
               final int itemPosition = itemView.getItemPosition();
               activateNewButton(itemPosition);
               mMenu.performItemAction(itemView.getItemData(), mPresenter, 0);
           }
       };

关键的代码是后面两句,其中一句是执行点击的动画,最后一句是执行menu点击的回调。那我们分别来看一下。

private void activateNewButton(int newButton) {
        if (mActiveButton == newButton) return;

        mAnimationHelper.beginDelayedTransition(this);

        mPresenter.setUpdateSuspended(true);
        mButtons[mActiveButton].setChecked(false);
        mButtons[newButton].setChecked(true);
        mPresenter.setUpdateSuspended(false);

        mActiveButton = newButton;
    }

在这里我们看到,主要的操作就是将原来的BottomNavigationItemViewcheck设置为false,将点击的设置为true,那我们来看BottomNavigationItemView的setCheck方法里又做了什么。
setCheck方法也是整个BottomNavigationItemView最核心的方法。

mItemData.setChecked(checked);

       ViewCompat.setPivotX(mLargeLabel, mLargeLabel.getWidth() / 2);
       ViewCompat.setPivotY(mLargeLabel, mLargeLabel.getBaseline());
       ViewCompat.setPivotX(mSmallLabel, mSmallLabel.getWidth() / 2);
       ViewCompat.setPivotY(mSmallLabel, mSmallLabel.getBaseline());

首先,它将设置menu的item是否为点击;设置两个文本的动画原点。

if (checked) {
               LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
               iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
               iconParams.topMargin = mDefaultMargin;
               mIcon.setLayoutParams(iconParams);
               mLargeLabel.setVisibility(VISIBLE);
               ViewCompat.setScaleX(mLargeLabel, 1f);
               ViewCompat.setScaleY(mLargeLabel, 1f);
           } else {
               LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
               iconParams.gravity = Gravity.CENTER;
               iconParams.topMargin = mDefaultMargin;
               mIcon.setLayoutParams(iconParams);
               mLargeLabel.setVisibility(INVISIBLE);
               ViewCompat.setScaleX(mLargeLabel, 0.5f);
               ViewCompat.setScaleY(mLargeLabel, 0.5f);
           }
           mSmallLabel.setVisibility(INVISIBLE);

在有移动的情况下,对选中和非选中都进行动画操作,同时,大文本显示,小文本隐藏。

if (checked) {
                LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
                iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
                iconParams.topMargin = mDefaultMargin + mShiftAmount;
                mIcon.setLayoutParams(iconParams);
                mLargeLabel.setVisibility(VISIBLE);
                mSmallLabel.setVisibility(INVISIBLE);

                ViewCompat.setScaleX(mLargeLabel, 1f);
                ViewCompat.setScaleY(mLargeLabel, 1f);
                ViewCompat.setScaleX(mSmallLabel, mScaleUpFactor);
                ViewCompat.setScaleY(mSmallLabel, mScaleUpFactor);
            } else {
                LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
                iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
                iconParams.topMargin = mDefaultMargin;
                mIcon.setLayoutParams(iconParams);
                mLargeLabel.setVisibility(INVISIBLE);
                mSmallLabel.setVisibility(VISIBLE);

                ViewCompat.setScaleX(mLargeLabel, mScaleDownFactor);
                ViewCompat.setScaleY(mLargeLabel, mScaleDownFactor);
                ViewCompat.setScaleX(mSmallLabel, 1f);
                ViewCompat.setScaleY(mSmallLabel, 1f);
            }

在不移动情况下,对icon的上距进行变化,同时选中时小文本变大,不选择时大文本变小文本。
在点击回调时,执行mMenu.performItemAction (itemView.getItemData(), mPresenter, 0);代码,该代码会调用MenuItemImplinvoke方法,并且最终调用callback回调。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,717评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,501评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,311评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,417评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,500评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,538评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,557评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,310评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,759评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,065评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,233评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,909评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,548评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,172评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,420评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,103评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,098评论 2 352

推荐阅读更多精彩内容