日期挑選器

日期挑選器可讓使用者選取日期、日期範圍或同時選取二者。這些方法會使用日曆對話方塊或文字輸入功能,讓使用者選取日期。

類型

日期挑選器分為三種類型:

  • 已固定:在版面配置中內嵌顯示。這個選項適合用於精簡版面配置,因為在這種情況下,專屬對話方塊可能會造成干擾。
  • 模態:以對話方塊形式顯示,並疊加在應用程式內容上。這可讓使用者清楚專注於日期選取作業。
  • 模態輸入:結合文字欄位和模態日期挑選器。

您可以使用下列可組合項,在應用程式中實作這些日期挑選器:

  • DatePicker:日期挑選器的一般可組合項。您使用的容器會決定它是固定式或模型。
  • DatePickerDialog:模態和模態輸入日期挑選工具的容器。
  • DateRangePicker:適用於任何日期挑選器,讓使用者選取含有開始日期和結束日期的範圍。

狀態

不同日期挑選器可組合項共用的鍵參數是 state,可接受 DatePickerStateDateRangePickerState 物件。這些屬性會擷取使用者使用日期挑選器所選取的資訊,例如目前選取的日期。

如要進一步瞭解如何使用所選日期,請參閱「使用所選日期」一節。

固定日期挑選器

在以下範例中,文字欄位會提示使用者輸入出生日期。當使用者點選欄位中的日曆圖示時,系統會在輸入欄位下方開啟已固定的日期選擇器。

@Composable
fun DatePickerDocked() {
    var showDatePicker by remember { mutableStateOf(false) }
    val datePickerState = rememberDatePickerState()
    val selectedDate = datePickerState.selectedDateMillis?.let {
        convertMillisToDate(it)
    } ?: ""

    Box(
        modifier = Modifier.fillMaxWidth()
    ) {
        OutlinedTextField(
            value = selectedDate,
            onValueChange = { },
            label = { Text("DOB") },
            readOnly = true,
            trailingIcon = {
                IconButton(onClick = { showDatePicker = !showDatePicker }) {
                    Icon(
                        imageVector = Icons.Default.DateRange,
                        contentDescription = "Select date"
                    )
                }
            },
            modifier = Modifier
                .fillMaxWidth()
                .height(64.dp)
        )

        if (showDatePicker) {
            Popup(
                onDismissRequest = { showDatePicker = false },
                alignment = Alignment.TopStart
            ) {
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .offset(y = 64.dp)
                        .shadow(elevation = 4.dp)
                        .background(MaterialTheme.colorScheme.surface)
                        .padding(16.dp)
                ) {
                    DatePicker(
                        state = datePickerState,
                        showModeToggle = false
                    )
                }
            }
        }
    }
}

@Composable
fun DatePickerFieldToModal(modifier: Modifier = Modifier) {
    var selectedDate by remember { mutableStateOf<Long?>(null) }
    var showModal by remember { mutableStateOf(false) }

    OutlinedTextField(
        value = selectedDate?.let { convertMillisToDate(it) } ?: "",
        onValueChange = { },
        label = { Text("DOB") },
        placeholder = { Text("MM/DD/YYYY") },
        trailingIcon = {
            Icon(Icons.Default.DateRange, contentDescription = "Select date")
        },
        modifier = modifier
            .fillMaxWidth()
            .pointerInput(selectedDate) {
                awaitEachGesture {
                    // Modifier.clickable doesn't work for text fields, so we use Modifier.pointerInput
                    // in the Initial pass to observe events before the text field consumes them
                    // in the Main pass.
                    awaitFirstDown(pass = PointerEventPass.Initial)
                    val upEvent = waitForUpOrCancellation(pass = PointerEventPass.Initial)
                    if (upEvent != null) {
                        showModal = true
                    }
                }
            }
    )

    if (showModal) {
        DatePickerModal(
            onDateSelected = { selectedDate = it },
            onDismiss = { showModal = false }
        )
    }
}

fun convertMillisToDate(millis: Long): String {
    val formatter = SimpleDateFormat("MM/dd/yyyy", Locale.getDefault())
    return formatter.format(Date(millis))
}

程式碼的重點

  • 使用者點選 IconButton 時,日期挑選器就會顯示。
    • 圖示按鈕可做為 OutlinedTextFieldtrailingIcon 參數的引數。
    • showDatePicker 狀態變數會控制已固定的日期挑選器的顯示狀態。
  • 日期挑選器的容器是 Popup 可組合項,可重疊內容,且不會影響其他元素的版面配置。
  • selectedDate 會從 DatePickerState 物件擷取所選日期的值,並使用 convertMillisToDate 函式設定格式。
  • 所選日期會顯示在文字欄位中。
  • 已固定的日期選擇器會使用 offset 修飾符,位於文字欄位下方。
  • 使用 Box 做為根容器,讓文字欄位和日期挑選器正確分層。

結果

點選日曆圖示後,這個實作會顯示如下:

固定日期挑選器範例。
圖 1. 已固定的日期挑選器。

模式日期挑選器會顯示浮動在畫面上的對話方塊。如要實作此功能,請建立 DatePickerDialog,並傳遞 DatePicker

