BottomNavigationView從入門到強行改造,取消位移動畫微姊?和ViewPager綁定?添加Badge分预?

原創(chuàng)版權申明:本文章從本人 csdn 博客轉到簡書兢交。
如有轉載,請申明:轉載自 IT天宇http://www.reibang.com/p/56ae38b2433d

前言

BottomNavigationView 這個官方控件出了幾個月了笼痹,也有一些介紹該控件的文章配喳,但我發(fā)現(xiàn)大部分博文只是做了簡單的用法介紹,并未解決一些需求凳干,比如:取消位移動畫晴裹、和ViewPager一起使用、加入Badge救赐。所以我又寫了這么一篇博客涧团。

考慮到一些人可能沒時間看到最后,我把改造的庫地址放在最前面 BottomNavigationViewEx经磅。

基本用法

1. 添加依賴

compile 'com.android.support:design:25.1.0'

這里添加的是 25.1.0泌绣,因為 25.0.0 版本有一個小bug,就是設置點擊監(jiān)聽事件的返回值不起作用预厌。

2. 在 xml 中使用庫

<android.support.design.widget.BottomNavigationView
    android:id="@+id/bnve"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:background="@color/colorPrimary"
    app:itemIconTint="@color/selector_item_color"
    app:itemTextColor="@color/selector_item_color"
    app:menu="@menu/menu_navigation_with_view_pager" />

background : 控件背景
app:itemBackground : 子菜單背景
app:itemIconTint : 圖標顏色
app:itemTextColor : 文本顏色
app:menu : 菜單

這里我把背景設置成主色調 colorPrimary阿迈,圖標和文本設置為一樣的顏色 selector_item_color,具體內容如下:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="#fff" android:state_checked="true"/>
    <item android:color="#fff" android:state_pressed="true"/>
    <item android:color="#bbb"/>
</selector>

也就是選中的時候是白色轧叽,默認為灰色苗沧。

最后是菜單 menu_navigation_with_view_pager ,和普通菜單一樣炭晒。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/menu_music"
        android:checked="true"
        android:icon="@drawable/ic_audiotrack_black_24dp"
        android:title="@string/music" />
    <item
        android:id="@+id/menu_backup"
        android:icon="@drawable/ic_backup_black_24dp"
        android:title="@string/backup" />
    <item
        android:id="@+id/menu_friends"
        android:icon="@drawable/ic_camera_black_24dp"
        android:title="@string/friends" />
</menu>

菜單的圖片建議用矢量圖片待逞,也就是 svg 導入后的xml文件。

最后運行出來是這樣的网严。

看到這里识樱,我只能說,沒毛病。

3. 方法

如果去看類的文檔牺荠,就會發(fā)現(xiàn)翁巍,公開的方法中,常用的就只有 setOnNavigationItemSelectedListener 休雌。

也就是設置一個點擊的監(jiān)聽器灶壶。

// set listener to do something then item selected
bnve.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
        Log.d(TAG, item.getItemId() + " item was selected-------------------");
        // you can return false to cancel select
        return true;
    }
});

回調方法有個返回值,如果返回 false杈曲,則你的點擊會被取消驰凛,也就是不會切換到下一個菜單。
回調方法中給的參數是 MenuItem担扑,你可以獲取到被點擊菜單的 id恰响,也就是你可以這么做。

int id = 0;
switch (item.getItemId()) {
    case R.id.menu_music:
        id = 0;
        break;
    case R.id.menu_backup:
        id = 1;
        break;
    case R.id.menu_friends:
        id = 2;
        break;
}
vp.setCurrentItem(id, false);// 改變的 ViewPager 的當前頁面

貌似依舊沒毛病涌献,官方的庫用法簡單實用胚宦。

官方庫的需求問題

1. 和 ViewPager 一起使用

但仔細想一下,如果我想滑動 ViewPager 時燕垃,順便改變控件的選中項(Material Design 反對這樣設計枢劝,但需求確實存在)。

2. 取消位移動畫

如果你的菜單數大于3個卜壕,則界面是這樣的您旁。


如果 PM 非要你改成沒有動畫的效果,如下圖轴捎,這庫是不是就很難用了鹤盒?


3. 加入 Badge

對于底部導航欄,一個帶數字的小紅圈是很常見的需求侦副,對于這種需求侦锯,又該怎么辦?


動手改造

由于種種原因跃洛,官方的底部導航欄目前滿足不了我的需求率触,所以我產生了改造庫的想法终议。
大致有兩種途徑:

  1. 把整個控件代碼復制一份汇竭,然后進行修改。
  2. 把類包裹一層穴张,利用反射去修改细燎。

這兩種途徑各有優(yōu)缺點。

