iT邦幫忙

2022 iThome 鐵人賽

DAY 12
0

Bottom app bar

BottomAppBars 使用操作上是類似 NavigationBars,但NavigationBars使用在切換頁面BottomAppBars使用上比較彈性可以是作為切換用,也可以是畫面上的特殊操作使用。

在使用BottomAppBar 元件API支援navigation icon、action items、overflow menu等新增操做案紐,雖然官方寫optional,但官方也強烈鼓勵使用。

開始前先介紹 Material 2(M2) 和 Material 3 (M3) 差別

https://ithelp.ithome.com.tw/upload/images/20220922/20144469KN3L7YtN1i.png
Material 2 : BottomAppBar的高度為8dp,不會包含FAB。
Material 3:BottomAppBar 高度變高,沒有陰影設定,可包含FAB。


從實作角度探討

API and source code:

1. 只有使用 Action item(s)

BottomAppBar的Action item是讀取menu的item,需新增 menu.xml。

https://ithelp.ithome.com.tw/upload/images/20220922/201444699shahJFNaY.png

activity_bottom_app_bar.xml

<androidx.coordinatorlayout.widget.CoordinatorLayout 
		xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.bottomappbar.BottomAppBar
        android:id="@+id/bottomAppBar"
        style="@style/Widget.Material3.BottomAppBar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        app:menu="@menu/bottom_app_bar"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

res/menu/bottom_app_bar.xml

menu新增item時記得設ID,OnMenuItemClickListener取得itemId時會使用。

官方建議放在BottomAppBar時的item最多四個,通常menu的**attributes**可以設定是否根據操作欄位顯示空間 ****透過app:showAsAction設定。

根據需求調整app:showAsAction顯示設定,也是會影響到BottomAppBar的顯示方式

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/search"
        android:icon="@drawable/ic_search_24"
        android:title="搜尋"
        app:showAsAction="always" />

    <item
        android:id="@+id/delete"
        android:icon="@drawable/ic_delete_outline_24"
        android:title="垃圾桶"
        app:showAsAction="always" />

    <item
        android:id="@+id/archive"
        android:icon="@drawable/ic_archive_24"
        android:title="下載"
        app:showAsAction="always" />

    <item
        android:id="@+id/arrow_outward"
        android:icon="@drawable/ic_arrow_outward_24"
        android:title="點擊"
        app:showAsAction="always" />

</menu>

app:showAsAction 的相關設定

https://ithelp.ithome.com.tw/upload/images/20220922/20144469D6XSrXkuh9.png

binding.bottomAppBar.setOnMenuItemClickListener{menuItemClick->
when (menuItemClick.itemId) {
        R.id.search-> true
        R.id.delete-> true
        R.id.archive-> true
        else -> true
    }
}


2. 使用 Action item(s) + FAB

延續第一個的實作,再多新增FloatingActionButton

https://ithelp.ithome.com.tw/upload/images/20220922/20144469UVxprHRR98.png

activity_bottom_app_bar.xml

<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.bottomappbar.BottomAppBar
        android:id="@+id/bottomAppBar"
        style="@style/Widget.Material3.BottomAppBar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        app:menu="@menu/bottom_app_bar" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:srcCompat="@drawable/ic_add_24"
        **app:layout_anchor="@id/bottomAppBar"** />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

3. BottomAppBar中間懸浮FAB

  1. navigation Icon
  2. Floating Action Button
  3. action icon

https://ithelp.ithome.com.tw/upload/images/20220922/20144469vd5d6lFWGI.png

延續前面實作更改style="@style/Widget.Material3.BottomAppBar.Legacy" 即可呈現中間中間懸浮FAB

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- Note: A RecyclerView can also be used -->
    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipToPadding="false"
        android:paddingBottom="100dp">

        <!-- Scrollable content -->

    </androidx.core.widget.NestedScrollView>

    <com.google.android.material.bottomappbar.BottomAppBar
        android:id="@+id/bottomAppBar"
        style="@style/Widget.Material3.BottomAppBar.Legacy"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        app:menu="@menu/bottom_app_bar"
        app:navigationIcon="@drawable/ic_menu_24" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_anchor="@id/bottomAppBar"
        app:srcCompat="@drawable/ic_add_24" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