@Composable
fun DatePickerModal(
    onDateSelected: (Long?) -> Unit,
    onDismiss: () -> Unit
) {
    val datePickerState = rememberDatePickerState()

    DatePickerDialog(
        onDismissRequest = onDismiss,
        confirmButton = {
            TextButton(onClick = {
                onDateSelected(datePickerState.selectedDateMillis)
                onDismiss()
            }) {
                Text("OK")
            }
        },
        dismissButton = {
            TextButton(onClick = onDismiss) {
                Text("Cancel")
            }
        }
    ) {
        DatePicker(state = datePickerState)
    }
}

  • DatePickerModal 可組合函式會顯示模態日期挑選器。
  • 當使用者選取日期時,系統會執行 onDateSelected lambda 運算式。
    • 並將所選日期公開給父項可組合項。
  • 當使用者關閉對話方塊時,系統會執行 onDismiss lambda 運算式。

結果

此實作方式如下所示:

模態日期挑選器範例。
圖 2. 模態日期挑選器。

輸入模式日期挑選器

具有輸入功能的模態日期挑選器會顯示一個浮動在畫面上的對話方塊,讓使用者輸入日期。

@Composable
fun DatePickerModalInput(
    onDateSelected: (Long?) -> Unit,
    onDismiss: () -> Unit
) {
    val datePickerState = rememberDatePickerState(initialDisplayMode = DisplayMode.Input)

    DatePickerDialog(
        onDismissRequest = onDismiss,
        confirmButton = {
            TextButton(onClick = {
                onDateSelected(datePickerState.selectedDateMillis)
                onDismiss()
            }) {
                Text("OK")
            }
        },
        dismissButton = {
            TextButton(onClick = onDismiss) {
                Text("Cancel")
            }
        }
    ) {
        DatePicker(state = datePickerState)
    }
}

這與模態日期挑選器範例非常相似。主要差異如下:

  • initialDisplayMode 參數會將初始顯示模式設為 DisplayMode.Input
含有輸入內容的模態日期挑選器。
圖 3. 含有輸入內容的模態日期挑選器。

內含範圍的日期挑選器

您可以建立日期挑選器,讓使用者選取開始日期和結束日期之間的範圍。方法是使用 DateRangePicker

DateRangePicker 的用法基本上與 DatePicker 相同。您可以將其用於固定的挑選器做為 PopUp 的子項,也可以將其用於模態挑選器並傳遞至 DatePickerDialog。主要差異在於您使用 DateRangePickerState,而非 DatePickerState

以下程式碼片段示範如何建立具有範圍的模態日期挑選器:

@Composable
fun DateRangePickerModal(
    onDateRangeSelected: (Pair<Long?, Long?>) -> Unit,
    onDismiss: () -> Unit
) {
    val dateRangePickerState = rememberDateRangePickerState()

    DatePickerDialog(
        onDismissRequest = onDismiss,
        confirmButton = {
            TextButton(
                onClick = {
                    onDateRangeSelected(
                        Pair(
                            dateRangePickerState.selectedStartDateMillis,
                            dateRangePickerState.selectedEndDateMillis
                        )
                    )
                    onDismiss()
                }
            ) {
                Text("OK")
            }
        },
        dismissButton = {
            TextButton(onClick = onDismiss) {
                Text("Cancel")
            }
        }
    ) {
        DateRangePicker(
            state = dateRangePickerState,
            title = {
                Text(
                    text = "Select date range"
                )
            },
            showModeToggle = false,
            modifier = Modifier
                .fillMaxWidth()
                .height(500.dp)
                .padding(16.dp)
        )
    }
}

程式碼的重點

  • onDateRangeSelected 參數是回呼,會接收代表所選開始和結束日期的 Pair<Long?, Long?>。這會讓父項可組合函式存取所選範圍。
  • rememberDateRangePickerState() 會為日期範圍挑選器建立狀態。
  • DatePickerDialog 會建立模式對話方塊容器。
  • 在確認按鈕的 onClick 處理常式中,onDateRangeSelected 會將所選範圍傳遞至父項可組合項。
  • DateRangePicker 可組合項會做為對話方塊內容。

結果

此實作方式如下所示:

模態範圍日期挑選器範例。
圖 4. 具有所選範圍的模式日期選擇器。

使用所選日期

如要擷取所選日期,請在父項可組合項中以 Long 的形式追蹤日期,並將值傳遞至 onDateSelected 中的 DatePicker。下列程式碼片段可說明這一點,不過您也可以在官方程式碼片段應用程式中查看完整的實作方式。

// ...
    var selectedDate by remember { mutableStateOf<Long?>(null) }
// ...
        if (selectedDate != null) {
            val date = Date(selectedDate!!)
            val formattedDate = SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()).format(date)
            Text("Selected date: $formattedDate")
        } else {
            Text("No date selected")
        }
// ...
        DatePickerModal(
            onDateSelected = {
                selectedDate = it
                showModal = false
            },
            onDismiss = { showModal = false }
        )
    }
// ...

範圍日期挑選器基本上也適用相同的做法,但您需要使用 Pair<Long?, Long?> 或資料類別擷取開始和結束值。

另請參閱