zukkeyの技術奮闘記

”当たり前”が誰かのためになる、はず

実例で学ぶBottomNavigationView

はじめに

突然ですが、皆さんは画面下部につけるタブバー?はどのように実装されていますか?

 

僕は以前はGridViewをなんとか使って実装していました。

サードパーティーのライブラリを使って実装している人もいると思います。

 

ですが、Androidには標準で用意されているBottomNavigationViewというものがあります。

今回はそのBottomNavigationViewについての紹介です。

 

今回話す内容は、

 ・BottomNavigationViewのサンプルの作成の仕方

 ・タブの幅を一定にする方法

 ・タブアイテムの文字の表示 / 非表示の仕方

について話します。

 

今回作成したサンプルはGitHubにも上げているので作成するのが面倒な人はクローンしてビルドしてください。

GitHub - yutaro6547/BottomNavigationSample

 

まずはプロジェクトを作成します

BottomNavigationViewを学ぶにはサンプル自体が標準で簡単に作成できるので普通に作って動かしてみましょう!

くどいくらい初っ端から説明していくのでプロジェクトの作り方を知っている人はこのセクションは飛ばしちゃってくださいな!

 

ともかくプロジェクトを作る

f:id:rozkey59:20180211220343p:plain

 File > New > New Projectから作成できます。

ApplicationNameには好きな名前を付けてください。Company domainも同様に。

Kotlinで書くので入っていなければ、Include Kotlin supportにチェックを入れておいてください。

 

つぎにターゲットバージョンを選ぶ

f:id:rozkey59:20180211220720p:plain

APIのレベルによってどのバージョンから対応するか選んだり端末など選べる画面です。今だと古い端末だと4系使っている人もいるのですがLolipopからが多いのでここは気にせずNextを押していきましょう。

 

今回はBottomNavigationActivityを選択する

f:id:rozkey59:20180211220922p:plain

BottomNavigationViewの機能の確認をしたいので、上の画像のように選択してください。勝手にある程度用意されたサンプルが作られた状態で始められます。

 

最初の画面の選択

f:id:rozkey59:20180211221109p:plain

ここは特に何も考えずFinishを押しましょう!名前を変えたい場合は変えてもよいですがそのままでOKです。

Finishを選択することでプロジェクトが勝手に作成されます。

 

通常状態

さて、上記のように作成し終えてビルドすると下記のようにサンプルを作成することができると思います。

f:id:rozkey59:20180211221309g:plain

エミュレータ上で実行すると上記のように表示が確認できサンプルを動かすことができます。

ここからちょっとずつカスタムしていきましょう!

 

まずはタブのアイテムを一個増やす

イコン画像を適当に用意しましょう

material.io

上のサイトから適当にアイコンをダウンロードしてきてください。

svgが良いと思います。

app > res > drawableを右クリックしてNew > Vector Assetからさっきダウンロードしたやつを追加することができます。

f:id:rozkey59:20180211222650p:plain

上記画像のように選択し終えたらnextを押してfinishを選択することで画像を入れられます。

 

そのあとは、app > res > menu > navigation.xmlを下記のように変更してください。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

<item
android:id="@+id/navigation_home"
android:icon="@drawable/ic_home_black_24dp"
android:title="@string/title_home"/>

<item
android:id="@+id/navigation_dashboard"
android:icon="@drawable/ic_dashboard_black_24dp"
android:title="@string/title_dashboard"/>

<item
android:id="@+id/navigation_notifications"
android:icon="@drawable/ic_notifications_black_24dp"
android:title="@string/title_notifications"/>

<item
android:id="@+id/navigation_android"
android:icon="@drawable/ic_android_black_24px"
android:title="@string/title_android"
/>

</menu>

 icon名はさっきのとこで指定したものを、idも適当につけちゃって良いです。

titleはstring.xmlのなかで新たに指定するかすでにあるものを利用してください。

app > res > values > string.xmlにて

