summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSam Blitzstein <sblitz@google.com>2013-04-01 12:52:38 -0700
committerSam Blitzstein <sblitz@google.com>2013-04-08 11:19:58 -0700
commitf3b38bd61d583d31200c501f5a74392aac510657 (patch)
treecf698fd25ee8dbe5cf4ead7b730ebcf107027a48
parent296f8f5d533485413e0245438a837edcceaaa6ce (diff)
downloadandroid_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.xml4
-rw-r--r--res/layout-land/time_picker_dialog.xml25
-rw-r--r--res/layout/date_picker_dialog.xml2
-rw-r--r--res/layout/time_picker_dialog.xml15
-rw-r--r--res/values-land/dimens.xml5
-rw-r--r--res/values-sw600dp-land/dimens.xml3
-rw-r--r--res/values-sw600dp/dimens.xml19
-rw-r--r--res/values/colors.xml12
-rw-r--r--res/values/dimens.xml9
-rw-r--r--res/values/strings.xml4
-rw-r--r--res/values/styles.xml4
-rw-r--r--src/com/android/datetimepicker/time/AmPmCirclesView.java20
-rw-r--r--src/com/android/datetimepicker/time/CircleView.java11
-rw-r--r--src/com/android/datetimepicker/time/RadialPickerLayout.java252
-rw-r--r--src/com/android/datetimepicker/time/RadialSelectorView.java72
-rw-r--r--src/com/android/datetimepicker/time/RadialTextsView.java44
-rw-r--r--src/com/android/datetimepicker/time/TimePickerDialog.java133
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;