第一種直接修改的途徑皂甘,優(yōu)點是簡單直接玻驻,性能高。缺點是需要把整個控件的代碼都復制一份,每次官方對控制做出修改后璧瞬,無法享受新特性户辫。
第二種包裹的途徑,優(yōu)點是只需要針對一個類進行包裹嗤锉,不容易影響到原來類的作用渔欢。缺點是反射性能不高。

在權衡一番之后瘟忱,我選擇了第二種方式奥额。

分析源碼

要改造庫,首先得了解庫的內部原理访诱。

1. BottomNavigationView

進入 BottomNavigationView垫挨,發(fā)現(xiàn)最主要的成員是下面兩個,由變量命名可以猜測出分別是作為視圖和控制器触菜。

private final BottomNavigationMenuView mMenuView;
private final BottomNavigationPresenter mPresenter = new BottomNavigationPresenter();

然后看構造方法九榔,是把 mMenuView 添加到Layout里了。所以涡相,如果想要了解界面怎么顯示的帚屉,還得分析 BottomNavigationMenuView

addView(mMenuView, params);

2. BottomNavigationMenuView

通過對成員變量的粗略查看漾峡,發(fā)現(xiàn)以下幾個關鍵的成員攻旦。

private final OnClickListener mOnClickListener;// 點擊監(jiān)聽器
private boolean mShiftingMode = true;// 控制導航條的位移模式
private BottomNavigationItemView[] mButtons;// 子菜單View

然后再看構造函數,設置了一個點擊監(jiān)聽器生逸,接收到的是 BottomNavigationItemView牢屋,處理的是點擊子菜單的事件。

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

然而這里并沒有直接的對 mButtons 賦值槽袄,這個時候就應該去找 presenter烙无,對MVP熟悉的就知道, presenter 負責把 M 和 V 聯(lián)系起來遍尺。
在 presenter 的 updateMenuView 方法中調用了 mMenuView 中的 updateMenuView 去創(chuàng)建 mButtons截酷。而 BottomNavigationView 中負責調用 presenter。

具體調用順序如下:

BottomNavigationView()// BottomNavigationView
->
    inflateMenu(a.getResourceId(R.styleable.BottomNavigationView_menu, 0));// BottomNavigationView 
    ->
        mPresenter.updateMenuView(true);// BottomNavigationView
        ->
            mMenuView.updateMenuView();// BottomNavigationPresenter
            ->
                buildMenuView();// BottomNavigationMenuView
                ->
                    mButtons = new BottomNavigationItemView[mMenu.size()];// BottomNavigationMenuView

所以乾戏,最后控制每個子菜單怎么顯示的是 mButtons 迂苛,也就是 BottomNavigationItemView 。

3. BottomNavigationItemView

查看成員變量鼓择,發(fā)現(xiàn)負責顯示的成員三幻。

private boolean mShiftingMode;// 子菜單的位移模式
private ImageView mIcon;// 圖片
private final TextView mSmallLabel;// 小文本
private final TextView mLargeLabel;// 大文本

分析到這里,基本算是了解主線了呐能。
底部菜單是由一個一個 BottomNavigationItemView 組成念搬,而 BottomNavigationItemView 是由 ImageViewTextView 組成的。

取消位移動畫

分析完源碼后,發(fā)現(xiàn)最容易做的是取消位移動畫首妖,因為在分析過程中爷恳,我發(fā)現(xiàn)了一個重要的 boolean 成員變量,從名字就可以看出是控制位移動畫的舌仍。事實上妒貌,這猜測也是正確的,在代碼里搜索 mShiftingMode 就會發(fā)現(xiàn)根據這個變量的真假铸豁,會有兩套顯示效果灌曙。這里就不展開了节芥,畢竟不是專門分析源碼的博文。

由于變量是私有的头镊,且沒有提供 set 方法蚣驼,所以只能通過反射來做。

這個位移變量有兩處相艇,控制的內容是不一樣的,我們先看 BottomNavigationMenuView 里面的留储。

1. BottomNavigationMenuView 中的 mShiftingMode

這里的 mShiftingMode 控制的是菜單之間的寬度咙轩,具體不太好說,對照上面的圖片就容易理解的活喊,選中的寬度大。
要想修改這個變量帅矗,必須先取得 mMenuView结缚,然后在設置里面的 mShiftingMode。具體的代碼如下。

/**
 * enable the shifting mode for navigation
 *
 * @param enable It will has a shift animation if true. Otherwise all items are the same width.
 */