<resources>
<string name="app_name">BottonNavigationSample</string>
<string name="title_home">Home</string>
<string name="title_dashboard">Dashboard</string>
<string name="title_notifications">Notifications</string>
<string name="title_android">Android</string> //追加
</resources>

こんな感じで好きな名前を付けて追加しましょう。

app > java > xxx.yyy(パッケージ名)の中にあるMainActivity.ktにて下記のように追加します。

private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.navigation_home -> {
message.setText(R.string.title_home)
return@OnNavigationItemSelectedListener true
}
R.id.navigation_dashboard -> {
message.setText(R.string.title_dashboard)
return@OnNavigationItemSelectedListener true
}
R.id.navigation_notifications -> {
message.setText(R.string.title_notifications)
return@OnNavigationItemSelectedListener true
}
// 追加
R.id.navigation_android -> {
message.setText(R.string.title_android)
return@OnNavigationItemSelectedListener true
}
}
false
}

ここではidを指定することで条件分岐を行っており、タップした際にviewの中で表示されているテキストを変えるということを行っています。

 

これでようやく一つのタブアイテムを追加することができます。

 

ここまで来てビルドをすると、以下のようになります。

f:id:rozkey59:20180211223517g:plain

自分で設定したアイコン画像と文字が表示されていることが確認できます。

 

同じようにしてもう一つ追加してみる

先ほどと同じような流れで一通り変更してみると以下のようになります。

f:id:rozkey59:20180211223804g:plain

こうしてみるとタブを選択したときに幅が広がって気持ち悪い感じがしますよね?

 

3つの時は問題なかったのですが、タブのアイテムが増えたときに選択したアイテムが広がってしまうのを防ぎたかったりしたいし、名前とか表示したくないとか色々とカスタムしたいと思います。

 

なので、今回は以下の内容を解説していこうかなと思います。

 ・タブの幅を一定にする

 ・選択したときのアイテムのタイトル名を消す

 ・逆に選択に関わらずアイテムのタイトル名を表示する

 

タブの幅を一定にする

タップした際に選択したタブの幅が広がってしまうのを防ぐということをやっていきたいと思います。

そのためにはカスタムViewを作る必要があります。

stackOverFlowでググると下記のように出てきますので同じことをやって多少変更を加えていきます。

stackoverflow.com

 

まずは適当にクラスを作ります。

f:id:rozkey59:20180212115637p:plain

白い矢印のとこで右クリックしてNew > Kotlin File / Classから作成します。

名前は何でも良いです。僕はCustomBottomNavigationViewにしました。

 

本来ならui > component > viewなどディレクトリを分けたりした方が良いですが、今回は学んでいくということが目的なので適当です。

 

下記のようにカスタムしたクラスを作っちゃってください!

class CustomBottomNavigationView : BottomNavigationView {

constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
disableShiftMode()
}

private fun disableShiftMode() {
val menuView = getBottomMenuView()!!
try {
val shiftingMode = menuView.javaClass.getDeclaredField("mShiftingMode")
shiftingMode.isAccessible = true
shiftingMode.setBoolean(menuView, false)
shiftingMode.isAccessible = false
} catch (e: NoSuchFieldException) {
Log.e("FieldError", e.message)
} catch (e: IllegalAccessException) {
Log.e("IllegalAccessError", e.message)
}
}

private fun getBottomMenuView(): BottomNavigationMenuView? {
var menuView: Any? = null
try {
val field = BottomNavigationView::class.java.getDeclaredField("mMenuView")
field.isAccessible = true
menuView = field.get(this)
} catch (e: NoSuchFieldException) {
Log.e("FieldError", e.message)
} catch (e: IllegalAccessException) {
Log.e("IllegalAccessError", e.message)
}
return menuView as BottomNavigationMenuView?
}
}

フィールドにアクセスしてリフレクションを使うことで無効化しています。

 

リフレクションとは?

プログラム自身の構造を読み取ったり書き換えたりすることです。

リフレクション (情報工学) - Wikipedia

 

