Bộ chọn ngày

Bộ chọn ngày cho phép người dùng chọn một ngày, một phạm vi ngày hoặc cả hai. Các thành phần này sử dụng hộp thoại lịch hoặc phương thức nhập văn bản để cho phép người dùng chọn ngày.

Loại

Có ba loại bộ chọn ngày:

  • Đã gắn: Xuất hiện cùng dòng trong bố cục. Phương thức này phù hợp với các bố cục nhỏ gọn, nơi hộp thoại chuyên dụng có thể gây khó chịu.
  • Cửa sổ bật lên: Xuất hiện dưới dạng hộp thoại phủ lên nội dung của ứng dụng. Điều này giúp bạn tập trung rõ ràng vào việc chọn ngày.
  • Nhập dữ liệu theo phương thức bật lên: Kết hợp trường văn bản với bộ chọn ngày theo phương thức bật lên.

Bạn có thể triển khai các bộ chọn ngày này trong ứng dụng bằng cách sử dụng các thành phần kết hợp sau:

  • DatePicker: Thành phần kết hợp chung cho bộ chọn ngày. Vùng chứa bạn sử dụng sẽ xác định xem vùng chứa đó là mô hình hay được kết nối.
  • DatePickerDialog: Vùng chứa cho cả bộ chọn ngày nhập dữ liệu theo phương thức phương thức và phương thức.
  • DateRangePicker: Đối với bất kỳ bộ chọn ngày nào mà người dùng có thể chọn một phạm vi có ngày bắt đầu và ngày kết thúc.

Trạng thái

Tham số chính mà các thành phần kết hợp bộ chọn ngày khác nhau có chung là state. Tham số này lấy đối tượng DatePickerState hoặc DateRangePickerState. Các thuộc tính của chúng thu thập thông tin về lựa chọn của người dùng bằng cách sử dụng bộ chọn ngày, chẳng hạn như ngày đã chọn hiện tại.

Để biết thêm thông tin về cách sử dụng ngày đã chọn, hãy xem phần Sử dụng ngày đã chọn.

Bộ chọn ngày được neo

Trong ví dụ sau, có một trường văn bản nhắc người dùng nhập ngày sinh. Khi người dùng nhấp vào biểu tượng lịch trong trường, một công cụ chọn ngày được neo sẽ mở ra bên dưới trường nhập.

@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))
}

Các điểm chính về mã

  • Bộ chọn ngày xuất hiện khi người dùng nhấp vào IconButton.
    • Nút biểu tượng đóng vai trò là đối số cho tham số trailingIcon của OutlinedTextField.
    • Biến trạng thái showDatePicker kiểm soát chế độ hiển thị của bộ chọn ngày được neo.
  • Vùng chứa của bộ chọn ngày là một thành phần kết hợp Popup, lớp phủ nội dung mà không ảnh hưởng đến bố cục của các phần tử khác.
  • selectedDate ghi lại giá trị của ngày đã chọn từ đối tượng DatePickerState và định dạng giá trị đó bằng hàm convertMillisToDate.
  • Ngày đã chọn sẽ xuất hiện trong trường văn bản.
  • Bộ chọn ngày được neo được đặt bên dưới trường văn bản bằng cách sử dụng đối tượng sửa đổi offset.
  • Box được dùng làm vùng chứa gốc để cho phép phân lớp đúng cách trường văn bản và bộ chọn ngày.

Kết quả

Sau khi nhấp vào biểu tượng lịch, phương thức triển khai này sẽ xuất hiện như sau:

Ví dụ về bộ chọn ngày được neo.
Hình 1. Bộ chọn ngày được neo.

Bộ chọn ngày theo phương thức phương thức hiển thị một hộp thoại nổi trên màn hình. Để triển khai, hãy tạo một DatePickerDialog và truyền vào đó một 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)
    }
}

  • Hàm có khả năng kết hợp DatePickerModal hiển thị một bộ chọn ngày theo phương thức bật lên.
  • Biểu thức lambda onDateSelected sẽ thực thi khi người dùng chọn một ngày.
    • Thành phần này hiển thị ngày đã chọn cho thành phần kết hợp mẹ.
  • Biểu thức lambda onDismiss sẽ thực thi khi người dùng đóng hộp thoại.

Kết quả

Phương thức triển khai có dạng như sau:

Ví dụ về bộ chọn ngày theo phương thức bật lên.
Hình 2. Bộ chọn ngày theo phương thức bật lên.

Bộ chọn ngày phương thức bật lên nhập

Bộ chọn ngày theo phương thức bật lên có dữ liệu đầu vào sẽ hiển thị một hộp thoại nổi trên màn hình và cho phép người dùng nhập ngày.

@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)
    }
}

Cách này rất giống với ví dụ về bộ chọn ngày theo phương thức bật lên. Điểm khác biệt chính là:

  • Tham số initialDisplayMode đặt chế độ hiển thị ban đầu thành DisplayMode.Input.
Bộ chọn ngày theo phương thức phương thức có dữ liệu đầu vào.
Hình 3. Bộ chọn ngày phương thức có dữ liệu đầu vào.

Bộ chọn ngày có phạm vi

Bạn có thể tạo một bộ chọn ngày cho phép người dùng chọn một phạm vi giữa ngày bắt đầu và ngày kết thúc. Để thực hiện việc này, hãy sử dụng DateRangePicker.

Về cơ bản, cách sử dụng DateRangePicker giống với DatePicker. Bạn có thể sử dụng thành phần này cho bộ chọn được neo dưới dạng phần tử con của PopUp hoặc bạn có thể sử dụng thành phần này làm bộ chọn phương thức và truyền thành phần này đến DatePickerDialog. Điểm khác biệt chính là bạn sử dụng DateRangePickerState thay vì DatePickerState.

Đoạn mã sau đây minh hoạ cách tạo bộ chọn ngày theo phương thức bật lên có phạm vi:

@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)
        )
    }
}

Các điểm chính về mã

  • Tham số onDateRangeSelected là một lệnh gọi lại nhận được Pair<Long?, Long?> đại diện cho ngày bắt đầu và ngày kết thúc đã chọn. Thao tác này cho phép thành phần kết hợp mẹ truy cập vào dải ô đã chọn.
  • rememberDateRangePickerState() tạo trạng thái cho bộ chọn phạm vi ngày.
  • DatePickerDialog tạo một vùng chứa hộp thoại phương thức.
  • Trong trình xử lý onClick của nút xác nhận, onDateRangeSelected sẽ truyền phạm vi đã chọn lên thành phần kết hợp mẹ.
  • Thành phần kết hợp DateRangePicker đóng vai trò là nội dung hộp thoại.

Kết quả

Phương thức triển khai có dạng như sau:

Ví dụ về bộ chọn ngày theo phạm vi của cửa sổ bật lên.
Hình 4. Một bộ chọn ngày theo phương thức bật lên có phạm vi đã chọn.

Sử dụng ngày đã chọn

Để ghi lại ngày đã chọn, hãy theo dõi ngày đó trong thành phần kết hợp mẹ dưới dạng Long và truyền giá trị đó đến DatePicker trong onDateSelected. Đoạn mã sau đây minh hoạ điều này, mặc dù bạn có thể xem toàn bộ quá trình triển khai trong ứng dụng đoạn mã chính thức.

// ...
    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 }
        )
    }
// ...

Về cơ bản, điều này cũng áp dụng cho bộ chọn ngày theo phạm vi, mặc dù bạn cần sử dụng Pair<Long?, Long?> hoặc lớp dữ liệu để ghi lại các giá trị bắt đầu và kết thúc.

Xem thêm