NavigationOnClickListener

bottomAppBar.setNavigationOnClickListener {
    // Handle navigation icon press
}

Applying scrolling behavior to the bottom app bar

顯示了當向下滾動可滾動內容時隱藏底部應用欄,並在向上滾動時出現。
https://firebasestorage.googleapis.com/v0/b/design-spec/o/projects%2Fm3%2Fimages%2Fl5vig8m9-scrolling_3P.mp4?alt=media&token=a652f69c-b761-43c2-bb25-796fb7fa8481

<androidx.coordinatorlayout.widget.CoordinatorLayout
    ...>

    ...

    <com.google.android.material.bottomappbar.BottomAppBar
        ...
        app:hideOnScroll="true"
        />

    ...

</androidx.coordinatorlayout.widget.CoordinatorLayout>

4. Theming Bottom app bars

  1. navigation Icon
  2. Floating Action Button
  3. action icon
  4. Overflow menu
    https://ithelp.ithome.com.tw/upload/images/20220922/201444690YsJTY6svP.png

Overflow menu

app:showAsAction="collapseActionViewneverwithText" 都可以收納到Overflow menu顯示。

其他ifRoomalways兩個是互斥的,ifRoom會根據裝置寬度動態顯示,always是固定顯示比較會照成少部分跑版。

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/search"
        android:icon="@drawable/ic_search_24"
        android:title="搜尋"
        app:showAsAction="always" />

    <item
        android:id="@+id/delete"
        android:icon="@drawable/ic_delete_outline_24"
        android:title="垃圾桶"
        app:showAsAction="collapseActionView" />

    <item
        android:id="@+id/archive"
        android:icon="@drawable/ic_archive_24"
        android:title="下載"
        app:showAsAction="collapseActionView" />
    definition
    <item
        android:id="@+id/arrow_outward"
        android:icon="@drawable/ic_arrow_outward_24"
        android:title="點擊"
        app:showAsAction="collapseActionView" />

</menu>

改Style → res/themes.xml

建立新專案時通常會有Base application theme預設建立的主題色,可以再新增Customize your theme,這邊示範調整顏色

<!-- Base application theme. -->
    <style name="Theme.MCDProject" parent="Theme.Material3.Light">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/purple_500</item>
        <item name="colorPrimaryVariant">@color/purple_700</item>
        <item name="colorOnPrimary">@color/white</item>
        <!-- Secondary brand color. -->
        <item name="colorSecondary">@color/teal_200</item> 
        <item name="colorSecondaryVariant">@color/teal_700</item>
        <item name="colorOnSecondary">@color/black</item> 
        <!-- Status bar color. -->
        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
        **<!-- Customize your theme here. -->**
        <item name="shapeAppearanceSmallComponent">@style/cutFab</item>
        <item name="bottomAppBarStyle">@style/Widget.App.BottomAppBar</item>
        <item name="floatingActionButtonStyle">@style/Widget.App.FloatingActionButton</item>
    </style>

    **<!--Shape Appearance -->**
   <style name="cutFab"  parent="">
        <item name="cornerFamily">cut</item>
        <item name="cornerSize">@null</item>
    </style>

    **<!--BottomAppBar 繼承主題後使用新的主題-->**
    <style name="Widget.App.BottomAppBar" parent="Widget.Material3.BottomAppBar.Legacy">
        <item name="materialThemeOverlay">@style/ThemeOverlay.App.BottomAppBar</item>
    </style>

    **<!--FloatingActionButton 繼承主題後使用新的主題-->**
    <style name="Widget.App.FloatingActionButton" parent="Widget.Material3.FloatingActionButton.Primary">
        <item name="materialThemeOverlay">@style/ThemeOverlay.App.FloatingActionButton</item>
    </style>

    **<!--BottomAppBar 調整顏色-->**
    <style name="ThemeOverlay.App.BottomAppBar" parent="">
        <item name="colorContainer">@color/purple_200</item>
        <item name="colorOnContainer">@color/purple_700</item>
    </style>

    **<!--FloatingActionButton 調整顏色-->**
    <style name="ThemeOverlay.App.FloatingActionButton" parent="">
        <item name="colorContainer">@color/purple_200</item>
        <item name="colorOnContainer">@color/purple_700</item>
    </style>

