Adding text input mode to TimePicker.

Test: Tests will have a follow up CL.
Bug:30044800

Change-Id: I71a0fba97026da76251768016edf1b159617abb9
This commit is contained in:
Aurimas Liutikas
2017-01-24 17:46:10 -08:00
parent 40eb0ba427
commit ab14d825c3
19 changed files with 712 additions and 155 deletions

View File

@@ -50543,6 +50543,7 @@ package android.widget {
method public void setIs24HourView(java.lang.Boolean);
method public void setMinute(int);
method public void setOnTimeChangedListener(android.widget.TimePicker.OnTimeChangedListener);
method public boolean validateInput();
}
public static abstract interface TimePicker.OnTimeChangedListener {

View File

@@ -54314,6 +54314,7 @@ package android.widget {
method public void setIs24HourView(java.lang.Boolean);
method public void setMinute(int);
method public void setOnTimeChangedListener(android.widget.TimePicker.OnTimeChangedListener);
method public boolean validateInput();
}
public static abstract interface TimePicker.OnTimeChangedListener {

View File

@@ -50868,6 +50868,7 @@ package android.widget {
method public void setIs24HourView(java.lang.Boolean);
method public void setMinute(int);
method public void setOnTimeChangedListener(android.widget.TimePicker.OnTimeChangedListener);
method public boolean validateInput();
field public static final int MODE_CLOCK = 2; // 0x2
field public static final int MODE_SPINNER = 1; // 0x1
}

View File

@@ -144,15 +144,26 @@ public class TimePickerDialog extends AlertDialog implements OnClickListener,
/* do nothing */
}
@Override
public void show() {
super.show();
getButton(BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mTimePicker.validateInput()) {
if (mTimeSetListener != null) {
mTimeSetListener.onTimeSet(mTimePicker, mTimePicker.getCurrentHour(),
mTimePicker.getCurrentMinute());
}
dismiss();
}
}
});
}
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case BUTTON_POSITIVE:
if (mTimeSetListener != null) {
mTimeSetListener.onTimeSet(mTimePicker, mTimePicker.getCurrentHour(),
mTimePicker.getCurrentMinute());
}
break;
case BUTTON_NEGATIVE:
cancel();
break;

View File

