iT邦幫忙

2022 iThome 鐵人賽

DAY 24
0

大綱

  • Standard bottom sheet
  • Modal bottom sheet
  • Expanding bottom sheet
  • Common Using
  • Anatomy
  • Key properties
  • Style

Standard bottom sheet

由於 Standard 的特性是與主畫面共存與輔助,所以實作上,要透過 Coordinator 佈局來設置 bottom sheet,透過 app:layout_behavior 屬性將這個佈局設為一個持久的 Bottom Sheet,這是一個從屏幕底部升起的 View,位於主要內容之上。可以垂直拖動它以顯示更多或更少的內容。

基本上所有的 Bottom Sheet 都會是 Framelayout,因為圖層上要覆蓋原本的主畫面

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

  <FrameLayout
    ...
    android:id="@+id/standard_bottom_sheet"
    app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">

    <!-- Bottom sheet contents. -->

  </FrameLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

Modal bottom sheet

Modal 則是不與主畫面共存,所以要透過 BottomSheetDialogFragment 來生成畫面,基本上與 Fragment 用法相似。在主畫面呼叫時,要先初始化並透過 show() 來顯示。這邊範例是在 Activity 去呼叫,如果是在 Fragment 的話,manager 要使用 childFragmentManager

class ModalBottomSheet : BottomSheetDialogFragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? = inflater.inflate(R.layout.modal_bottom_sheet_content, container, false)

    companion object {
        const val TAG = "ModalBottomSheet"
    }
}

class MainActivity : AppCompatActivity() {
    ...
    val modalBottomSheet = ModalBottomSheet()
    modalBottomSheet.show(supportFragmentManager, ModalBottomSheet.TAG)
    ...
}

Expanding bottom sheet

由於官方沒有提供實作,這邊就用我學過得東西簡單實作一下。透過 Container transform 、navigation 還有 Fragment 的結合來實現,結果發現並沒有用到 BottomSheetDialogFragment。Material Deisgn 之所以沒特別介紹,是因為在實作上可以透過各種不同方法達成,甚至可以說它算是動畫實作的一環,而不算是 Bottom Sheet

in layout

layout 設置上的重點放在 transitionName 上面,透過它可以實現 Transform 動畫,也就是我們想要的讓被縮小的 Bottom sheet 放大成 Full-screen 的效果,如果對 Transform 不太理解,我剛好寫過文章可以先看看,或是觀看官方文檔

  • Expanding
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:transitionName="@string/trans_expand_bottomsheet"
    android:orientation="vertical">

    <com.google.android.material.appbar.MaterialToolbar
        android:id="@+id/fullscreen_bottomsheet_tb"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/darkBlue"
        android:paddingVertical="5dp"
        app:navigationIcon="@drawable/ic_baseline_close_24"
        app:title="Expanding Bottom Sheet">

    </com.google.android.material.appbar.MaterialToolbar>

    <!-- Bottom sheet Content-->
    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="10dp">
        ...
        ...
    </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
  • Collapsed
<com.google.android.material.button.MaterialButton
            ...
            android:id="@+id/expand_bottom_sheet_btn"
            android:text="Expanding Sheet"
            android:transitionName="@string/trans_expand_bottomsheet"
            ...
            />

Fragment

  • Expanding

用 Fragment 就能實現我們要的功能,接著在裡面設置 shareElementEnterTransition 給予 MaterialContainerTransform 物件,就能讓系統為我們設置動畫,當中有許多參數可以調整,有興趣的可以看官方文檔更加仔細的介紹。記得要設置返回 Collapsed 狀態的操作,透過 navigateUp() 導回上個畫面

class ExpandingBottomSheetFragment :
    BaseFragment<FragmentExpandingBottomSheetBinding>(
        FragmentExpandingBottomSheetBinding::inflate
    ) {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        sharedElementEnterTransition = MaterialContainerTransform().apply {
            duration = 1000
            fadeMode = MaterialContainerTransform.FADE_MODE_CROSS
        }
    }
    private val navController by lazy { findNavController() }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.fullscreenBottomsheetTb.setNavigationOnClickListener {
            navController.navigateUp()
        }
    }
}
  • Collapsed

用 navigation 實現 transform 動畫不能使用一般的導航方式,要透過 extras 物件綁定要執行動畫的 View 與對應的 transitionName,Name 的部分要與我們剛剛在 Expanding layout 的父層佈局所設置的相同,否則系統不會知道哪兩個元件要執行動畫,建議儲存為資源檔案以免遺失或輸入錯誤

binding.expandBottomSheetBtn.setOnClickListener {
     val extras = FragmentNavigatorExtras(binding.expandBottomSheetBtn to getString(R.string.trans_expand_bottomsheet))
            navController.navigate(R.id.action_standardBottomSheetsFragment_to_expandingBottomSheetFragment,null,null,extras)
      }

Common Using

在 Bottom sheet 中,有著相同可控制的屬性與更細部的設定,下面會切分成幾個部分來介紹

Setting behavior

這邊介紹幾個方法,可以用來控制 standard 與 modal 在主畫面的交互行為

in standard bottom sheet

可以通過將它們設置在具有 app:layout_behavior 設置的同一個子視圖上來在 xml 中應用它們,或者在編程中去設置

in code

val standardBottomSheetBehavior = BottomSheetBehavior.from(standardBottomSheet)

in modal bottom sheet

也是有兩種方法可以設置,但不能透過剛剛上述的屬性 app:layout_behavior因為是獨立於主畫面的,所以就只能透過 style 與 theme 來下手

<style name="ModalBottomSheet" parent="Widget.MaterialComponents.BottomSheet.Modal">
  <!-- Apply attributes here -->
</style>

