diff options
author | Sam Blitzstein <sblitz@google.com> | 2013-03-22 17:56:25 -0700 |
---|---|---|
committer | Sam Blitzstein <sblitz@google.com> | 2013-03-29 15:36:18 -0700 |
commit | b8f95646fc0510eebfeaa27864023d630f34090f (patch) | |
tree | aa52ea44901c22e76975337cffbe0c9ff57bf920 /src/com | |
parent | 3d5a23b698cb8c59f43914ea2f9bb4fb36575f88 (diff) | |
download | datetimepicker-b8f95646fc0510eebfeaa27864023d630f34090f.tar.gz |
Support for accessibility and hardware IME.
Change-Id: If6096d4105e78cad8e082091776213756b0ebde1
Diffstat (limited to 'src/com')
-rw-r--r-- | src/com/android/datetimepicker/FakeButton.java | 46 | ||||
-rw-r--r-- | src/com/android/datetimepicker/TimePickerDialog.java | 307 | ||||
-rw-r--r-- | src/com/android/datetimepicker/Utils.java | 25 | ||||
-rw-r--r-- | src/com/android/datetimepicker/time/AmPmCirclesView.java (renamed from src/com/android/datetimepicker/AmPmCirclesView.java) | 12 | ||||
-rw-r--r-- | src/com/android/datetimepicker/time/CircleView.java (renamed from src/com/android/datetimepicker/CircleView.java) | 7 | ||||
-rw-r--r-- | src/com/android/datetimepicker/time/RadialPickerLayout.java (renamed from src/com/android/datetimepicker/TimePicker.java) | 346 | ||||
-rw-r--r-- | src/com/android/datetimepicker/time/RadialSelectorView.java (renamed from src/com/android/datetimepicker/RadialSelectorView.java) | 28 | ||||
-rw-r--r-- | src/com/android/datetimepicker/time/RadialTextsView.java (renamed from src/com/android/datetimepicker/RadialTextsView.java) | 9 | ||||
-rw-r--r-- | src/com/android/datetimepicker/time/TimePickerDialog.java | 823 |
9 files changed, 1159 insertions, 444 deletions
diff --git a/src/com/android/datetimepicker/FakeButton.java b/src/com/android/datetimepicker/FakeButton.java new file mode 100644 index 0000000..6ab1b61 --- /dev/null +++ b/src/com/android/datetimepicker/FakeButton.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2013 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.datetimepicker; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.Button; +import android.widget.TextView; + +/** + * Fake Button class, used so TextViews can announce themselves as Buttons, for accessibility. + */ +public class FakeButton extends TextView { + + public FakeButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(Button.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(Button.class.getName()); + } +} diff --git a/src/com/android/datetimepicker/TimePickerDialog.java b/src/com/android/datetimepicker/TimePickerDialog.java deleted file mode 100644 index 2b3960d..0000000 --- a/src/com/android/datetimepicker/TimePickerDialog.java +++ /dev/null @@ -1,307 +0,0 @@ -/* - * Copyright (C) 2013 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.datetimepicker; - -import android.app.ActionBar.LayoutParams; -import android.app.Activity; -import android.app.AlertDialog; -import android.app.DialogFragment; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Typeface; -import android.os.Bundle; -import android.os.Handler; -import android.os.SystemClock; -import android.text.style.AlignmentSpan; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.View.OnTouchListener; -import android.view.ViewGroup; -import android.view.Window; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import com.android.datetimepicker.R; - -import com.android.datetimepicker.TimePicker.OnValueSelectedListener; - -/** - * Dialog to set a time. - */ -public class TimePickerDialog extends DialogFragment implements OnValueSelectedListener{ - private static final String TAG = "TimePickerDialog"; - - private static final String KEY_HOUR_OF_DAY = "hour_of_day"; - private static final String KEY_MINUTE = "minute"; - private static final String KEY_IS_24_HOUR_VIEW = "is_24_hour_view"; - private static final String KEY_CURRENT_ITEM_SHOWING = "current_item_showing"; - public static final int HOUR_INDEX = 0; - public static final int MINUTE_INDEX = 1; - public static final int AMPM_INDEX = 2; // NOT a real index for the purpose of what's showing. - public static final int AM = 0; - public static final int PM = 1; - - private Handler mHandler = new Handler(); - - private OnTimeSetListener mCallback; - - private TextView mDoneButton; - private TextView mHourView; - private TextView mMinuteView; - private TextView mAmPmTextView; - private TimePicker mTimePicker; - - private int mBlue; - private int mBlack; - private String mAmText; - private String mPmText; - - private boolean mAllowAutoAdvance; - private int mInitialHourOfDay; - private int mInitialMinute; - private boolean mIs24HourMode; - private int mWidthPixels; - - /** - * The callback interface used to indicate the user is done filling in - * the time (they clicked on the 'Set' button). - */ - public interface OnTimeSetListener { - - /** - * @param view The view associated with this listener. - * @param hourOfDay The hour that was set. - * @param minute The minute that was set. - */ - void onTimeSet(TimePicker view, int hourOfDay, int minute); - } - - public TimePickerDialog() { - // Empty constructor required for dialog fragment. - } - - public TimePickerDialog(Context context, int theme, OnTimeSetListener callback, - int hourOfDay, int minute, boolean is24HourMode) { - // Empty constructor required for dialog fragment. - } - - public static TimePickerDialog newInstance(OnTimeSetListener callback, - int hourOfDay, int minute, boolean is24HourMode) { - TimePickerDialog ret = new TimePickerDialog(); - ret.initialize(callback, hourOfDay, minute, is24HourMode); - return ret; - } - - public void initialize(OnTimeSetListener callback, - int hourOfDay, int minute, boolean is24HourMode) { - mCallback = callback; - - mInitialHourOfDay = hourOfDay; - mInitialMinute = minute; - mIs24HourMode = is24HourMode; - } - - public void setOnTimeSetListener(OnTimeSetListener callback) { - mCallback = callback; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (savedInstanceState != null && savedInstanceState.containsKey(KEY_HOUR_OF_DAY) - && savedInstanceState.containsKey(KEY_MINUTE) - && savedInstanceState.containsKey(KEY_IS_24_HOUR_VIEW)) { - mInitialHourOfDay = savedInstanceState.getInt(KEY_HOUR_OF_DAY); - mInitialMinute = savedInstanceState.getInt(KEY_MINUTE); - mIs24HourMode = savedInstanceState.getBoolean(KEY_IS_24_HOUR_VIEW); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE); - - View view = inflater.inflate(R.layout.time_picker_dialog, null); - Resources res = getResources(); - - mBlue = res.getColor(R.color.blue); - mBlack = res.getColor(R.color.black_80); - - mHourView = (TextView) view.findViewById(R.id.hours); - mMinuteView = (TextView) view.findViewById(R.id.minutes); - mAmPmTextView = (TextView) view.findViewById(R.id.ampm_label); - mAmText = res.getString(R.string.am_label); - mPmText = res.getString(R.string.pm_label); - - mTimePicker = (TimePicker) view.findViewById(R.id.time_picker); - mTimePicker.setOnValueSelectedListener(this); - mTimePicker.initialize(getActivity(), mInitialHourOfDay, mInitialMinute, mIs24HourMode); - int currentItemShowing = HOUR_INDEX; - if (savedInstanceState != null && - savedInstanceState.containsKey(KEY_CURRENT_ITEM_SHOWING)) { - currentItemShowing = savedInstanceState.getInt(KEY_CURRENT_ITEM_SHOWING); - } - setCurrentItemShowing(currentItemShowing, false); - mTimePicker.invalidate(); - - mHourView.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - setCurrentItemShowing(HOUR_INDEX, true); - mTimePicker.tryVibrate(); - } - }); - mMinuteView.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - setCurrentItemShowing(MINUTE_INDEX, true); - mTimePicker.tryVibrate(); - } - }); - - mDoneButton = (TextView) view.findViewById(R.id.done_button); - mDoneButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - mTimePicker.tryVibrate(); - if (mCallback != null) { - mCallback.onTimeSet(mTimePicker, - mTimePicker.getHours(), mTimePicker.getMinutes()); - } - dismiss(); - } - }); - - DisplayMetrics metrics = new DisplayMetrics(); - getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics); - mWidthPixels = metrics.widthPixels; - - if (mIs24HourMode) { - mAmPmTextView.setVisibility(View.GONE); - - RelativeLayout.LayoutParams paramsSeparator = new RelativeLayout.LayoutParams( - LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); - paramsSeparator.addRule(RelativeLayout.CENTER_IN_PARENT); - TextView separatorView = (TextView) view.findViewById(R.id.separator); - separatorView.setLayoutParams(paramsSeparator); - } else { - mAmPmTextView.setVisibility(View.VISIBLE); - updateAmPmDisplay(mInitialHourOfDay < 12? AM : PM); - View amPmHitspace = view.findViewById(R.id.ampm_hitspace); - amPmHitspace.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - mTimePicker.tryVibrate(); - int amOrPm = mTimePicker.getIsCurrentlyAmOrPm(); - if (amOrPm == AM) { - amOrPm = PM; - } else if (amOrPm == PM){ - amOrPm = AM; - } - updateAmPmDisplay(amOrPm); - mTimePicker.setAmOrPm(amOrPm); - } - }); - } - - mAllowAutoAdvance = true; - setHour(mInitialHourOfDay); - setMinute(mInitialMinute); - - return view; - } - - private void updateAmPmDisplay(int amOrPm) { - if (amOrPm == AM) { - mAmPmTextView.setText(mAmText); - } else if (amOrPm == PM){ - mAmPmTextView.setText(mPmText); - } - } - - @Override - public void onSaveInstanceState(Bundle outState) { - if (mTimePicker != null) { - outState.putInt(KEY_HOUR_OF_DAY, mTimePicker.getHours()); - outState.putInt(KEY_MINUTE, mTimePicker.getMinutes()); - outState.putBoolean(KEY_IS_24_HOUR_VIEW, mIs24HourMode); - outState.putInt(KEY_CURRENT_ITEM_SHOWING, mTimePicker.getCurrentItemShowing()); - } - } - - @Override - public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance) { - if (pickerIndex == HOUR_INDEX) { - setHour(newValue); - if (mAllowAutoAdvance && autoAdvance) { - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - setCurrentItemShowing(MINUTE_INDEX, true); - } - }, 150); - } - } else if (pickerIndex == MINUTE_INDEX){ - setMinute(newValue); - } else if (pickerIndex == AMPM_INDEX) { - updateAmPmDisplay(newValue); - } - } - - private void setHour(int value) { - String format; - if (mIs24HourMode) { - format = "%02d"; - } else { - format = "%d"; - value = value % 12; - if (value == 0) { - value = 12; - } - } - - mHourView.setText(String.format(format, value)); - } - - private void setMinute(int value) { - if (value == 60) { - value = 0; - } - mMinuteView.setText(String.format("%02d", value)); - } - - private void setCurrentItemShowing(int index, boolean animate) { -/* - if (mAllowAutoAdvance && index == 1) { - // Once we've seen the minutes, no need to auto-advance. - mAllowAutoAdvance = false; - } -*/ - mTimePicker.setCurrentItemShowing(index, animate); - int hourColor = (index == HOUR_INDEX)? mBlue : mBlack; - int minuteColor = (index == MINUTE_INDEX)? mBlue : mBlack; - mHourView.setTextColor(hourColor); - mMinuteView.setTextColor(minuteColor); - } -} diff --git a/src/com/android/datetimepicker/Utils.java b/src/com/android/datetimepicker/Utils.java new file mode 100644 index 0000000..8772fb8 --- /dev/null +++ b/src/com/android/datetimepicker/Utils.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2013 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.datetimepicker; + +import android.os.Build; + +public class Utils { + public static boolean isJellybeanOrLater() { + return Build.VERSION.SDK_INT >= 16; + } +} diff --git a/src/com/android/datetimepicker/AmPmCirclesView.java b/src/com/android/datetimepicker/time/AmPmCirclesView.java index 0e2bdb4..1ead926 100644 --- a/src/com/android/datetimepicker/AmPmCirclesView.java +++ b/src/com/android/datetimepicker/time/AmPmCirclesView.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.datetimepicker; +package com.android.datetimepicker.time; import android.content.Context; import android.content.res.Resources; @@ -22,14 +22,13 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Typeface; import android.graphics.Paint.Align; -import android.util.AttributeSet; import android.util.Log; -import android.view.MotionEvent; import android.view.View; -import android.view.View.OnTouchListener; import com.android.datetimepicker.R; +import java.text.DateFormatSymbols; + public class AmPmCirclesView extends View { private static final String TAG = "AmPmCirclesView"; @@ -79,8 +78,9 @@ public class AmPmCirclesView extends View { Float.parseFloat(res.getString(R.string.circle_radius_multiplier)); mAmPmCircleRadiusMultiplier = Float.parseFloat(res.getString(R.string.ampm_circle_radius_multiplier)); - mAmText = res.getString(R.string.am_label); - mPmText = res.getString(R.string.pm_label); + String[] amPmTexts = new DateFormatSymbols().getAmPmStrings(); + mAmText = amPmTexts[0]; + mPmText = amPmTexts[1]; setAmOrPm(amOrPm); mAmOrPmPressed = -1; diff --git a/src/com/android/datetimepicker/CircleView.java b/src/com/android/datetimepicker/time/CircleView.java index d7ec92a..b588db5 100644 --- a/src/com/android/datetimepicker/CircleView.java +++ b/src/com/android/datetimepicker/time/CircleView.java @@ -14,19 +14,14 @@ * limitations under the License. */ -package com.android.datetimepicker; +package com.android.datetimepicker.time; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.Point; -import android.graphics.Typeface; -import android.util.AttributeSet; import android.util.Log; -import android.view.MotionEvent; import android.view.View; -import android.view.View.OnTouchListener; import com.android.datetimepicker.R; diff --git a/src/com/android/datetimepicker/TimePicker.java b/src/com/android/datetimepicker/time/RadialPickerLayout.java index 3f4b4ef..7faa377 100644 --- a/src/com/android/datetimepicker/TimePicker.java +++ b/src/com/android/datetimepicker/time/RadialPickerLayout.java @@ -14,45 +14,45 @@ * limitations under the License. */ -package com.android.datetimepicker; +package com.android.datetimepicker.time; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; +import android.annotation.SuppressLint; import android.app.Service; import android.content.Context; import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Typeface; +import android.os.Bundle; import android.os.Handler; import android.os.SystemClock; import android.os.Vibrator; +import android.text.format.DateUtils; +import android.text.format.Time; import android.util.AttributeSet; import android.util.Log; -import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; -import android.view.View.MeasureSpec; -import android.view.View.OnClickListener; import android.view.View.OnTouchListener; import android.view.ViewConfiguration; -import android.view.animation.AnimationSet; -import android.view.animation.TranslateAnimation; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import com.android.datetimepicker.R; -public class TimePicker extends FrameLayout implements OnTouchListener { +public class RadialPickerLayout extends FrameLayout implements OnTouchListener { private static final String TAG = "TimePicker"; private final int TOUCH_SLOP; private final int TAP_TIMEOUT; - private final int PRESSED_STATE_DURATION; private static final int HOUR_VALUE_TO_DEGREES_STEP_SIZE = 30; private static final int MINUTE_VALUE_TO_DEGREES_STEP_SIZE = 6; private static final int HOUR_INDEX = TimePickerDialog.HOUR_INDEX; private static final int MINUTE_INDEX = TimePickerDialog.MINUTE_INDEX; private static final int AMPM_INDEX = TimePickerDialog.AMPM_INDEX; + private static final int ENABLE_PICKER_INDEX = TimePickerDialog.ENABLE_PICKER_INDEX; private static final int AM = TimePickerDialog.AM; private static final int PM = TimePickerDialog.PM; @@ -65,6 +65,7 @@ public class TimePicker extends FrameLayout implements OnTouchListener { private int mCurrentHoursOfDay; private int mCurrentMinutes; private boolean mIs24HourMode; + private boolean mHideAmPm; private int mCurrentItemShowing; private CircleView mCircleView; @@ -73,14 +74,16 @@ public class TimePicker extends FrameLayout implements OnTouchListener { private RadialTextsView mMinuteRadialTextsView; private RadialSelectorView mHourRadialSelectorView; private RadialSelectorView mMinuteRadialSelectorView; + private View mGrayBox; + private boolean mInputEnabled; private int mIsTouchingAmOrPm = -1; private boolean mDoingMove; + private boolean mDoingTouch; private int mDownDegrees; private float mDownX; private float mDownY; - - private ReselectSelectorRunnable mReselectSelectorRunnable; + private AccessibilityManager mAccessibilityManager; private Handler mHandler = new Handler(); @@ -88,14 +91,13 @@ public class TimePicker extends FrameLayout implements OnTouchListener { void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance); } - public TimePicker(Context context, AttributeSet attrs) { + public RadialPickerLayout(Context context, AttributeSet attrs) { super(context, attrs); setOnTouchListener(this); ViewConfiguration vc = ViewConfiguration.get(context); TOUCH_SLOP = vc.getScaledTouchSlop(); TAP_TIMEOUT = ViewConfiguration.getTapTimeout(); - PRESSED_STATE_DURATION = ViewConfiguration.getPressedStateDuration(); mDoingMove = false; mCircleView = new CircleView(context); @@ -114,13 +116,21 @@ public class TimePicker extends FrameLayout implements OnTouchListener { mMinuteRadialSelectorView = new RadialSelectorView(context); addView(mMinuteRadialSelectorView); - mReselectSelectorRunnable = new ReselectSelectorRunnable(this); - mVibrator = (Vibrator) context.getSystemService(Service.VIBRATOR_SERVICE); mLastVibrate = 0; mLastValueSelected = -1; mTimeInitialized = false; + + mInputEnabled = true; + mGrayBox = new View(context); + mGrayBox.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + mGrayBox.setBackgroundColor(getResources().getColor(R.color.black_50)); + mGrayBox.setVisibility(View.INVISIBLE); + addView(mGrayBox); + + mAccessibilityManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); } @Override @@ -141,41 +151,67 @@ public class TimePicker extends FrameLayout implements OnTouchListener { Log.e(TAG, "Time has already been initialized."); return; } - - setValueForItem(HOUR_INDEX, initialHoursOfDay); - setValueForItem(MINUTE_INDEX, initialMinutes); mIs24HourMode = is24HourMode; + mHideAmPm = mAccessibilityManager.isTouchExplorationEnabled()? true : mIs24HourMode; - mCircleView.initialize(context, is24HourMode); + mCircleView.initialize(context, mHideAmPm); mCircleView.invalidate(); - if (!is24HourMode) { + if (!mHideAmPm) { mAmPmCirclesView.initialize(context, initialHoursOfDay < 12? AM : PM); mAmPmCirclesView.invalidate(); } Resources res = context.getResources(); - String[] hoursTexts = res.getStringArray(is24HourMode? R.array.hours_24 : R.array.hours); - String[] innerHoursTexts = res.getStringArray(R.array.hours); - String[] minutesTexts = res.getStringArray(R.array.minutes); + int[] hours = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + int[] hours_24 = {0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}; + int[] minutes = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55}; + String[] hoursTexts = new String[12]; + String[] innerHoursTexts = new String[12]; + String[] minutesTexts = new String[12]; + for (int i = 0; i < 12; i++) { + hoursTexts[i] = is24HourMode? + String.format("%02d", hours_24[i]) : String.format("%d", hours[i]); + innerHoursTexts[i] = String.format("%d", hours[i]); + minutesTexts[i] = String.format("%02d", minutes[i]); + } mHourRadialTextsView.initialize(res, - hoursTexts, (is24HourMode? innerHoursTexts : null), is24HourMode, true); + hoursTexts, (is24HourMode? innerHoursTexts : null), mHideAmPm, true); mHourRadialTextsView.invalidate(); - mMinuteRadialTextsView.initialize(res, minutesTexts, null, is24HourMode, false); + mMinuteRadialTextsView.initialize(res, minutesTexts, null, mHideAmPm, false); mMinuteRadialTextsView.invalidate(); - int initialHourDegrees = (initialHoursOfDay % 12) * HOUR_VALUE_TO_DEGREES_STEP_SIZE; - int initialMinuteDegrees = initialMinutes * MINUTE_VALUE_TO_DEGREES_STEP_SIZE; - mHourRadialSelectorView.initialize(context, initialHourDegrees, - is24HourMode, is24HourMode, isHourInnerCircle(initialHoursOfDay), true); - mHourRadialSelectorView.invalidate(); - mMinuteRadialSelectorView.initialize(context, initialMinuteDegrees, - is24HourMode, false, false, false); - mHourRadialSelectorView.invalidate(); - + setValueForItem(HOUR_INDEX, initialHoursOfDay); + setValueForItem(MINUTE_INDEX, initialMinutes); + int hourDegrees = (initialHoursOfDay % 12) * HOUR_VALUE_TO_DEGREES_STEP_SIZE; + mHourRadialSelectorView.initialize(context, mHideAmPm, is24HourMode, true, + hourDegrees, isHourInnerCircle(initialHoursOfDay)); + int minuteDegrees = initialMinutes * MINUTE_VALUE_TO_DEGREES_STEP_SIZE; + mMinuteRadialSelectorView.initialize(context, mHideAmPm, false, false, + minuteDegrees, false); mTimeInitialized = true; } + public void setTime(int hours, int minutes) { + setItem(HOUR_INDEX, hours); + setItem(MINUTE_INDEX, minutes); + } + + private void setItem(int index, int value) { + if (index == HOUR_INDEX) { + setValueForItem(HOUR_INDEX, value); + int hourDegrees = (value % 12) * HOUR_VALUE_TO_DEGREES_STEP_SIZE; + mHourRadialSelectorView.setSelection(hourDegrees, isHourInnerCircle(value), + false, false, false); + mHourRadialSelectorView.invalidate(); + } else if (index == MINUTE_INDEX) { + setValueForItem(MINUTE_INDEX, value); + int minuteDegrees = value * MINUTE_VALUE_TO_DEGREES_STEP_SIZE; + mMinuteRadialSelectorView.setSelection(minuteDegrees, false, false, false, false); + mMinuteRadialSelectorView.invalidate(); + } + } + private boolean isHourInnerCircle(int hourOfDay) { // We'll have the 00 hours on the outside circle. return mIs24HourMode && (hourOfDay <= 12 && hourOfDay != 0); @@ -229,38 +265,63 @@ public class TimePicker extends FrameLayout implements OnTouchListener { setValueForItem(AMPM_INDEX, amOrPm); } - private int reselectSelector(int index, int degrees, boolean isInnerCircle, + private int highPass30sFilter(int degrees) { + int offset = (degrees + 2) / 30; + degrees = Math.max(degrees - (30*offset + 4), 0) + 20*offset; + degrees /= 4; + degrees *= 6; + /* // less aggressive filtering. + degrees /= 5; + int offset = degrees / 6; + degrees = degrees - offset; + degrees *= 6; */ + return degrees; + } + + private int snapToStepSize(int degrees, int stepSize, int ceilingOrFloor) { + int floor = (degrees / stepSize) * stepSize; + int ceiling = floor + stepSize; + if (ceilingOrFloor == 1) { + degrees = ceiling; + } else if (ceilingOrFloor == -1) { + if (degrees == floor) { + floor -= stepSize; + } + degrees = floor; + } else { + if ((degrees - floor) < (ceiling - degrees)) { + degrees = floor; + } else { + degrees = ceiling; + } + } + return degrees; + } + + private int reselectSelector(int degrees, boolean isInnerCircle, boolean forceNotFineGrained, boolean forceDrawLine, boolean forceDrawDot) { - if (degrees == -1 || (index != 0 && index != 1)) { + if (degrees == -1) { return -1; } + int currentShowing = getCurrentItemShowing(); int stepSize; - int currentShowing = getCurrentItemShowing(); - if (!forceNotFineGrained && (currentShowing == 1)) { - stepSize = MINUTE_VALUE_TO_DEGREES_STEP_SIZE; + boolean allowFineGrained = !forceNotFineGrained && (currentShowing == MINUTE_INDEX); + if (allowFineGrained) { + degrees = highPass30sFilter(degrees); } else { - stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE; - } - int floor = (degrees / stepSize) * stepSize; - int ceiling = floor + stepSize; - if ((degrees - floor) < (ceiling - degrees)) { - degrees = floor; - } else { - degrees = ceiling; + degrees = snapToStepSize(degrees, HOUR_VALUE_TO_DEGREES_STEP_SIZE, 0); } RadialSelectorView radialSelectorView; - if (index == 0) { - // Index == 0, hours. + if (currentShowing == HOUR_INDEX) { radialSelectorView = mHourRadialSelectorView; stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE; } else { - // Index == 1, minutes. radialSelectorView = mMinuteRadialSelectorView; stepSize = MINUTE_VALUE_TO_DEGREES_STEP_SIZE; } - radialSelectorView.setSelection(degrees, isInnerCircle, forceDrawLine, forceDrawDot); + radialSelectorView.setSelection(degrees, isInnerCircle, forceDrawLine, forceDrawDot, false); radialSelectorView.invalidate(); @@ -288,10 +349,10 @@ public class TimePicker extends FrameLayout implements OnTouchListener { private int getDegreesFromCoords(float pointX, float pointY, boolean forceLegal, final Boolean[] isInnerCircle) { int currentItem = getCurrentItemShowing(); - if (currentItem == 0) { + if (currentItem == HOUR_INDEX) { return mHourRadialSelectorView.getDegreesFromCoords( pointX, pointY, forceLegal, isInnerCircle); - } else if (currentItem == 1) { + } else if (currentItem == MINUTE_INDEX) { return mMinuteRadialSelectorView.getDegreesFromCoords( pointX, pointY, forceLegal, isInnerCircle); } else { @@ -313,7 +374,10 @@ public class TimePicker extends FrameLayout implements OnTouchListener { return; } - if (animate && (index != getCurrentItemShowing())) { + int lastIndex = getCurrentItemShowing(); + mCurrentItemShowing = index; + + if (animate && (index != lastIndex)) { ObjectAnimator[] anims = new ObjectAnimator[4]; if (index == MINUTE_INDEX) { anims[0] = mHourRadialTextsView.getDisappearAnimator(); @@ -331,15 +395,14 @@ public class TimePicker extends FrameLayout implements OnTouchListener { transition.playTogether(anims); transition.start(); } else { - int hourAlpha = (index == 0) ? 255 : 0; - int minuteAlpha = (index == 1) ? 255 : 0; + int hourAlpha = (index == HOUR_INDEX) ? 255 : 0; + int minuteAlpha = (index == MINUTE_INDEX) ? 255 : 0; mHourRadialTextsView.setAlpha(hourAlpha); mHourRadialSelectorView.setAlpha(hourAlpha); mMinuteRadialTextsView.setAlpha(minuteAlpha); mMinuteRadialSelectorView.setAlpha(minuteAlpha); } - mCurrentItemShowing = index; } @Override @@ -348,7 +411,6 @@ public class TimePicker extends FrameLayout implements OnTouchListener { final float eventY = event.getY(); int degrees; int value; - final int currentShowing = getCurrentItemShowing(); final Boolean[] isInnerCircle = new Boolean[1]; isInnerCircle[0] = false; @@ -356,12 +418,17 @@ public class TimePicker extends FrameLayout implements OnTouchListener { switch(event.getAction()) { case MotionEvent.ACTION_DOWN: + if (!mInputEnabled) { + return true; + } + mDownX = eventX; mDownY = eventY; mLastValueSelected = -1; mDoingMove = false; - if (!mIs24HourMode) { + mDoingTouch = true; + if (!mHideAmPm) { mIsTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY); } else { mIsTouchingAmOrPm = -1; @@ -377,16 +444,17 @@ public class TimePicker extends FrameLayout implements OnTouchListener { } }, TAP_TIMEOUT); } else { - mDownDegrees = getDegreesFromCoords(eventX, eventY, false, isInnerCircle); + boolean forceLegal = mAccessibilityManager.isTouchExplorationEnabled(); + mDownDegrees = getDegreesFromCoords(eventX, eventY, forceLegal, isInnerCircle); if (mDownDegrees != -1) { - tryTick(); - mLastValueSelected = getCurrentlyShowingValue(); + tryVibrate(); mHandler.postDelayed(new Runnable() { @Override public void run() { mDoingMove = true; - int value = reselectSelector(currentShowing, mDownDegrees, + int value = reselectSelector(mDownDegrees, isInnerCircle[0], false, true, true); + mLastValueSelected = value; mListener.onValueSelected(getCurrentItemShowing(), value, false); } }, TAP_TIMEOUT); @@ -394,6 +462,12 @@ public class TimePicker extends FrameLayout implements OnTouchListener { } return true; case MotionEvent.ACTION_MOVE: + if (!mInputEnabled) { + // We shouldn't be in this state, because input is disabled. + Log.e(TAG, "Input was disabled, but received ACTION_MOVE."); + return true; + } + float dY = Math.abs(eventY - mDownY); float dX = Math.abs(eventX - mDownX); @@ -425,17 +499,24 @@ public class TimePicker extends FrameLayout implements OnTouchListener { mHandler.removeCallbacksAndMessages(null); degrees = getDegreesFromCoords(eventX, eventY, true, isInnerCircle); if (degrees != -1) { - value = reselectSelector(currentShowing, degrees, + value = reselectSelector(degrees, isInnerCircle[0], false, true, true); if (value != mLastValueSelected) { - tryTick(); + tryVibrate(); mLastValueSelected = value; + mListener.onValueSelected(getCurrentItemShowing(), value, false); } - mListener.onValueSelected(getCurrentItemShowing(), value, false); } return true; case MotionEvent.ACTION_UP: + if (!mInputEnabled) { + Log.d(TAG, "Input was disabled, but received ACTION_UP."); + mListener.onValueSelected(ENABLE_PICKER_INDEX, 1, false); + return true; + } + mHandler.removeCallbacksAndMessages(null); + mDoingTouch = false; if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) { int isTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY); @@ -456,11 +537,9 @@ public class TimePicker extends FrameLayout implements OnTouchListener { if (mDownDegrees != -1) { degrees = getDegreesFromCoords(eventX, eventY, mDoingMove, isInnerCircle); if (degrees != -1) { - value = reselectSelector(currentShowing, degrees, isInnerCircle[0], + value = reselectSelector(degrees, isInnerCircle[0], !mDoingMove, true, false); - mListener.onValueSelected(getCurrentItemShowing(), value, true); - - if (currentShowing == HOUR_INDEX && !mIs24HourMode) { + if (getCurrentItemShowing() == HOUR_INDEX && !mIs24HourMode) { int amOrPm = getIsCurrentlyAmOrPm(); if (amOrPm == AM && value == 12) { value = 0; @@ -469,6 +548,7 @@ public class TimePicker extends FrameLayout implements OnTouchListener { } } setValueForItem(getCurrentItemShowing(), value); + mListener.onValueSelected(getCurrentItemShowing(), value, true); } } mDoingMove = false; @@ -479,47 +559,105 @@ public class TimePicker extends FrameLayout implements OnTouchListener { return false; } - private class ReselectSelectorRunnable implements Runnable { - TimePicker mTimePicker; - private int mIndex; - private int mDegrees; - private boolean mIsInnerCircle; - private boolean mForceNotFineGrained; - private boolean mForceDrawLine; - private boolean mForceDrawDot; - - public ReselectSelectorRunnable(TimePicker timePicker) { - mTimePicker = timePicker; - } - - public void initializeValues(int index, int degrees, boolean isInnerCircle, - boolean forceNotFineGrained, boolean forceDrawLine, boolean forceDrawDot) { - mIndex = index; - mDegrees = degrees; - mIsInnerCircle = isInnerCircle; - mForceNotFineGrained = forceNotFineGrained; - mForceDrawDot = forceDrawDot; - } - - @Override - public void run() { - mTimePicker.reselectSelector(mIndex, mDegrees, mIsInnerCircle, mForceNotFineGrained, - mForceDrawLine, mForceDrawDot); - } - } - public void tryVibrate() { if (mVibrator != null) { long now = SystemClock.uptimeMillis(); // We want to try to vibrate each individual tick discretely. - if (now - mLastVibrate >= 100) { + if (now - mLastVibrate >= 125) { mVibrator.vibrate(5); mLastVibrate = now; } } } - public void tryTick() { - tryVibrate(); + public boolean trySettingInputEnabled(boolean inputEnabled) { + if (mDoingTouch && !inputEnabled) { + // If we're trying to disable input, but we're in the middle of a touch event, + // we'll allow the touch event to continue before disabling input. + return false; + } + mInputEnabled = inputEnabled; + mGrayBox.setVisibility(inputEnabled? View.INVISIBLE : View.VISIBLE); + return true; + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { + event.getText().clear(); + Time time = new Time(); + time.hour = getHours(); + time.minute = getMinutes(); + long millis = time.normalize(true); + int flags = DateUtils.FORMAT_SHOW_TIME; + if (mIs24HourMode) { + flags |= DateUtils.FORMAT_24HOUR; + } + String timeString = DateUtils.formatDateTime(getContext(), millis, flags); + event.getText().add(timeString); + return true; + } + return super.dispatchPopulateAccessibilityEvent(event); + } + + @SuppressLint("NewApi") + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + if (super.performAccessibilityAction(action, arguments)) { + return true; + } + + int changeMultiplier = 0; + if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) { + changeMultiplier = 1; + } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { + changeMultiplier = -1; + } + if (changeMultiplier != 0) { + int value = getCurrentlyShowingValue(); + int stepSize = 0; + int currentItemShowing = getCurrentItemShowing(); + if (currentItemShowing == HOUR_INDEX) { + stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE; + value %= 12; + } else if (currentItemShowing == MINUTE_INDEX) { + stepSize = MINUTE_VALUE_TO_DEGREES_STEP_SIZE; + } + + int degrees = value * stepSize; + degrees = snapToStepSize(degrees, HOUR_VALUE_TO_DEGREES_STEP_SIZE, changeMultiplier); + value = degrees / stepSize; + int maxValue = 0; + int minValue = 0; + if (currentItemShowing == HOUR_INDEX) { + if (mIs24HourMode) { + maxValue = 23; + } else { + maxValue = 12; + minValue = 1; + } + } else { + maxValue = 55; + } + if (value > maxValue) { + // If we scrolled forward past the highest number, wrap around to the lowest. + value = minValue; + } else if (value < minValue) { + // If we scrolled backward past the lowest number, wrap around to the highest. + value = maxValue; + } + setItem(currentItemShowing, value); + mListener.onValueSelected(currentItemShowing, value, false); + return true; + } + + return false; } } diff --git a/src/com/android/datetimepicker/RadialSelectorView.java b/src/com/android/datetimepicker/time/RadialSelectorView.java index bb1f78a..92324a2 100644 --- a/src/com/android/datetimepicker/RadialSelectorView.java +++ b/src/com/android/datetimepicker/time/RadialSelectorView.java @@ -14,26 +14,19 @@ * limitations under the License. */ -package com.android.datetimepicker; +package com.android.datetimepicker.time; import android.animation.Keyframe; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.app.Service; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.Typeface; -import android.os.SystemClock; -import android.os.Vibrator; -import android.util.AttributeSet; import android.util.Log; -import android.view.MotionEvent; import android.view.View; -import android.view.View.OnTouchListener; import com.android.datetimepicker.R; @@ -66,6 +59,7 @@ public class RadialSelectorView extends View { private int mSelectionDegrees; private double mSelectionRadians; + private boolean mHideSelector; private boolean mDrawLine; private boolean mForceDrawDot; @@ -74,8 +68,8 @@ public class RadialSelectorView extends View { mIsInitialized = false; } - public void initialize(Context context, int selectionDegrees, boolean is24HourMode, - boolean hasInnerCircle, boolean isInnerCircle, boolean disappearsOut) { + public void initialize(Context context, boolean is24HourMode, boolean hasInnerCircle, + boolean disappearsOut, int selectionDegrees, boolean isInnerCircle) { if (mIsInitialized) { Log.e(TAG, "This RadialSelectorView may only be initialized once."); return; @@ -111,18 +105,17 @@ public class RadialSelectorView extends View { mSelectionRadiusMultiplier = Float.parseFloat(res.getString(R.string.selection_radius_multiplier)); - setSelection(selectionDegrees, isInnerCircle, false, false); - mAnimationRadiusMultiplier = 1; mTransitionMidRadiusMultiplier = 1f + (0.05f * (disappearsOut? -1 : 1)); mTransitionEndRadiusMultiplier = 1f + (0.3f * (disappearsOut? 1 : -1)); mInvalidateUpdateListener = new InvalidateUpdateListener(); + setSelection(selectionDegrees, isInnerCircle, false, false, false); mIsInitialized = true; } public void setSelection(int selectionDegrees, boolean isInnerCircle, - boolean drawLine, boolean forceDrawDot) { + boolean drawLine, boolean forceDrawDot, boolean hideSelector) { mSelectionDegrees = selectionDegrees; mSelectionRadians = selectionDegrees * Math.PI / 180; mDrawLine = drawLine; @@ -135,6 +128,7 @@ public class RadialSelectorView extends View { mNumbersRadiusMultiplier = mOuterNumbersRadiusMultiplier; } } + mHideSelector = hideSelector; } public void setDrawLine(boolean drawLine) { @@ -252,6 +246,10 @@ public class RadialSelectorView extends View { } mLineLength = (int) (mCircleRadius * mNumbersRadiusMultiplier * mAnimationRadiusMultiplier); + if (mHideSelector) { + return; + } + int pointX = mXCenter + (int) (mLineLength * Math.sin(mSelectionRadians)); int pointY = mYCenter - (int) (mLineLength * Math.cos(mSelectionRadians)); @@ -316,8 +314,8 @@ public class RadialSelectorView extends View { // The time points are half of what they would normally be, because this animation is // staggered against the disappear so they happen seamlessly. The reappear starts // halfway into the disappear. - float delayMultiplier = 0.5f; - float transitionDurationMultiplier = 0.75f; + float delayMultiplier = 0.25f; + float transitionDurationMultiplier = 1f; float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier; int totalDuration = (int) (duration * totalDurationMultiplier); float delayPoint = (delayMultiplier * duration) / totalDuration; diff --git a/src/com/android/datetimepicker/RadialTextsView.java b/src/com/android/datetimepicker/time/RadialTextsView.java index b128d0d..5e159c8 100644 --- a/src/com/android/datetimepicker/RadialTextsView.java +++ b/src/com/android/datetimepicker/time/RadialTextsView.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.datetimepicker; +package com.android.datetimepicker.time; import android.animation.Keyframe; import android.animation.ObjectAnimator; @@ -27,11 +27,8 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Typeface; import android.graphics.Paint.Align; -import android.util.AttributeSet; import android.util.Log; -import android.view.MotionEvent; import android.view.View; -import android.view.View.OnTouchListener; import com.android.datetimepicker.R; @@ -274,8 +271,8 @@ public class RadialTextsView extends View { // Set up animator for reappearing. - float delayMultiplier = 0.5f; - float transitionDurationMultiplier = 0.75f; + float delayMultiplier = 0.25f; + float transitionDurationMultiplier = 1f; float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier; int totalDuration = (int) (duration * totalDurationMultiplier); float delayPoint = (delayMultiplier * duration) / totalDuration; diff --git a/src/com/android/datetimepicker/time/TimePickerDialog.java b/src/com/android/datetimepicker/time/TimePickerDialog.java new file mode 100644 index 0000000..c9e5a02 --- /dev/null +++ b/src/com/android/datetimepicker/time/TimePickerDialog.java @@ -0,0 +1,823 @@ +/* + * Copyright (C) 2013 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.datetimepicker.time; + +import android.annotation.SuppressLint; +import android.app.ActionBar.LayoutParams; +import android.app.DialogFragment; +import android.content.Context; +import android.content.res.Resources; +import android.os.Bundle; +import android.util.Log; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnKeyListener; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.android.datetimepicker.R; + +import com.android.datetimepicker.time.RadialPickerLayout.OnValueSelectedListener; +import com.android.datetimepicker.Utils; + +import java.text.DateFormatSymbols; +import java.util.ArrayList; +import java.util.Locale; + +/** + * Dialog to set a time. + */ +public class TimePickerDialog extends DialogFragment implements OnValueSelectedListener{ + private static final String TAG = "TimePickerDialog"; + + private static final String KEY_HOUR_OF_DAY = "hour_of_day"; + private static final String KEY_MINUTE = "minute"; + private static final String KEY_IS_24_HOUR_VIEW = "is_24_hour_view"; + private static final String KEY_CURRENT_ITEM_SHOWING = "current_item_showing"; + private static final String KEY_IN_KB_MODE = "in_kb_mode"; + private static final String KEY_TYPED_TIMES = "typed_times"; + + public static final int HOUR_INDEX = 0; + public static final int MINUTE_INDEX = 1; + public static final int AMPM_INDEX = 2; // NOT a real index for the purpose of what's showing. + public static final int ENABLE_PICKER_INDEX = 3; // Also NOT a real index, just used for KB mode. + public static final int AM = 0; + public static final int PM = 1; + + private OnTimeSetListener mCallback; + + private TextView mDoneButton; + private TextView mHourView; + private TextView mMinuteView; + private TextView mAmPmTextView; + private View mAmPmHitspace; + private RadialPickerLayout mTimePicker; + + private int mBlue; + private int mBlack; + private String mAmText; + private String mPmText; + + private boolean mAllowAutoAdvance; + private int mInitialHourOfDay; + private int mInitialMinute; + private boolean mIs24HourMode; + + // For hardware IME input. + private char mPlaceholderText; + private String mDoublePlaceholderText; + private boolean mInKbMode; + private ArrayList<Integer> mTypedTimes; + private Node mLegalTimesTree; + private int mAmKeyCode; + private int mPmKeyCode; + + // Accessibility strings. + private String mHourPickerDescription; + private String mSelectHours; + private String mMinutePickerDescription; + private String mSelectMinutes; + + /** + * The callback interface used to indicate the user is done filling in + * the time (they clicked on the 'Set' button). + */ + public interface OnTimeSetListener { + + /** + * @param view The view associated with this listener. + * @param hourOfDay The hour that was set. + * @param minute The minute that was set. + */ + void onTimeSet(RadialPickerLayout view, int hourOfDay, int minute); + } + + public TimePickerDialog() { + // Empty constructor required for dialog fragment. + } + + public TimePickerDialog(Context context, int theme, OnTimeSetListener callback, + int hourOfDay, int minute, boolean is24HourMode) { + // Empty constructor required for dialog fragment. + } + + public static TimePickerDialog newInstance(OnTimeSetListener callback, + int hourOfDay, int minute, boolean is24HourMode) { + TimePickerDialog ret = new TimePickerDialog(); + ret.initialize(callback, hourOfDay, minute, is24HourMode); + return ret; + } + + public void initialize(OnTimeSetListener callback, + int hourOfDay, int minute, boolean is24HourMode) { + mCallback = callback; + + mInitialHourOfDay = hourOfDay; + mInitialMinute = minute; + mIs24HourMode = is24HourMode; + mInKbMode = false; + } + + public void setOnTimeSetListener(OnTimeSetListener callback) { + mCallback = callback; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null && savedInstanceState.containsKey(KEY_HOUR_OF_DAY) + && savedInstanceState.containsKey(KEY_MINUTE) + && savedInstanceState.containsKey(KEY_IS_24_HOUR_VIEW)) { + mInitialHourOfDay = savedInstanceState.getInt(KEY_HOUR_OF_DAY); + mInitialMinute = savedInstanceState.getInt(KEY_MINUTE); + mIs24HourMode = savedInstanceState.getBoolean(KEY_IS_24_HOUR_VIEW); + mInKbMode = savedInstanceState.getBoolean(KEY_IN_KB_MODE); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE); + + View view = inflater.inflate(R.layout.time_picker_dialog, null); + KeyboardListener keyboardListener = new KeyboardListener(); + view.findViewById(R.id.time_picker_dialog).setOnKeyListener(keyboardListener); + + Resources res = getResources(); + mHourPickerDescription = res.getString(R.string.hour_picker_description); + mSelectHours = res.getString(R.string.select_hours); + mMinutePickerDescription = res.getString(R.string.minute_picker_description); + mSelectMinutes = res.getString(R.string.select_minutes); + mBlue = res.getColor(R.color.blue); + mBlack = res.getColor(R.color.black_80); + + mHourView = (TextView) view.findViewById(R.id.hours); + mHourView.setOnKeyListener(keyboardListener); + mMinuteView = (TextView) view.findViewById(R.id.minutes); + mMinuteView.setOnKeyListener(keyboardListener); + mAmPmTextView = (TextView) view.findViewById(R.id.ampm_label); + mAmPmTextView.setOnKeyListener(keyboardListener); + String[] amPmTexts = new DateFormatSymbols().getAmPmStrings(); + mAmText = amPmTexts[0]; + mPmText = amPmTexts[1]; + + mTimePicker = (RadialPickerLayout) view.findViewById(R.id.time_picker); + mTimePicker.setOnValueSelectedListener(this); + mTimePicker.setOnKeyListener(keyboardListener); + mTimePicker.initialize(getActivity(), mInitialHourOfDay, mInitialMinute, mIs24HourMode); + int currentItemShowing = HOUR_INDEX; + if (savedInstanceState != null && + savedInstanceState.containsKey(KEY_CURRENT_ITEM_SHOWING)) { + currentItemShowing = savedInstanceState.getInt(KEY_CURRENT_ITEM_SHOWING); + } + setCurrentItemShowing(currentItemShowing, false); + mTimePicker.invalidate(); + + mHourView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + setCurrentItemShowing(HOUR_INDEX, true); + mTimePicker.tryVibrate(); + } + }); + mMinuteView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + setCurrentItemShowing(MINUTE_INDEX, true); + mTimePicker.tryVibrate(); + } + }); + + mDoneButton = (TextView) view.findViewById(R.id.done_button); + mDoneButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mInKbMode && isTypedTimeFullyLegal()) { + finishKbMode(false); + } else { + mTimePicker.tryVibrate(); + } + if (mCallback != null) { + mCallback.onTimeSet(mTimePicker, + mTimePicker.getHours(), mTimePicker.getMinutes()); + } + dismiss(); + } + }); + mDoneButton.setOnKeyListener(keyboardListener); + + mAmPmHitspace = view.findViewById(R.id.ampm_hitspace); + if (mIs24HourMode) { + mAmPmTextView.setVisibility(View.GONE); + + RelativeLayout.LayoutParams paramsSeparator = new RelativeLayout.LayoutParams( + LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + paramsSeparator.addRule(RelativeLayout.CENTER_IN_PARENT); + TextView separatorView = (TextView) view.findViewById(R.id.separator); + separatorView.setLayoutParams(paramsSeparator); + } else { + mAmPmTextView.setVisibility(View.VISIBLE); + updateAmPmDisplay(mInitialHourOfDay < 12? AM : PM); + mAmPmHitspace.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mTimePicker.tryVibrate(); + int amOrPm = mTimePicker.getIsCurrentlyAmOrPm(); + if (amOrPm == AM) { + amOrPm = PM; + } else if (amOrPm == PM){ + amOrPm = AM; + } + updateAmPmDisplay(amOrPm); + mTimePicker.setAmOrPm(amOrPm); + } + }); + } + + mAllowAutoAdvance = true; + setHour(mInitialHourOfDay); + setMinute(mInitialMinute); + + mDoublePlaceholderText = res.getString(R.string.time_placeholder); + mPlaceholderText = mDoublePlaceholderText.charAt(0); + mAmKeyCode = mPmKeyCode = -1; + generateLegalTimesTree(); + if (mInKbMode) { + mTypedTimes = savedInstanceState.getIntegerArrayList(KEY_TYPED_TIMES); + tryStartingKbMode(-1); + mHourView.invalidate(); + } else if (mTypedTimes == null) { + mTypedTimes = new ArrayList<Integer>(); + } + + return view; + } + + private void updateAmPmDisplay(int amOrPm) { + if (amOrPm == AM) { + mAmPmTextView.setText(mAmText); + tryAccessibilityAnnounce(mAmText); + mAmPmHitspace.setContentDescription(mAmText); + } else if (amOrPm == PM){ + mAmPmTextView.setText(mPmText); + tryAccessibilityAnnounce(mPmText); + mAmPmHitspace.setContentDescription(mPmText); + } else { + mAmPmTextView.setText(mDoublePlaceholderText); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + if (mTimePicker != null) { + outState.putInt(KEY_HOUR_OF_DAY, mTimePicker.getHours()); + outState.putInt(KEY_MINUTE, mTimePicker.getMinutes()); + outState.putBoolean(KEY_IS_24_HOUR_VIEW, mIs24HourMode); + outState.putInt(KEY_CURRENT_ITEM_SHOWING, mTimePicker.getCurrentItemShowing()); + outState.putBoolean(KEY_IN_KB_MODE, mInKbMode); + if (mInKbMode) { + outState.putIntegerArrayList(KEY_TYPED_TIMES, mTypedTimes); + } + } + } + + @Override + public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance) { + if (pickerIndex == HOUR_INDEX) { + setHour(newValue); + if (mAllowAutoAdvance && autoAdvance) { + setCurrentItemShowing(MINUTE_INDEX, true); + } + } else if (pickerIndex == MINUTE_INDEX){ + setMinute(newValue); + } else if (pickerIndex == AMPM_INDEX) { + updateAmPmDisplay(newValue); + } else if (pickerIndex == ENABLE_PICKER_INDEX) { + if (!isTypedTimeFullyLegal()) { + mTypedTimes.clear(); + } + finishKbMode(true); + } + } + + private void setHour(int value) { + String format; + if (mIs24HourMode) { + format = "%02d"; + } else { + format = "%d"; + value = value % 12; + if (value == 0) { + value = 12; + } + } + + CharSequence text = String.format(format, value); + tryAccessibilityAnnounce(text); + mHourView.setText(text); + } + + private void setMinute(int value) { + if (value == 60) { + value = 0; + } + CharSequence text = String.format(Locale.getDefault(), "%02d", value); + tryAccessibilityAnnounce(text); + mMinuteView.setText(text); + } + + private void setCurrentItemShowing(int index, boolean animate) { + mTimePicker.setCurrentItemShowing(index, animate); + + if (index == HOUR_INDEX) { + int hours = mTimePicker.getHours(); + if (!mIs24HourMode) { + hours = hours % 12; + } + mTimePicker.setContentDescription(mHourPickerDescription+": "+hours); + tryAccessibilityAnnounce(mSelectHours); + } else { + int minutes = mTimePicker.getMinutes(); + mTimePicker.setContentDescription(mMinutePickerDescription+": "+minutes); + tryAccessibilityAnnounce(mSelectMinutes); + } + + int hourColor = (index == HOUR_INDEX)? mBlue : mBlack; + int minuteColor = (index == MINUTE_INDEX)? mBlue : mBlack; + mHourView.setTextColor(hourColor); + mMinuteView.setTextColor(minuteColor); + } + + @SuppressLint("NewApi") + private void tryAccessibilityAnnounce(CharSequence text) { + if (Utils.isJellybeanOrLater() && mTimePicker != null && text != null) { + mTimePicker.announceForAccessibility(text); + } + } + + private boolean processKeyUp(int keyCode) { + if (keyCode == KeyEvent.KEYCODE_ESCAPE || keyCode == KeyEvent.KEYCODE_BACK) { + dismiss(); + return true; + } else if (keyCode == KeyEvent.KEYCODE_TAB) { + if(mInKbMode) { + if (isTypedTimeFullyLegal()) { + finishKbMode(true); + } + return true; + } + } else if (keyCode == KeyEvent.KEYCODE_ENTER) { + if (mInKbMode) { + if (!isTypedTimeFullyLegal()) { + return true; + } + finishKbMode(false); + } + if (mCallback != null) { + mCallback.onTimeSet(mTimePicker, + mTimePicker.getHours(), mTimePicker.getMinutes()); + } + dismiss(); + return true; + } else if (keyCode == KeyEvent.KEYCODE_DEL) { + if (mInKbMode) { + if (!mTypedTimes.isEmpty()) { + deleteLastTypedKey(); + updateDisplay(true); + } + } + } else if (keyCode == KeyEvent.KEYCODE_0 || keyCode == KeyEvent.KEYCODE_1 + || keyCode == KeyEvent.KEYCODE_2 || keyCode == KeyEvent.KEYCODE_3 + || keyCode == KeyEvent.KEYCODE_4 || keyCode == KeyEvent.KEYCODE_5 + || keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7 + || keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9 + || (!mIs24HourMode && + (keyCode == getAmOrPmKeyCode(AM) || keyCode == getAmOrPmKeyCode(PM)))) { + if (!mInKbMode) { + if (mTimePicker == null) { + // Something's wrong, because time picker should definitely not be null. + Log.e(TAG, "Unable to initiate keyboard mode, TimePicker was null."); + return true; + } + mTypedTimes.clear(); + tryStartingKbMode(keyCode); + return true; + } + // We're already in keyboard mode. + if (addKeyIfLegal(keyCode)) { + updateDisplay(false); + } + return true; + } + return false; + } + + private void tryStartingKbMode(int keyCode) { + if (mTimePicker.trySettingInputEnabled(false) && (keyCode == -1 || addKeyIfLegal(keyCode))) { + mInKbMode = true; + mDoneButton.setEnabled(false); + updateDisplay(false); + } + } + + private boolean addKeyIfLegal(int keyCode) { + // If we're in 24hour mode, we'll need to check if the input is full. If in AM/PM mode, + // we'll need to see if AM/PM have been typed. + if ((mIs24HourMode && mTypedTimes.size() == 4) || + (!mIs24HourMode && isTypedTimeFullyLegal())) { + return false; + } + + mTypedTimes.add(keyCode); + if (!isTypedTimeLegalSoFar()) { + deleteLastTypedKey(); + return false; + } + + // Automatically fill in 0's if AM or PM was legally entered. + if (isTypedTimeFullyLegal()) { + if (!mIs24HourMode && mTypedTimes.size() <= 3) { + mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0); + mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0); + } + mDoneButton.setEnabled(true); + } + + return true; + } + + private boolean isTypedTimeLegalSoFar() { + Node node = mLegalTimesTree; + for (int keyCode : mTypedTimes) { + node = node.canReach(keyCode); + if (node == null) { + return false; + } + } + return true; + } + + private boolean isTypedTimeFullyLegal() { + // The time is legal if it contains an AM or PM, as those can only be legally added at + // specific times based on the tree's algorithm. + if (mIs24HourMode) { + // getEnteredTime() will ONLY call isTypedTimeFullyLegal() when NOT in 24hour mode. + int[] values = getEnteredTime(null); + return (values[0] >= 0 && values[1] >= 0 && values[1] < 60); + } else { + return (mTypedTimes.contains(getAmOrPmKeyCode(AM)) || + mTypedTimes.contains(getAmOrPmKeyCode(PM))); + } + } + + private void deleteLastTypedKey() { + mTypedTimes.remove(mTypedTimes.size() - 1); + if (!isTypedTimeFullyLegal()) { + mDoneButton.setEnabled(false); + } + } + + private void finishKbMode(boolean changeDisplays) { + mInKbMode = false; + if (!mTypedTimes.isEmpty()) { + int values[] = getEnteredTime(null); + mTimePicker.setTime(values[0], values[1]); + if (!mIs24HourMode) { + mTimePicker.setAmOrPm(values[2]); + } + mTypedTimes.clear(); + } + if (changeDisplays) { + updateDisplay(false); + mTimePicker.trySettingInputEnabled(true); + } + } + + private void updateDisplay(boolean allowEmpty) { + if (!allowEmpty && mTypedTimes.isEmpty()) { + int hour = mTimePicker.getHours(); + int minute = mTimePicker.getMinutes(); + setHour(hour); + setMinute(minute); + if (!mIs24HourMode) { + updateAmPmDisplay(hour < 12? AM : PM); + } + setCurrentItemShowing(mTimePicker.getCurrentItemShowing(), true); + mDoneButton.setEnabled(true); + } else { + Boolean[] enteredZeros = {false, false}; + int[] values = getEnteredTime(enteredZeros); + String hourFormat = enteredZeros[0]? "%02d" : "%2d"; + String minuteFormat = (enteredZeros[1])? "%02d" : "%2d"; + String hourStr = (values[0] == -1)? mDoublePlaceholderText : + String.format(hourFormat, values[0]).replace(' ', mPlaceholderText); + String minuteStr = (values[1] == -1)? mDoublePlaceholderText : + String.format(minuteFormat, values[1]).replace(' ', mPlaceholderText); + mHourView.setText(hourStr); + mHourView.setTextColor(mBlack); + mMinuteView.setText(minuteStr); + mMinuteView.setTextColor(mBlack); + if (!mIs24HourMode) { + updateAmPmDisplay(values[2]); + } + } + } + + private int getValFromKeyCode(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_0: + return 0; + case KeyEvent.KEYCODE_1: + return 1; + case KeyEvent.KEYCODE_2: + return 2; + case KeyEvent.KEYCODE_3: + return 3; + case KeyEvent.KEYCODE_4: + return 4; + case KeyEvent.KEYCODE_5: + return 5; + case KeyEvent.KEYCODE_6: + return 6; + case KeyEvent.KEYCODE_7: + return 7; + case KeyEvent.KEYCODE_8: + return 8; + case KeyEvent.KEYCODE_9: + return 9; + default: + return -1; + } + } + + private int[] getEnteredTime(Boolean[] enteredZeros) { + int amOrPm = -1; + int startIndex = 1; + if (!mIs24HourMode && isTypedTimeFullyLegal()) { + int keyCode = mTypedTimes.get(mTypedTimes.size() - 1); + if (keyCode == getAmOrPmKeyCode(AM)) { + amOrPm = AM; + } else if (keyCode == getAmOrPmKeyCode(PM)){ + amOrPm = PM; + } + startIndex = 2; + } + int minute = -1; + int hour = -1; + for (int i = startIndex; i <= mTypedTimes.size(); i++) { + int val = getValFromKeyCode(mTypedTimes.get(mTypedTimes.size() - i)); + if (i == startIndex) { + minute = val; + } else if (i == startIndex+1) { + minute += 10*val; + if (enteredZeros != null && val == 0) { + enteredZeros[1] = true; + } + } else if (i == startIndex+2) { + hour = val; + } else if (i == startIndex+3) { + hour += 10*val; + if (enteredZeros != null && val == 0) { + enteredZeros[0] = true; + } + } + } + + int[] ret = {hour, minute, amOrPm}; + return ret; + } + + private int getAmOrPmKeyCode(int amOrPm) { + // Cache the codes. + if (mAmKeyCode == -1 || mPmKeyCode == -1) { + // Find the first character in the AM/PM text that is unique. + KeyCharacterMap kcm = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); + char amChar; + char pmChar; + for (int i = 0; i < Math.max(mAmText.length(), mPmText.length()); i++) { + amChar = mAmText.toLowerCase(Locale.getDefault()).charAt(i); + pmChar = mPmText.toLowerCase(Locale.getDefault()).charAt(i); + if (amChar != pmChar) { + KeyEvent[] events = kcm.getEvents(new char[]{amChar, pmChar}); + // There should be 4 events: a down and up for both AM and PM. + if (events != null && events.length == 4) { + mAmKeyCode = events[0].getKeyCode(); + mPmKeyCode = events[2].getKeyCode(); + Log.d(TAG, "am char: "+amChar+" keycode: "+mAmKeyCode); + Log.d(TAG, "pm char: "+pmChar+" keycode: "+mPmKeyCode); + } else { + Log.d(TAG, "am char: "+amChar+" keycode: "+mAmKeyCode); + Log.d(TAG, "pm char: "+pmChar+" keycode: "+mPmKeyCode); + if (events != null) { + for (int j = 0; j < events.length; j++) { + Log.d(TAG, "event code: "+events[j].getKeyCode()+" events: "+events[j]); + } + } + Log.e(TAG, "Unable to find keycodes for AM and PM."); + } + break; + } + } + } + if (amOrPm == AM) { + return mAmKeyCode; + } else if (amOrPm == PM) { + return mPmKeyCode; + } + + return -1; + } + + private void generateLegalTimesTree() { + // Create a quick cache of numbers to their keycodes. + int k0 = KeyEvent.KEYCODE_0; + int k1 = KeyEvent.KEYCODE_1; + int k2 = KeyEvent.KEYCODE_2; + int k3 = KeyEvent.KEYCODE_3; + int k4 = KeyEvent.KEYCODE_4; + int k5 = KeyEvent.KEYCODE_5; + int k6 = KeyEvent.KEYCODE_6; + int k7 = KeyEvent.KEYCODE_7; + int k8 = KeyEvent.KEYCODE_8; + int k9 = KeyEvent.KEYCODE_9; + + // The root of the tree doesn't contain any numbers. + mLegalTimesTree = new Node(); + if (mIs24HourMode) { + // We'll be re-using these nodes, so we'll save them. + Node minuteFirstDigit = new Node(k0, k1, k2, k3, k4, k5); + Node minuteSecondDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); + // The first digit must be followed by the second digit. + minuteFirstDigit.addChild(minuteSecondDigit); + + // The first digit may be 0-1. + Node firstDigit = new Node(k0, k1); + mLegalTimesTree.addChild(firstDigit); + + // When the first digit is 0-1, the second digit may be 0-5. + Node secondDigit = new Node(k0, k1, k2, k3, k4, k5); + firstDigit.addChild(secondDigit); + // We may now be followed by the first minute digit. E.g. 00:09, 15:58. + secondDigit.addChild(minuteFirstDigit); + + // When the first digit is 0-1, and the second digit is 0-5, the third digit may be 6-9. + Node thirdDigit = new Node(k6, k7, k8, k9); + // The time must now be finished. E.g. 0:55, 1:08. + secondDigit.addChild(thirdDigit); + + // When the first digit is 0-1, the second digit may be 6-9. + secondDigit = new Node(k6, k7, k8, k9); + firstDigit.addChild(secondDigit); + // We must now be followed by the first minute digit. E.g. 06:50, 18:20. + secondDigit.addChild(minuteFirstDigit); + + // The first digit may be 2. + firstDigit = new Node(k2); + mLegalTimesTree.addChild(firstDigit); + + // When the first digit is 2, the second digit may be 0-3. + secondDigit = new Node(k0, k1, k2, k3); + firstDigit.addChild(secondDigit); + // We must now be followed by the first minute digit. E.g. 20:50, 23:09. + secondDigit.addChild(minuteFirstDigit); + + // When the first digit is 2, the second digit may be 4-5. + secondDigit = new Node(k4, k5); + firstDigit.addChild(secondDigit); + // We must now be followd by the last minute digit. E.g. 2:40, 2:53. + secondDigit.addChild(minuteSecondDigit); + + // The first digit may be 3-9. + firstDigit = new Node(k3, k4, k5, k6, k7, k8, k9); + mLegalTimesTree.addChild(firstDigit); + // We must now be followed by the first minute digit. E.g. 3:57, 8:12. + firstDigit.addChild(minuteFirstDigit); + } else { + // We'll need to use the AM/PM node a lot. + // Set up AM and PM to respond to "a" and "p". + Node ampm = new Node(getAmOrPmKeyCode(AM), getAmOrPmKeyCode(PM)); + + // The first hour digit may be 1. + Node firstDigit = new Node(k1); + mLegalTimesTree.addChild(firstDigit); + // We'll allow quick input of on-the-hour times. E.g. 1pm. + firstDigit.addChild(ampm); + + // When the first digit is 1, the second digit may be 0-2. + Node secondDigit = new Node(k0, k1, k2); + firstDigit.addChild(secondDigit); + // Also for quick input of on-the-hour times. E.g. 10pm, 12am. + secondDigit.addChild(ampm); + + // When the first digit is 1, and the second digit is 0-2, the third digit may be 0-5. + Node thirdDigit = new Node(k0, k1, k2, k3, k4, k5); + secondDigit.addChild(thirdDigit); + // The time may be finished now. E.g. 1:02pm, 1:25am. + thirdDigit.addChild(ampm); + + // When the first digit is 1, the second digit is 0-2, and the third digit is 0-5, + // the fourth digit may be 0-9. + Node fourthDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); + thirdDigit.addChild(fourthDigit); + // The time must be finished now. E.g. 10:49am, 12:40pm. + fourthDigit.addChild(ampm); + + // When the first digit is 1, and the second digit is 0-2, the third digit may be 6-9. + thirdDigit = new Node(k6, k7, k8, k9); + secondDigit.addChild(thirdDigit); + // The time must be finished now. E.g. 1:08am, 1:26pm. + thirdDigit.addChild(ampm); + + // When the first digit is 1, the second digit may be 3-5. + secondDigit = new Node(k3, k4, k5); + firstDigit.addChild(secondDigit); + + // When the first digit is 1, and the second digit is 3-5, the third digit may be 0-9. + thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); + secondDigit.addChild(thirdDigit); + // The time must be finished now. E.g. 1:39am, 1:50pm. + thirdDigit.addChild(ampm); + + // The hour digit may be 2-9. + firstDigit = new Node(k2, k3, k4, k5, k6, k7, k8, k9); + mLegalTimesTree.addChild(firstDigit); + // We'll allow quick input of on-the-hour-times. E.g. 2am, 5pm. + firstDigit.addChild(ampm); + + // When the first digit is 2-9, the second digit may be 0-5. + secondDigit = new Node(k0, k1, k2, k3, k4, k5); + firstDigit.addChild(secondDigit); + + // When the first digit is 2-9, and the second digit is 0-5, the third digit may be 0-9. + thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); + secondDigit.addChild(thirdDigit); + // The time must be finished now. E.g. 2:57am, 9:30pm. + thirdDigit.addChild(ampm); + } + } + + private class Node { + private int[] mLegalKeys; + private ArrayList<Node> mChildren; + + public Node(int... legalKeys) { + mLegalKeys = legalKeys; + mChildren = new ArrayList<Node>(); + } + + public void addChild(Node child) { + mChildren.add(child); + } + + public boolean containsKey(int key) { + for (int i = 0; i < mLegalKeys.length; i++) { + if (mLegalKeys[i] == key) { + return true; + } + } + return false; + } + + public Node canReach(int key) { + if (mChildren == null) { + return null; + } + for (Node child : mChildren) { + if (child.containsKey(key)) { + return child; + } + } + return null; + } + } + + private class KeyboardListener implements OnKeyListener { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_UP) { + return processKeyUp(keyCode); + } + return false; + } + } +} |