iT邦幫忙

2022 iThome 鐵人賽

DAY 20
0

大綱

  • Alert dialog
  • Simple dialog
  • Confirmation dialog
  • Full-screen dialog
  • Anatomy
  • Key properties
  • Custom Style

Alert dialog

image alt

實作上,透過 MaterialAlertDialogBuilder 物件來創建 Dialog,而它的設計邏輯是來自於所謂的 Builder Pattern,而又是這麼剛好(無情業配),我以前也有寫過一篇文章講到,有興趣的大家,歡迎閱讀~

所以在 Builder 模式下,會有許多方法讓我們可以用來客製化想要的屬性,使用上與 Snackbar 非常相似。設置完成後,再透過show()方法顯示

MaterialAlertDialogBuilder(context)
        .setTitle(resources.getString(R.string.title))
        .setMessage(resources.getString(R.string.supporting_text))
        .setNeutralButton(resources.getString(R.string.cancel)) { dialog, which ->
            // Respond to neutral button press
        }
        .setNegativeButton(resources.getString(R.string.decline)) { dialog, which ->
            // Respond to negative button press
        }
        .setPositiveButton(resources.getString(R.string.accept)) { dialog, which ->
            // Respond to positive button press
        }
        .show()

Simple Dialog

image alt

在 Simple Dialog 實作中,由於設計上,只能有 list item,就透過 setItems()方法設置,裡面要傳入一個作為選項的 array

val items = arrayOf<String>("Item 1", "Item 2", "Item 3")

MaterialAlertDialogBuilder(requireContext())
            .setTitle("Simple Dialog")
            .setItems(items) { dialog, which ->
                // do something when click any items
            }.show()

Confirmation dialog

Single Type

image alt

實作上,有點像是 Simple Dialog 的進階版,但選項的部分是透過 setSingleChoiceItems 將 Dialog 的 item 變成 radioButton 的樣式與達到單選的功能,還要再加入 Netural、Positive Action

val singleItems = arrayOf("Item 1", "Item 2", "Item 3")
val checkedItem = 1

MaterialAlertDialogBuilder(context)
        .setTitle(resources.getString(R.string.title))
        .setNeutralButton(resources.getString(R.string.cancel)) { dialog, which ->
            // Respond to neutral button press
        }
        .setPositiveButton(resources.getString(R.string.ok)) { dialog, which ->
            // Respond to positive button press
        }
        // Single-choice items (initialized with checked item)
        .setSingleChoiceItems(singleItems, checkedItem) { dialog, which ->
             // Respond to item chosen
         }
        .show()

如果在 title 上的文字無法更清楚的表達,可以透過 setIcon 在 title 旁邊添加圖示

Multiple Type

而 Multiple Type 實作上與 Single Type 相似,差別在於 item 的設置上,是透過 setMultiChoiceItems,當中的監聽 lambda 還有 checked 參數讓我們能判斷用戶點選哪一個

val singleItems = arrayOf("Item 1", "Item 2", "Item 3")
val checkedItems = booleanArrayOf(true, false, false, false)

MaterialAlertDialogBuilder(requireContext())
    ...
    setMultiChoiceItems(singleItems, checkedItems) { dialog, which, checked ->
        if (checked) Log.d("Multi-choice", "checked item :$which")
    }
    ...
    .show()

Full-screen dialog

與其他前面的 Dialog 都不同,不是透過 MaterialAlertDialog 來生成,而是要直接實作一個 Fragment 來 show 出畫面,由於官方文檔沒有直接的範例,反而是要我們自己去看 DialogFragment 的文檔,如果有興趣的話可以先看看,如果懶得看的話,這邊就用我做過的一個專案中其中一個新增 Event 的功能來當範例

DialogFragment

第一步就是從 DialogFragment 開始,作為生成 Dialog 的重要物件,我自己先魔改成類似 BaseFragment 的架構來設計。這樣以後有多個 DialogFragment 的時候,就不用重寫這麼多次 inflate 了。
還有一個地方要注意,就是要透過設置 Style 的屬性去啟動全屏模式,否則 Dialog 不會填滿整個畫面,所以在 onCreate 去設置了 STYLE_NORMAL 與我自定義的 theme,這樣就完成了

  • Theme
    這邊還能透過 windowAnimationStyle 來改變 Dialog 出現的動畫,但是系統只提供 BottomSheet 這一個模式