<style name="ModalBottomSheetDialog" parent="ThemeOverlay.MaterialComponents.BottomSheetDialog">
  <item name="bottomSheetStyle">@style/ModalBottomSheet</item>
</style>

<style name="AppTheme" parent="Theme.MaterialComponents.*">
  <item name="bottomSheetDialogTheme">@style/ModalBottomSheetDialog</item>
</style>

或是在頁面生成時透過編程設置

val modalBottomSheetBehavior = (modalBottomSheet.dialog as BottomSheetDialog).behavior

Retaining behavior on configuration change

為了在配置更改時保存和恢復 BottomSheet 的特定行為,可設置以下標誌 :

SAVE_PEEK_HEIGHT: 保留 behavior_peekHeight 該屬性

SAVE_FIT_TO_CONTENTS:保留 behavior_fitToContents 該屬性

SAVE_HIDEABLE:保留 behavior_hideable 該屬性

SAVE_SKIP_COLLAPSED:保留 behavior_skipCollapsed 該屬性

SAVE_ALL:將保留所有上述所有屬性

SAVE_NONE:將不保留任何屬性,這是系統預設值

in code :

val bottomSheetBehavior = BottomSheetBehavior.from(binding.bottomNavigationView)
bottomSheetBehavior.saveFlags = BottomSheetBehavior.SAVE_ALL

in layout :

<FrameLayout
        android:id="@+id/standard_bottom_sheet"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        **app:behavior_saveFlags="none"
        app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />

Setting state

Standard 與 Modal 都是擁有相同的 State attributes

  • STATE_COLLAPSED : Bottom Sheet 可見僅其窺視高度。這種狀態通常是 “靜止位置”,並且應該有足夠的高度來表明有下方還有更多內容
  • STATE_EXPANDED : 展開到其最大高度
  • STATE_HALF_EXPANDED : 展開到最大高度一半(僅當 behavior_fitToContents 設置為 false 時才適用)
  • STATE_HIDDEN : Bottom sheet 不再可見,只能在透過編成方式顯示
  • STATE_DRAGGING : 用戶正在主動向上或向下拖動
  • STATE_SETTLING : 在拖動/滑動手勢後,Bottom Sheet 會穩定到特定高度。這將是窺視高度、展開高度或 0,以防用戶操作導致Bottom sheet 收起隱藏

in code :

bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED

Listening to state and slide changes

狀態改變時,可以透過 BottomSheetCallback 來監聽 Bottom Sheet 來響應用戶的各種行為

val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {

    override fun onStateChanged(bottomSheet: View, newState: Int) {
        // Do something for new state.
    }

    override fun onSlide(bottomSheet: View, slideOffset: Float) {
        // Do something for slide offset.
    }
}

// To add the callback:
bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback)

// To remove the callback:
bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallback)

Handling insets and fullscreen

BottomSheetBehavior 可以通過指定以下任何一項來自動處理 insets 間距:

  • app : paddingBottomSystemWindowInsets
  • app : paddingLeftSystemWindowInsets
  • app : paddingRightSystemWindowInsets
  • app : paddingTopSystemWindowInsets

在 API 21 及更高版本上,如果導航欄是透明的並且 app:enableEdgeToEdge 為 true,則 Modal bottom sheet 將全屏呈現

如果上述任何 padding attributes 在樣式中設置為 true,它可以自動添加插入,方法是更新傳遞給構造函數的 Style,或者更新主題中的 ?attr/bottomSheetDialogTheme 屬性指定的默認樣式

BottomSheetDialog 會在滑動到 status bar 下方時,添加 padding 以防繪製阻擋到 status bar


Anatomy

image alt

  1. Sheet
  2. Content
  3. Scrim (in modal bottom sheets)

Key properties

Sheet attributes

Behavior attributes

若是想控制 standard bottom sheet 收起的高度,可透過 app:behavior_peekHeight
若想完全收起 sheet,將 app:behavior_hideable="true"

To save behavior on configuration change


Style

Custom

自定義的部分,由於 Material Design 在預設上已經幫我們處理好,基本上只要改變形狀跟顏色就足以滿足許多情境。至於要改變像是動畫或一些行為的屬性,就要挖得比較深,也可看上方的屬性表來參考。但實際上也不建議去動,因為作者我已經體驗過,就像是進了精神時光屋,所以入坑前請三思/images/emoticon/emoticon06.gif

<style name="ModalBottomSheet" parent="Widget.MaterialComponents.BottomSheet.Modal">
        <item name="backgroundTint">@color/darkGrey</item>
        <item name="shapeAppearance">@style/ShapeAppearance.App.LargeComponent</item>
</style>
    
<style name="ShapeAppearance.App.LargeComponent" parent="ShapeAppearance.MaterialComponents.LargeComponent">
        <item name="cornerFamily">cut</item>
        <item name="cornerSize">24dp</item>
</style>

小結

Bottom sheet 在實作上非常有彈性,甚至不需要用 BottomSheetDialogFragment 就能透過 Fragment 做出類似的效果。因為這些也只是 Material Design 幫我們包好的 API 與套件。建議在高度客製化的場景不要使用,因為它的很多屬性與細部的操控都要透過 theme 與 style,或是一些方法來實現,使用上要花比較多時間去閱讀文件或測試。而如果使用上是想做出簡單動畫或呈現的 Bottom sheet 就非常推薦,只要將想要的畫面做出來,剩下的 Material Design 幫我們實現,建議還沒玩過的大家歡迎試試 (入坑)/images/emoticon/emoticon42.gif

若對實作還是有點不懂的,這邊提供我的 Github 方便大家參考


上一篇
Day 23 - Bottom Sheet ( Design )
下一篇
Day 25 - Bottom Navigation ( Design )
系列文
從 Google Material Design Components 來了解與實作 Android 的 UI/UX 元件設計30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言