pubiqq 1dc0e6b490 [MaterialTimePicker] Fix AM/PM toggle
Before the CL we set AM/PM selection in the listeners no matter the target view (AM/PM) is being checked or being unchecked. So the order of calling listeners of AM and PM can "revert" the selection, if the callback for being unchecked is called later.

This logic is apparently wrong - we should select AM/PM when they are being unchecked anyways.

Resolves https://github.com/material-components/material-components-android/pull/2791
Resolves https://github.com/material-components/material-components-android/issues/2788

GIT_ORIGIN_REV_ID=018e053394f44d6334827fc3f95406c2937ae582
PiperOrigin-RevId: 458027987
2022-07-06 04:51:59 -07:00

285 lines
9.9 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.View.GONE;
import static com.google.android.material.timepicker.TimeFormat.CLOCK_12H;
import static java.util.Calendar.AM;
import static java.util.Calendar.HOUR;
import static java.util.Calendar.MINUTE;
import static java.util.Calendar.PM;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import androidx.appcompat.content.res.AppCompatResources;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import com.google.android.material.button.MaterialButtonToggleGroup;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.internal.TextWatcherAdapter;
import com.google.android.material.internal.ViewUtils;
import com.google.android.material.timepicker.TimePickerView.OnSelectionChange;
import java.lang.reflect.Field;
import java.util.Locale;
class TimePickerTextInputPresenter implements OnSelectionChange, TimePickerPresenter {
private final LinearLayout timePickerView;
private final TimeModel time;
private final TextWatcher minuteTextWatcher =
new TextWatcherAdapter() {
@Override
public void afterTextChanged(Editable s) {
try {
if (TextUtils.isEmpty(s)) {
time.setMinute(0);
return;
}
int minute = Integer.parseInt(s.toString());
time.setMinute(minute);
} catch (NumberFormatException ok) {
// ignore invalid input
}
}
};
private final TextWatcher hourTextWatcher =
new TextWatcherAdapter() {
@Override
public void afterTextChanged(Editable s) {
try {
if (TextUtils.isEmpty(s)) {
time.setHour(0);
return;
}
int hour = Integer.parseInt(s.toString());
time.setHour(hour);
} catch (NumberFormatException ok) {
// ignore invalid input
}
}
};
private final ChipTextInputComboView minuteTextInput;
private final ChipTextInputComboView hourTextInput;
private final TimePickerTextInputKeyController controller;
private final EditText hourEditText;
private final EditText minuteEditText;
private MaterialButtonToggleGroup toggle;
public TimePickerTextInputPresenter(final LinearLayout timePickerView, final TimeModel time) {
this.timePickerView = timePickerView;
this.time = time;
Resources res = timePickerView.getResources();
minuteTextInput = timePickerView.findViewById(R.id.material_minute_text_input);
hourTextInput = timePickerView.findViewById(R.id.material_hour_text_input);
TextView minuteLabel = minuteTextInput.findViewById(R.id.material_label);
TextView hourLabel = hourTextInput.findViewById(R.id.material_label);
minuteLabel.setText(res.getString(R.string.material_timepicker_minute));
hourLabel.setText(res.getString(R.string.material_timepicker_hour));
minuteTextInput.setTag(R.id.selection_type, MINUTE);
hourTextInput.setTag(R.id.selection_type, HOUR);
if (time.format == CLOCK_12H) {
setupPeriodToggle();
}
OnClickListener onClickListener =
new OnClickListener() {
@Override
public void onClick(View v) {
onSelectionChanged((int) v.getTag(R.id.selection_type));
}
};
hourTextInput.setOnClickListener(onClickListener);
minuteTextInput.setOnClickListener(onClickListener);
hourTextInput.addInputFilter(time.getHourInputValidator());
minuteTextInput.addInputFilter(time.getMinuteInputValidator());
hourEditText = hourTextInput.getTextInput().getEditText();
minuteEditText = minuteTextInput.getTextInput().getEditText();
if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) {
// Our XML drawable is not colored for pre-lollipop, set color programmatically.
int primaryColor = MaterialColors.getColor(timePickerView, R.attr.colorPrimary);
setCursorDrawableColor(hourEditText, primaryColor);
setCursorDrawableColor(minuteEditText, primaryColor);
}
controller = new TimePickerTextInputKeyController(hourTextInput, minuteTextInput, time);
hourTextInput.setChipDelegate(
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(
R.string.material_hour_suffix, String.valueOf(time.getHourForDisplay())));
}
});
minuteTextInput.setChipDelegate(
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)));
}
});
initialize();
}
@Override
public void initialize() {
addTextWatchers();
setTime(time);
controller.bind();
}
private void addTextWatchers() {
hourEditText.addTextChangedListener(hourTextWatcher);
minuteEditText.addTextChangedListener(minuteTextWatcher);
}
private void removeTextWatchers() {
hourEditText.removeTextChangedListener(hourTextWatcher);
minuteEditText.removeTextChangedListener(minuteTextWatcher);
}
private void setTime(TimeModel time) {
removeTextWatchers();
Locale current = timePickerView.getResources().getConfiguration().locale;
String minuteFormatted = String.format(current, "%02d", time.minute);
String hourFormatted = String.format(current, "%02d", time.getHourForDisplay());
minuteTextInput.setText(minuteFormatted);
hourTextInput.setText(hourFormatted);
addTextWatchers();
updateSelection();
}
private void setupPeriodToggle() {
toggle = timePickerView.findViewById(R.id.material_clock_period_toggle);
toggle.addOnButtonCheckedListener((group, checkedId, isChecked) -> {
if (!isChecked) {
return;
}
int period = checkedId == R.id.material_clock_period_pm_button ? PM : AM;
time.setPeriod(period);
});
toggle.setVisibility(View.VISIBLE);
updateSelection();
}
private void updateSelection() {
if (toggle == null) {
return;
}
toggle.check(
time.period == AM
? R.id.material_clock_period_am_button
: R.id.material_clock_period_pm_button);
}
@Override
public void onSelectionChanged(int selection) {
time.selection = selection;
minuteTextInput.setChecked(selection == MINUTE);
hourTextInput.setChecked(selection == HOUR);
updateSelection();
}
@Override
public void show() {
timePickerView.setVisibility(View.VISIBLE);
onSelectionChanged(time.selection);
}
@Override
public void hide() {
View currentFocus = timePickerView.getFocusedChild();
if (currentFocus != null) {
ViewUtils.hideKeyboard(currentFocus);
}
timePickerView.setVisibility(GONE);
}
@Override
public void invalidate() {
setTime(time);
}
/*
* android:textColorDrawable doesn't have an app compat version to be able to use theme attributes
* for colors. We have to apply a color filter manually. This method is only meant to be used
* before API 21.
*/
private static void setCursorDrawableColor(EditText view, @ColorInt int color) {
try {
Context context = view.getContext();
Field cursorDrawableResField = TextView.class.getDeclaredField("mCursorDrawableRes");
cursorDrawableResField.setAccessible(true);
int cursorDrawableResId = cursorDrawableResField.getInt(view);
Field editorField = TextView.class.getDeclaredField("mEditor");
editorField.setAccessible(true);
Object editor = editorField.get(view);
Class<?> clazz = editor.getClass();
Field cursorDrawableField = clazz.getDeclaredField("mCursorDrawable");
cursorDrawableField.setAccessible(true);
Drawable drawable = AppCompatResources.getDrawable(context, cursorDrawableResId);
drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
Drawable[] drawables = {drawable, drawable};
cursorDrawableField.set(editor, drawables);
} catch (Throwable ignored) {
// ignore use the drawable default color (black).
}
}
public void resetChecked() {
minuteTextInput.setChecked(time.selection == MINUTE);
hourTextInput.setChecked(time.selection == HOUR);
}
public void clearCheck() {
minuteTextInput.setChecked(false);
hourTextInput.setChecked(false);
}
}