<style name="FullDialogStyle" parent="Theme.IThomeIRonContest">
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowAnimationStyle">@style/Animation.Design.BottomSheetDialog</item>
</style>
  • BaseDialogFragment
abstract class BaseDialogFragment<VB : ViewBinding>(private val inflate: (LayoutInflater, ViewGroup?, Boolean) -> VB) :
    DialogFragment() {
    private var _binding: VB? = null
    val binding get() = _binding!!
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setStyle(STYLE_NORMAL, R.style.FullDialogStyle)
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = inflate(inflater, container, false)
        return binding.root
    }

    override fun onDestroy() {
        _binding = null
        super.onDestroy()
    }
}

In layout

畫面的部分,重點在於 Top bar,這邊只貼一部分的 code,剩下的可以到 Github 看完整的程式碼

<?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"
    style="@style/Animation.Design.BottomSheetDialog"
    android:orientation="vertical">

    <com.google.android.material.appbar.MaterialToolbar
        android:id="@+id/fullscreen_dialog_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="New Event">

        <com.google.android.material.button.MaterialButton
            android:id="@+id/fullscreen_save_btn"
            android:layout_width="wrap_content"
            android:layout_gravity="end"
            android:text="SAVE"
            android:textColor="@color/white"
            android:layout_marginEnd="10dp"
            style="@style/Widget.MaterialComponents.Button.TextButton"
            android:layout_height="wrap_content"/>
    </com.google.android.material.appbar.MaterialToolbar>

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="10dp">
    <!--  Content -->
        ...
        ...
    </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>

In code

DialogFragment 實作的部分我就不貼了,讓各位自由發揮,這邊就單純貼如何呼叫它。先初始化 DialogFragment,再透過當中的方法show(),裡面有兩個參數要帶入,分別為 manage 與 tag。 manager 在不同層級要帶不同的參數,如果是在 Activity 要使用 supportFragmentManager,在 Fragment的話是 childFragmentManager

private val fullScreenDialogFragment by lazy { FullScreenDialogFragment() }

fullScreenDialogFragment.show(childFragmentManager, "FullScreen Dialog")

成品


Anatomy

image alt

  1. Container
  2. Title (optional)
  3. Content
  4. Buttons (optional)
  5. Scrim

Key properties

Container attributes

Title attributes

Content attributes

Supporting text

List item

Buttons attributes

Scrim attributes

Theme overlays

Theme attributes


Custom Style

除了 DialogFragment 能讓我們自由設置畫面與元件,其他的 Dialog 都是系統透過主題的預設大小與色彩。可以調整的屬性非常多,這邊我也只能帶大家走馬看花。色彩上,由於當中的組件很多,有 TextButton、TextView、Container 等,所吃的屬性也不同

  • Container : colorSurface
  • TextView : colorOnSurface
  • Text Button : buttonBar(Positive、Negative、Neutral) ButtonStyle
  • Scrim : backgroundDimAmount

透過各種主題色,將顏色設計改了一下,然後透過 alertDialogStyle 去改變 Dialog 的形狀。基本上這樣就能改變當中許多元件的設計

<style name="ThemeOverlay.App.MaterialAlertDialog" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog">
        <item name="colorPrimary">@android:color/holo_orange_light</item>
        <item name="colorSecondary">@color/red</item>
        <item name="colorSurface">@color/darkBlue</item>
        <item name="colorOnSurface">@color/white</item>
        <item name="alertDialogStyle">@style/MaterialAlertDialog.App</item>
</style>

<style name="MaterialAlertDialog.App" parent="MaterialAlertDialog.MaterialComponents">
        <item name="shapeAppearance">@style/ShapeAppearance.App.MediumComponent</item>
</style>

<style name="ShapeAppearance.App.MediumComponent" parent="ShapeAppearance.MaterialComponents.MediumComponent">
        <item name="cornerFamily">cut</item>
        <item name="cornerSize">8dp</item>