public void enableShiftingMode(boolean enable) {
    /*
    1. get field in this class
    private final BottomNavigationMenuView mMenuView;

    2. change field mShiftingMode value in mMenuView
    private boolean mShiftingMode = true;
     */
    // 1. get mMenuView
    BottomNavigationMenuView mMenuView = getBottomNavigationMenuView();
    // 2. change field mShiftingMode value in mMenuView
    setField(mMenuView.getClass(), mMenuView, "mShiftingMode", enable);

    mMenuView.updateMenuView();
}

這里沒有把反射細節(jié)代碼寫出來茵宪,因為反射很簡單,只是步驟繁瑣稀火,所以節(jié)省篇幅,就略過篇裁,有興趣可以查看我寫的庫的代碼赡若。

2. BottomNavigationItemView 中的 mShiftingMode

這個位移模式是只文字的顯示,如果開啟逾冬,則選擇項顯示圖標和文字黍聂,其他的只顯示圖片身腻。
修改方法和上面類似。

/**
 * enable the shifting mode for each item
 *
 * @param enable It will has a shift animation for item if true. Otherwise the item text always be shown.
 */
public void enableItemShiftingMode(boolean enable) {
    /*
    1. get field in this class
    private final BottomNavigationMenuView mMenuView;

    2. get field in this mMenuView
    private BottomNavigationItemView[] mButtons;

    3. change field mShiftingMode value in mButtons
    private boolean mShiftingMode = true;
     */
    // 1. get mMenuView
    BottomNavigationMenuView mMenuView = getBottomNavigationMenuView();
    // 2. get mButtons
    BottomNavigationItemView[] mButtons = getBottomNavigationItemViews();
    // 3. change field mShiftingMode value in mButtons
    for (BottomNavigationItemView button : mButtons) {
        setField(button.getClass(), button, "mShiftingMode", enable);
    }
    mMenuView.updateMenuView();
}

設置當前選中項

還記得在 BottomNavigationMenuView 看到的 mOnClickListener 嗎脐区?
那個就是關鍵坡椒,只要能模擬發(fā)出一個 click 事件尤溜,就能設置當前選中項。

onClick 方法需要傳遞一個 View丈攒,而且是 BottomNavigationItemView授霸。
為了調用這一方法,需要先獲取到對應位置的 BottomNavigationItemView显设。而這個 View 似曾相識辛辨。
沒錯瑟枫,就是 mButtons 指攒,只要取得了 mButtons允悦,然后獲取數組對應位置的值,就是這個參數了隙弛。
具體代碼如下:

    /**
     * set the current checked item
     *
     * @param item start from 0.
     */
    public void setCurrentItem(int item) {
        // check bounds
        if (item < 0 || item >= getMaxItemCount()) {
            throw new ArrayIndexOutOfBoundsException("item is out of bounds, we expected 0 - "
                    + (getMaxItemCount() - 1) + ". Actually " + item);
        }

        /*
        1. get field in this class
        private final BottomNavigationMenuView mMenuView;

        2. get field in mMenuView
        private BottomNavigationItemView[] mButtons;
        private final OnClickListener mOnClickListener;

        3. call mOnClickListener.onClick();
         */
        // 1. get mMenuView
        BottomNavigationMenuView mMenuView = getBottomNavigationMenuView();
        // 2. get mButtons
        BottomNavigationItemView[] mButtons = getBottomNavigationItemViews();
        // get mOnClickListener
        View.OnClickListener mOnClickListener = getField(mMenuView.getClass(), mMenuView, "mOnClickListener", View.OnClickListener.class);

//        System.out.println("mMenuView:" + mMenuView + " mButtons:" + mButtons + " mOnClickListener" + mOnClickListener);
        // 3. call mOnClickListener.onClick();
        mOnClickListener.onClick(mButtons[item]);

    }

加入 Badge

Bagde 就是字面意思全闷,一個標記室埋。一般都是一個小紅圈,里面有數字姚淆。
給控件加上 Bagde 的思路大致有以下幾種:

  1. 給控件加個紅點 ImageView
  2. 給控件圖片的 Drawable 外面套一個帶紅點的 Drawable腌逢,然后替換 Drawable。
  3. 在頂級容器上加入小紅點搏讶,調整位置媒惕,偽裝成和控件一體。

事實上這幾種方法對于底部導航欄來說都行得通妒蔚。
但實現(xiàn)起來難度不一樣肴盏。
我為了省事,直接用了第三方庫 BadgeView 贞绵。

本想采用第一種方法恍飘,但發(fā)現(xiàn)谴垫,加在圖片或 BottomNavigationItemView 上都會導致排版錯亂蜡饵。
于是嘗試第三種方案胳施,發(fā)現(xiàn)行得通。

具體代碼如下:

