`
iaiai
  • 浏览: 2198078 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

BottomNavigationView

 
阅读更多
Android Support Library 25.0.0 版本中,新增加了一个API –> BottomNavigationView – 底部导航视图。

先来看看这个控件的实现效果。



基本使用

使用起来也很简单

首先在xml中引入该控件
<android.support.design.widget.BottomNavigationView
        android:id="@+id/bottom_navi_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        app:itemBackground="@android:color/white"
        app:menu="@menu/menu_bottom_navi" />

该控件的基本属性有:

引用

app:itemIconTint : 设置菜单图标着色
app:itemTextColor : 设置菜单文本颜色
app:menu : 设置菜单
app:itemBackground : 设置导航栏的背景色

@menu/menu_buttom_navi
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/menu_recent"
        android:icon="@drawable/ic_history_black_24dp"
        android:title="@string/menu_recents" />
    <item
        android:id="@+id/menu_favorites"
        android:icon="@drawable/ic_favorite_black_24dp"
        android:title="@string/menu_favorites" />
    <item
        android:id="@+id/menu_nearby"
        android:icon="@drawable/ic_place_black_24dp"
        android:title="@string/menu_nearby" />
    <item
        android:id="@+id/menu_navi"
        android:icon="@drawable/ic_navigation_black_24dp"
        android:title="@string/menu_navigation" />
</menu>

与定义普通menu布局一样。

接下来是Java代码
bottomNaviView = (BottomNavigationView) findViewById(R.id.bottom_navi_view);
        bottomNaviView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                switch (item.getItemId()) {
                    case R.id.menu_recent:
                        break;
                    case R.id.menu_favorites:
                        break;
                    case R.id.menu_nearby:
                        break;
                    case R.id.menu_navi:
                        break;
                }
                return true;
            }
        });

对BottomNavigationView设置选择监听器就可以做一些item切换事件了。

注意事项
  • 底部导航栏默认高度是56dp
  • 菜单只能是3-5个


源码分析

BottomNavigationView 有几个先关的重要类
  • BottomNavigationView
  • BottomNavigationMenu
  • BottomNavigationMenuView
  • BottomNavigationPresenter

它的设计有点类似于开发中的 MVP模式。

先来看 BottomNavigationView 的构造函数
public BottomNavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        ThemeUtils.checkAppCompatTheme(context); //检测当前主题
        // Create the menu
        mMenu = new BottomNavigationMenu(context);
        mMenuView = new BottomNavigationMenuView(context);
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        params.gravity = Gravity.CENTER;
        mMenuView.setLayoutParams(params);
        mPresenter.setBottomNavigationMenuView(mMenuView);
        mMenuView.setPresenter(mPresenter);
        mMenu.addMenuPresenter(mPresenter);
        // Custom attributes

        // ...省略若干代码

        if (a.hasValue(R.styleable.BottomNavigationView_menu)) {
            inflateMenu(a.getResourceId(R.styleable.BottomNavigationView_menu, 0)); // 加载menu
        }
        a.recycle();
        addView(mMenuView, params); //
        mMenu.setCallback(new MenuBuilder.Callback() { // 设置监听器
            @Override
            public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
                return mListener != null && mListener.onNavigationItemSelected(item);
            }
            @Override
            public void onMenuModeChange(MenuBuilder menu) {}
        });
    }

ThemeUtils.checkAppCompatTheme(context) 是用来检测当前主题的。代码很简单,就是判断是否有 colorPrimary 属性。
class ThemeUtils {
    private static final int[] APPCOMPAT_CHECK_ATTRS = {
            android.support.v7.appcompat.R.attr.colorPrimary
    };
    static void checkAppCompatTheme(Context context) {
        TypedArray a = context.obtainStyledAttributes(APPCOMPAT_CHECK_ATTRS);
        final boolean failed = !a.hasValue(0);
        if (a != null) {
            a.recycle();
        }
        if (failed) {
            throw new IllegalArgumentException("You need to use a Theme.AppCompat theme "
                    + "(or descendant) with the design library.");
        }
    }
}

构造函数中接下来调用了 inflateMenu()
public void inflateMenu(int resId) {
        mPresenter.setUpdateSuspended(true);
        getMenuInflater().inflate(resId, mMenu);
        mPresenter.initForMenu(getContext(), mMenu);
        mPresenter.setUpdateSuspended(false);
        mPresenter.updateMenuView(true);
    }

可以看出都是调用的BottomNavigationPresenter的函数。

setUpdateSuspended(true) – 暂停修改menu

setUpdateSuspended(false) — 可以修改menu

在initForMenu()一前一后,设置一个标志来表示当前正在操作menu。

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

函数内部又是调用的 BottomNavigationMenuView.initialize()
@Override
    public void initialize(MenuBuilder menu) {
        mMenu = menu;
        if (mMenu == null) return;
        if (mMenu.size() > mActiveButton) {
            mMenu.getItem(mActiveButton).setChecked(true);
        }
    }

代码中就是一些简单的初始化操作。

接下来看 BottomNavigationPresenter.updateMenuView(true)
@Override
    public void updateMenuView(boolean cleared) {
        if (mUpdateSuspended) return;
        if (cleared) {
            mMenuView.buildMenuView();
        } else {
            mMenuView.updateMenuView();
        }
    }

第一次创建Menu时,调用的是buildMenuView方法。

BottomNavigationMenuView.buildMenuView()
public void buildMenuView() {
        if (mButtons != null) {
            for (BottomNavigationItemView item : mButtons) {
                sItemPool.release(item);
            }
        }
        removeAllViews();
        mButtons = new BottomNavigationItemView[mMenu.size()];

        // 当menu item大于3个的时候,会出现缩放动画
        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);

            // 单个item -- BottomNavigationItenView 的初始化操作
            child.initialize((MenuItemImpl) mMenu.getItem(i), 0);
            child.setItemPosition(i);

            // 设置点击事件
            child.setOnClickListener(mOnClickListener);

            // 添加子视图
            addView(child);
        }
    }

在 BottomNavigationMenuView类中定义了一个Pool对象,用来缓存BottomNavigationItemView对象。
private static final Pools.Pool<BottomNavigationItemView> sItemPool = new Pools.SynchronizedPool<>(5);

通过for循环创建了nMenu.size() 个BottomNavigationItemView 对象。
BottomNavigationItemView child = getNewItem();
mButtons[i] = child;

getNewItem()
private BottomNavigationItemView getNewItem() {
        BottomNavigationItemView item = sItemPool.acquire();
        if (item == null) {
            item = new BottomNavigationItemView(getContext());
        }
        return item;
    }

getNewItem() 类似于 Message.obtain() 的机制。

接着看buildMenuView() ,方法最后面,调用了 BottomNavigationItemView.initialize() ,然后调用 addView(child) 来添加子item视图。
@Override
    public void initialize(MenuItemImpl itemData, int menuType) {
        mItemData = itemData;
        setCheckable(itemData.isCheckable());
        setChecked(itemData.isChecked());
        setEnabled(itemData.isEnabled());
        setIcon(itemData.getIcon());
        setTitle(itemData.getTitle());
        setId(itemData.getItemId());
    }

看一下 BottomNavigationItemView的构造函数
public BottomNavigationItemView(@NonNull Context context) {
        this(context, null);
    }
    public BottomNavigationItemView(@NonNull Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public BottomNavigationItemView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        final Resources res = getResources();
        int inactiveLabelSize =
                res.getDimensionPixelSize(R.dimen.design_bottom_navigation_text_size);
        int activeLabelSize = res.getDimensionPixelSize(
                R.dimen.design_bottom_navigation_active_text_size);
        mDefaultMargin = res.getDimensionPixelSize(R.dimen.design_bottom_navigation_margin);
        mShiftAmount = inactiveLabelSize - activeLabelSize;
        mScaleUpFactor = 1f * activeLabelSize / inactiveLabelSize;
        mScaleDownFactor = 1f * inactiveLabelSize / activeLabelSize;
        // 注意下面的代码
        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);
    }

代码中映射了一个布局文件

design_bottom_navigation_item.xml
<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <ImageView
        android:id="@+id/icon"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="@dimen/design_bottom_navigation_margin"
        android:layout_marginBottom="@dimen/design_bottom_navigation_margin"
        android:duplicateParentState="true" />
    <android.support.design.internal.BaselineLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="@dimen/design_bottom_navigation_margin"
        android:layout_gravity="bottom|center_horizontal"
        android:duplicateParentState="true">
        <TextView
            android:id="@+id/smallLabel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="@dimen/design_bottom_navigation_text_size"
            android:duplicateParentState="true" />
        <TextView
            android:id="@+id/largeLabel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="invisible"
            android:textSize="@dimen/design_bottom_navigation_active_text_size"
            android:duplicateParentState="true" />
    </android.support.design.internal.BaselineLayout>
</merge>

该布局文件由系统提供,包括一个ImageView和两个TextView。
引用
当菜单项大于3个,切换item时,被选中的item会将largeLabel显示,将smallLabel隐藏。然后改变ImageView和TextView的margin值达到动画效果。

在BottomNavigationMenuView的构造函数中对mAnimationHelper进行了实例化
private final BottomNavigationAnimationHelperBase mAnimationHelper;

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            mAnimationHelper = new BottomNavigationAnimationHelperIcs();
        } else {
            mAnimationHelper = new BottomNavigationAnimationHelperBase();
        }

当版本小于14(Android 4.0)时,是没有动画效果的。
class BottomNavigationAnimationHelperBase {
    void beginDelayedTransition(ViewGroup view) {
        // Do nothing.
    }
}

Android 4.0及之上的版本
class BottomNavigationAnimationHelperIcs extends BottomNavigationAnimationHelperBase {
    private static final long ACTIVE_ANIMATION_DURATION_MS = 115L;
    private final TransitionSet mSet;
    BottomNavigationAnimationHelperIcs() {
        mSet = new AutoTransition();
        mSet.setOrdering(TransitionSet.ORDERING_TOGETHER);
        mSet.setDuration(ACTIVE_ANIMATION_DURATION_MS);
        mSet.setInterpolator(new FastOutSlowInInterpolator());
        TextScale textScale = new TextScale();
        mSet.addTransition(textScale);
    }
    void beginDelayedTransition(ViewGroup view) {
        TransitionManager.beginDelayedTransition(view, mSet);
    }
}

代码中定义了文本缩放动画 – TextScale

然后来看 BottomNavigationItemView的点击事件。

在初始化这些子itemView的时候就设置了onClickListener
child.initialize((MenuItemImpl) mMenu.getItem(i), 0);
child.setItemPosition(i);
// 设置点击事件
child.setOnClickListener(mOnClickListener);

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);
            }
        };

activateNewButtom()
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; // 记录当前被激活按钮的位置
    }

先产生动画,然后切换被选中和之前选中的item的状态。
@Override
    public void setChecked(boolean checked) {
        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());
        if (mShiftingMode) {
            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);
        } else {
            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);
            }
        }
        refreshDrawableState();
    }

mShiftingMode 表示是否偏移值。当菜单个数大于3时,为true。
  • 大小: 120 KB
  • 大小: 168.6 KB
  • 大小: 22.8 KB
分享到:
评论

相关推荐

    BottomNavigationView去除菜单项动画效果代码

    在Android开发中,BottomNavigationView(底部导航视图)是一个常用组件,用于在多个视图间进行切换。在设计用户界面时,我们有时需要对它的默认行为进行自定义,比如在这个场景下,我们要去除 BottomNavigationView...

    用Fragment+Viewpager+BottomNavigationView实现界面切换

    `Fragment`、`ViewPager`和`BottomNavigationView`是Android SDK提供的重要组件,它们可以帮助开发者轻松实现这样的设计。本教程将详细解释如何利用这些组件来构建一个可以进行页面切换的App。 首先,`Fragment`是...

    BottomNavigationView和viewpager解决图片不显示只改变颜色的问题

    在Android应用开发中,BottomNavigationView是一个常用的组件,用于在底部展示多个页面间的切换导航,而ViewPager则用于实现页面滑动浏览。当这两个组件结合使用时,可能会遇到一个问题:BottomNavigationView的图标...

    BottomNavigationView实现底部导航

    在Android应用开发中,`BottomNavigationView` 是一个非常重要的组件,它用于实现底部导航功能,让用户可以在多个主要视图之间轻松切换。这个组件通常包含3到5个图标和相应的文字标签,代表应用的主要功能模块。在...

    底部导航栏控件BottomNavigationView的使用和修改样式

    在Android应用开发中,底部导航栏(BottomNavigationView)是一个常用组件,它允许用户在多个顶层导航之间进行切换,通常位于屏幕底部。这个控件在Android Design Support Library中提供,可以帮助开发者实现符合...

    Kotlin中使用BottomNavigationView实现底部导航

    在Android应用开发中,BottomNavigationView是一个非常常用的组件,它用于在底部展示多个可选的页面,使得用户可以方便地在各个功能之间切换。Kotlin作为Google推荐的Android开发语言,其简洁的语法使得与Bottom...

    BottomNavigationView实现material design 的tab选项卡效果

    BottomNavigationView是Android支持库中的一个组件,用于在底部显示一组可切换的标签,通常用于在应用的主要功能之间进行导航。这个组件遵循Material Design指南,提供了美观且易于使用的选项卡效果。 要在Android...

    BottomNavigationView底部导航demo

    【BottomNavigationView底部导航demo】是Android开发中的一个关键知识点,用于在应用的底部提供一种便捷的多页面切换方式。在Android应用设计中,底部导航栏(Bottom Navigation)是一种常见的界面元素,它允许用户...

    Android 使用BottomNavigationView实现底部导航栏

    Android 使用BottomNavigationView实现底部导航栏,在Android Support Library 25 中增加了 BottomNavigationView 控件,官方为我们提供了这样这一个控件。

    带红点的BottomNavigationView+viewpager demo

    "带红点的BottomNavigationView+viewpager demo" 这个标题表明我们将会探讨一个Android应用开发中的示例项目,该项目结合了`BottomNavigationView`和`ViewPager`,并且在底部导航栏的某个选项上显示了一个红色的...

    BottomNavigationView+viewPager

    在Android开发中,`BottomNavigationView` 和 `ViewPager` 是两种常用的组件,用于构建具有多个页面切换功能的用户界面。`BottomNavigationView` 提供了一个底部导航栏,通常包含多个图标和对应的标签,允许用户在...

    BottomNavigationView+ViewPager实现导航栏代码

    在Android应用开发中,BottomNavigationView和ViewPager是两个常用的组件,它们可以协同工作,为用户提供一个交互式的底部导航体验。本文将深入探讨如何使用这两个组件来实现一个类似小程序的底部导航栏功能。 首先...

    Android Studio 使用BottomNavigationView 实现底部 tabs (一)

    在Android应用开发中,BottomNavigationView是一种常用的组件,用于在底部展示多个可切换的视图,类似于底部导航栏。本教程将详细介绍如何在Android Studio中利用BottomNavigationView实现底部tabs功能。 首先,...

    ViewPager+BottomNavigationView的demo(AndroidStudio项目)

    在Android应用开发中,`ViewPager`和`BottomNavigationView`是两个非常重要的组件,它们用于创建交互丰富的用户界面。`ViewPager`通常用来实现页面滑动效果,而`BottomNavigationView`则常用于底部导航栏,提供多...

    BottomNavigationView+ViewPager+Fragment底部导航栏切换功能的实现

    在Android应用开发中,创建一个带有底部导航栏(BottomNavigationView)的应用是非常常见的需求。这个功能允许用户在多个页面间轻松切换,通常与ViewPager结合使用,用于管理不同的Fragment。本篇文章将详细讲解如何...

    BottomNavigationView+ViewPager+Fragment

    在Android应用开发中,`BottomNavigationView`是一种常用的组件,用于创建底部导航栏,它允许用户在不同的内容区域之间切换,通常结合`ViewPager`和`Fragment`来实现更流畅的交互体验。下面我们将深入探讨`Bottom...

    使用自定义 BottomNavigationView +Menu 快速实现界面切换效果

    通过自定义BottomNavigationView +menu 快速实现底部栏切换 ,并可以结合ViewPager +Fragment ,代码量少,实现方便

    AnAdroid底部导航——BottomNavigationView

    在Android应用开发中,BottomNavigationView是一种常见的用户界面组件,它位于屏幕底部,通常包含3到5个图标,用于在不同的页面或功能之间切换。这个组件是Google Material Design规范的一部分,可以提供良好的用户...

    BottomNavigationView》底部导向栏 和 《viewPager》的结合使用

    在Android应用开发中,`BottomNavigationView`和`ViewPager`是两个非常重要的组件,它们常常被结合起来使用,以实现用户友好的界面导航。`BottomNavigationView`是Google提供的Material Design组件,用于在屏幕底部...

    Andoid实现顶部导航栏和底部导航滑动隐藏(BottomNavigationView,CoordinatorLayout)

    本文将深入探讨如何使用`BottomNavigationView`和`CoordinatorLayout`组件来实现顶部导航栏和底部导航栏随着用户的滑动动作而进行隐藏与显示的效果。这两个组件是Android支持库中的关键组件,能够帮助开发者构建现代...

Global site tag (gtag.js) - Google Analytics