</style>

如果上面那樣不滿足,想再做到更深入的客製化,就要從裡面的元件著手,包括 title、body 跟代表 action 的 TextButton。也就是說我們要直接去寫這些元件的風格並且統一套用在 Dialog 上面

由於 Container 的顏色一定要透過 colorSurface,所以其他上述所設定的主題色都刪除只留下它。這邊新增了 title、body 的 TextStyle,還有透過 buttonBarButtonStyle 設置每個 action 的 TextButton Style。由於這些元件在基礎上的大小、間距與形狀都被配置好了,如果要特別去改動,要注意可能會出現非預期的狀況,建議只改顏色就好

<style name="ThemeOverlay.App.MaterialAlertDialog" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog">
        <!-- Dialog background color -->
        <item name="colorSurface">@color/darkBlue</item>
        <item name="alertDialogStyle">@style/MaterialAlertDialog.App</item>
        <item name="buttonBarPositiveButtonStyle">@style/Widget.App.Button.TextButton.Dialog</item>
        <item name="buttonBarNegativeButtonStyle">@style/Widget.App.Button.TextButton.Dialog</item>
        <item name="buttonBarNeutralButtonStyle">@style/Widget.App.Button.TextButton.Dialog</item>
        <item name="materialAlertDialogTitleTextStyle">@style/MaterialAlertDialog.App.Title.Text</item>
        <item name="materialAlertDialogBodyTextStyle">@style/MaterialAlertDialog.App.Body.Text</item>
</style>

<style name="MaterialAlertDialog.App" parent="MaterialAlertDialog.MaterialComponents">
        <item name="shapeAppearance">@style/ShapeAppearance.App.MediumComponent</item>
</style>

<style name="MaterialAlertDialog.App.Title.Text" parent="MaterialAlertDialog.MaterialComponents.Title.Text">
        <item name="android:textColor">@android:color/holo_orange_light</item>
</style>

<style name="MaterialAlertDialog.App.Body.Text" parent="MaterialAlertDialog.MaterialComponents.Body.Text">
        <item name="android:textColor">@color/white</item>
</style>

<style name="Widget.App.Button.TextButton.Dialog" parent="Widget.MaterialComponents.Button.TextButton.Dialog">
        <item name="android:textColor">@android:color/holo_red_light</item>
        <item name="shapeAppearance">@style/ShapeAppearance.App.SmallComponent</item>
</style>

最後再介紹幾個屬性,分別為 buttonBarStyle (能改動 Dialog 下方的 bar)、materialAlertDialogTitlePanelStyle (能改動 Dialog 上方的 bar)。可透過這兩個屬性去改動 Dialog 上下方的 background,讓整個 Dialog 不在只是單一色

<!-- Bottom bar style -->
<item name="buttonBarStyle">@style/Widget.App.Dialog.BottomBar</item>
<!-- Top bar style -->
<item name="materialAlertDialogTitlePanelStyle">@style/TitlePaneStyleCenter</item>

<style name="Widget.App.Dialog.ButtonBar" parent="Widget.AppCompat.Button.ButtonBar.AlertDialog">
        <item name="android:background">@color/white</item>
</style>

<style name="Widget.App.Dialog.Title.Panel" parent="MaterialAlertDialog.MaterialComponents.Title.Panel.CenterStacked">
        <item name="android:background">@color/black</item>
</style>

實作完自定義的 style 後,記得別忘了在 theme 裡面去套用

<item name="materialAlertDialogTheme">@style/ThemeOverlay.App.MaterialAlertDialog</item>

小結

Dialogs 實作上除了 Full-screen DialogFragment 之外,都是透過 MaterialDialog 的 Builder 模式來建立,讓我們能一步一步去建立想要的 Dialog,但也僅限物件中所提供的功能,如果想要更加客製化,除了上述提到的修改 style 之後,還能透過setView的方法注入自定義的 layout,但由於這部分主要不是 Material Design 的範圍,所以就沒特別提到。若是想知道系統原生的 Dialog layout 配置如何,可以在 android studio 中搜尋 mtrl_alert_dialog.xml

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


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

尚未有邦友留言

立即登入留言