layout.xml

FAB透過自訂義Style CutFab 設定cut 設定菱型

<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- Note: A RecyclerView can also be used -->
    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipToPadding="false"
        android:paddingBottom="100dp">

        <!-- Scrollable content -->

    </androidx.core.widget.NestedScrollView>

    <com.google.android.material.bottomappbar.BottomAppBar
        android:id="@+id/bottomAppBar"
        style="@style/Widget.App.BottomAppBar" #自定義Style名稱
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        app:menu="@menu/advanced_bottom_app_bar"
        app:navigationIcon="@drawable/ic_menu_24" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        style="@style/Widget.App.FloatingActionButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_anchor="@id/bottomAppBar"
        app:shapeAppearanceOverlay="@style/cutFab" #自定義菱形的形狀Style名稱
        app:srcCompat="@drawable/ic_add_24" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

BottomAppBarCutCornersTopEdge: Class source

FAB 自定義菱形的形狀Style 完成後,再來需要調整BottomAppBar 的形狀,才能吻合菱形

https://ithelp.ithome.com.tw/upload/images/20220922/20144469FGOjqPaHmw.png

主要是繼承BottomAppBarTopEdgeTreatment然後在覆寫getEdgePath的方法,官方有提供Class source可以直接使用。

class **BottomAppBarCutCornersTopEdge** internal constructor(
    private val fabMargin: Float,
    roundedCornerRadius: Float,
    private val cradleVerticalOffset: Float
) :
    BottomAppBarTopEdgeTreatment(fabMargin, roundedCornerRadius, cradleVerticalOffset) {
    @SuppressLint("RestrictedApi")
    override fun getEdgePath(
        length: Float,
        center: Float,
        interpolation: Float,
        shapePath: ShapePath
    ) {
        val fabDiameter = fabDiameter
        if (fabDiameter == 0f) {
            shapePath.lineTo(length, 0f)
            return
        }
        val diamondSize = fabDiameter / 2f
        val middle = center + horizontalOffset
        val verticalOffsetRatio = cradleVerticalOffset / diamondSize
        if (verticalOffsetRatio >= 1.0f) {
            shapePath.lineTo(length, 0f)
            return
        }
        shapePath.lineTo(middle - (fabMargin + diamondSize - cradleVerticalOffset), 0f)
        shapePath.lineTo(middle, (diamondSize - cradleVerticalOffset + fabMargin) * interpolation)
        shapePath.lineTo(middle + (fabMargin + diamondSize - cradleVerticalOffset), 0f)
        shapePath.lineTo(length, 0f)
    }
}

activity將剛剛新增的自定義BottomAppBarCutCornersTopEdge透過bottomAppBar去設定background的MaterialShapeDrawable,setTopEdge(topEdge)。

val bottomAppBar = binding.bottomAppBar  // 這邊使用viewBinding取得bottomAppBar
val topEdge = BottomAppBarCutCornersTopEdge(
            bottomAppBar.fabCradleMargin,
            bottomAppBar.fabCradleRoundedCornerRadius,
            0.5f
)
val background = bottomAppBar.background as MaterialShapeDrawable
background.shapeAppearanceModel = background.shapeAppearanceModel
		.toBuilder()
	  .setTopEdge(topEdge)
    .build()

