日期挑選器

日期挑選器可讓使用者選取日期、日期範圍或同時選取二者。可使用日曆對話方塊或文字輸入內容,讓使用者選取日期。

類型

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

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

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

  • 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 可組合項,可在不影響其他元素的版面配置的情況下疊加內容。
  • selectedDateDatePickerState 物件擷取所選日期的值,並使用 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?> 或資料類別擷取開始和結束值。

另請參閱