@@ -0,0 +1,249 @@
/*
* Copyright (C) 2017 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 android.widget;
import android.content.Context;
import android.text.Editable;
import android.text.InputFilter;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.MathUtils;
import android.view.View;
import com.android.internal.R;
/**
* View to show text input based time picker with hour and minute fields and an optional AM/PM
* spinner.
*
* @hide
*/
public class TextInputTimePickerView extends RelativeLayout {
public static final int HOURS = 0;
public static final int MINUTES = 1;
public static final int AMPM = 2;
private static final int AM = 0;
private static final int PM = 1;
private final EditText mHourEditText;
private final EditText mMinuteEditText;
private final TextView mInputSeparatorView;
private final Spinner mAmPmSpinner;
private final TextView mErrorLabel;
private final TextView mHourLabel;
private final TextView mMinuteLabel;
private boolean mIs24Hour;
private boolean mHourFormatStartsAtZero;
private OnValueTypedListener mListener;
private boolean mErrorShowing;
interface OnValueTypedListener {
void onValueChanged(int inputType, int newValue);
}
public TextInputTimePickerView(Context context) {
this(context, null);
}
public TextInputTimePickerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TextInputTimePickerView(Context context, AttributeSet attrs, int defStyle) {
this(context, attrs, defStyle, 0);
}
public TextInputTimePickerView(Context context, AttributeSet attrs, int defStyle,
int defStyleRes) {
super(context, attrs, defStyle, defStyleRes);
inflate(context, R.layout.time_picker_text_input_material, this);
mHourEditText = (EditText) findViewById(R.id.input_hour);
mMinuteEditText = (EditText) findViewById(R.id.input_minute);
mInputSeparatorView = (TextView) findViewById(R.id.input_separator);
mErrorLabel = (TextView) findViewById(R.id.label_error);
mHourLabel = (TextView) findViewById(R.id.label_hour);
mMinuteLabel = (TextView) findViewById(R.id.label_minute);
mHourEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
@Override
public void afterTextChanged(Editable editable) {
parseAndSetHourInternal(editable.toString());
}
});
mMinuteEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
@Override
public void afterTextChanged(Editable editable) {
parseAndSetMinuteInternal(editable.toString());
}
});
mAmPmSpinner = (Spinner) findViewById(R.id.am_pm_spinner);
final String[] amPmStrings = TimePicker.getAmPmStrings(context);
ArrayAdapter<CharSequence> adapter =
new ArrayAdapter<CharSequence>(context, R.layout.simple_spinner_dropdown_item);
adapter.add(TimePickerClockDelegate.obtainVerbatim(amPmStrings[0]));
adapter.add(TimePickerClockDelegate.obtainVerbatim(amPmStrings[1]));
mAmPmSpinner.setAdapter(adapter);
mAmPmSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int position,
long id) {
if (position == 0) {
mListener.onValueChanged(AMPM, AM);
} else {
mListener.onValueChanged(AMPM, PM);
}
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {}
});
}
void setListener(OnValueTypedListener listener) {
mListener = listener;
}
void setHourFormat(int maxCharLength) {
mHourEditText.setFilters(new InputFilter[] {
new InputFilter.LengthFilter(maxCharLength)});
mMinuteEditText.setFilters(new InputFilter[] {
new InputFilter.LengthFilter(maxCharLength)});
}
boolean validateInput() {
final boolean inputValid = parseAndSetHourInternal(mHourEditText.getText().toString())
&& parseAndSetMinuteInternal(mMinuteEditText.getText().toString());
setError(!inputValid);
return inputValid;
}
void updateSeparator(String separatorText) {
mInputSeparatorView.setText(separatorText);
}
private void setError(boolean enabled) {
mErrorShowing = enabled;
mErrorLabel.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE);
mHourLabel.setVisibility(enabled ? View.INVISIBLE : View.VISIBLE);
mMinuteLabel.setVisibility(enabled ? View.INVISIBLE : View.VISIBLE);
}
/**
* Computes the display value and updates the text of the view.
* <p>
* This method should be called whenever the current value or display
* properties (leading zeroes, max digits) change.
*/
void updateTextInputValues(int localizedHour, int minute, int amOrPm, boolean is24Hour,
boolean hourFormatStartsAtZero) {
final String format = "%d";
mIs24Hour = is24Hour;
mHourFormatStartsAtZero = hourFormatStartsAtZero;
mAmPmSpinner.setVisibility(is24Hour ? View.INVISIBLE : View.VISIBLE);
mHourEditText.setText(String.format(format, localizedHour));
mMinuteEditText.setText(String.format(format, minute));
if (amOrPm == AM) {
mAmPmSpinner.setSelection(0);
} else {
mAmPmSpinner.setSelection(1);
}
if (mErrorShowing) {
validateInput();
}
}
private boolean parseAndSetHourInternal(String input) {
try {
final int hour = Integer.parseInt(input);
if (!isValidLocalizedHour(hour)) {
final int minHour = mHourFormatStartsAtZero ? 0 : 1;
final int maxHour = mIs24Hour ? 23 : 11 + minHour;
mListener.onValueChanged(HOURS, getHourOfDayFromLocalizedHour(
MathUtils.constrain(hour, minHour, maxHour)));
return false;
}
mListener.onValueChanged(HOURS, getHourOfDayFromLocalizedHour(hour));
return true;
} catch (NumberFormatException e) {
// Do nothing since we cannot parse the input.
return false;
}
}
private boolean parseAndSetMinuteInternal(String input) {
try {
final int minutes = Integer.parseInt(input);
if (minutes < 0 || minutes > 59) {
mListener.onValueChanged(MINUTES, MathUtils.constrain(minutes, 0, 59));
return false;
}
mListener.onValueChanged(MINUTES, minutes);
return true;
} catch (NumberFormatException e) {
// Do nothing since we cannot parse the input.
return false;
}
}
private boolean isValidLocalizedHour(int localizedHour) {
final int minHour = mHourFormatStartsAtZero ? 0 : 1;
final int maxHour = (mIs24Hour ? 23 : 11) + minHour;
return localizedHour >= minHour && localizedHour <= maxHour;
}
private int getHourOfDayFromLocalizedHour(int localizedHour) {
int hourOfDay = localizedHour;
if (mIs24Hour) {
if (!mHourFormatStartsAtZero && localizedHour == 24) {
hourOfDay = 0;
}
} else {
if (!mHourFormatStartsAtZero && localizedHour == 12) {
hourOfDay = 0;
}
if (mAmPmSpinner.getSelectedItemPosition() == 1) {
hourOfDay += 12;
}
}
return hourOfDay;
}
}