元のコードを書き換えているのであまりやらない方がいいことですが、現状BottonNavigationViewのアニメーションをしないようにするにはこの方法しかないようです。DroidKaigi2018のアプリでもこの方法をとっていたと思います。

 

カスタムしたBottomNavigationをactivity_main.xml(BottomNavigationViewを使用しているレイアウト)の中で使用しましょう!


<android.support.design.widget.BottomNavigationView
android:id="@+id/navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="0dp"
android:layout_marginStart="0dp"
android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/navigation"/>

 xmlの中でこの部分を下記のように変更します。

//変更
<zukkey.bottonnavigationsample.CustomBottomNavigationView
android:id="@+id/navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="0dp"
android:layout_marginStart="0dp"
android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/navigation"/>

一行目だけ変えました!

 

これで実行をすると下記のようになります。

f:id:rozkey59:20180212123027g:plain

 

stackOverFlowのようにやる方法だとcompanion objectの中でdisableShiftModeを作成して任意のタイミングで呼び出す必要があったりしますが先ほどのように内部で完結させてあげるとxmlを変更するだけで良いです。

 

リフレクションを使用しているので、proguardで難読化のルール追加を忘れずに行いましょう。

Projectの中のBottomNavigationSample > app > proguard-rules.proに下記のように付け加えてください。

-keepclassmembers class android.support.design.internal.BottomNavigationMenuView {
boolean mShiftingMode;
}

これをしないとproguardでmShiftingModeフィールドを難読化する場合は機能することはありません。

 

選択したときにタブアイテムの文字を非表示にする方法

選択したときにアイコンだけにしたい人もいると思います。

その方法についてです。先ほど、CustomしたBottomNavigationViewクラスにて以下の部分を付け足すことで可能になります。

@SuppressLint("RestrictedApi")
private fun disableSmallLabel() {
val menuView = getBottomMenuView()!!
try {
for (i in 0 until menuView.childCount) {
val itemView = menuView.getChildAt(i) as BottomNavigationItemView
itemView.setShiftingMode(true)
itemView.setChecked(false)
}
} catch (e: NoSuchFieldException) {
Log.e("FieldError", e.message)
} catch (e: IllegalAccessException) {
Log.e("IllegalAccessError", e.message)
}
}

onMeasureメソッドで作成したdisableSmallLabelを呼び出すことを忘れないでください。

こうすることでタブアイテム下部のテキストを非表示にすることができます。

実際に動かしてみたものが以下になります。

f:id:rozkey59:20180212131145g:plain

 

 

逆に選択に関わらずタブアイテムの文字を表示する方法

タブアイテムに文字をずっと表示させたい場合は、CustomしたBottomNavigationViewクラスにて以下の部分を付け足すことで可能になります。


@SuppressLint("RestrictedApi")
private fun showSmallLabel() {
val menuView = getBottomMenuView()!!
try {
for (i in 0 until menuView.childCount) {
val itemView = menuView.getChildAt(i) as BottomNavigationItemView
itemView.setShiftingMode(false)
itemView.setChecked(false)
}
} catch (e: NoSuchFieldException) {
Log.e("FieldError", e.message)
} catch (e: IllegalAccessException) {
Log.e("IllegalAccessError", e.message)
}
}

前のセクションと変わったのが、itemView.setShiftingModeがfalseになったことです。

onMeasureの中でshowSmallLabelを呼び出すことも忘れずにお願いします。

先ほどのdisableSmallLabelを呼び出している場合はコメントアウトを忘れずに。

 

このように変更することで、下記のように表示が可能です。

f:id:rozkey59:20180212130352g:plain

 

まとめ

今回はBottomNavigationViewの使い方について紹介させて頂きました!

もちろんこれが正解というわけではないと思いますが、質問や間違っている部分などあればコメントをよろしくお願いします!

また、後でバッジを追加したい場合についても後ほど書こうかなと思います。

最後まで見ていただきありがとうございました。