Anatomy and key properties

https://ithelp.ithome.com.tw/upload/images/20220922/20144469TaLIQsfLM4.png

  1. Container attributes
Element Attribute Related method(s) Default value
Color app:backgroundTint setBackgroundTintgetBackgroundTint ?attr/colorSurface
Elevation app:elevation setElevation 3dp
Height android:minHeight setMinimumHeightgetMinimumHeight 56dp (default) and 64dp (w600dp)
  1. Navigation icon attributes
Element Attribute Related method(s) Default value
Icon app:navigationIcon setNavigationIcongetNavigationIcon null
Color app:navigationIconTint setNavigationIconTint ?attr/colorOnSurfaceVariant (as Drawable tint)
  1. Floating action button (FAB) (optional) attributes
Element Attribute Related method(s) Default value
Alignment mode
對齊位置 app:fabAlignmentMode setFabAlignmentModegetFabAlignmentMode end
Animation mode app:fabAnimationMode setFabAnimationModegetFabAnimationMode slide
Anchor mode app:fabAnchorMode setFabAnchorModegetFabAnchorMode embed
Cradle margin app:fabCradleMargin setFabCradleMargingetFabCradleMargin 6dp
Cradle rounded corner radius 圓角半徑 app:fabCradleRoundedCornerRadius setFabCradleRoundedCornerRadiusgetFabCradleRoundedCornerRadius 4dp
Cradle vertical offset 垂直偏移 app:fabCradleVerticalOffset setCradleVerticalOffsetgetCradleVerticalOffset 12dp
End margin app:fabAlignmentModeEndMargin setFabAlignmentModeEndMargingetFabAlignmentModeEndMargin N/A
Embedded elevation app:removeEmbeddedFabElevation N/A true
  1. Action item(s) (optional) attributes
Element Attribute Related method(s) Default value
Menu app:menu replaceMenugetMenu null
Icon color N/A N/A ?attr/colorControlNormal (as Drawable tint)
Alignment mode 對齊位置 app:menuAlignmentMode setMenuAlignmentModegetMenuAlignmentMode auto
  1. Overflow menu (optional)
Element Attribute Related method(s) Default value
Icon android:src and app:srcCompat in actionOverflowButtonStyle (in app theme) setOverflowIcongetOverflowIcon @drawable/abc_ic_menu_overflow_material (before API 23) or @drawable/ic_menu_moreoverflow_material (after API 23)
Theme app:popupTheme setPopupThemegetPopupTheme @style/ThemeOverlay.Material3.*
Item typography textAppearanceSmallPopupMenu and textAppearanceLargePopupMenu in app:popupTheme or app theme N/A ?attr/textAppearanceTitleMedium

Styles

Default style theme attribute: bottomAppBarStyle

Element Style
Default style Widget.Material3.BottomAppBar
Widget.Material3.BottomAppBar.Legacy

Widget.Material3.BottomAppBar

  • app:fabAnchorMode="embed"會FAB放在BottomAppBar中。
  • app:fabAlignmentMode="end" 設定FAB放的位置在最後
  • app:menuAlignmentMode="start" menu設定開始對齊開頭

https://ithelp.ithome.com.tw/upload/images/20220922/20144469LbHVObA9Fw.png

