diff options
author | Sam Blitzstein <sblitz@google.com> | 2013-04-01 12:52:38 -0700 |
---|---|---|
committer | Sam Blitzstein <sblitz@google.com> | 2013-04-08 11:19:58 -0700 |
commit | f3b38bd61d583d31200c501f5a74392aac510657 (patch) | |
tree | cf698fd25ee8dbe5cf4ead7b730ebcf107027a48 | |
parent | 296f8f5d533485413e0245438a837edcceaaa6ce (diff) | |
download | android_frameworks_opt_datetimepicker-f3b38bd61d583d31200c501f5a74392aac510657.tar.gz android_frameworks_opt_datetimepicker-f3b38bd61d583d31200c501f5a74392aac510657.tar.bz2 android_frameworks_opt_datetimepicker-f3b38bd61d583d31200c501f5a74392aac510657.zip |
Further tweaks and bug fixes, and code cleanup.
Bug: 8530194
Bug: 8531032
Change-Id: I908e7df4a432f9f6b338bc601e7be08f26b93b98
-rw-r--r-- | res/layout-land/date_picker_dialog.xml | 4 | ||||
-rw-r--r-- | res/layout-land/time_picker_dialog.xml | 25 | ||||
-rw-r--r-- | res/layout/date_picker_dialog.xml | 2 | ||||
-rw-r--r-- | res/layout/time_picker_dialog.xml | 15 | ||||
-rw-r--r-- | res/values-land/dimens.xml | 5 | ||||
-rw-r--r-- | res/values-sw600dp-land/dimens.xml | 3 | ||||
-rw-r--r-- | res/values-sw600dp/dimens.xml | 19 | ||||
-rw-r--r-- | res/values/colors.xml | 12 | ||||
-rw-r--r-- | res/values/dimens.xml | 9 | ||||
-rw-r--r-- | res/values/strings.xml | 4 | ||||
-rw-r--r-- | res/values/styles.xml | 4 | ||||
-rw-r--r-- | src/com/android/datetimepicker/time/AmPmCirclesView.java | 20 | ||||
-rw-r--r-- | src/com/android/datetimepicker/time/CircleView.java | 11 | ||||
-rw-r--r-- | src/com/android/datetimepicker/time/RadialPickerLayout.java | 252 | ||||
-rw-r--r-- | src/com/android/datetimepicker/time/RadialSelectorView.java | 72 | ||||
-rw-r--r-- | src/com/android/datetimepicker/time/RadialTextsView.java | 44 | ||||
-rw-r--r-- | src/com/android/datetimepicker/time/TimePickerDialog.java | 133 |
17 files changed, 478 insertions, 156 deletions
diff --git a/res/layout-land/date_picker_dialog.xml b/res/layout-land/date_picker_dialog.xml index 22d4e6c..4b3f684 100644 --- a/res/layout-land/date_picker_dialog.xml +++ b/res/layout-land/date_picker_dialog.xml @@ -31,7 +31,7 @@ <View android:layout_width="match_parent" android:layout_height="1dip" - android:background="@color/black_20" /> + android:background="@color/line_background" /> <include layout="@layout/date_picker_done_button" /> </LinearLayout> @@ -39,7 +39,7 @@ <View android:layout_width="1dip" android:layout_height="match_parent" - android:background="@color/black_20" /> + android:background="@color/line_background" /> <FrameLayout android:layout_width="@dimen/pager_width" diff --git a/res/layout-land/time_picker_dialog.xml b/res/layout-land/time_picker_dialog.xml index e8c43ea..4501d19 100644 --- a/res/layout-land/time_picker_dialog.xml +++ b/res/layout-land/time_picker_dialog.xml @@ -15,12 +15,15 @@ ~ limitations under the License --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" + android:id="@+id/time_picker_dialog" android:layout_height="@dimen/dialog_height" + android:layout_width="wrap_content" android:orientation="horizontal" - android:background="@color/black_03" - android:id="@+id/time_picker_dialog" - android:focusable="true" > + android:focusable="true" + android:layout_marginLeft="@dimen/minimum_margin_sides" + android:layout_marginRight="@dimen/minimum_margin_sides" + android:layout_marginTop="@dimen/minimum_margin_top_bottom" + android:layout_marginBottom="@dimen/minimum_margin_top_bottom" > <LinearLayout android:layout_width="wrap_content" android:layout_height="match_parent" @@ -39,7 +42,7 @@ <View android:layout_width="match_parent" android:layout_height="1dip" - android:background="@color/black_20" /> + android:background="@color/line_background" /> <LinearLayout style="?android:attr/buttonBarStyle" android:layout_width="match_parent" @@ -51,15 +54,17 @@ style="?android:attr/buttonBarButtonStyle" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/done_label" /> + android:text="@string/done_label" + android:textSize="@dimen/done_label_size" + android:textColor="@color/done_text_color" /> </LinearLayout> </LinearLayout> <com.android.datetimepicker.time.RadialPickerLayout android:id="@+id/time_picker" - android:layout_width="0dip" + android:layout_width="@dimen/picker_dimen" android:layout_height="match_parent" - android:layout_weight="1" - android:gravity="center" + android:layout_gravity="center" android:focusable="true" - android:focusableInTouchMode="true" /> + android:focusableInTouchMode="true" + android:background="@color/circle_background" /> </LinearLayout> diff --git a/res/layout/date_picker_dialog.xml b/res/layout/date_picker_dialog.xml index ece59fc..596b3bd 100644 --- a/res/layout/date_picker_dialog.xml +++ b/res/layout/date_picker_dialog.xml @@ -33,7 +33,7 @@ <View android:layout_width="match_parent" android:layout_height="1dip" - android:background="@color/black_20" /> + android:background="@color/line_background" /> <include layout="@layout/date_picker_done_button" android:id="@+id/date_picker_done_button" /> diff --git a/res/layout/time_picker_dialog.xml b/res/layout/time_picker_dialog.xml index 18b6d4c..67f5977 100644 --- a/res/layout/time_picker_dialog.xml +++ b/res/layout/time_picker_dialog.xml @@ -15,10 +15,10 @@ ~ limitations under the License --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" + android:layout_width="wrap_content" android:layout_height="match_parent" android:orientation="vertical" - android:background="@color/black_03" + android:background="@color/circle_background" android:id="@+id/time_picker_dialog" android:focusable="true" > <FrameLayout @@ -33,16 +33,15 @@ </FrameLayout> <com.android.datetimepicker.time.RadialPickerLayout android:id="@+id/time_picker" - android:layout_width="match_parent" - android:layout_height="0dip" - android:layout_weight="1" - android:gravity="center" + android:layout_height="@dimen/picker_dimen" + android:layout_width="wrap_content" + android:layout_gravity="center" android:focusable="true" android:focusableInTouchMode="true" /> <View android:layout_width="match_parent" android:layout_height="1dip" - android:background="@color/black_20" /> + android:background="@color/line_background" /> <LinearLayout style="?android:attr/buttonBarStyle" android:layout_width="match_parent" @@ -54,7 +53,7 @@ android:layout_height="wrap_content" android:text="@string/done_label" android:textSize="@dimen/done_label_size" - android:textColor="@color/black_60" + android:textColor="@color/done_text_color" style="?android:attr/buttonBarButtonStyle" /> </LinearLayout> </LinearLayout> diff --git a/res/values-land/dimens.xml b/res/values-land/dimens.xml index 09cf316..e80e3c7 100644 --- a/res/values-land/dimens.xml +++ b/res/values-land/dimens.xml @@ -20,8 +20,7 @@ <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:android="http://schemas.android.com/apk/res/android" > - - <dimen name="dialog_height">300dip</dimen> - <dimen name="left_side_width">270dip</dimen> + <dimen name="dialog_height">200dip</dimen> + <dimen name="left_side_width">250dip</dimen> <dimen name="date_picker_dialog_height">282dip</dimen> </resources>
\ No newline at end of file diff --git a/res/values-sw600dp-land/dimens.xml b/res/values-sw600dp-land/dimens.xml index 58a6c67..390e753 100644 --- a/res/values-sw600dp-land/dimens.xml +++ b/res/values-sw600dp-land/dimens.xml @@ -17,7 +17,7 @@ */ --> -<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:android="http://schemas.android.com/apk/res/android"> <dimen name="time_label_right_padding">16sp</dimen> @@ -30,4 +30,5 @@ <dimen name="footer_height">48dip</dimen> <dimen name="date_picker_dialog_height">410dip</dimen> + <dimen name="left_side_width">315dip</dimen> </resources>
\ No newline at end of file diff --git a/res/values-sw600dp/dimens.xml b/res/values-sw600dp/dimens.xml index 83bea40..03ddbca 100644 --- a/res/values-sw600dp/dimens.xml +++ b/res/values-sw600dp/dimens.xml @@ -20,15 +20,6 @@ <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:android="http://schemas.android.com/apk/res/android"> - <dimen name="time_label_right_padding">34sp</dimen> - <dimen name="time_label_size">96sp</dimen> - <dimen name="ampm_label_size">32sp</dimen> - <dimen name="done_label_size">21sp</dimen> - <dimen name="ampm_left_padding">16dip</dimen> - <dimen name="separator_padding">8dip</dimen> - <dimen name="header_height">144dip</dimen> - <dimen name="footer_height">72dip</dimen> - <dimen name="pager_height">400dp</dimen> <dimen name="pager_width">400dp</dimen> <dimen name="date_picker_width">400dp</dimen> @@ -48,4 +39,14 @@ <dimen name="day_number_size">24sp</dimen> <dimen name="month_day_label_text_size">15sp</dimen> + <dimen name="time_label_size">75sp</dimen> + <dimen name="ampm_label_size">20sp</dimen> + <dimen name="ampm_left_padding">8dip</dimen> + <dimen name="separator_padding">5dip</dimen> + <dimen name="header_height">120dip</dimen> + <dimen name="footer_height">60dip</dimen> + <dimen name="minimum_margin_sides">48dip</dimen> + <dimen name="minimum_margin_top_bottom">24dip</dimen> + + <dimen name="picker_dimen">375dip</dimen> </resources>
\ No newline at end of file diff --git a/res/values/colors.xml b/res/values/colors.xml index 3568ed4..506d640 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -17,11 +17,13 @@ <resources> <color name="white">#ffffff</color> - <color name="black_03">#08000000</color> - <color name="black_20">#33000000</color> - <color name="black_50">#7F000000</color> - <color name="black_60">#99000000</color> - <color name="black_80">#CC000000</color> + <color name="circle_background">#08000000</color> + <color name="line_background">#33000000</color> + <color name="ampm_text_color">#7F000000</color> + <color name="done_text_color">#99000000</color> + <color name="numbers_text_color">#CC000000</color> + + <color name="transparent_black">#7F000000</color> <color name="blue">#33b5e5</color> <color name="calendar_header">#999999</color> diff --git a/res/values/dimens.xml b/res/values/dimens.xml index affa7ba..a874c7d 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -19,7 +19,7 @@ <item name="circle_radius_multiplier" format="float" type="string">0.82</item> <item name="circle_radius_multiplier_24HourMode" format="float" type="string">0.85</item> - <item name="selection_radius_multiplier" format="float" type="string">0.14</item> + <item name="selection_radius_multiplier" format="float" type="string">0.16</item> <item name="ampm_circle_radius_multiplier" format="float" type="string">0.19</item> <item name="numbers_radius_multiplier_normal" format="float" type="string">0.81</item> <item name="numbers_radius_multiplier_inner" format="float" type="string">0.60</item> @@ -28,7 +28,7 @@ <item name="text_size_multiplier_inner" format="float" type="string">0.14</item> <item name="text_size_multiplier_outer" format="float" type="string">0.11</item> - <dimen name="time_label_right_padding">12sp</dimen> + <dimen name="time_label_size">60sp</dimen> <dimen name="ampm_label_size">16sp</dimen> <dimen name="done_label_size">16sp</dimen> @@ -36,6 +36,10 @@ <dimen name="separator_padding">4dip</dimen> <dimen name="header_height">96dip</dimen> <dimen name="footer_height">48dip</dimen> + <dimen name="minimum_margin_sides">48dip</dimen> + <dimen name="minimum_margin_top_bottom">24dip</dimen> + <dimen name="picker_dimen">270dip</dimen> + <dimen name="pager_height">270dp</dimen> <dimen name="pager_width">270dp</dimen> <dimen name="date_picker_width">270dp</dimen> @@ -54,5 +58,4 @@ <dimen name="month_label_size">16sp</dimen> <dimen name="day_number_size">16sp</dimen> <dimen name="month_list_item_size">16sp</dimen> - </resources>
\ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 57eaddd..3129b0d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -21,13 +21,13 @@ <!-- Content description for the hour selector in the time picker, which displays selectable hours of the day along the inside edge of a circle, as in an analog clock. - [CHAR LIMIT=50] + [CHAR LIMIT=50] --> <string name="hour_picker_description">Hours circular slider</string> <!-- Content description for the minute selector in the time picker, which displays selectable five-minute intervals along the inside edge of a circle, as in an analog clock. - [CHAR LIMIT=50] + [CHAR LIMIT=50] --> <string name="minute_picker_description">Minutes circular slider</string> <!-- Accessibility announcement for hour circular picker [CHAR LIMIT=NONE] --> diff --git a/res/values/styles.xml b/res/values/styles.xml index c8fcfa6..32ac1b9 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -39,12 +39,12 @@ <style name="time_label" parent="time_label_thin"> <item name="android:textSize">@dimen/time_label_size</item> - <item name="android:textColor">@color/black_80</item> + <item name="android:textColor">@color/numbers_text_color</item> </style> <style name="ampm_label"> <item name="android:textSize">@dimen/ampm_label_size</item> <item name="android:textAllCaps">true</item> - <item name="android:textColor">@color/black_50</item> + <item name="android:textColor">@color/ampm_text_color</item> </style> </resources>
\ No newline at end of file diff --git a/src/com/android/datetimepicker/time/AmPmCirclesView.java b/src/com/android/datetimepicker/time/AmPmCirclesView.java index 1ead926..6890bb6 100644 --- a/src/com/android/datetimepicker/time/AmPmCirclesView.java +++ b/src/com/android/datetimepicker/time/AmPmCirclesView.java @@ -29,12 +29,15 @@ import com.android.datetimepicker.R; import java.text.DateFormatSymbols; +/** + * Draw the two smaller AM and PM circles next to where the larger circle will be. + */ public class AmPmCirclesView extends View { private static final String TAG = "AmPmCirclesView"; private final Paint mPaint = new Paint(); private int mWhite; - private int mBlack50; + private int mAmPmTextColor; private int mBlue; private float mCircleRadiusMultiplier; private float mAmPmCircleRadiusMultiplier; @@ -66,7 +69,7 @@ public class AmPmCirclesView extends View { Resources res = context.getResources(); mWhite = res.getColor(R.color.white); - mBlack50 = res.getColor(R.color.black_50); + mAmPmTextColor = res.getColor(R.color.ampm_text_color); mBlue = res.getColor(R.color.blue); String typefaceFamily = res.getString(R.string.sans_serif); Typeface tf = Typeface.create(typefaceFamily, Typeface.NORMAL); @@ -96,6 +99,9 @@ public class AmPmCirclesView extends View { mAmOrPmPressed = amOrPmPressed; } + /** + * Calculate whether the coordinates are touching the AM or PM circle. + */ public int getIsTouchingAmOrPm(float xCoord, float yCoord) { if (!mDrawValuesReady) { return -1; @@ -145,16 +151,18 @@ public class AmPmCirclesView extends View { mDrawValuesReady = true; } + // We'll need to draw either a ligther blue (for selection), a darker blue (for touching) + // or white (for not selected). int amColor = mWhite; int amAlpha = 255; int pmColor = mWhite; int pmAlpha = 255; if (mAmOrPm == AM) { amColor = mBlue; - amAlpha = 38; + amAlpha = 60; } else if (mAmOrPm == PM) { pmColor = mBlue; - pmAlpha = 38; + pmAlpha = 60; } if (mAmOrPmPressed == AM) { amColor = mBlue; @@ -164,6 +172,7 @@ public class AmPmCirclesView extends View { pmAlpha = 175; } + // Draw the two circles. mPaint.setColor(amColor); mPaint.setAlpha(amAlpha); canvas.drawCircle(mAmXCenter, mAmPmYCenter, mAmPmCircleRadius, mPaint); @@ -171,7 +180,8 @@ public class AmPmCirclesView extends View { mPaint.setAlpha(pmAlpha); canvas.drawCircle(mPmXCenter, mAmPmYCenter, mAmPmCircleRadius, mPaint); - mPaint.setColor(mBlack50); + // Draw the AM/PM texts on top. + mPaint.setColor(mAmPmTextColor); int textYCenter = mAmPmYCenter - (int) (mPaint.descent() + mPaint.ascent()) / 2; canvas.drawText(mAmText, mAmXCenter, textYCenter, mPaint); canvas.drawText(mPmText, mPmXCenter, textYCenter, mPaint); diff --git a/src/com/android/datetimepicker/time/CircleView.java b/src/com/android/datetimepicker/time/CircleView.java index b588db5..89b577a 100644 --- a/src/com/android/datetimepicker/time/CircleView.java +++ b/src/com/android/datetimepicker/time/CircleView.java @@ -25,13 +25,16 @@ import android.view.View; import com.android.datetimepicker.R; +/** + * Draws a simple white circle on which the numbers will be drawn. + */ public class CircleView extends View { private static final String TAG = "CircleView"; private final Paint mPaint = new Paint(); private boolean mIs24HourMode; private int mWhite; - private int mBlack80; + private int mBlack; private float mCircleRadiusMultiplier; private float mAmPmCircleRadiusMultiplier; private boolean mIsInitialized; @@ -46,7 +49,7 @@ public class CircleView extends View { Resources res = context.getResources(); mWhite = res.getColor(R.color.white); - mBlack80 = res.getColor(R.color.black_80); + mBlack = res.getColor(R.color.numbers_text_color); mPaint.setAntiAlias(true); mIsInitialized = false; @@ -97,10 +100,12 @@ public class CircleView extends View { mDrawValuesReady = true; } + // Draw the white circle. mPaint.setColor(mWhite); canvas.drawCircle(mXCenter, mYCenter, mCircleRadius, mPaint); - mPaint.setColor(mBlack80); + // Draw a small black circle in the center. + mPaint.setColor(mBlack); canvas.drawCircle(mXCenter, mYCenter, 2, mPaint); } } diff --git a/src/com/android/datetimepicker/time/RadialPickerLayout.java b/src/com/android/datetimepicker/time/RadialPickerLayout.java index 7faa377..e895e98 100644 --- a/src/com/android/datetimepicker/time/RadialPickerLayout.java +++ b/src/com/android/datetimepicker/time/RadialPickerLayout.java @@ -42,12 +42,16 @@ import android.widget.FrameLayout; import com.android.datetimepicker.R; +import java.util.HashMap; + public class RadialPickerLayout extends FrameLayout implements OnTouchListener { - private static final String TAG = "TimePicker"; + private static final String TAG = "RadialPickerLayout"; private final int TOUCH_SLOP; private final int TAP_TIMEOUT; - private static final int HOUR_VALUE_TO_DEGREES_STEP_SIZE = 30; + + private static final int VISIBLE_DEGREES_STEP_SIZE = 30; + private static final int HOUR_VALUE_TO_DEGREES_STEP_SIZE = VISIBLE_DEGREES_STEP_SIZE; 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; @@ -76,6 +80,7 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { private RadialSelectorView mMinuteRadialSelectorView; private View mGrayBox; + private int[] mSnapPrefer30sMap; private boolean mInputEnabled; private int mIsTouchingAmOrPm = -1; private boolean mDoingMove; @@ -85,6 +90,7 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { private float mDownY; private AccessibilityManager mAccessibilityManager; + private AnimatorSet mTransition; private Handler mHandler = new Handler(); public interface OnValueSelectedListener { @@ -116,35 +122,52 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { mMinuteRadialSelectorView = new RadialSelectorView(context); addView(mMinuteRadialSelectorView); + // Prepare mapping to snap touchable degrees to selectable degrees. + preparePrefer30sMap(); + 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.setBackgroundColor(getResources().getColor(R.color.transparent_black)); mGrayBox.setVisibility(View.INVISIBLE); addView(mGrayBox); mAccessibilityManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); + + mTimeInitialized = false; } + /** + * Measure the view to end up as a square, based on the minimum of the height and width. + */ @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); int measuredHeight = MeasureSpec.getSize(heightMeasureSpec); - super.onMeasure(widthMeasureSpec, - measuredWidth < measuredHeight? widthMeasureSpec : heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int minDimension = Math.min(measuredWidth, measuredHeight); + + super.onMeasure(MeasureSpec.makeMeasureSpec(minDimension, widthMode), + MeasureSpec.makeMeasureSpec(minDimension, heightMode)); } public void setOnValueSelectedListener(OnValueSelectedListener listener) { mListener = listener; } + /** + * Initialize the Layout with starting values. + * @param context + * @param initialHoursOfDay + * @param initialMinutes + * @param is24HourMode + */ public void initialize(Context context, int initialHoursOfDay, int initialMinutes, boolean is24HourMode) { if (mTimeInitialized) { @@ -154,6 +177,7 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { mIs24HourMode = is24HourMode; mHideAmPm = mAccessibilityManager.isTouchExplorationEnabled()? true : mIs24HourMode; + // Initialize the circle and AM/PM circles if applicable. mCircleView.initialize(context, mHideAmPm); mCircleView.invalidate(); if (!mHideAmPm) { @@ -161,6 +185,7 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { mAmPmCirclesView.invalidate(); } + // Initialize the hours and minutes numbers. Resources res = context.getResources(); 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}; @@ -180,6 +205,7 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { mMinuteRadialTextsView.initialize(res, minutesTexts, null, mHideAmPm, false); mMinuteRadialTextsView.invalidate(); + // Initialize the currently-selected hour and minute. setValueForItem(HOUR_INDEX, initialHoursOfDay); setValueForItem(MINUTE_INDEX, initialMinutes); int hourDegrees = (initialHoursOfDay % 12) * HOUR_VALUE_TO_DEGREES_STEP_SIZE; @@ -197,21 +223,27 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { setItem(MINUTE_INDEX, minutes); } + /** + * Set either the hour or the minute. Will set the internal value, and set the selection. + */ 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.setSelection(hourDegrees, isHourInnerCircle(value), 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.setSelection(minuteDegrees, false, false); mMinuteRadialSelectorView.invalidate(); } } + /** + * Check if a given hour appears in the outer circle or the inner circle + * @return true if the hour is in the inner circle, false if it's in the outer circle. + */ private boolean isHourInnerCircle(int hourOfDay) { // We'll have the 00 hours on the outside circle. return mIs24HourMode && (hourOfDay <= 12 && hourOfDay != 0); @@ -225,6 +257,10 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { return mCurrentMinutes; } + /** + * If the hours are showing, return the current hour. If the minutes are showing, return the + * current minute. + */ private int getCurrentlyShowingValue() { int currentIndex = getCurrentItemShowing(); if (currentIndex == HOUR_INDEX) { @@ -245,6 +281,9 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { return -1; } + /** + * Set the internal value for the hour, minute, or AM/PM. + */ private void setValueForItem(int index, int value) { if (index == HOUR_INDEX) { mCurrentHoursOfDay = value; @@ -259,31 +298,102 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { } } + /** + * Set the internal value as either AM or PM, and update the AM/PM circle displays. + * @param amOrPm + */ public void setAmOrPm(int amOrPm) { mAmPmCirclesView.setAmOrPm(amOrPm); mAmPmCirclesView.invalidate(); setValueForItem(AMPM_INDEX, amOrPm); } - 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; + /** + * Split up the 360 degrees of the circle among the 60 selectable values. Assigns a larger + * selectable area to each of the 12 visible values, such that the ratio of space apportioned + * to a visible value : space apportioned to a non-visible value will be 14 : 4. + * E.g. the output of 30 degrees should have a higher range of input associated with it than + * the output of 24 degrees, because 30 degrees corresponds to a visible number on the clock + * circle (5 on the minutes, 1 or 13 on the hours). + */ + private void preparePrefer30sMap() { + // We'll split up the visible output and the non-visible output such that each visible + // output will correspond to a range of 14 associated input degrees, and each non-visible + // output will correspond to a range of 4 associate input degrees, so visible numbers + // are more than 3 times easier to get than non-visible numbers: + // {354-359,0-7}:0, {8-11}:6, {12-15}:12, {16-19}:18, {20-23}:24, {24-37}:30, etc. + // + // If an output of 30 degrees should correspond to a range of 14 associated degrees, then + // we'll need any input between 24 - 37 to snap to 30. Working out from there, 20-23 should + // snap to 24, while 38-41 should snap to 36. This is somewhat counter-intuitive, that you + // can be touching 36 degrees but have the selection snapped to 30 degrees; however, this + // inconsistency isn't noticeable at such fine-grained degrees, and it affords us the + // ability to aggressively prefer the visible values by a factor of more than 3:1, which + // greatly contributes to the selectability of these values. + + // Our input will be 0 through 360. + mSnapPrefer30sMap = new int[361]; + + // The first output is 0, and each following output will increment by 6 {0, 6, 12, ...}. + int snappedOutputDegrees = 0; + // Count of how many inputs we've designated to the specified output. + int count = 1; + // How many input we expect for a specified output. This will be 14 for output divisible + // by 30, and 4 for the remaining output. We'll special case the outputs of 0 and 360, so + // the caller can decide which they need. + int expectedCount = 8; + // Iterate through the input. + for (int degrees = 0; degrees < 361; degrees++) { + // Save the input-output mapping. + mSnapPrefer30sMap[degrees] = snappedOutputDegrees; + // If this is the last input for the specified output, calculate the next output and + // the next expected count. + if (count == expectedCount) { + snappedOutputDegrees += 6; + if (snappedOutputDegrees == 360) { + expectedCount = 7; + } else if (snappedOutputDegrees % 30 == 0) { + expectedCount = 14; + } else { + expectedCount = 4; + } + count = 1; + } else { + count++; + } + } } - private int snapToStepSize(int degrees, int stepSize, int ceilingOrFloor) { + /** + * Returns mapping of any input degrees (0 to 360) to one of 60 selectable output degrees, + * where the degrees corresponding to visible numbers (i.e. those divisible by 30) will be + * weighted heavier than the degrees corresponding to non-visible numbers. + * See {@link #preparePrefer30sMap()} documentation for the rationale and generation of the + * mapping. + */ + private int snapPrefer30s(int degrees) { + if (mSnapPrefer30sMap == null) { + return -1; + } + return mSnapPrefer30sMap[degrees]; + } + + /** + * Returns mapping of any input degrees (0 to 360) to one of 12 visible output degrees (all + * multiples of 30), where the input will be "snapped" to the closest visible degrees. + * @param degrees The input degrees + * @param forceAboveOrBelow The output may be forced to either the higher or lower step, or may + * be allowed to snap to whichever is closer. Use 1 to force strictly higher, -1 to force + * strictly lower, and 0 to snap to the closer one. + * @return output degrees, will be a multiple of 30 + */ + private int snapOnly30s(int degrees, int forceHigherOrLower) { + int stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE; int floor = (degrees / stepSize) * stepSize; int ceiling = floor + stepSize; - if (ceilingOrFloor == 1) { + if (forceHigherOrLower == 1) { degrees = ceiling; - } else if (ceilingOrFloor == -1) { + } else if (forceHigherOrLower == -1) { if (degrees == floor) { floor -= stepSize; } @@ -298,19 +408,32 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { return degrees; } + /** + * For the currently showing view (either hours or minutes), re-calculate the position for the + * selector, and redraw it at that position. The input degrees will be snapped to a selectable + * value. + * @param degrees Degrees which should be selected. + * @param isInnerCircle Whether the selection should be in the inner circle; will be ignored + * if there is no inner circle. + * @param forceToVisibleValue Even if the currently-showing circle allows for fine-grained + * selection (i.e. minutes), force the selection to one of the visibly-showing values. + * @param forceDrawDot The dot in the circle will generally only be shown when the selection + * is on non-visible values, but use this to force the dot to be shown. + * @return The value that was selected, i.e. 0-23 for hours, 0-59 for minutes. + */ private int reselectSelector(int degrees, boolean isInnerCircle, - boolean forceNotFineGrained, boolean forceDrawLine, boolean forceDrawDot) { + boolean forceToVisibleValue, boolean forceDrawDot) { if (degrees == -1) { return -1; } int currentShowing = getCurrentItemShowing(); int stepSize; - boolean allowFineGrained = !forceNotFineGrained && (currentShowing == MINUTE_INDEX); + boolean allowFineGrained = !forceToVisibleValue && (currentShowing == MINUTE_INDEX); if (allowFineGrained) { - degrees = highPass30sFilter(degrees); + degrees = snapPrefer30s(degrees); } else { - degrees = snapToStepSize(degrees, HOUR_VALUE_TO_DEGREES_STEP_SIZE, 0); + degrees = snapOnly30s(degrees, 0); } RadialSelectorView radialSelectorView; @@ -321,7 +444,7 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { radialSelectorView = mMinuteRadialSelectorView; stepSize = MINUTE_VALUE_TO_DEGREES_STEP_SIZE; } - radialSelectorView.setSelection(degrees, isInnerCircle, forceDrawLine, forceDrawDot, false); + radialSelectorView.setSelection(degrees, isInnerCircle, forceDrawDot); radialSelectorView.invalidate(); @@ -346,6 +469,18 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { return value; } + /** + * Calculate the degrees within the circle that corresponds to the specified coordinates, if + * the coordinates are within the range that will trigger a selection. + * @param pointX The x coordinate. + * @param pointY The y coordinate. + * @param forceLegal Force the selection to be legal, regardless of how far the coordinates are + * from the actual numbers. + * @param isInnerCircle If the selection may be in the inner circle, pass in a size-1 boolean + * array here, inside which the value will be true if the selection is in the inner circle, + * and false if in the outer circle. + * @return Degrees from 0 to 360, if the selection was within the legal range. -1 if not. + */ private int getDegreesFromCoords(float pointX, float pointY, boolean forceLegal, final Boolean[] isInnerCircle) { int currentItem = getCurrentItemShowing(); @@ -360,6 +495,9 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { } } + /** + * Get the item (hours or minutes) that is currently showing. + */ public int getCurrentItemShowing() { if (mCurrentItemShowing != HOUR_INDEX && mCurrentItemShowing != MINUTE_INDEX) { Log.e(TAG, "Current item showing was unfortunately set to "+mCurrentItemShowing); @@ -368,6 +506,10 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { return mCurrentItemShowing; } + /** + * Set either minutes or hours as showing. + * @param animate True to animate the transition, false to show with no animation. + */ public void setCurrentItemShowing(int index, boolean animate) { if (index != HOUR_INDEX && index != MINUTE_INDEX) { Log.e(TAG, "TimePicker does not support view at index "+index); @@ -391,9 +533,12 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { anims[3] = mMinuteRadialSelectorView.getDisappearAnimator(); } - AnimatorSet transition = new AnimatorSet(); - transition.playTogether(anims); - transition.start(); + if (mTransition != null && mTransition.isRunning()) { + mTransition.end(); + } + mTransition = new AnimatorSet(); + mTransition.playTogether(anims); + mTransition.start(); } else { int hourAlpha = (index == HOUR_INDEX) ? 255 : 0; int minuteAlpha = (index == MINUTE_INDEX) ? 255 : 0; @@ -428,12 +573,15 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { mLastValueSelected = -1; mDoingMove = false; mDoingTouch = true; + // If we're showing the AM/PM, check to see if the user is touching it. if (!mHideAmPm) { mIsTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY); } else { mIsTouchingAmOrPm = -1; } if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) { + // If the touch is on AM or PM, set it as "touched" after the TAP_TIMEOUT + // in case the user moves their finger quickly. tryVibrate(); mDownDegrees = -1; mHandler.postDelayed(new Runnable() { @@ -444,16 +592,21 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { } }, TAP_TIMEOUT); } else { + // If we're in accessibility mode, force the touch to be legal. Otherwise, + // it will only register within the given touch target zone. boolean forceLegal = mAccessibilityManager.isTouchExplorationEnabled(); + // Calculate the degrees that is currently being touched. mDownDegrees = getDegreesFromCoords(eventX, eventY, forceLegal, isInnerCircle); if (mDownDegrees != -1) { + // If it's a legal touch, set that number as "selected" after the + // TAP_TIMEOUT in case the user moves their finger quickly. tryVibrate(); mHandler.postDelayed(new Runnable() { @Override public void run() { mDoingMove = true; - int value = reselectSelector(mDownDegrees, - isInnerCircle[0], false, true, true); + int value = reselectSelector(mDownDegrees, isInnerCircle[0], + false, true); mLastValueSelected = value; mListener.onValueSelected(getCurrentItemShowing(), value, false); } @@ -495,12 +648,12 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { break; } + // We're doing a move along the circle, so move the selection as appropriate. mDoingMove = true; mHandler.removeCallbacksAndMessages(null); degrees = getDegreesFromCoords(eventX, eventY, true, isInnerCircle); if (degrees != -1) { - value = reselectSelector(degrees, - isInnerCircle[0], false, true, true); + value = reselectSelector(degrees, isInnerCircle[0], false, true); if (value != mLastValueSelected) { tryVibrate(); mLastValueSelected = value; @@ -510,6 +663,7 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { return true; case MotionEvent.ACTION_UP: if (!mInputEnabled) { + // If our touch input was disabled, tell the listener to re-enable us. Log.d(TAG, "Input was disabled, but received ACTION_UP."); mListener.onValueSelected(ENABLE_PICKER_INDEX, 1, false); return true; @@ -518,6 +672,7 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { mHandler.removeCallbacksAndMessages(null); mDoingTouch = false; + // If we're touching AM or PM, set it as selected, and tell the listener. if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) { int isTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY); mAmPmCirclesView.setAmOrPmPressed(-1); @@ -534,11 +689,11 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { break; } + // If we have a legal degrees selected, set the value and tell the listener. if (mDownDegrees != -1) { degrees = getDegreesFromCoords(eventX, eventY, mDoingMove, isInnerCircle); if (degrees != -1) { - value = reselectSelector(degrees, isInnerCircle[0], - !mDoingMove, true, false); + value = reselectSelector(degrees, isInnerCircle[0], !mDoingMove, false); if (getCurrentItemShowing() == HOUR_INDEX && !mIs24HourMode) { int amOrPm = getIsCurrentlyAmOrPm(); if (amOrPm == AM && value == 12) { @@ -559,6 +714,10 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { return false; } + /** + * Try to vibrate. To prevent this becoming a single continuous vibration, nothing will + * happen if we have vibrated very recently. + */ public void tryVibrate() { if (mVibrator != null) { long now = SystemClock.uptimeMillis(); @@ -570,6 +729,9 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { } } + /** + * Set touch input as enabled or disabled, for use with keyboard mode. + */ 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, @@ -581,6 +743,10 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { return true; } + /** + * Necessary for accessibility, to ensure we support "scrolling" forward and backward + * in the circle. + */ @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); @@ -588,9 +754,13 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); } + /** + * Announce the currently-selected time when launched. + */ @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { + // Clear the event's current text so that only the current time will be spoken. event.getText().clear(); Time time = new Time(); time.hour = getHours(); @@ -607,6 +777,10 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { return super.dispatchPopulateAccessibilityEvent(event); } + /** + * When scroll forward/backward events are received, jump the time to the higher/lower + * discrete, visible value on the circle. + */ @SuppressLint("NewApi") @Override public boolean performAccessibilityAction(int action, Bundle arguments) { @@ -632,7 +806,7 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { } int degrees = value * stepSize; - degrees = snapToStepSize(degrees, HOUR_VALUE_TO_DEGREES_STEP_SIZE, changeMultiplier); + degrees = snapOnly30s(degrees, changeMultiplier); value = degrees / stepSize; int maxValue = 0; int minValue = 0; diff --git a/src/com/android/datetimepicker/time/RadialSelectorView.java b/src/com/android/datetimepicker/time/RadialSelectorView.java index 92324a2..8fee657 100644 --- a/src/com/android/datetimepicker/time/RadialSelectorView.java +++ b/src/com/android/datetimepicker/time/RadialSelectorView.java @@ -30,6 +30,10 @@ import android.view.View; import com.android.datetimepicker.R; +/** + * View to show what number is selected. This will draw a blue circle over the number, with a blue + * line coming from the center of the main circle to the edge of the blue selection. + */ public class RadialSelectorView extends View { private static final String TAG = "RadialSelectorView"; @@ -59,8 +63,6 @@ public class RadialSelectorView extends View { private int mSelectionDegrees; private double mSelectionRadians; - private boolean mHideSelector; - private boolean mDrawLine; private boolean mForceDrawDot; public RadialSelectorView(Context context) { @@ -68,6 +70,19 @@ public class RadialSelectorView extends View { mIsInitialized = false; } + /** + * Initialize this selector with the state of the picker. + * @param context Current context. + * @param is24HourMode Whether the selector is in 24-hour mode, which will tell us + * whether the circle's center is moved up slightly to make room for the AM/PM circles. + * @param hasInnerCircle Whether we have both an inner and an outer circle of numbers + * that may be selected. Should be true for 24-hour mode in the hours circle. + * @param disappearsOut Whether the numbers' animation will have them disappearing out + * or disappearing in. + * @param selectionDegrees The initial degrees to be selected. + * @param isInnerCircle Whether the initial selection is in the inner or outer circle. + * Will be ignored when hasInnerCircle is false. + */ public void initialize(Context context, boolean is24HourMode, boolean hasInnerCircle, boolean disappearsOut, int selectionDegrees, boolean isInnerCircle) { if (mIsInitialized) { @@ -81,6 +96,7 @@ public class RadialSelectorView extends View { mPaint.setColor(blue); mPaint.setAntiAlias(true); + // Calculate values for the circle radius size. mIs24HourMode = is24HourMode; if (is24HourMode) { mCircleRadiusMultiplier = Float.parseFloat( @@ -92,6 +108,7 @@ public class RadialSelectorView extends View { Float.parseFloat(res.getString(R.string.ampm_circle_radius_multiplier)); } + // Calculate values for the radius size(s) of the numbers circle(s). mHasInnerCircle = hasInnerCircle; if (hasInnerCircle) { mInnerNumbersRadiusMultiplier = @@ -105,20 +122,28 @@ public class RadialSelectorView extends View { mSelectionRadiusMultiplier = Float.parseFloat(res.getString(R.string.selection_radius_multiplier)); + // Calculate values for the transition mid-way states. 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); + setSelection(selectionDegrees, isInnerCircle, false); mIsInitialized = true; } - public void setSelection(int selectionDegrees, boolean isInnerCircle, - boolean drawLine, boolean forceDrawDot, boolean hideSelector) { + /** + * Set the selection. + * @param selectionDegrees The degrees to be selected. + * @param isInnerCircle Whether the selection should be in the inner circle or outer. Will be + * ignored if hasInnerCircle was initialized to false. + * @param forceDrawDot Whether to force the dot in the center of the selection circle to be + * drawn. If false, the dot will be drawn only when the degrees is not a multiple of 30, i.e. + * the selection is not on a visible number. + */ + public void setSelection(int selectionDegrees, boolean isInnerCircle, boolean forceDrawDot) { mSelectionDegrees = selectionDegrees; mSelectionRadians = selectionDegrees * Math.PI / 180; - mDrawLine = drawLine; mForceDrawDot = forceDrawDot; if (mHasInnerCircle) { @@ -128,18 +153,19 @@ public class RadialSelectorView extends View { mNumbersRadiusMultiplier = mOuterNumbersRadiusMultiplier; } } - mHideSelector = hideSelector; - } - - public void setDrawLine(boolean drawLine) { - mDrawLine = drawLine; } + /** + * Allows for smoother animations. + */ @Override public boolean hasOverlappingRendering() { return false; } + /** + * Set the multiplier for the radius. Will be used during animations to move in/out. + */ public void setAnimationRadiusMultiplier(float animationRadiusMultiplier) { mAnimationRadiusMultiplier = animationRadiusMultiplier; } @@ -153,7 +179,7 @@ public class RadialSelectorView extends View { double hypotenuse = Math.sqrt( (pointY - mYCenter)*(pointY - mYCenter) + (pointX - mXCenter)*(pointX - mXCenter)); - // Check if we're outside the range + // Check if we're outside the range if (mHasInnerCircle) { if (forceLegal) { // If we're told to force the coordinates to be legal, we'll set the isInnerCircle @@ -245,33 +271,32 @@ public class RadialSelectorView extends View { mDrawValuesReady = true; } + // Calculate the current radius at which to place the selection circle. mLineLength = (int) (mCircleRadius * mNumbersRadiusMultiplier * mAnimationRadiusMultiplier); - if (mHideSelector) { - return; - } - int pointX = mXCenter + (int) (mLineLength * Math.sin(mSelectionRadians)); int pointY = mYCenter - (int) (mLineLength * Math.cos(mSelectionRadians)); + // Draw the selection circle. mPaint.setAlpha(60); canvas.drawCircle(pointX, pointY, mSelectionRadius, mPaint); if (mForceDrawDot | mSelectionDegrees % 30 != 0) { - // We're not on a direct tick. + // We're not on a direct tick (or we've been told to draw the dot anyway). mPaint.setAlpha(255); canvas.drawCircle(pointX, pointY, (mSelectionRadius * 2 / 7), mPaint); } else { + // We're not drawing the dot, so shorten the line to only go as far as the edge of the + // selection circle. int lineLength = mLineLength; lineLength -= mSelectionRadius; pointX = mXCenter + (int) (lineLength * Math.sin(mSelectionRadians)); pointY = mYCenter - (int) (lineLength * Math.cos(mSelectionRadians)); } - if (mDrawLine || true) { - mPaint.setAlpha(255); - mPaint.setStrokeWidth(1); - canvas.drawLine(mXCenter, mYCenter, pointX, pointY, mPaint); - } + // Draw the line from the center of the circle. + mPaint.setAlpha(255); + mPaint.setStrokeWidth(1); + canvas.drawLine(mXCenter, mYCenter, pointX, pointY, mPaint); } public ObjectAnimator getDisappearAnimator() { @@ -339,6 +364,9 @@ public class RadialSelectorView extends View { return reappearAnimator; } + /** + * We'll need to invalidate during the animation. + */ private class InvalidateUpdateListener implements AnimatorUpdateListener { @Override public void onAnimationUpdate(ValueAnimator animation) { diff --git a/src/com/android/datetimepicker/time/RadialTextsView.java b/src/com/android/datetimepicker/time/RadialTextsView.java index 5e159c8..ea59ec6 100644 --- a/src/com/android/datetimepicker/time/RadialTextsView.java +++ b/src/com/android/datetimepicker/time/RadialTextsView.java @@ -32,6 +32,9 @@ import android.view.View; import com.android.datetimepicker.R; +/** + * A view to show a series of numbers in a circular pattern. + */ public class RadialTextsView extends View { private final static String TAG = "RadialTextsView"; @@ -83,8 +86,9 @@ public class RadialTextsView extends View { return; } - int black80 = res.getColor(R.color.black_80); - mPaint.setColor(black80); + // Set up the paint. + int numbersTextColor = res.getColor(R.color.numbers_text_color); + mPaint.setColor(numbersTextColor); String typefaceFamily = res.getString(R.string.radial_numbers_typeface); mTypefaceLight = Typeface.create(typefaceFamily, Typeface.NORMAL); String typefaceFamilyRegular = res.getString(R.string.sans_serif); @@ -97,6 +101,7 @@ public class RadialTextsView extends View { mIs24HourMode = is24HourMode; mHasInnerCircle = (innerTexts != null); + // Calculate the radius for the main circle. if (is24HourMode) { mCircleRadiusMultiplier = Float.parseFloat( res.getString(R.string.circle_radius_multiplier_24HourMode)); @@ -107,6 +112,7 @@ public class RadialTextsView extends View { Float.parseFloat(res.getString(R.string.ampm_circle_radius_multiplier)); } + // Initialize the widths and heights of the grid, and calculate the values for the numbers. mTextGridHeights = new float[7]; mTextGridWidths = new float[7]; if (mHasInnerCircle) { @@ -137,11 +143,17 @@ public class RadialTextsView extends View { mIsInitialized = true; } + /** + * Allows for smoother animation. + */ @Override public boolean hasOverlappingRendering() { return false; } + /** + * Used by the animation to move the numbers in and out. + */ public void setAnimationRadiusMultiplier(float animationRadiusMultiplier) { mAnimationRadiusMultiplier = animationRadiusMultiplier; mTextGridValuesDirty = true; @@ -171,20 +183,23 @@ public class RadialTextsView extends View { mInnerTextSize = mCircleRadius * mInnerTextSizeMultiplier; } - // Set up the spots for the animation. + // Because the text positions will be static, pre-render the animations. renderAnimations(); mTextGridValuesDirty = true; mDrawValuesReady = true; } + // Calculate the text positions, but only if they've changed since the last onDraw. if (mTextGridValuesDirty) { float numbersRadius = mCircleRadius * mNumbersRadiusMultiplier * mAnimationRadiusMultiplier; + // Calculate the positions for the 12 numbers in the main circle. calculateGridSizes(numbersRadius, mXCenter, mYCenter, mTextSize, mTextGridHeights, mTextGridWidths); if (mHasInnerCircle) { + // If we have an inner circle, calculate those positions too. float innerNumbersRadius = mCircleRadius * mInnerNumbersRadiusMultiplier * mAnimationRadiusMultiplier; calculateGridSizes(innerNumbersRadius, mXCenter, mYCenter, @@ -193,6 +208,7 @@ public class RadialTextsView extends View { mTextGridValuesDirty = false; } + // Draw the texts in the pre-calculated positions. drawTexts(canvas, mTextSize, mTypefaceLight, mTexts, mTextGridWidths, mTextGridHeights); if (mHasInnerCircle) { drawTexts(canvas, mInnerTextSize, mTypefaceRegular, mInnerTexts, @@ -200,21 +216,25 @@ public class RadialTextsView extends View { } } + /** + * Using the trigonometric Unit Circle, calculate the positions that the text will need to be + * drawn at based on the specified circle radius. Place the values in the textGridHeights and + * textGridWidths parameters. + */ private void calculateGridSizes(float numbersRadius, float xCenter, float yCenter, float textSize, float[] textGridHeights, float[] textGridWidths) { /* - * In the interest of efficient drawing, the following formulas have been simplified - * as much as possible. - * The numbers need to be drawn in a 7x7 grid representing the points on the Unit Circle. + * The numbers need to be drawn in a 7x7 grid, representing the points on the Unit Circle. */ float offset1 = numbersRadius; // cos(30) = a / r => r * cos(30) = a => r * √3/2 = a float offset2 = numbersRadius * ((float) Math.sqrt(3)) / 2f; // sin(30) = o / r => r * sin(30) = o => r / 2 = a float offset3 = numbersRadius / 2f; - // We'll need yTextBase to be slightly lower to account for the text's baseline. mPaint.setTextSize(textSize); + // We'll need yTextBase to be slightly lower to account for the text's baseline. yCenter -= (mPaint.descent() + mPaint.ascent()) / 2; + textGridHeights[0] = yCenter - offset1; textGridWidths[0] = xCenter - offset1; textGridHeights[1] = yCenter - offset2; @@ -231,6 +251,9 @@ public class RadialTextsView extends View { textGridWidths[6] = xCenter + offset1; } + /** + * Draw the 12 text values at the positions specified by the textGrid parameters. + */ private void drawTexts(Canvas canvas, float textSize, Typeface typeface, String[] texts, float[] textGridWidths, float[] textGridHeights) { mPaint.setTextSize(textSize); @@ -249,6 +272,9 @@ public class RadialTextsView extends View { canvas.drawText(texts[11], textGridWidths[2], textGridHeights[1], mPaint); } + /** + * Render the animations for appearing and disappearing. + */ private void renderAnimations() { Keyframe kf0, kf1, kf2, kf3; float midwayPoint = 0.2f; @@ -296,7 +322,7 @@ public class RadialTextsView extends View { } public ObjectAnimator getDisappearAnimator() { - if (!mIsInitialized || !mDrawValuesReady) { + if (!mIsInitialized || !mDrawValuesReady || mDisappearAnimator == null) { Log.e(TAG, "RadialTextView was not ready for animation."); return null; } @@ -305,7 +331,7 @@ public class RadialTextsView extends View { } public ObjectAnimator getReappearAnimator() { - if (!mIsInitialized || !mDrawValuesReady) { + if (!mIsInitialized || !mDrawValuesReady || mReappearAnimator == null) { Log.e(TAG, "RadialTextView was not ready for animation."); return null; } diff --git a/src/com/android/datetimepicker/time/TimePickerDialog.java b/src/com/android/datetimepicker/time/TimePickerDialog.java index c9e5a02..57ce93a 100644 --- a/src/com/android/datetimepicker/time/TimePickerDialog.java +++ b/src/com/android/datetimepicker/time/TimePickerDialog.java @@ -58,8 +58,10 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL 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. + // NOT a real index for the purpose of what's showing. + public static final int AMPM_INDEX = 2; + // Also NOT a real index, just used for keyboard mode. + public static final int ENABLE_PICKER_INDEX = 3; public static final int AM = 0; public static final int PM = 1; @@ -141,6 +143,12 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL mCallback = callback; } + public void setStartTime(int hourOfDay, int minute) { + mInitialHourOfDay = hourOfDay; + mInitialMinute = minute; + mInKbMode = false; + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -169,7 +177,7 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL 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); + mBlack = res.getColor(R.color.numbers_text_color); mHourView = (TextView) view.findViewById(R.id.hours); mHourView.setOnKeyListener(keyboardListener); @@ -190,20 +198,20 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL savedInstanceState.containsKey(KEY_CURRENT_ITEM_SHOWING)) { currentItemShowing = savedInstanceState.getInt(KEY_CURRENT_ITEM_SHOWING); } - setCurrentItemShowing(currentItemShowing, false); + setCurrentItemShowing(currentItemShowing, false, true); mTimePicker.invalidate(); mHourView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - setCurrentItemShowing(HOUR_INDEX, true); + setCurrentItemShowing(HOUR_INDEX, true, true); mTimePicker.tryVibrate(); } }); mMinuteView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - setCurrentItemShowing(MINUTE_INDEX, true); + setCurrentItemShowing(MINUTE_INDEX, true, true); mTimePicker.tryVibrate(); } }); @@ -226,6 +234,7 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL }); mDoneButton.setOnKeyListener(keyboardListener); + // Enable or disable the AM/PM view. mAmPmHitspace = view.findViewById(R.id.ampm_hitspace); if (mIs24HourMode) { mAmPmTextView.setVisibility(View.GONE); @@ -255,9 +264,10 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL } mAllowAutoAdvance = true; - setHour(mInitialHourOfDay); + setHour(mInitialHourOfDay, true); setMinute(mInitialMinute); + // Set up for keyboard mode. mDoublePlaceholderText = res.getString(R.string.time_placeholder); mPlaceholderText = mDoublePlaceholderText.charAt(0); mAmKeyCode = mPmKeyCode = -1; @@ -301,13 +311,19 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL } } + /** + * Called by the picker for updating the header display. + */ @Override public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance) { if (pickerIndex == HOUR_INDEX) { - setHour(newValue); + setHour(newValue, false); + String announcement = String.format("%d", newValue); if (mAllowAutoAdvance && autoAdvance) { - setCurrentItemShowing(MINUTE_INDEX, true); + setCurrentItemShowing(MINUTE_INDEX, true, false); + announcement += ". " + mSelectMinutes; } + tryAccessibilityAnnounce(announcement); } else if (pickerIndex == MINUTE_INDEX){ setMinute(newValue); } else if (pickerIndex == AMPM_INDEX) { @@ -320,7 +336,7 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL } } - private void setHour(int value) { + private void setHour(int value, boolean announce) { String format; if (mIs24HourMode) { format = "%02d"; @@ -333,8 +349,10 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL } CharSequence text = String.format(format, value); - tryAccessibilityAnnounce(text); mHourView.setText(text); + if (announce) { + tryAccessibilityAnnounce(text); + } } private void setMinute(int value) { @@ -346,7 +364,8 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL mMinuteView.setText(text); } - private void setCurrentItemShowing(int index, boolean animate) { + // Show either Hours or Minutes. + private void setCurrentItemShowing(int index, boolean animate, boolean announce) { mTimePicker.setCurrentItemShowing(index, animate); if (index == HOUR_INDEX) { @@ -355,11 +374,15 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL hours = hours % 12; } mTimePicker.setContentDescription(mHourPickerDescription+": "+hours); - tryAccessibilityAnnounce(mSelectHours); + if (announce) { + tryAccessibilityAnnounce(mSelectHours); + } } else { int minutes = mTimePicker.getMinutes(); mTimePicker.setContentDescription(mMinutePickerDescription+": "+minutes); - tryAccessibilityAnnounce(mSelectMinutes); + if (announce) { + tryAccessibilityAnnounce(mSelectMinutes); + } } int hourColor = (index == HOUR_INDEX)? mBlue : mBlack; @@ -368,6 +391,10 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL mMinuteView.setTextColor(minuteColor); } + /** + * Try to speak the specified text, for accessibility. Only available on JB or later. + * @param text + */ @SuppressLint("NewApi") private void tryAccessibilityAnnounce(CharSequence text) { if (Utils.isJellybeanOrLater() && mTimePicker != null && text != null) { @@ -375,6 +402,11 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL } } + /** + * For keyboard mode, processes key events. + * @param keyCode the pressed key. + * @return true if the key was successfully processed, false otherwise. + */ private boolean processKeyUp(int keyCode) { if (keyCode == KeyEvent.KEYCODE_ESCAPE || keyCode == KeyEvent.KEYCODE_BACK) { dismiss(); @@ -432,8 +464,16 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL return false; } + /** + * Try to start keyboard mode with the specified key, as long as the timepicker is not in the + * middle of a touch-event. + * @param keyCode The key to use as the first press. Keyboard mode will not be started if the + * key is not legal to start with. Or, pass in -1 to get into keyboard mode without a starting + * key. + */ private void tryStartingKbMode(int keyCode) { - if (mTimePicker.trySettingInputEnabled(false) && (keyCode == -1 || addKeyIfLegal(keyCode))) { + if (mTimePicker.trySettingInputEnabled(false) && + (keyCode == -1 || addKeyIfLegal(keyCode))) { mInKbMode = true; mDoneButton.setEnabled(false); updateDisplay(false); @@ -466,6 +506,10 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL return true; } + /** + * Traverse the tree to see if the keys that have been typed so far are legal as is, + * or may become legal as more keys are typed (excluding backspace). + */ private boolean isTypedTimeLegalSoFar() { Node node = mLegalTimesTree; for (int keyCode : mTypedTimes) { @@ -477,14 +521,18 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL return true; } + /** + * Check if the time that has been typed so far is completely legal, as is. + */ 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) { + // For 24-hour mode, the time is legal if the hours and minutes are each legal. Note: // 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 { + // For AM/PM mode, 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. return (mTypedTimes.contains(getAmOrPmKeyCode(AM)) || mTypedTimes.contains(getAmOrPmKeyCode(PM))); } @@ -497,7 +545,11 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL } } - private void finishKbMode(boolean changeDisplays) { + /** + * Get out of keyboard mode. If there is nothing in typedTimes, revert to TimePicker's time. + * @param changeDisplays If true, update the displays with the relevant time. + */ + private void finishKbMode(boolean updateDisplays) { mInKbMode = false; if (!mTypedTimes.isEmpty()) { int values[] = getEnteredTime(null); @@ -507,22 +559,29 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL } mTypedTimes.clear(); } - if (changeDisplays) { + if (updateDisplays) { updateDisplay(false); mTimePicker.trySettingInputEnabled(true); } } - private void updateDisplay(boolean allowEmpty) { - if (!allowEmpty && mTypedTimes.isEmpty()) { + /** + * Update the hours, minutes, and AM/PM displays with the typed times. If the typedTimes is + * empty, either show an empty display (filled with the placeholder text), or update from the + * timepicker's values. + * @param allowEmptyDisplay if true, then if the typedTimes is empty, use the placeholder text. + * Otherwise, revert to the timepicker's values. + */ + private void updateDisplay(boolean allowEmptyDisplay) { + if (!allowEmptyDisplay && mTypedTimes.isEmpty()) { int hour = mTimePicker.getHours(); int minute = mTimePicker.getMinutes(); - setHour(hour); + setHour(hour, true); setMinute(minute); if (!mIs24HourMode) { updateAmPmDisplay(hour < 12? AM : PM); } - setCurrentItemShowing(mTimePicker.getCurrentItemShowing(), true); + setCurrentItemShowing(mTimePicker.getCurrentItemShowing(), true, true); mDoneButton.setEnabled(true); } else { Boolean[] enteredZeros = {false, false}; @@ -570,6 +629,14 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL } } + /** + * Get the currently-entered time, as integer values of the hours and minutes typed. + * @param enteredZeros A size-2 boolean array, which the caller should initialize, and which + * may then be used for the caller to know whether zeros had been explicitly entered as either + * hours of minutes. This is helpful for deciding whether to show the dashes, or actual 0's. + * @return A size-3 int array. The first value will be the hours, the second value will be the + * minutes, and the third will be either TimePickerDialog.AM or TimePickerDialog.PM. + */ private int[] getEnteredTime(Boolean[] enteredZeros) { int amOrPm = -1; int startIndex = 1; @@ -607,6 +674,9 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL return ret; } + /** + * Get the keycode value for AM and PM in the current language. + */ private int getAmOrPmKeyCode(int amOrPm) { // Cache the codes. if (mAmKeyCode == -1 || mPmKeyCode == -1) { @@ -623,16 +693,7 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL 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; @@ -648,6 +709,9 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL return -1; } + /** + * Create a tree for deciding what keys can legally be typed. + */ private void generateLegalTimesTree() { // Create a quick cache of numbers to their keycodes. int k0 = KeyEvent.KEYCODE_0; @@ -776,6 +840,11 @@ public class TimePickerDialog extends DialogFragment implements OnValueSelectedL } } + /** + * Simple node class to be used for traversal to check for legal times. + * mLegalKeys represents the keys that can be typed to get to the node. + * mChildren are the children that can be reached from this node. + */ private class Node { private int[] mLegalKeys; private ArrayList<Node> mChildren; |