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回调。


阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