pubiqq 80593b2a4e [TimePicker] Fix number format on 24-hour dial
Resolves https://github.com/material-components/material-components-android/pull/4523

GIT_ORIGIN_REV_ID=dcee0b2865bd9c9512b581d34d95aac998cac6a0
PiperOrigin-RevId: 713018088
2025-01-08 18:06:19 +00:00

259 lines
9.1 KiB
Java

/*
* Copyright (C) 2020 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.timepicker;
import com.google.android.material.R;
import static android.view.HapticFeedbackConstants.CLOCK_TICK;
import static android.view.View.GONE;
import static androidx.core.content.ContextCompat.getSystemService;
import static com.google.android.material.timepicker.RadialViewGroup.LEVEL_1;
import static com.google.android.material.timepicker.RadialViewGroup.LEVEL_2;
import static com.google.android.material.timepicker.TimeFormat.CLOCK_12H;
import static com.google.android.material.timepicker.TimeFormat.CLOCK_24H;
import static java.util.Calendar.HOUR;
import static java.util.Calendar.MINUTE;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import com.google.android.material.timepicker.ClockHandView.OnActionUpListener;
import com.google.android.material.timepicker.ClockHandView.OnRotateListener;
import com.google.android.material.timepicker.TimePickerControls.ActiveSelection;
import com.google.android.material.timepicker.TimePickerView.OnPeriodChangeListener;
import com.google.android.material.timepicker.TimePickerView.OnSelectionChange;
class TimePickerClockPresenter
implements OnRotateListener,
OnSelectionChange,
OnPeriodChangeListener,
OnActionUpListener,
TimePickerPresenter {
private static final String[] HOUR_CLOCK_VALUES = {
"12", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"
};
private static final String[] HOUR_CLOCK_24_VALUES = {
"00", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16",
"17", "18", "19", "20", "21", "22", "23"
};
private static final String[] MINUTE_CLOCK_VALUES = {
"00", "5", "10", "15", "20", "25", "30", "35", "40", "45", "50", "55"
};
private static final int DEGREES_PER_HOUR = 30;
private static final int DEGREES_PER_MINUTE = 6;
private final TimePickerView timePickerView;
private final TimeModel time;
private float minuteRotation;
private float hourRotation;
private boolean broadcasting = false;
public TimePickerClockPresenter(TimePickerView timePickerView, TimeModel time) {
this.timePickerView = timePickerView;
this.time = time;
initialize();
}
@Override
public void initialize() {
if (time.format == CLOCK_12H) {
timePickerView.showToggle();
}
timePickerView.addOnRotateListener(this);
timePickerView.setOnSelectionChangeListener(this);
timePickerView.setOnPeriodChangeListener(this);
timePickerView.setOnActionUpListener(this);
updateValues();
invalidate();
}
@Override
public void invalidate() {
hourRotation = getHourRotation();
minuteRotation = time.minute * DEGREES_PER_MINUTE;
setSelection(time.selection, false);
updateTime();
}
@Override
public void show() {
timePickerView.setVisibility(View.VISIBLE);
}
@Override
public void hide() {
timePickerView.setVisibility(GONE);
}
private String[] getHourClockValues() {
return time.format == CLOCK_24H ? HOUR_CLOCK_24_VALUES : HOUR_CLOCK_VALUES;
}
@Override
public void onRotate(float rotation, boolean animating) {
// Do not update the displayed and actual time during an animation
if (broadcasting || animating) {
return;
}
int prevHour = time.hour;
int prevMinute = time.minute;
int rotationInt = Math.round(rotation);
if (time.selection == MINUTE) {
int minuteOffset = DEGREES_PER_MINUTE / 2;
time.setMinute((rotationInt + minuteOffset) / DEGREES_PER_MINUTE);
minuteRotation = (float) Math.floor(time.minute * DEGREES_PER_MINUTE);
} else {
int hourOffset = DEGREES_PER_HOUR / 2;
int hour = (rotationInt + hourOffset) / DEGREES_PER_HOUR;
if (time.format == CLOCK_24H) {
hour %= 12; // To correct hour (12 -> 0) in the 345-360 rotation section.
if (timePickerView.getCurrentLevel() == LEVEL_2) {
hour += 12;
}
}
time.setHour(hour);
hourRotation = getHourRotation();
}
updateTime();
performHapticFeedback(prevHour, prevMinute);
}
private void performHapticFeedback(int prevHour, int prevMinute) {
if (time.minute != prevMinute || time.hour != prevHour) {
timePickerView.performHapticFeedback(CLOCK_TICK);
}
}
@Override
public void onSelectionChanged(int selection) {
setSelection(selection, true);
}
@Override
public void onPeriodChange(int period) {
time.setPeriod(period);
}
void setSelection(@ActiveSelection int selection, boolean animate) {
boolean isMinute = selection == MINUTE;
// Don't animate hours since we are going to auto switch to the minute selection.
timePickerView.setAnimateOnTouchUp(isMinute);
time.selection = selection;
timePickerView.setValues(
isMinute ? MINUTE_CLOCK_VALUES : getHourClockValues(),
isMinute ? R.string.material_minute_suffix : time.getHourContentDescriptionResId());
updateCurrentLevel();
timePickerView.setHandRotation(isMinute ? minuteRotation : hourRotation, animate);
timePickerView.setActiveSelection(selection);
timePickerView.setMinuteHourDelegate(
new ClickActionDelegate(timePickerView.getContext(), R.string.material_hour_selection) {
@Override
public void onInitializeAccessibilityNodeInfo(
View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
info.setContentDescription(
host.getResources()
.getString(
time.getHourContentDescriptionResId(),
String.valueOf(time.getHourForDisplay())));
}
});
timePickerView.setHourClickDelegate(
new ClickActionDelegate(timePickerView.getContext(), R.string.material_minute_selection) {
@Override
public void onInitializeAccessibilityNodeInfo(
View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
info.setContentDescription(
host.getResources()
.getString(R.string.material_minute_suffix, String.valueOf(time.minute)));
}
});
}
private void updateCurrentLevel() {
int currentLevel = LEVEL_1;
if (time.selection == HOUR && time.format == CLOCK_24H && time.hour >= 12) {
currentLevel = LEVEL_2;
}
timePickerView.setCurrentLevel(currentLevel);
}
@Override
public void onActionUp(float rotation, boolean moveInEventStream) {
broadcasting = true;
int prevMinute = time.minute;
int prevHour = time.hour;
if (time.selection == HOUR) {
// Current rotation might be half way to an exact hour position.
// Snap to the closest hour before animating to the position the minute selection is on.
timePickerView.setHandRotation(hourRotation, /* animate= */ false);
// Automatically move to minutes once the user finishes choosing the hour.
AccessibilityManager am =
getSystemService(timePickerView.getContext(), AccessibilityManager.class);
boolean isExploreByTouchEnabled = am != null && am.isTouchExplorationEnabled();
if (!isExploreByTouchEnabled) {
setSelection(MINUTE, /* animate= */ true);
}
} else {
int rotationInt = Math.round(rotation);
if (!moveInEventStream) {
// snap minute to 5 minute increment if there was only a touch down/up.
int newRotation = (rotationInt + 15) / 30;
time.setMinute(newRotation * 5);
minuteRotation = time.minute * DEGREES_PER_MINUTE;
}
timePickerView.setHandRotation(minuteRotation, /* animate= */ moveInEventStream);
}
broadcasting = false;
updateTime();
performHapticFeedback(prevHour, prevMinute);
}
private void updateTime() {
timePickerView.updateTime(time.period, time.getHourForDisplay(), time.minute);
}
/** Update values with the correct number format */
private void updateValues() {
updateValues(HOUR_CLOCK_VALUES, TimeModel.NUMBER_FORMAT);
updateValues(HOUR_CLOCK_24_VALUES, TimeModel.NUMBER_FORMAT);
updateValues(MINUTE_CLOCK_VALUES, TimeModel.ZERO_LEADING_NUMBER_FORMAT);
}
private void updateValues(String[] values, String format) {
for (int i = 0; i < values.length; ++i) {
values[i] = TimeModel.formatText(timePickerView.getResources(), values[i], format);
}
}
private int getHourRotation() {
return (time.getHourForDisplay() * DEGREES_PER_HOUR) % 360;
}
}