View File

@@ -278,6 +278,16 @@ public class TimePicker extends FrameLayout {
return mDelegate.getBaseline();
}
/**
* Validates whether current input by the user is a valid time based on the locale. TimePicker
* will show an error message to the user if the time is not valid.
*
* @return {@code true} if the input is valid, {@code false} otherwise
*/
public boolean validateInput() {
return mDelegate.validateInput();
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
@@ -341,6 +351,8 @@ public class TimePicker extends FrameLayout {
void setIs24Hour(boolean is24Hour);
boolean is24Hour();
boolean validateInput();
void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener);
void setEnabled(boolean enabled);

View File

@@ -16,12 +16,14 @@
package android.widget;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.annotation.TestApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.icu.text.DecimalFormatSymbols;
import android.os.Parcelable;
import android.text.SpannableStringBuilder;
import android.text.format.DateFormat;
@@ -40,11 +42,14 @@ import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.widget.RadialTimePickerView.OnValueSelectedListener;
import android.widget.TextInputTimePickerView.OnValueTypedListener;
import com.android.internal.R;
import com.android.internal.widget.NumericTextView;
import com.android.internal.widget.NumericTextView.OnValueChangedListener;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Calendar;
/**
@@ -58,6 +63,13 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
*/
private static final long DELAY_COMMIT_MILLIS = 2000;
@IntDef({FROM_EXTERNAL_API, FROM_RADIAL_PICKER, FROM_INPUT_PICKER})
@Retention(RetentionPolicy.SOURCE)
private @interface ChangeSource {}
private static final int FROM_EXTERNAL_API = 0;
private static final int FROM_RADIAL_PICKER = 1;
private static final int FROM_INPUT_PICKER = 2;
// Index used by RadialPickerLayout
private static final int HOUR_INDEX = RadialTimePickerView.HOURS;
private static final int MINUTE_INDEX = RadialTimePickerView.MINUTES;
@@ -78,6 +90,15 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
private final RadialTimePickerView mRadialTimePickerView;
private final TextView mSeparatorView;
private boolean mRadialPickerModeEnabled = true;
private final ImageButton mRadialTimePickerModeButton;
private final String mRadialTimePickerModeEnabledDescription;
private final String mTextInputPickerModeEnabledDescription;
private final View mRadialTimePickerHeader;
private final View mTextInputPickerHeader;
private final TextInputTimePickerView mTextInputPickerView;
private final Calendar mTempCalendar;
// Accessibility strings.
@@ -116,8 +137,8 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
final int layoutResourceId = a.getResourceId(R.styleable.TimePicker_internalLayout,
R.layout.time_picker_material);
final View mainView = inflater.inflate(layoutResourceId, delegator);
final View headerView = mainView.findViewById(R.id.time_header);
headerView.setOnTouchListener(new NearestTouchDelegate());
mRadialTimePickerHeader = mainView.findViewById(R.id.time_header);
mRadialTimePickerHeader.setOnTouchListener(new NearestTouchDelegate());
// Set up hour/minute labels.
mHourView = (NumericTextView) mainView.findViewById(R.id.hours);
@@ -170,6 +191,8 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
headerTextColor = a.getColorStateList(R.styleable.TimePicker_headerTextColor);
}
mTextInputPickerHeader = mainView.findViewById(R.id.input_header);
if (headerTextColor != null) {
mHourView.setTextColor(headerTextColor);
mSeparatorView.setTextColor(headerTextColor);
@@ -180,7 +203,10 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
// Set up header background, if available.
if (a.hasValueOrEmpty(R.styleable.TimePicker_headerBackground)) {
headerView.setBackground(a.getDrawable(R.styleable.TimePicker_headerBackground));
mRadialTimePickerHeader.setBackground(a.getDrawable(
R.styleable.TimePicker_headerBackground));
mTextInputPickerHeader.setBackground(a.getDrawable(
R.styleable.TimePicker_headerBackground));
}
a.recycle();
@@ -189,6 +215,22 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
mRadialTimePickerView.applyAttributes(attrs, defStyleAttr, defStyleRes);
mRadialTimePickerView.setOnValueSelectedListener(mOnValueSelectedListener);
mTextInputPickerView = (TextInputTimePickerView) mainView.findViewById(R.id.input_mode);
mTextInputPickerView.setListener(mOnValueTypedListener);
mRadialTimePickerModeButton =
(ImageButton) mainView.findViewById(R.id.toggle_mode);
mRadialTimePickerModeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
toggleRadialPickerMode();
}
});
mRadialTimePickerModeEnabledDescription = context.getResources().getString(
R.string.time_picker_radial_mode_description);
mTextInputPickerModeEnabledDescription = context.getResources().getString(
R.string.time_picker_text_input_mode_description);
mAllowAutoAdvance = true;
updateHourFormat();
@@ -200,6 +242,34 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
initialize(currentHour, currentMinute, mIs24Hour, HOUR_INDEX);
}
private void toggleRadialPickerMode() {
if (mRadialPickerModeEnabled) {
mRadialTimePickerView.setVisibility(View.GONE);
mRadialTimePickerHeader.setVisibility(View.GONE);
mTextInputPickerHeader.setVisibility(View.VISIBLE);
mTextInputPickerView.setVisibility(View.VISIBLE);
mRadialTimePickerModeButton.setImageResource(R.drawable.btn_event_material);
mRadialTimePickerModeButton.setContentDescription(
mRadialTimePickerModeEnabledDescription);
mRadialPickerModeEnabled = false;
} else {
mRadialTimePickerView.setVisibility(View.VISIBLE);
mRadialTimePickerHeader.setVisibility(View.VISIBLE);
mTextInputPickerHeader.setVisibility(View.GONE);
mTextInputPickerView.setVisibility(View.GONE);
mRadialTimePickerModeButton.setImageResource(R.drawable.btn_keyboard_key_material);
mRadialTimePickerModeButton.setContentDescription(
mTextInputPickerModeEnabledDescription);
updateTextInputPicker();
mRadialPickerModeEnabled = true;
}
}
@Override
public boolean validateInput() {
return mTextInputPickerView.validateInput();
}
/**
* Ensures that a TextView is wide enough to contain its text without
* wrapping or clipping. Measures the specified view and sets the minimum
@@ -249,9 +319,16 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
final int maxHour = (mIs24Hour ? 23 : 11) + minHour;
mHourView.setRange(minHour, maxHour);
mHourView.setShowLeadingZeroes(mHourFormatShowLeadingZero);
final String[] digits = DecimalFormatSymbols.getInstance(mLocale).getDigitStrings();
int maxCharLength = 0;
for (int i = 0; i < 10; i++) {
maxCharLength = Math.max(maxCharLength, digits[i].length());
}
mTextInputPickerView.setHourFormat(maxCharLength * 2);
}
private static final CharSequence obtainVerbatim(String text) {
static final CharSequence obtainVerbatim(String text) {
return new SpannableStringBuilder().append(text,
new TtsSpan.VerbatimBuilder(text).build(), 0);
}
@@ -333,10 +410,16 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
updateHeaderSeparator();
updateHeaderMinute(mCurrentMinute, false);
updateRadialPicker(index);
updateTextInputPicker();
mDelegator.invalidate();
}
private void updateTextInputPicker() {
mTextInputPickerView.updateTextInputValues(getLocalizedHour(mCurrentHour), mCurrentMinute,
mCurrentHour < 12 ? AM : PM, mIs24Hour, mHourFormatStartsAtZero);
}
private void updateRadialPicker(int index) {
mRadialTimePickerView.initialize(mCurrentHour, mCurrentMinute, mIs24Hour);
setCurrentItemShowing(index, false, true);
@@ -381,10 +464,10 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
*/
@Override
public void setHour(int hour) {
setHourInternal(hour, false, true);
setHourInternal(hour, FROM_EXTERNAL_API, true);
}
private void setHourInternal(int hour, boolean isFromPicker, boolean announce) {
private void setHourInternal(int hour, @ChangeSource int source, boolean announce) {
if (mCurrentHour == hour) {
return;
}
@@ -393,10 +476,13 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
updateHeaderHour(hour, announce);
updateHeaderAmPm();
if (!isFromPicker) {
if (source != FROM_RADIAL_PICKER) {
mRadialTimePickerView.setCurrentHour(hour);
mRadialTimePickerView.setAmOrPm(hour < 12 ? AM : PM);
}
if (source != FROM_INPUT_PICKER) {
updateTextInputPicker();
}
mDelegator.invalidate();
onTimeChanged();
@@ -424,10 +510,10 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
*/
@Override
public void setMinute(int minute) {
setMinuteInternal(minute, false);
setMinuteInternal(minute, FROM_EXTERNAL_API);
}
private void setMinuteInternal(int minute, boolean isFromPicker) {
private void setMinuteInternal(int minute, @ChangeSource int source) {
if (mCurrentMinute == minute) {
return;
}
@@ -435,9 +521,12 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
mCurrentMinute = minute;
updateHeaderMinute(minute, true);
if (!isFromPicker) {
if (source != FROM_RADIAL_PICKER) {
mRadialTimePickerView.setCurrentMinute(minute);
}
if (source != FROM_INPUT_PICKER) {
updateTextInputPicker();
}
mDelegator.invalidate();
onTimeChanged();
@@ -661,6 +750,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
separatorText = Character.toString(bestDateTimePattern.charAt(hIndex + 1));
}
mSeparatorView.setText(separatorText);
mTextInputPickerView.updateSeparator(separatorText);
}
static private int lastIndexOfAny(String str, char[] any) {
@@ -712,7 +802,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
if (mRadialTimePickerView.setAmOrPm(amOrPm)) {
mCurrentHour = getHour();
updateTextInputPicker();
if (mOnTimeChangedListener != null) {
mOnTimeChangedListener.onTimeChanged(mDelegator, getHour(), getMinute());
}
@@ -726,7 +816,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
switch (pickerType) {
case RadialTimePickerView.HOURS:
final boolean isTransition = mAllowAutoAdvance && autoAdvance;
setHourInternal(newValue, true, !isTransition);
setHourInternal(newValue, FROM_RADIAL_PICKER, !isTransition);
if (isTransition) {
setCurrentItemShowing(MINUTE_INDEX, true, false);
@@ -735,7 +825,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
}
break;
case RadialTimePickerView.MINUTES:
setMinuteInternal(newValue, true);
setMinuteInternal(newValue, FROM_RADIAL_PICKER);
break;
}
@@ -745,6 +835,23 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
}
};
private final OnValueTypedListener mOnValueTypedListener = new OnValueTypedListener() {
@Override
public void onValueChanged(int pickerType, int newValue) {
switch (pickerType) {
case TextInputTimePickerView.HOURS:
setHourInternal(newValue, FROM_INPUT_PICKER, false);
break;
case TextInputTimePickerView.MINUTES:
setMinuteInternal(newValue, FROM_INPUT_PICKER);
break;
case TextInputTimePickerView.AMPM:
setAmOrPm(newValue);
break;
}
}
};
/** Listener for keyboard interaction. */
private final OnValueChangedListener mDigitEnteredListener = new OnValueChangedListener() {
@Override

View File

@@ -219,6 +219,11 @@ class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate {
}
}
@Override
public boolean validateInput() {
return true;
}
private void getHourFormatData() {
final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mLocale,
(mIs24HourView) ? "Hm" : "hm");

View File

@@ -1,76 +0,0 @@
/*
* Copyright (C) 2015 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.android.internal.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
/**
* Implementation of {@link android.widget.Space} that uses normal View drawing
* rather than a no-op. Useful for dialogs and other places where the base View
* class is too greedy when measured with AT_MOST.
*/
public final class DrawingSpace extends View {
public DrawingSpace(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public DrawingSpace(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public DrawingSpace(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DrawingSpace(Context context) {
this(context, null);
}
/**
* Compare to: {@link View#getDefaultSize(int, int)}
* <p>
* If mode is AT_MOST, return the child size instead of the parent size
* (unless it is too big).
*/
private static int getDefaultSizeNonGreedy(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
result = Math.min(size, specSize);
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSizeNonGreedy(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSizeNonGreedy(getSuggestedMinimumHeight(), heightMeasureSpec));
}
}

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2015 The Android Open Source Project
<!--
Copyright (C) 2017 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.
@@ -14,7 +15,15 @@
limitations under the License.
-->
<resources>
<item type="dimen" format="integer" name="time_picker_column_start_material">1</item>
<item type="dimen" format="integer" name="time_picker_column_end_material">0</item>
</resources>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#90ffffff"
android:pathData="M17 12h-5v5h5v-5zM16 1v2H8V1H6v2H5c-1.11 0-1.99 .9 -1.99 2L3 19c0 1.1 .89 2 2
2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2h-1V1h-2zm3 18H5V8h14v11z" />
<path android:pathData="M0 0h24v24H0z" />
</vector>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2017 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#90ffffff"
android:pathData="M20 5H4c-1.1 0-1.99 .9 -1.99 2L2 17c0 1.1 .9 2 2 2h16c1.1 0 2-.9
2-2V7c0-1.1-.9-2-2-2zm-9 3h2v2h-2V8zm0 3h2v2h-2v-2zM8 8h2v2H8V8zm0
3h2v2H8v-2zm-1 2H5v-2h2v2zm0-3H5V8h2v2zm9
7H8v-2h8v2zm0-4h-2v-2h2v2zm0-3h-2V8h2v2zm3 3h-2v-2h2v2zm0-3h-2V8h2v2z" />
<path
android:pathData="M0 0h24v24H0zm0 0h24v24H0z" />
</vector>

View File

@@ -15,28 +15,17 @@
limitations under the License.
-->
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layoutDirection="ltr">
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Provides a background for the time layout that extends into the button bar area. -->
<com.android.internal.widget.DrawingSpace
<LinearLayout
android:id="@+id/time_header"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_column="@dimen/time_picker_column_start_material"
android:layout_row="0"
android:layout_rowSpan="3"
android:layout_gravity="center|fill"
android:layoutDirection="locale" />
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_column="@dimen/time_picker_column_start_material"
android:layout_row="1"
android:layout_gravity="center|fill"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:paddingStart="?attr/dialogPreferredPadding"
android:paddingEnd="?attr/dialogPreferredPadding">
@@ -44,10 +33,8 @@
android:id="@+id/time_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_centerInParent="true"
android:paddingTop="@dimen/timepicker_radial_picker_top_margin"
android:layout_marginBottom="-12dp">
android:orientation="horizontal">
<!-- The hour should always be to the left of the separator,
regardless of the current locale's layout direction. -->
@@ -125,38 +112,71 @@
android:includeFontPadding="false"
android:button="@null" />
</RadioGroup>
</RelativeLayout>
</LinearLayout>
<ViewStub
android:id="@id/topPanel"
android:layout="@layout/alert_dialog_title_material"
<TextView
android:visibility="gone"
android:id="@+id/input_header"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:paddingStart="@dimen/dialog_padding_material"
android:paddingEnd="@dimen/dialog_padding_material"
android:paddingTop="20dp"
android:paddingBottom="20dp"
android:includeFontPadding="false"
android:textAppearance="@style/TextAppearance.Material.TimePicker.InputHeader"
android:text="@string/time_picker_header_text"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_column="@dimen/time_picker_column_end_material"
android:layout_row="0"
android:layout_gravity="top|fill_horizontal"
android:layoutDirection="locale" />
android:orientation="vertical">
<android.widget.RadialTimePickerView
android:id="@+id/radial_picker"
android:layout_width="@dimen/timepicker_radial_picker_dimen"
android:layout_height="@dimen/timepicker_radial_picker_dimen"
android:layout_column="@dimen/time_picker_column_end_material"
android:layout_row="1"
android:layout_rowWeight="1"
android:layout_gravity="center|fill"
android:layout_marginTop="@dimen/timepicker_radial_picker_top_margin"
android:layout_marginStart="@dimen/timepicker_radial_picker_horizontal_margin"
android:layout_marginEnd="@dimen/timepicker_radial_picker_horizontal_margin"
android:layoutDirection="locale" />
<android.widget.RadialTimePickerView
android:id="@+id/radial_picker"
android:layout_width="@dimen/timepicker_radial_picker_dimen"
android:layout_height="@dimen/timepicker_radial_picker_dimen"
android:layout_gravity="center|fill"
android:layout_marginTop="@dimen/timepicker_radial_picker_top_margin"
android:layout_marginStart="@dimen/timepicker_radial_picker_horizontal_margin"
android:layout_marginEnd="@dimen/timepicker_radial_picker_horizontal_margin"
android:layoutDirection="locale" />
<ViewStub
android:id="@id/buttonPanel"
android:layout="@layout/alert_dialog_button_bar_material"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_column="@dimen/time_picker_column_end_material"
android:layout_row="2"
android:layout_gravity="bottom|fill_horizontal"
android:layoutDirection="locale" />
</GridLayout>
<android.widget.TextInputTimePickerView
android:id="@+id/input_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="?attr/dialogPreferredPadding"
android:paddingEnd="?attr/dialogPreferredPadding"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageButton
android:id="@+id/toggle_mode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:padding="12dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:tint="?attr/colorControlNormal"
android:src="@drawable/btn_keyboard_key_material"
android:contentDescription="@string/time_picker_text_input_mode_description" />
<Space
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1" />
<ViewStub
android:id="@id/buttonPanel"
android:layout="@layout/alert_dialog_button_bar_material"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layoutDirection="locale" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -34,4 +34,53 @@
android:layout_marginTop="@dimen/timepicker_radial_picker_top_margin"
android:layout_marginStart="@dimen/timepicker_radial_picker_horizontal_margin"
android:layout_marginEnd="@dimen/timepicker_radial_picker_horizontal_margin" />
<TextView
android:visibility="gone"
android:id="@+id/input_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/dialog_padding_material"
android:paddingEnd="@dimen/dialog_padding_material"
android:paddingTop="20dp"
android:paddingBottom="20dp"
android:includeFontPadding="false"
android:fontFamily="sans-serif-medium"
android:textSize="34sp"
android:textColor="@color/white"
android:text="@string/time_picker_header_text"/>
<android.widget.TextInputTimePickerView
android:id="@+id/input_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="?attr/dialogPreferredPadding"
android:paddingEnd="?attr/dialogPreferredPadding"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageButton
android:id="@+id/toggle_mode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:padding="12dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:tint="?attr/colorControlNormal"
android:src="@drawable/btn_keyboard_key_material"
android:contentDescription="@string/time_picker_text_input_mode_description" />
<Space
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1" />
<ViewStub
android:id="@id/buttonPanel"
android:layout="@layout/alert_dialog_button_bar_material"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layoutDirection="locale" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2017 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.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@+id/top_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="26dp"
android:layout_marginBottom="8dp"
android:text="@string/time_picker_prompt_label"
android:textAppearance="@style/TextAppearance.Material.TimePicker.PromptLabel" />
<RelativeLayout
android:id="@+id/input_block"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/top_label"
android:layoutDirection="ltr">
<EditText
android:id="@+id/input_hour"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:inputType="number"
android:textAppearance="@style/TextAppearance.Material.TimePicker.InputField" />
<TextView
android:id="@+id/label_hour"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/input_hour"
android:layout_alignStart="@id/input_hour"
android:text="@string/time_picker_hour_label"/>
<TextView
android:id="@+id/input_separator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignEnd="@id/input_hour"
android:layout_alignBaseline="@id/input_hour"
android:textAppearance="@style/TextAppearance.Material.TimePicker.InputField" />
<EditText
android:id="@+id/input_minute"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@id/input_hour"
android:layout_toEndOf="@id/input_separator"
android:inputType="number"
android:textAppearance="@style/TextAppearance.Material.TimePicker.InputField" />
<TextView
android:id="@+id/label_minute"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/input_minute"
android:layout_alignStart="@id/input_minute"
android:text="@string/time_picker_minute_label"/>
<TextView
android:visibility="invisible"
android:id="@+id/label_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/input_hour"
android:layout_alignStart="@id/input_hour"
android:textColor="?attr/textColorError"
android:text="@string/time_picker_input_error" />
</RelativeLayout>
<Spinner
android:id="@+id/am_pm_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@id/input_block"
android:layout_alignParentEnd="true"/>
</merge>

View File

@@ -475,9 +475,6 @@
<dimen name="chooser_grid_padding">0dp</dimen>
<item type="dimen" format="integer" name="time_picker_column_start_material">0</item>
<item type="dimen" format="integer" name="time_picker_column_end_material">1</item>
<item type="dimen" name="aerr_padding_list_top">15dp</item>
<item type="dimen" name="aerr_padding_list_bottom">8dp</item>

View File

@@ -149,6 +149,7 @@
<dimen name="timepicker_radial_picker_dimen">296dp</dimen>
<dimen name="timepicker_radial_picker_top_margin">16dp</dimen>
<dimen name="timepicker_radial_picker_horizontal_margin">16dp</dimen>
<dimen name="timepicker_edit_text_size">24sp</dimen>
<!-- Used by RadialTimePicker in clock-style TimePicker. -->
<dimen name="timepicker_selector_radius">20dp</dimen>

View File

@@ -4471,4 +4471,20 @@
<string name="device_storage_monitor_notification_channel">Device storage</string>
<!-- Channel name for UsbDeviceManager adb debugging notifications -->
<string name="adb_debugging_notification_channel_tv">USB debugging</string>
<!-- Label for the time picker hour input field. [CHAR LIMIT=20] -->
<string name="time_picker_hour_label">hour</string>
<!-- Label for the time picker minute input field. [CHAR LIMIT=20] -->
<string name="time_picker_minute_label">minute</string>
<!-- The title for the time picker dialog. [CHAR LIMIT=NONE] -->
<string name="time_picker_header_text">Set time</string>
<!-- Error shown to the user if they type in invalid hour or minute in the time picker. [CHAR LIMIT=NONE] -->
<string name="time_picker_input_error">Enter a valid time</string>
<!-- Label shown to the user in time picker to let them know that should type in time. [CHAR LIMIT=NONE] -->
<string name="time_picker_prompt_label">Type in time</string>
<!-- Accessibility string used for describing the button in time picker that changes the dialog to text input mode. [CHAR LIMIT=NONE] -->
<string name="time_picker_text_input_mode_description">Switch to text input mode for the time input.</string>
<!-- Accessibility string used for describing the button in time picker that changes the dialog to circular clock mode. [CHAR LIMIT=NONE] -->
<string name="time_picker_radial_mode_description">Switch to clock mode for the time input.</string>
</resources>

View File

@@ -426,6 +426,21 @@ please see styles_device_defaults.xml.
<item name="textColor">@color/primary_text_secondary_when_activated_material_inverse</item>
</style>
<style name="TextAppearance.Material.TimePicker.InputHeader" parent="TextAppearance.Material">
<item name="textSize">@dimen/text_size_display_1_material</item>
<item name="textColor">@color/white</item>
<item name="fontFamily">sans-serif-medium</item>
</style>
<style name="TextAppearance.Material.TimePicker.InputField" parent="TextAppearance.Material">
<item name="textSize">@dimen/timepicker_edit_text_size</item>
</style>
<style name="TextAppearance.Material.TimePicker.PromptLabel" parent="TextAppearance.Material">
<item name="textSize">@dimen/timepicker_text_size_normal</item>
<item name="fontFamily">sans-serif-medium</item>
</style>
<style name="TextAppearance.Material.DatePicker.YearLabel" parent="TextAppearance.Material">
<item name="textColor">@color/primary_text_secondary_when_activated_material_inverse</item>
<item name="textSize">@dimen/date_picker_year_label_size</item>

View File

@@ -2842,4 +2842,21 @@
<!-- Accessibility fingerprint gestures -->
<java-symbol type="string" name="capability_title_canCaptureFingerprintGestures" />
<java-symbol type="string" name="capability_desc_canCaptureFingerprintGestures" />
<!-- Time picker -->
<java-symbol type="id" name="toggle_mode"/>
<java-symbol type="id" name="input_mode"/>
<java-symbol type="id" name="input_header"/>
<java-symbol type="id" name="input_separator"/>
<java-symbol type="id" name="input_hour"/>
<java-symbol type="id" name="input_minute"/>
<java-symbol type="id" name="am_pm_spinner"/>
<java-symbol type="id" name="label_hour"/>
<java-symbol type="id" name="label_minute"/>
<java-symbol type="id" name="label_error"/>
<java-symbol type="layout" name="time_picker_text_input_material"/>
<java-symbol type="drawable" name="btn_keyboard_key_material"/>
<java-symbol type="drawable" name="btn_event_material"/>
<java-symbol type="string" name="time_picker_text_input_mode_description"/>
<java-symbol type="string" name="time_picker_radial_mode_description"/>
</resources>