private void initView() {
    // disable all animations
    bind.bnve.enableAnimation(false);
    bind.bnve.enableShiftingMode(false);
    bind.bnve.enableItemShiftingMode(false);


    // add a BadgeView at second icon
    bind.bnve.post(new Runnable() {
        @Override
        public void run() {
            badgeView1 = addBadgeViewAt(1, "1", BadgeView.SHAPE_OVAL);
            badgeView3 = addBadgeViewAt(3, "99", BadgeView.SHAPE_OVAL);

            // hide the red circle when click
            bind.bnve.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
                @Override
                public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                    int position = bind.bnve.getMenuItemPosition(item);
                    switch (position) {
                        case 1:
                            toggleBadgeView(badgeView1);
                            break;
                        case 3:
                            toggleBadgeView(badgeView3);
                            break;
                    }
                    return true;
                }
            });
        }
    });

}

/**
 * show or hide badgeView
 * @param badgeView
 */
private void toggleBadgeView(BadgeView badgeView) {
    badgeView.setVisibility(badgeView.getVisibility() == View.VISIBLE ? View.INVISIBLE : View.VISIBLE);
}

/**
 * add a BadgeView on icon at position
 * @param position add to which icon
 * @param text the text show on badge
 * @param shape the badge view shape
 * @return
 */
private BadgeView addBadgeViewAt(int position, String text, int shape) {
    // get position
    ImageView icon = bind.bnve.getIconAt(position);
    int[] pos = new int[2];
    icon.getLocationInWindow(pos);
    // action bar height
    ActionBar actionBar = getSupportActionBar();
    int actionBarHeight = 0;
    if (null != actionBar) {
        actionBarHeight = actionBar.getHeight();
    }
    int x = (int) (pos[0] + icon.getMeasuredWidth() * 0.7f);
    int y = (int) (pos[1] - actionBarHeight - icon.getMeasuredHeight() * 1.25f);
    // calculate width
    int width = 16 + 4 * (text.length() - 1);
    int height = 16;

    BadgeView badgeView = BadgeFactory.create(this)
            .setTextColor(Color.WHITE)
            .setWidthAndHeight(width, height)
            .setBadgeBackground(Color.RED)
            .setTextSize(10)
            .setBadgeGravity(Gravity.LEFT | Gravity.TOP)
            .setBadgeCount(text)
            .setShape(shape)
//                .setMargin(0, 0, 0, 0)
            .bind(this.bind.rlRoot);
    badgeView.setX(x);
    badgeView.setY(y);
    return badgeView;
}

把紅點加載根布局上椿胯,然后獲取到目標圖片的位置,計算出間距就行了前方。

代碼放在了 Github 廉油,地址在最前面,若有興趣抒线,記得 star 收藏嘶炭。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市抑进,隨后出現(xiàn)的幾起案子睡陪,更是在濱河造成了極大的恐慌,老刑警劉巖户秤,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逮矛,死亡現(xiàn)場離奇詭異须鼎,居然都是意外死亡府蔗,警方通過查閱死者的電腦和手機汞窗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進店門仲吏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人裹唆,你說我怎么就攤上這事±涂樱” “怎么了成畦?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵循帐,是天一觀的道長。 經常有香客問我存和,道長衷旅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任茄袖,我火速辦了婚禮嘁锯,結果婚禮上,老公的妹妹穿的比我還像新娘蝗羊。我一直安慰自己仁锯,他們只是感情好,可當我...
    茶點故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布野芒。 她就那樣靜靜地躺著,像睡著了一般撮抓。 火紅的嫁衣襯著肌膚如雪摇锋。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天咽笼,我揣著相機與錄音,去河邊找鬼媳纬。 笑死,一個胖子當著我的面吹牛茅糜,可吹牛的內容都是我干的素挽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼缩赛,長吁一口氣:“原來是場噩夢啊……” “哼撰糠!你這毒婦竟也來了阅酪?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤砚尽,失蹤者是張志新(化名)和其女友劉穎辉词,沒想到半個月后,有當地人在樹林里發(fā)現(xiàn)了一具尸體隧魄,經...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年襟企,在試婚紗的時候發(fā)現(xiàn)自己被綠了顽悼。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片几迄。...
    茶點故事閱讀 39,764評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖木羹,靈堂內的尸體忽然破棺而出解孙,到底是詐尸還是另有隱情,我是刑警寧澤脐瑰,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布廷臼,位于F島的核電站,受9級特大地震影響寂恬,放射性物質發(fā)生泄漏结啼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一朴译、第九天 我趴在偏房一處隱蔽的房頂上張望眠寿。 院中可真熱鬧焦蘑,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽雀彼。三九已至,卻和暖如春徊哑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背著蟹。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工窒盐, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蟹漓,地道東北人源内。 一個月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像嗽交,于是被迫代替她去往敵國和親颂斜。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,665評論 2 354

推薦閱讀更多精彩內容