mirror of
https://github.com/material-components/material-components-android.git
synced 2026-01-16 18:01:42 +08:00
Resolves https://github.com/material-components/material-components-android/issues/1949 PiperOrigin-RevId: 496865765
390 lines
12 KiB
Java
390 lines
12 KiB
Java
/*
|
|
* Copyright 2019 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package com.google.android.material.datepicker;
|
|
|
|
import android.os.Bundle;
|
|
import android.os.Parcel;
|
|
import android.os.Parcelable;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.core.util.ObjectsCompat;
|
|
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
|
import java.util.Arrays;
|
|
import java.util.Calendar;
|
|
import java.util.Objects;
|
|
|
|
/**
|
|
* Used to limit the display range of the calendar and set an openAt month.
|
|
*
|
|
* <p>Implements {@link Parcelable} in order to maintain the {@code CalendarConstraints} across
|
|
* device configuration changes. Parcelable breaks when passed between processes.
|
|
*/
|
|
public final class CalendarConstraints implements Parcelable {
|
|
|
|
@NonNull private final Month start;
|
|
@NonNull private final Month end;
|
|
@NonNull private final DateValidator validator;
|
|
|
|
@Nullable private Month openAt;
|
|
private final int firstDayOfWeek;
|
|
|
|
private final int yearSpan;
|
|
private final int monthSpan;
|
|
|
|
/**
|
|
* Used to determine whether calendar days are enabled.
|
|
*
|
|
* <p>Extends {@link Parcelable} in order to maintain the {@code DateValidator} across device
|
|
* configuration changes. Parcelable breaks when passed between processes.
|
|
*/
|
|
public interface DateValidator extends Parcelable {
|
|
|
|
/** Returns true if the provided {@code date} is enabled. */
|
|
boolean isValid(long date);
|
|
}
|
|
|
|
private CalendarConstraints(
|
|
@NonNull Month start,
|
|
@NonNull Month end,
|
|
@NonNull DateValidator validator,
|
|
@Nullable Month openAt,
|
|
int firstDayOfWeek) {
|
|
Objects.requireNonNull(start, "start cannot be null");
|
|
Objects.requireNonNull(end, "end cannot be null");
|
|
Objects.requireNonNull(validator, "validator cannot be null");
|
|
this.start = start;
|
|
this.end = end;
|
|
this.openAt = openAt;
|
|
this.firstDayOfWeek = firstDayOfWeek;
|
|
this.validator = validator;
|
|
if (openAt != null && start.compareTo(openAt) > 0) {
|
|
throw new IllegalArgumentException("start Month cannot be after current Month");
|
|
}
|
|
if (openAt != null && openAt.compareTo(end) > 0) {
|
|
throw new IllegalArgumentException("current Month cannot be after end Month");
|
|
}
|
|
if (firstDayOfWeek < 0
|
|
|| firstDayOfWeek > UtcDates.getUtcCalendar().getMaximum(Calendar.DAY_OF_WEEK)) {
|
|
throw new IllegalArgumentException("firstDayOfWeek is not valid");
|
|
}
|
|
monthSpan = start.monthsUntil(end) + 1;
|
|
yearSpan = end.year - start.year + 1;
|
|
}
|
|
|
|
boolean isWithinBounds(long date) {
|
|
return start.getDay(1) <= date && date <= end.getDay(end.daysInMonth);
|
|
}
|
|
|
|
/**
|
|
* Returns the {@link DateValidator} that determines whether a date can be clicked and selected.
|
|
*/
|
|
public DateValidator getDateValidator() {
|
|
return validator;
|
|
}
|
|
|
|
/** Returns the earliest month allowed by this set of bounds. */
|
|
@NonNull
|
|
Month getStart() {
|
|
return start;
|
|
}
|
|
|
|
/** Returns the latest month allowed by this set of bounds. */
|
|
@NonNull
|
|
Month getEnd() {
|
|
return end;
|
|
}
|
|
|
|
/** Returns the openAt month within this set of bounds. */
|
|
@Nullable
|
|
Month getOpenAt() {
|
|
return openAt;
|
|
}
|
|
|
|
/** Sets the openAt month. */
|
|
void setOpenAt(@Nullable Month openAt) {
|
|
this.openAt = openAt;
|
|
}
|
|
|
|
/** Returns the firstDayOfWeek. */
|
|
int getFirstDayOfWeek() {
|
|
return firstDayOfWeek;
|
|
}
|
|
|
|
/**
|
|
* Returns the total number of {@link java.util.Calendar#MONTH} included in {@code start} to
|
|
* {@code end}.
|
|
*/
|
|
int getMonthSpan() {
|
|
return monthSpan;
|
|
}
|
|
|
|
/**
|
|
* Returns the total number of {@link java.util.Calendar#YEAR} included in {@code start} to {@code
|
|
* end}.
|
|
*/
|
|
int getYearSpan() {
|
|
return yearSpan;
|
|
}
|
|
|
|
/** Returns the earliest time in milliseconds allowed by this set of bounds. */
|
|
public long getStartMs() {
|
|
return start.timeInMillis;
|
|
}
|
|
|
|
/** Returns the latest time in milliseconds allowed by this set of bounds. */
|
|
public long getEndMs() {
|
|
return end.timeInMillis;
|
|
}
|
|
|
|
/**
|
|
* Returns the openAt time in milliseconds within this set of bounds. Returns null if not
|
|
* available.
|
|
*/
|
|
@Nullable
|
|
public Long getOpenAtMs() {
|
|
return openAt == null ? null : openAt.timeInMillis;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (this == o) {
|
|
return true;
|
|
}
|
|
if (!(o instanceof CalendarConstraints)) {
|
|
return false;
|
|
}
|
|
CalendarConstraints that = (CalendarConstraints) o;
|
|
return start.equals(that.start)
|
|
&& end.equals(that.end)
|
|
&& ObjectsCompat.equals(openAt, that.openAt)
|
|
&& firstDayOfWeek == that.firstDayOfWeek
|
|
&& validator.equals(that.validator);
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
Object[] hashedFields = {start, end, openAt, firstDayOfWeek, validator};
|
|
return Arrays.hashCode(hashedFields);
|
|
}
|
|
|
|
/* Parcelable interface */
|
|
|
|
/** {@link Parcelable.Creator} */
|
|
public static final Parcelable.Creator<CalendarConstraints> CREATOR =
|
|
new Parcelable.Creator<CalendarConstraints>() {
|
|
@NonNull
|
|
@Override
|
|
public CalendarConstraints createFromParcel(@NonNull Parcel source) {
|
|
Month start = source.readParcelable(Month.class.getClassLoader());
|
|
Month end = source.readParcelable(Month.class.getClassLoader());
|
|
Month openAt = source.readParcelable(Month.class.getClassLoader());
|
|
DateValidator validator = source.readParcelable(DateValidator.class.getClassLoader());
|
|
int firstDayOfWeek = source.readInt();
|
|
return new CalendarConstraints(start, end, validator, openAt, firstDayOfWeek);
|
|
}
|
|
|
|
@NonNull
|
|
@Override
|
|
public CalendarConstraints[] newArray(int size) {
|
|
return new CalendarConstraints[size];
|
|
}
|
|
};
|
|
|
|
@Override
|
|
public int describeContents() {
|
|
return 0;
|
|
}
|
|
|
|
@Override
|
|
public void writeToParcel(Parcel dest, int flags) {
|
|
dest.writeParcelable(start, /* parcelableFlags= */ 0);
|
|
dest.writeParcelable(end, /* parcelableFlags= */ 0);
|
|
dest.writeParcelable(openAt, /* parcelableFlags= */ 0);
|
|
dest.writeParcelable(validator, /* parcelableFlags = */ 0);
|
|
dest.writeInt(firstDayOfWeek);
|
|
}
|
|
|
|
/**
|
|
* Returns the given month if it's within the constraints or the closest bound if it's outside.
|
|
*/
|
|
Month clamp(Month month) {
|
|
if (month.compareTo(start) < 0) {
|
|
return start;
|
|
}
|
|
|
|
if (month.compareTo(end) > 0) {
|
|
return end;
|
|
}
|
|
|
|
return month;
|
|
}
|
|
|
|
/** Builder for {@link com.google.android.material.datepicker.CalendarConstraints}. */
|
|
public static final class Builder {
|
|
|
|
/**
|
|
* Default UTC timeInMilliseconds for the first selectable month unless {@link Builder#setStart}
|
|
* is called. Set to January, 1900.
|
|
*/
|
|
static final long DEFAULT_START =
|
|
UtcDates.canonicalYearMonthDay(Month.create(1900, Calendar.JANUARY).timeInMillis);
|
|
/**
|
|
* Default UTC timeInMilliseconds for the last selectable month unless {@link Builder#setEnd} is
|
|
* called. Set to December, 2100.
|
|
*/
|
|
static final long DEFAULT_END =
|
|
UtcDates.canonicalYearMonthDay(Month.create(2100, Calendar.DECEMBER).timeInMillis);
|
|
|
|
private static final String DEEP_COPY_VALIDATOR_KEY = "DEEP_COPY_VALIDATOR_KEY";
|
|
|
|
private long start = DEFAULT_START;
|
|
private long end = DEFAULT_END;
|
|
private Long openAt;
|
|
private int firstDayOfWeek;
|
|
private DateValidator validator = DateValidatorPointForward.from(Long.MIN_VALUE);
|
|
|
|
public Builder() {}
|
|
|
|
Builder(@NonNull CalendarConstraints clone) {
|
|
start = clone.start.timeInMillis;
|
|
end = clone.end.timeInMillis;
|
|
openAt = clone.openAt.timeInMillis;
|
|
firstDayOfWeek = clone.firstDayOfWeek;
|
|
validator = clone.validator;
|
|
}
|
|
|
|
/**
|
|
* A UTC timeInMilliseconds contained within the earliest month the calendar will page to.
|
|
* Defaults January, 1900.
|
|
*
|
|
* <p>If you have access to java.time in Java 8, you can obtain a long using {@code
|
|
* java.time.ZonedDateTime}.
|
|
*
|
|
* <pre>{@code
|
|
* LocalDateTime local = LocalDateTime.of(year, month, 1, 0, 0);
|
|
* local.atZone(ZoneId.ofOffset("UTC", ZoneOffset.UTC)).toInstant().toEpochMilli();
|
|
* }</pre>
|
|
*
|
|
* <p>If you don't have access to java.time in Java 8, you can obtain this value using a {@code
|
|
* java.util.Calendar} instance from the UTC timezone.
|
|
*
|
|
* <pre>{@code
|
|
* Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
|
|
* c.set(year, month, 1);
|
|
* c.getTimeInMillis();
|
|
* }</pre>
|
|
*/
|
|
@NonNull
|
|
@CanIgnoreReturnValue
|
|
public Builder setStart(long month) {
|
|
start = month;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* A UTC timeInMilliseconds contained within the latest month the calendar will page to.
|
|
* Defaults December, 2100.
|
|
*
|
|
* <p>If you have access to java.time in Java 8, you can obtain a long using {@code
|
|
* java.time.ZonedDateTime}.
|
|
*
|
|
* <pre>{@code
|
|
* LocalDateTime local = LocalDateTime.of(year, month, 1, 0, 0);
|
|
* local.atZone(ZoneId.ofOffset("UTC", ZoneOffset.UTC)).toInstant().toEpochMilli();
|
|
* }</pre>
|
|
*
|
|
* <p>If you don't have access to java.time in Java 8, you can obtain this value using a {@code
|
|
* java.util.Calendar} instance from the UTC timezone.
|
|
*
|
|
* <pre>{@code
|
|
* Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
|
|
* c.set(year, month, 1);
|
|
* c.getTimeInMillis();
|
|
* }</pre>
|
|
*/
|
|
@NonNull
|
|
@CanIgnoreReturnValue
|
|
public Builder setEnd(long month) {
|
|
end = month;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* A UTC timeInMilliseconds contained within the month the calendar should openAt. Defaults to
|
|
* the month containing today if within bounds; otherwise, defaults to the starting month.
|
|
*
|
|
* <p>If you have access to java.time in Java 8, you can obtain a long using {@code
|
|
* java.time.ZonedDateTime}.
|
|
*
|
|
* <pre>{@code
|
|
* LocalDateTime local = LocalDateTime.of(year, month, 1, 0, 0);
|
|
* local.atZone(ZoneId.ofOffset("UTC", ZoneOffset.UTC)).toInstant().toEpochMilli();
|
|
* }</pre>
|
|
*
|
|
* <p>If you don't have access to java.time in Java 8, you can obtain this value using a {@code
|
|
* java.util.Calendar} instance from the UTC timezone.
|
|
*
|
|
* <pre>{@code
|
|
* Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
|
|
* c.set(year, month, 1);
|
|
* c.getTimeInMillis();
|
|
* }</pre>
|
|
*/
|
|
@NonNull
|
|
@CanIgnoreReturnValue
|
|
public Builder setOpenAt(long month) {
|
|
openAt = month;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets what the first day of the week is; e.g., <code>Calendar.SUNDAY</code> in the U.S.,
|
|
* <code>Calendar.MONDAY</code> in France.
|
|
*/
|
|
@NonNull
|
|
@CanIgnoreReturnValue
|
|
public Builder setFirstDayOfWeek(int firstDayOfWeek) {
|
|
this.firstDayOfWeek = firstDayOfWeek;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Limits valid dates to those for which {@link DateValidator#isValid(long)} is true. Defaults
|
|
* to all dates as valid.
|
|
*/
|
|
@NonNull
|
|
@CanIgnoreReturnValue
|
|
public Builder setValidator(@NonNull DateValidator validator) {
|
|
Objects.requireNonNull(validator, "validator cannot be null");
|
|
this.validator = validator;
|
|
return this;
|
|
}
|
|
|
|
/** Builds the {@link CalendarConstraints} object using the set parameters or defaults. */
|
|
@NonNull
|
|
public CalendarConstraints build() {
|
|
Bundle deepCopyBundle = new Bundle();
|
|
deepCopyBundle.putParcelable(DEEP_COPY_VALIDATOR_KEY, validator);
|
|
return new CalendarConstraints(
|
|
Month.create(start),
|
|
Month.create(end),
|
|
(DateValidator) deepCopyBundle.getParcelable(DEEP_COPY_VALIDATOR_KEY),
|
|
openAt == null ? null : Month.create(openAt),
|
|
firstDayOfWeek);
|
|
}
|
|
}
|
|
}
|