<style name="Widget.Material3.BottomAppBar" parent="Widget.MaterialComponents.BottomAppBar">
    <item name="android:minHeight">@dimen/m3_bottomappbar_height</item>
    <item name="maxButtonHeight">@dimen/m3_bottomappbar_height</item>
    <item name="fabAnimationMode">slide</item>
    <item name="fabAnchorMode">embed</item>
    <item name="removeEmbeddedFabElevation">true</item>
    <item name="fabAlignmentMode">end</item>
    <item name="menuAlignmentMode">start</item>
    <item name="fabAlignmentModeEndMargin">@dimen/m3_bottomappbar_fab_end_margin</item>
    <item name="fabCradleMargin">@dimen/m3_bottomappbar_fab_cradle_margin</item>
    <item name="fabCradleRoundedCornerRadius">
      @dimen/m3_bottomappbar_fab_cradle_rounded_corner_radius
    </item>
    <item name="fabCradleVerticalOffset">
      @dimen/m3_bottomappbar_fab_cradle_vertical_offset
    </item>
    <item name="elevation">@dimen/m3_comp_bottom_app_bar_container_elevation</item>

    <item name="backgroundTint">@macro/m3_comp_bottom_app_bar_container_color</item>
    <item name="navigationIconTint">?attr/colorOnSurfaceVariant</item>
    <item name="android:paddingLeft">@dimen/m3_bottomappbar_horizontal_padding</item>
    <item name="android:paddingStart">@dimen/m3_bottomappbar_horizontal_padding</item>
    <item name="android:paddingRight">@dimen/m3_bottomappbar_horizontal_padding</item>
    <item name="android:paddingEnd">@dimen/m3_bottomappbar_horizontal_padding</item>
    <item name="materialThemeOverlay">@style/ThemeOverlay.Material3.BottomAppBar</item>
  </style>

Widget.Material3.BottomAppBar.Legacy

  • app:fabAnchorMode="cradle" FAB顯示方式cradle是頂部邊緣位置。
  • app:fabAlignmentMode="center" 設定FAB放的位置在中間
  • app:menuAlignmentMode="auto" FAB 居中對齊時,menu設定將在末尾對齊。

https://ithelp.ithome.com.tw/upload/images/20220922/20144469HY7UaHEuIK.png

<style name="Widget.Material3.BottomAppBar.Legacy" parent="Widget.MaterialComponents.BottomAppBar">
    <item name="fabAnimationMode">slide</item>
    <item name="fabAnchorMode">cradle</item>
    <item name="removeEmbeddedFabElevation">false</item>
    <item name="fabAlignmentMode">center</item>
    <item name="menuAlignmentMode">auto</item>
    <item name="fabAlignmentModeEndMargin">@null</item>
    <item name="fabCradleMargin">@dimen/m3_bottomappbar_fab_cradle_margin</item>
    <item name="fabCradleRoundedCornerRadius">
      @dimen/m3_bottomappbar_fab_cradle_rounded_corner_radius
    </item>
    <item name="fabCradleVerticalOffset">
      @dimen/m3_bottomappbar_fab_cradle_vertical_offset
    </item>
    <item name="elevation">@dimen/m3_comp_bottom_app_bar_container_elevation</item>

    <item name="backgroundTint">@macro/m3_comp_bottom_app_bar_container_color</item>
    <item name="navigationIconTint">?attr/colorOnSurfaceVariant</item>
    <item name="materialThemeOverlay">@style/ThemeOverlay.Material3.BottomAppBar.Legacy</item>
  </style>

layout 預設 padding and size

https://ithelp.ithome.com.tw/upload/images/20220922/20144469AzLfABrAvs.png

Layout attribute Value
Width 根據裝置寬度
Height 80dp
Alignment Vertically centered
Left/right padding 16dp
Padding between elements 8dp

Responsive layout 寬度可以根據裝置
可以根據適合螢幕寬度改變顯示更多或更少的操作按鈕。

https://firebasestorage.googleapis.com/v0/b/design-spec/o/projects%2Fm3%2Fimages%2Fl62jdyeq-responsive_layout_1%20(1).mp4?alt=media&token=3cf05b48-4e25-46b7-addc-ede7355ee302

參考資料:Material Design Component Bottom App Bar


上一篇
Day 11 使用 M3 的 Extended Floating Action Button(下)
下一篇
Day 13 使用 M3 的Chips
系列文
Kotlin 實踐 Material Design 懶人包30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言