summaryrefslogtreecommitdiff
path: root/src/com
diff options
context:
space:
mode:
authorSam Blitzstein <sblitz@google.com>2013-02-15 16:46:06 -0800
committerSam Blitzstein <sblitz@google.com>2013-03-20 10:21:01 -0700
commit6e896f805cac499b777c98755149f07ccd7ba5c3 (patch)
tree1a5467332ae3039da901983fdf46725211417229 /src/com
parentebd2a4069dda00781262a1cbfd4a9d22fce15ed7 (diff)
downloaddatetimepicker-6e896f805cac499b777c98755149f07ccd7ba5c3.tar.gz
Adding new timepicker library.
Timepicker is a radial, animated selector. Change-Id: Ib6a6deebf7673dcb14561261314a0e082d4a3ffc
Diffstat (limited to 'src/com')
-rw-r--r--src/com/android/datetimepicker/AmPmCirclesView.java178
-rw-r--r--src/com/android/datetimepicker/CircleView.java111
-rw-r--r--src/com/android/datetimepicker/RadialSelectorView.java345
-rw-r--r--src/com/android/datetimepicker/RadialTextsView.java315
-rw-r--r--src/com/android/datetimepicker/TimePicker.java527
-rw-r--r--src/com/android/datetimepicker/TimePickerDialog.java301
6 files changed, 1777 insertions, 0 deletions
diff --git a/src/com/android/datetimepicker/AmPmCirclesView.java b/src/com/android/datetimepicker/AmPmCirclesView.java
new file mode 100644
index 0000000..cb04cad
--- /dev/null
+++ b/src/com/android/datetimepicker/AmPmCirclesView.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.datetimepicker;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.Paint.Align;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+
+import com.android.datetimepicker.R;
+
+public class AmPmCirclesView extends View {
+ private static final String TAG = "AmPmCirclesView";
+
+ private final Paint mPaint = new Paint();
+ private int mWhite;
+ private int mDarkGray;
+ private int mBlue;
+ private float mCircleRadiusMultiplier;
+ private float mAmPmCircleRadiusMultiplier;
+ private String mAmText;
+ private String mPmText;
+ private boolean mIsInitialized;
+
+ private static final int AM = TimePickerDialog.AM;
+ private static final int PM = TimePickerDialog.PM;
+
+ private boolean mDrawValuesReady;
+ private int mAmPmCircleRadius;
+ private int mAmXCenter;
+ private int mPmXCenter;
+ private int mAmPmYCenter;
+ private int mAmOrPm;
+ private int mAmOrPmPressed;
+
+ public AmPmCirclesView(Context context) {
+ super(context);
+ mIsInitialized = false;
+ }
+
+ public void initialize(Context context, int amOrPm) {
+ if (mIsInitialized) {
+ Log.e(TAG, "AmPmCirclesView may only be initialized once.");
+ return;
+ }
+
+ Resources res = context.getResources();
+ mWhite = res.getColor(R.color.white);
+ mDarkGray = res.getColor(R.color.dark_gray);
+ mBlue = res.getColor(R.color.blue);
+ Typeface tf = Typeface.create("sans-serif-thin", Typeface.NORMAL);
+ mPaint.setTypeface(tf);
+ mPaint.setAntiAlias(true);
+ mPaint.setTextAlign(Align.CENTER);
+
+ mCircleRadiusMultiplier =
+ Float.parseFloat(res.getString(R.string.circle_radius_multiplier));
+ mAmPmCircleRadiusMultiplier =
+ Float.parseFloat(res.getString(R.string.ampm_circle_radius_multiplier));
+ mAmText = res.getString(R.string.am_label);
+ mPmText = res.getString(R.string.pm_label);
+
+ setAmOrPm(amOrPm);
+ mAmOrPmPressed = -1;
+
+ mIsInitialized = true;
+ }
+
+ public void setAmOrPm(int amOrPm) {
+ mAmOrPm = amOrPm;
+ }
+
+ public void setAmOrPmPressed(int amOrPmPressed) {
+ mAmOrPmPressed = amOrPmPressed;
+ }
+
+ public int getIsTouchingAmOrPm(float xCoord, float yCoord) {
+ if (!mDrawValuesReady) {
+ return -1;
+ }
+
+ int squaredYDistance = (int) ((yCoord - mAmPmYCenter)*(yCoord - mAmPmYCenter));
+
+ int distanceToAmCenter =
+ (int) Math.sqrt((xCoord - mAmXCenter)*(xCoord - mAmXCenter) + squaredYDistance);
+ if (distanceToAmCenter <= mAmPmCircleRadius) {
+ return AM;
+ }
+
+ int distanceToPmCenter =
+ (int) Math.sqrt((xCoord - mPmXCenter)*(xCoord - mPmXCenter) + squaredYDistance);
+ if (distanceToPmCenter <= mAmPmCircleRadius) {
+ return PM;
+ }
+
+ // Neither was close enough.
+ return -1;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ int viewWidth = getWidth();
+ if (viewWidth == 0 || !mIsInitialized) {
+ return;
+ }
+
+ if (!mDrawValuesReady) {
+ int layoutXCenter = getWidth() / 2;
+ int layoutYCenter = getHeight() / 2;
+ int circleRadius =
+ (int) (Math.min(layoutXCenter, layoutYCenter) * mCircleRadiusMultiplier);
+ mAmPmCircleRadius = (int) (circleRadius * mAmPmCircleRadiusMultiplier);
+ int textSize = mAmPmCircleRadius * 2 / 3;
+ mPaint.setTextSize(textSize);
+
+ // Line up the vertical center of the AM/PM circles with the bottom of the main circle.
+ mAmPmYCenter = layoutYCenter - mAmPmCircleRadius / 2 + circleRadius;
+ // Line up the horizontal edges of the AM/PM circles with the horizontal edges
+ // of the main circle.
+ mAmXCenter = layoutXCenter - circleRadius + mAmPmCircleRadius;
+ mPmXCenter = layoutXCenter + circleRadius - mAmPmCircleRadius;
+
+ mDrawValuesReady = true;
+ }
+
+ int amColor = mWhite;
+ int amAlpha = 255;
+ int pmColor = mWhite;
+ int pmAlpha = 255;
+ if (mAmOrPm == AM) {
+ amColor = mBlue;
+ amAlpha = 45;
+ } else if (mAmOrPm == PM) {
+ pmColor = mBlue;
+ pmAlpha = 45;
+ }
+ if (mAmOrPmPressed == AM) {
+ amColor = mBlue;
+ amAlpha = 175;
+ } else if (mAmOrPmPressed == PM) {
+ pmColor = mBlue;
+ pmAlpha = 175;
+ }
+
+ mPaint.setColor(amColor);
+ mPaint.setAlpha(amAlpha);
+ canvas.drawCircle(mAmXCenter, mAmPmYCenter, mAmPmCircleRadius, mPaint);
+ mPaint.setColor(pmColor);
+ mPaint.setAlpha(pmAlpha);
+ canvas.drawCircle(mPmXCenter, mAmPmYCenter, mAmPmCircleRadius, mPaint);
+
+ mPaint.setColor(mDarkGray);
+ 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/CircleView.java b/src/com/android/datetimepicker/CircleView.java
new file mode 100644
index 0000000..cd89410
--- /dev/null
+++ b/src/com/android/datetimepicker/CircleView.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.datetimepicker;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.Typeface;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+
+import com.android.datetimepicker.R;
+
+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 mBlack;
+ private float mCircleRadiusMultiplier;
+ private float mAmPmCircleRadiusMultiplier;
+ private boolean mIsInitialized;
+
+ private boolean mDrawValuesReady;
+ private int mXCenter;
+ private int mYCenter;
+ private int mCircleRadius;
+
+ public CircleView(Context context) {
+ super(context);
+
+ Resources res = context.getResources();
+ mWhite = res.getColor(R.color.white);
+ mBlack = res.getColor(R.color.black);
+ mPaint.setAntiAlias(true);
+
+ mIsInitialized = false;
+ }
+
+ public void initialize(Context context, boolean is24HourMode) {
+ if (mIsInitialized) {
+ Log.e(TAG, "CircleView may only be initialized once.");
+ return;
+ }
+
+ Resources res = context.getResources();
+ mIs24HourMode = is24HourMode;
+ if (is24HourMode) {
+ mCircleRadiusMultiplier = Float.parseFloat(
+ res.getString(R.string.circle_radius_multiplier_24HourMode));
+ } else {
+ mCircleRadiusMultiplier = Float.parseFloat(
+ res.getString(R.string.circle_radius_multiplier));
+ mAmPmCircleRadiusMultiplier =
+ Float.parseFloat(res.getString(R.string.ampm_circle_radius_multiplier));
+ }
+
+ mIsInitialized = true;
+ }
+
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ int viewWidth = getWidth();
+ if (viewWidth == 0 || !mIsInitialized) {
+ return;
+ }
+
+ if (!mDrawValuesReady) {
+ mXCenter = getWidth() / 2;
+ mYCenter = getHeight() / 2;
+ mCircleRadius = (int) (Math.min(mXCenter, mYCenter) * mCircleRadiusMultiplier);
+
+ if (!mIs24HourMode) {
+ // We'll need to draw the AM/PM circles, so the main circle will need to have
+ // a slightly higher center. To keep the entire view centered vertically, we'll
+ // have to push it up by half the radius of the AM/PM circles.
+ int amPmCircleRadius = (int) (mCircleRadius * mAmPmCircleRadiusMultiplier);
+ mYCenter -= amPmCircleRadius / 2;
+ }
+
+ mDrawValuesReady = true;
+ }
+
+ mPaint.setColor(mWhite);
+ canvas.drawCircle(mXCenter, mYCenter, mCircleRadius, mPaint);
+
+ mPaint.setColor(mBlack);
+ canvas.drawCircle(mXCenter, mYCenter, 2, mPaint);
+ }
+}
diff --git a/src/com/android/datetimepicker/RadialSelectorView.java b/src/com/android/datetimepicker/RadialSelectorView.java
new file mode 100644
index 0000000..5f56b56
--- /dev/null
+++ b/src/com/android/datetimepicker/RadialSelectorView.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.datetimepicker;
+
+import android.animation.Keyframe;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.app.Service;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.os.SystemClock;
+import android.os.Vibrator;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+
+import com.android.datetimepicker.R;
+
+public class RadialSelectorView extends View {
+ private static final String TAG = "RadialSelectorView";
+
+ private final Paint mPaint = new Paint();
+
+ private boolean mIsInitialized;
+ private boolean mDrawValuesReady;
+
+ private float mCircleRadiusMultiplier;
+ private float mAmPmCircleRadiusMultiplier;
+ private float mInnerNumbersRadiusMultiplier;
+ private float mOuterNumbersRadiusMultiplier;
+ private float mNumbersRadiusMultiplier;
+ private float mSelectionRadiusMultiplier;
+ private float mAnimationRadiusMultiplier;
+ private boolean mIs24HourMode;
+ private boolean mHasInnerCircle;
+
+ private int mXCenter;
+ private int mYCenter;
+ private int mCircleRadius;
+ private float mTransitionMidRadiusMultiplier;
+ private float mTransitionEndRadiusMultiplier;
+ private int mLineLength;
+ private int mSelectionRadius;
+ private InvalidateUpdateListener mInvalidateUpdateListener;
+
+ private int mSelectionDegrees;
+ private double mSelectionRadians;
+ private boolean mDrawLine;
+ private boolean mForceDrawDot;
+
+ public RadialSelectorView(Context context) {
+ super(context);
+ mIsInitialized = false;
+ }
+
+ public void initialize(Context context, int selectionDegrees, boolean is24HourMode,
+ boolean hasInnerCircle, boolean isInnerCircle, boolean disappearsOut) {
+ if (mIsInitialized) {
+ Log.e(TAG, "This RadialSelectorView may only be initialized once.");
+ return;
+ }
+
+ Resources res = context.getResources();
+
+ int blue = res.getColor(R.color.blue);
+ mPaint.setColor(blue);
+ mPaint.setAntiAlias(true);
+
+ mIs24HourMode = is24HourMode;
+ if (is24HourMode) {
+ mCircleRadiusMultiplier = Float.parseFloat(
+ res.getString(R.string.circle_radius_multiplier_24HourMode));
+ } else {
+ mCircleRadiusMultiplier = Float.parseFloat(
+ res.getString(R.string.circle_radius_multiplier));
+ mAmPmCircleRadiusMultiplier =
+ Float.parseFloat(res.getString(R.string.ampm_circle_radius_multiplier));
+ }
+
+ mHasInnerCircle = hasInnerCircle;
+ if (hasInnerCircle) {
+ mInnerNumbersRadiusMultiplier =
+ Float.parseFloat(res.getString(R.string.numbers_radius_multiplier_inner));
+ mOuterNumbersRadiusMultiplier =
+ Float.parseFloat(res.getString(R.string.numbers_radius_multiplier_outer));
+ } else {
+ mNumbersRadiusMultiplier =
+ Float.parseFloat(res.getString(R.string.numbers_radius_multiplier_normal));
+ }
+ mSelectionRadiusMultiplier =
+ Float.parseFloat(res.getString(R.string.selection_radius_multiplier));
+
+ setSelection(selectionDegrees, isInnerCircle, false, false);
+
+ mAnimationRadiusMultiplier = 1;
+ mTransitionMidRadiusMultiplier = 1f + (0.05f * (disappearsOut? -1 : 1));
+ mTransitionEndRadiusMultiplier = 1f + (0.3f * (disappearsOut? 1 : -1));
+ mInvalidateUpdateListener = new InvalidateUpdateListener();
+
+ mIsInitialized = true;
+ }
+
+ public void setSelection(int selectionDegrees, boolean isInnerCircle,
+ boolean drawLine, boolean forceDrawDot) {
+ mSelectionDegrees = selectionDegrees;
+ mSelectionRadians = selectionDegrees * Math.PI / 180;
+ mDrawLine = drawLine;
+ mForceDrawDot = forceDrawDot;
+
+ if (mHasInnerCircle) {
+ if (isInnerCircle) {
+ mNumbersRadiusMultiplier = mInnerNumbersRadiusMultiplier;
+ } else {
+ mNumbersRadiusMultiplier = mOuterNumbersRadiusMultiplier;
+ }
+ }
+ }
+
+ public void setDrawLine(boolean drawLine) {
+ mDrawLine = drawLine;
+ }
+
+ public void setAnimationRadiusMultiplier(float animationRadiusMultiplier) {
+ mAnimationRadiusMultiplier = animationRadiusMultiplier;
+ }
+
+ public int getDegreesFromCoords(float pointX, float pointY, boolean forceLegal,
+ final Boolean[] isInnerCircle) {
+ if (!mDrawValuesReady) {
+ return -1;
+ }
+
+ double hypotenuse = Math.sqrt(
+ (pointY - mYCenter)*(pointY - mYCenter) +
+ (pointX - mXCenter)*(pointX - mXCenter));
+ // 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
+ // boolean based based off whichever number the coordinates are closer to.
+ int innerNumberRadius = (int) (mCircleRadius * mInnerNumbersRadiusMultiplier);
+ int distanceToInnerNumber = (int) Math.abs(hypotenuse - innerNumberRadius);
+ int outerNumberRadius = (int) (mCircleRadius * mOuterNumbersRadiusMultiplier);
+ int distanceToOuterNumber = (int) Math.abs(hypotenuse - outerNumberRadius);
+
+ isInnerCircle[0] = (distanceToInnerNumber <= distanceToOuterNumber);
+ } else {
+ // Otherwise, if we're close enough to either number (with the space between the
+ // two allotted equally), set the isInnerCircle boolean as the closer one.
+ // appropriately, but otherwise return -1.
+ int minAllowedHypotenuseForInnerNumber =
+ (int) (mCircleRadius * mInnerNumbersRadiusMultiplier) - mSelectionRadius;
+ int maxAllowedHypotenuseForOuterNumber =
+ (int) (mCircleRadius * mOuterNumbersRadiusMultiplier) + mSelectionRadius;
+ int halfwayHypotenusePoint = (int) (mCircleRadius *
+ ((mOuterNumbersRadiusMultiplier + mInnerNumbersRadiusMultiplier) / 2));
+
+ if (hypotenuse >= minAllowedHypotenuseForInnerNumber &&
+ hypotenuse <= halfwayHypotenusePoint) {
+ isInnerCircle[0] = true;
+ } else if (hypotenuse <= maxAllowedHypotenuseForOuterNumber &&
+ hypotenuse >= halfwayHypotenusePoint) {
+ isInnerCircle[0] = false;
+ } else {
+ return -1;
+ }
+ }
+ } else {
+ // If there's just one circle, we'll need to return -1 if:
+ // we're not told to force the coordinates to be legal, and
+ // the coordinates' distance to the number is within the allowed distance.
+ if (!forceLegal) {
+ int distanceToNumber = (int) Math.abs(hypotenuse - mLineLength);
+ // The max allowed distance will be defined as the distance from the center of the
+ // number to the edge of the circle.
+ int maxAllowedDistance = (int) (mCircleRadius * (1 - mNumbersRadiusMultiplier));
+ if (distanceToNumber > maxAllowedDistance) {
+ return -1;
+ }
+ }
+ }
+
+
+ float opposite = Math.abs(pointY - mYCenter);
+ double radians = Math.asin(opposite / hypotenuse);
+ int degrees = (int) (radians * 180 / Math.PI);
+
+ // Now we have to translate to the correct quadrant.
+ boolean rightSide = (pointX > mXCenter);
+ boolean topSide = (pointY < mYCenter);
+ if (rightSide && topSide) {
+ degrees = 90 - degrees;
+ } else if (rightSide && !topSide) {
+ degrees = 90 + degrees;
+ } else if (!rightSide && !topSide) {
+ degrees = 270 - degrees;
+ } else if (!rightSide && topSide) {
+ degrees = 270 + degrees;
+ }
+ return degrees;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ int viewWidth = getWidth();
+ if (viewWidth == 0 || !mIsInitialized) {
+ return;
+ }
+
+ if (!mDrawValuesReady) {
+ mXCenter = getWidth() / 2;
+ mYCenter = getHeight() / 2;
+ mCircleRadius = (int) (Math.min(mXCenter, mYCenter) * mCircleRadiusMultiplier);
+
+ if (!mIs24HourMode) {
+ // We'll need to draw the AM/PM circles, so the main circle will need to have
+ // a slightly higher center. To keep the entire view centered vertically, we'll
+ // have to push it up by half the radius of the AM/PM circles.
+ int amPmCircleRadius = (int) (mCircleRadius * mAmPmCircleRadiusMultiplier);
+ mYCenter -= amPmCircleRadius / 2;
+ }
+
+ mSelectionRadius = (int) (mCircleRadius * mSelectionRadiusMultiplier);
+
+ mDrawValuesReady = true;
+ }
+
+ mLineLength = (int) (mCircleRadius * mNumbersRadiusMultiplier * mAnimationRadiusMultiplier);
+ int pointX = mXCenter + (int) (mLineLength * Math.sin(mSelectionRadians));
+ int pointY = mYCenter - (int) (mLineLength * Math.cos(mSelectionRadians));
+
+ mPaint.setAlpha(75);
+ canvas.drawCircle(pointX, pointY, mSelectionRadius, mPaint);
+
+ if (mForceDrawDot | mSelectionDegrees % 30 != 0) {
+ // We're not on a direct tick.
+ mPaint.setAlpha(255);
+ canvas.drawCircle(pointX, pointY, mSelectionRadius / 4, mPaint);
+ } else {
+ 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);
+ }
+ }
+
+ public ObjectAnimator getDisappearAnimator() {
+ if (!mIsInitialized || !mDrawValuesReady) {
+ Log.e(TAG, "RadialSelectorView was not ready for animation.");
+ return null;
+ }
+
+ Keyframe kf0, kf1, kf2;
+ float midwayPoint = 0.2f;
+ int duration = 500;
+
+ kf0 = Keyframe.ofFloat(0f, 1);
+ kf1 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier);
+ kf2 = Keyframe.ofFloat(1f, mTransitionEndRadiusMultiplier);
+ PropertyValuesHolder radiusDisappear = PropertyValuesHolder.ofKeyframe(
+ "animationRadiusMultiplier", kf0, kf1, kf2);
+
+ kf0 = Keyframe.ofFloat(0f, 1f);
+ kf1 = Keyframe.ofFloat(1f, 0f);
+ PropertyValuesHolder fadeOut = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1);
+
+ ObjectAnimator disappearAnimator = ObjectAnimator.ofPropertyValuesHolder(
+ this, radiusDisappear, fadeOut).setDuration(duration);
+ disappearAnimator.addUpdateListener(mInvalidateUpdateListener);
+
+ return disappearAnimator;
+ }
+
+ public ObjectAnimator getReappearAnimator() {
+ if (!mIsInitialized || !mDrawValuesReady) {
+ Log.e(TAG, "RadialSelectorView was not ready for animation.");
+ return null;
+ }
+
+ Keyframe kf0, kf1, kf2, kf3;
+ float midwayPoint = 0.2f;
+ int duration = 500;
+
+ // The time points are half of what they would normally be, because this animation is
+ // staggered against the disappear so they happen seamlessly. The reappear starts
+ // halfway into the disappear.
+ float delayMultiplier = 0.5f;
+ float transitionDurationMultiplier = 0.75f;
+ float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier;
+ int totalDuration = (int) (duration * totalDurationMultiplier);
+ float delayPoint = (delayMultiplier * duration) / totalDuration;
+ midwayPoint = 1 - (midwayPoint * (1 - delayPoint));
+
+ kf0 = Keyframe.ofFloat(0f, mTransitionEndRadiusMultiplier);
+ kf1 = Keyframe.ofFloat(delayPoint, mTransitionEndRadiusMultiplier);
+ kf2 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier);
+ kf3 = Keyframe.ofFloat(1f, 1);
+ PropertyValuesHolder radiusReappear = PropertyValuesHolder.ofKeyframe(
+ "animationRadiusMultiplier", kf0, kf1, kf2, kf3);
+
+ kf0 = Keyframe.ofFloat(0f, 0f);
+ kf1 = Keyframe.ofFloat(delayPoint, 0f);
+ kf2 = Keyframe.ofFloat(1f, 1f);
+ PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1, kf2);
+
+ ObjectAnimator reappearAnimator = ObjectAnimator.ofPropertyValuesHolder(
+ this, radiusReappear, fadeIn).setDuration(totalDuration);
+ reappearAnimator.addUpdateListener(mInvalidateUpdateListener);
+ return reappearAnimator;
+ }
+
+ private class InvalidateUpdateListener implements AnimatorUpdateListener {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ RadialSelectorView.this.invalidate();
+ }
+ }
+}
diff --git a/src/com/android/datetimepicker/RadialTextsView.java b/src/com/android/datetimepicker/RadialTextsView.java
new file mode 100644
index 0000000..6909d5c
--- /dev/null
+++ b/src/com/android/datetimepicker/RadialTextsView.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.datetimepicker;
+
+import android.animation.Keyframe;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.Paint.Align;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+
+import com.android.datetimepicker.R;
+
+public class RadialTextsView extends View {
+ private final static String TAG = "RadialTextsView";
+
+ private final Paint mPaint = new Paint();
+
+ private boolean mDrawValuesReady;
+ private boolean mIsInitialized;
+
+ private String[] mTexts;
+ private String[] mInnerTexts;
+ private boolean mIs24HourMode;
+ private boolean mHasInnerCircle;
+ private float mCircleRadiusMultiplier;
+ private float mAmPmCircleRadiusMultiplier;
+ private float mNumbersRadiusMultiplier;
+ private float mInnerNumbersRadiusMultiplier;
+ private float mTextSizeMultiplier;
+ private float mInnerTextSizeMultiplier;
+
+ private int mXCenter;
+ private int mYCenter;
+ private float mCircleRadius;
+ private boolean mTextGridValuesDirty;
+ private float mTextSize;
+ private float mInnerTextSize;
+ private float[] mTextGridHeights;
+ private float[] mTextGridWidths;
+ private float[] mInnerTextGridHeights;
+ private float[] mInnerTextGridWidths;
+
+ private float mAnimationRadiusMultiplier;
+ private float mTransitionMidRadiusMultiplier;
+ private float mTransitionEndRadiusMultiplier;
+ ObjectAnimator mDisappearAnimator;
+ ObjectAnimator mReappearAnimator;
+ private InvalidateUpdateListener mInvalidateUpdateListener;
+
+ public RadialTextsView(Context context) {
+ super(context);
+ mIsInitialized = false;
+ }
+
+ public void initialize(Resources res, String[] texts, String[] innerTexts,
+ boolean is24HourMode, boolean disappearsOut) {
+ if (mIsInitialized) {
+ Log.e(TAG, "This RadialTextsView may only be initialized once.");
+ return;
+ }
+
+ int black = res.getColor(R.color.black);
+ mPaint.setColor(black);
+ Typeface tf = Typeface.create("sans-serif-thin", Typeface.NORMAL);
+ mPaint.setTypeface(tf);
+ mPaint.setAntiAlias(true);
+ mPaint.setTextAlign(Align.CENTER);
+
+ mTexts = texts;
+ mInnerTexts = innerTexts;
+ mIs24HourMode = is24HourMode;
+ mHasInnerCircle = (innerTexts != null);
+
+ if (is24HourMode) {
+ mCircleRadiusMultiplier = Float.parseFloat(
+ res.getString(R.string.circle_radius_multiplier_24HourMode));
+ } else {
+ mCircleRadiusMultiplier = Float.parseFloat(
+ res.getString(R.string.circle_radius_multiplier));
+ mAmPmCircleRadiusMultiplier =
+ Float.parseFloat(res.getString(R.string.ampm_circle_radius_multiplier));
+ }
+
+ mTextGridHeights = new float[7];
+ mTextGridWidths = new float[7];
+ if (mHasInnerCircle) {
+ mNumbersRadiusMultiplier = Float.parseFloat(
+ res.getString(R.string.numbers_radius_multiplier_outer));
+ mTextSizeMultiplier = Float.parseFloat(
+ res.getString(R.string.text_size_multiplier_outer));
+ mInnerNumbersRadiusMultiplier = Float.parseFloat(
+ res.getString(R.string.numbers_radius_multiplier_inner));
+ mInnerTextSizeMultiplier = Float.parseFloat(
+ res.getString(R.string.text_size_multiplier_inner));
+
+ mInnerTextGridHeights = new float[7];
+ mInnerTextGridWidths = new float[7];
+ } else {
+ mNumbersRadiusMultiplier = Float.parseFloat(
+ res.getString(R.string.numbers_radius_multiplier_normal));
+ mTextSizeMultiplier = Float.parseFloat(
+ res.getString(R.string.text_size_multiplier_normal));
+ }
+
+ mAnimationRadiusMultiplier = 1;
+ mTransitionMidRadiusMultiplier = 1f + (0.05f * (disappearsOut? -1 : 1));
+ mTransitionEndRadiusMultiplier = 1f + (0.3f * (disappearsOut? 1 : -1));
+ mInvalidateUpdateListener = new InvalidateUpdateListener();
+
+ mTextGridValuesDirty = true;
+ mIsInitialized = true;
+ }
+
+ public void setAnimationRadiusMultiplier(float animationRadiusMultiplier) {
+ mAnimationRadiusMultiplier = animationRadiusMultiplier;
+ mTextGridValuesDirty = true;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ int viewWidth = getWidth();
+ if (viewWidth == 0 || !mIsInitialized) {
+ return;
+ }
+
+ if (!mDrawValuesReady) {
+ mXCenter = getWidth() / 2;
+ mYCenter = getHeight() / 2;
+ mCircleRadius = Math.min(mXCenter, mYCenter) * mCircleRadiusMultiplier;
+ if (!mIs24HourMode) {
+ // We'll need to draw the AM/PM circles, so the main circle will need to have
+ // a slightly higher center. To keep the entire view centered vertically, we'll
+ // have to push it up by half the radius of the AM/PM circles.
+ float amPmCircleRadius = mCircleRadius * mAmPmCircleRadiusMultiplier;
+ mYCenter -= amPmCircleRadius / 2;
+ }
+
+ mTextSize = mCircleRadius * mTextSizeMultiplier;
+ if (mHasInnerCircle) {
+ mInnerTextSize = mCircleRadius * mInnerTextSizeMultiplier;
+ }
+
+ // Set up the spots for the animation.
+ renderAnimations();
+
+ mTextGridValuesDirty = true;
+ mDrawValuesReady = true;
+ }
+
+ if (mTextGridValuesDirty) {
+ float numbersRadius =
+ mCircleRadius * mNumbersRadiusMultiplier * mAnimationRadiusMultiplier;
+
+ calculateGridSizes(numbersRadius, mXCenter, mYCenter,
+ mTextSize, mTextGridHeights, mTextGridWidths);
+ if (mHasInnerCircle) {
+ float innerNumbersRadius =
+ mCircleRadius * mInnerNumbersRadiusMultiplier * mAnimationRadiusMultiplier;
+ calculateGridSizes(innerNumbersRadius, mXCenter, mYCenter,
+ mInnerTextSize, mInnerTextGridHeights, mInnerTextGridWidths);
+ }
+ mTextGridValuesDirty = false;
+ }
+
+ drawTexts(canvas, mTextSize, mTexts, mTextGridWidths, mTextGridHeights);
+ if (mHasInnerCircle) {
+ drawTexts(canvas, mInnerTextSize, mInnerTexts,
+ mInnerTextGridWidths, mInnerTextGridHeights);
+ }
+ }
+
+ 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.
+ */
+ 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);
+ yCenter -= (mPaint.descent() + mPaint.ascent()) / 2;
+ textGridHeights[0] = yCenter - offset1;
+ textGridWidths[0] = xCenter - offset1;
+ textGridHeights[1] = yCenter - offset2;
+ textGridWidths[1] = xCenter - offset2;
+ textGridHeights[2] = yCenter - offset3;
+ textGridWidths[2] = xCenter - offset3;
+ textGridHeights[3] = yCenter;
+ textGridWidths[3] = xCenter;
+ textGridHeights[4] = yCenter + offset3;
+ textGridWidths[4] = xCenter + offset3;
+ textGridHeights[5] = yCenter + offset2;
+ textGridWidths[5] = xCenter + offset2;
+ textGridHeights[6] = yCenter + offset1;
+ textGridWidths[6] = xCenter + offset1;
+ }
+
+ private void drawTexts(Canvas canvas, float textSize, String[] texts,
+ float[] textGridWidths, float[] textGridHeights) {
+ mPaint.setTextSize(textSize);
+ canvas.drawText(texts[0], textGridWidths[3], textGridHeights[0], mPaint);
+ canvas.drawText(texts[1], textGridWidths[4], textGridHeights[1], mPaint);
+ canvas.drawText(texts[2], textGridWidths[5], textGridHeights[2], mPaint);
+ canvas.drawText(texts[3], textGridWidths[6], textGridHeights[3], mPaint);
+ canvas.drawText(texts[4], textGridWidths[5], textGridHeights[4], mPaint);
+ canvas.drawText(texts[5], textGridWidths[4], textGridHeights[5], mPaint);
+ canvas.drawText(texts[6], textGridWidths[3], textGridHeights[6], mPaint);
+ canvas.drawText(texts[7], textGridWidths[2], textGridHeights[5], mPaint);
+ canvas.drawText(texts[8], textGridWidths[1], textGridHeights[4], mPaint);
+ canvas.drawText(texts[9], textGridWidths[0], textGridHeights[3], mPaint);
+ canvas.drawText(texts[10], textGridWidths[1], textGridHeights[2], mPaint);
+ canvas.drawText(texts[11], textGridWidths[2], textGridHeights[1], mPaint);
+ }
+
+ private void renderAnimations() {
+ Keyframe kf0, kf1, kf2, kf3;
+ float midwayPoint = 0.2f;
+ int duration = 500;
+
+ // Set up animator for disappearing.
+ kf0 = Keyframe.ofFloat(0f, 1);
+ kf1 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier);
+ kf2 = Keyframe.ofFloat(1f, mTransitionEndRadiusMultiplier);
+ PropertyValuesHolder radiusDisappear = PropertyValuesHolder.ofKeyframe(
+ "animationRadiusMultiplier", kf0, kf1, kf2);
+
+ kf0 = Keyframe.ofFloat(0f, 1f);
+ kf1 = Keyframe.ofFloat(1f, 0f);
+ PropertyValuesHolder fadeOut = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1);
+
+ mDisappearAnimator = ObjectAnimator.ofPropertyValuesHolder(
+ this, radiusDisappear, fadeOut).setDuration(duration);
+ mDisappearAnimator.addUpdateListener(mInvalidateUpdateListener);
+
+
+ // Set up animator for reappearing.
+ float delayMultiplier = 0.5f;
+ float transitionDurationMultiplier = 0.75f;
+ float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier;
+ int totalDuration = (int) (duration * totalDurationMultiplier);
+ float delayPoint = (delayMultiplier * duration) / totalDuration;
+ midwayPoint = 1 - (midwayPoint * (1 - delayPoint));
+
+ kf0 = Keyframe.ofFloat(0f, mTransitionEndRadiusMultiplier);
+ kf1 = Keyframe.ofFloat(delayPoint, mTransitionEndRadiusMultiplier);
+ kf2 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier);
+ kf3 = Keyframe.ofFloat(1f, 1);
+ PropertyValuesHolder radiusReappear = PropertyValuesHolder.ofKeyframe(
+ "animationRadiusMultiplier", kf0, kf1, kf2, kf3);
+
+ kf0 = Keyframe.ofFloat(0f, 0f);
+ kf1 = Keyframe.ofFloat(delayPoint, 0f);
+ kf2 = Keyframe.ofFloat(1f, 1f);
+ PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1, kf2);
+
+ mReappearAnimator = ObjectAnimator.ofPropertyValuesHolder(
+ this, radiusReappear, fadeIn).setDuration(totalDuration);
+ mReappearAnimator.addUpdateListener(mInvalidateUpdateListener);
+ }
+
+ public ObjectAnimator getDisappearAnimator() {
+ if (!mIsInitialized || !mDrawValuesReady) {
+ Log.e(TAG, "RadialTextView was not ready for animation.");
+ return null;
+ }
+
+ return mDisappearAnimator;
+ }
+
+ public ObjectAnimator getReappearAnimator() {
+ if (!mIsInitialized || !mDrawValuesReady) {
+ Log.e(TAG, "RadialTextView was not ready for animation.");
+ return null;
+ }
+
+ return mReappearAnimator;
+ }
+
+ private class InvalidateUpdateListener implements AnimatorUpdateListener {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ RadialTextsView.this.invalidate();
+ }
+ }
+}
diff --git a/src/com/android/datetimepicker/TimePicker.java b/src/com/android/datetimepicker/TimePicker.java
new file mode 100644
index 0000000..201ba75
--- /dev/null
+++ b/src/com/android/datetimepicker/TimePicker.java
@@ -0,0 +1,527 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.datetimepicker;
+
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.app.Service;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.os.Vibrator;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.View.OnClickListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
+import android.view.animation.AnimationSet;
+import android.view.animation.TranslateAnimation;
+import android.widget.FrameLayout;
+
+import com.android.datetimepicker.R;
+
+public class TimePicker extends FrameLayout implements OnTouchListener {
+ private static final String TAG = "TimePicker";
+
+ private final int TOUCH_SLOP;
+ private final int TAP_TIMEOUT;
+ private final int PRESSED_STATE_DURATION;
+ private static final int HOUR_VALUE_TO_DEGREES_STEP_SIZE = 30;
+ private static final int MINUTE_VALUE_TO_DEGREES_STEP_SIZE = 6;
+ private static final int HOUR_INDEX = TimePickerDialog.HOUR_INDEX;
+ private static final int MINUTE_INDEX = TimePickerDialog.MINUTE_INDEX;
+ private static final int AMPM_INDEX = TimePickerDialog.AMPM_INDEX;
+ private static final int AM = TimePickerDialog.AM;
+ private static final int PM = TimePickerDialog.PM;
+
+ private Vibrator mVibrator;
+ private long mLastVibrate;
+ private int mLastValueSelected;
+
+ private OnValueSelectedListener mListener;
+ private boolean mTimeInitialized;
+ private int mCurrentHoursOfDay;
+ private int mCurrentMinutes;
+ private boolean mIs24HourMode;
+ private int mCurrentItemShowing;
+
+ private CircleView mCircleView;
+ private AmPmCirclesView mAmPmCirclesView;
+ private RadialTextsView mHourRadialTextsView;
+ private RadialTextsView mMinuteRadialTextsView;
+ private RadialSelectorView mHourRadialSelectorView;
+ private RadialSelectorView mMinuteRadialSelectorView;
+
+ private int mIsTouchingAmOrPm = -1;
+ private boolean mDoingMove;
+ private int mDownDegrees;
+ private float mDownX;
+ private float mDownY;
+
+ private ReselectSelectorRunnable mReselectSelectorRunnable;
+
+ private Handler mHandler = new Handler();
+
+ public interface OnValueSelectedListener {
+ void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance);
+ }
+
+ public TimePicker(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ setOnTouchListener(this);
+ ViewConfiguration vc = ViewConfiguration.get(context);
+ TOUCH_SLOP = vc.getScaledTouchSlop();
+ TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
+ PRESSED_STATE_DURATION = ViewConfiguration.getPressedStateDuration();
+ mDoingMove = false;
+
+ mCircleView = new CircleView(context);
+ addView(mCircleView);
+
+ mAmPmCirclesView = new AmPmCirclesView(context);
+ addView(mAmPmCirclesView);
+
+ mHourRadialTextsView = new RadialTextsView(context);
+ addView(mHourRadialTextsView);
+ mMinuteRadialTextsView = new RadialTextsView(context);
+ addView(mMinuteRadialTextsView);
+
+ mHourRadialSelectorView = new RadialSelectorView(context);
+ addView(mHourRadialSelectorView);
+ mMinuteRadialSelectorView = new RadialSelectorView(context);
+ addView(mMinuteRadialSelectorView);
+
+ setCurrentItemShowing(HOUR_INDEX, false);
+
+ mReselectSelectorRunnable = new ReselectSelectorRunnable(this);
+
+ mVibrator = (Vibrator) context.getSystemService(Service.VIBRATOR_SERVICE);
+ mLastVibrate = 0;
+ mLastValueSelected = -1;
+
+ mTimeInitialized = false;
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
+ int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
+ super.onMeasure(widthMeasureSpec,
+ measuredWidth < measuredHeight? widthMeasureSpec : heightMeasureSpec);
+ }
+
+ public void setOnValueSelectedListener(OnValueSelectedListener listener) {
+ mListener = listener;
+ }
+
+ public void initialize(Context context, int initialHoursOfDay, int initialMinutes,
+ boolean is24HourMode) {
+ if (mTimeInitialized) {
+ Log.e(TAG, "Time has already been initialized.");
+ return;
+ }
+
+ setValueForItem(HOUR_INDEX, initialHoursOfDay);
+ setValueForItem(MINUTE_INDEX, initialMinutes);
+ mIs24HourMode = is24HourMode;
+
+ mCircleView.initialize(context, is24HourMode);
+ mCircleView.invalidate();
+ if (!is24HourMode) {
+ mAmPmCirclesView.initialize(context, initialHoursOfDay < 12? AM : PM);
+ mAmPmCirclesView.invalidate();
+ }
+
+ Resources res = context.getResources();
+ String[] hoursTexts = res.getStringArray(is24HourMode? R.array.hours_24 : R.array.hours);
+ String[] innerHoursTexts = res.getStringArray(R.array.hours);
+ String[] minutesTexts = res.getStringArray(R.array.minutes);
+ mHourRadialTextsView.initialize(res,
+ hoursTexts, (is24HourMode? innerHoursTexts : null), is24HourMode, true);
+ mHourRadialTextsView.invalidate();
+ mMinuteRadialTextsView.initialize(res, minutesTexts, null, is24HourMode, false);
+ mMinuteRadialTextsView.invalidate();
+
+ int initialHourDegrees = (initialHoursOfDay % 12) * HOUR_VALUE_TO_DEGREES_STEP_SIZE;
+ int initialMinuteDegrees = initialMinutes * MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
+ mHourRadialSelectorView.initialize(context, initialHourDegrees,
+ is24HourMode, is24HourMode, isHourInnerCircle(initialHoursOfDay), true);
+ mHourRadialSelectorView.invalidate();
+ mMinuteRadialSelectorView.initialize(context, initialMinuteDegrees,
+ is24HourMode, false, false, false);
+ mHourRadialSelectorView.invalidate();
+
+
+ mTimeInitialized = true;
+ }
+
+ private boolean isHourInnerCircle(int hourOfDay) {
+ // We'll have the 00 hours on the outside circle.
+ return mIs24HourMode && (hourOfDay <= 12 && hourOfDay != 0);
+ }
+
+ public int getHours() {
+ return mCurrentHoursOfDay;
+ }
+
+ public int getMinutes() {
+ return mCurrentMinutes;
+ }
+
+ private int getCurrentlyShowingValue() {
+ int currentIndex = getCurrentItemShowing();
+ if (currentIndex == HOUR_INDEX) {
+ return mCurrentHoursOfDay;
+ } else if (currentIndex == MINUTE_INDEX) {
+ return mCurrentMinutes;
+ } else {
+ return -1;
+ }
+ }
+
+ public int getIsCurrentlyAmOrPm() {
+ if (mCurrentHoursOfDay < 12) {
+ return AM;
+ } else if (mCurrentHoursOfDay < 24) {
+ return PM;
+ }
+ return -1;
+ }
+
+ private void setValueForItem(int index, int value) {
+ if (index == HOUR_INDEX) {
+ mCurrentHoursOfDay = value;
+ } else if (index == MINUTE_INDEX){
+ mCurrentMinutes = value;
+ } else if (index == AMPM_INDEX) {
+ if (value == AM) {
+ mCurrentHoursOfDay = mCurrentHoursOfDay % 12;
+ } else if (value == PM) {
+ mCurrentHoursOfDay = (mCurrentHoursOfDay % 12) + 12;
+ }
+ }
+ }
+
+ public void setAmOrPm(int amOrPm) {
+ mAmPmCirclesView.setAmOrPm(amOrPm);
+ mAmPmCirclesView.invalidate();
+ setValueForItem(AMPM_INDEX, amOrPm);
+ }
+
+ private int reselectSelector(int index, int degrees, boolean isInnerCircle,
+ boolean forceNotFineGrained, boolean forceDrawLine, boolean forceDrawDot) {
+ if (degrees == -1 || (index != 0 && index != 1)) {
+ return -1;
+ }
+
+ int stepSize;
+ int currentShowing = getCurrentItemShowing();
+ if (!forceNotFineGrained && (currentShowing == 1)) {
+ stepSize = MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
+ } else {
+ stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE;
+ }
+ int floor = (degrees / stepSize) * stepSize;
+ int ceiling = floor + stepSize;
+ if ((degrees - floor) < (ceiling - degrees)) {
+ degrees = floor;
+ } else {
+ degrees = ceiling;
+ }
+
+ RadialSelectorView radialSelectorView;
+ if (index == 0) {
+ // Index == 0, hours.
+ radialSelectorView = mHourRadialSelectorView;
+ stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE;
+ } else {
+ // Index == 1, minutes.
+ radialSelectorView = mMinuteRadialSelectorView;
+ stepSize = MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
+ }
+ radialSelectorView.setSelection(degrees, isInnerCircle, forceDrawLine, forceDrawDot);
+ radialSelectorView.invalidate();
+
+
+ if (currentShowing == HOUR_INDEX) {
+ if (mIs24HourMode) {
+ if (degrees == 0 && isInnerCircle) {
+ degrees = 360;
+ } else if (degrees == 360 && !isInnerCircle) {
+ degrees = 0;
+ }
+ } else if (degrees == 0) {
+ degrees = 360;
+ }
+ } else if (degrees == 360 && currentShowing == MINUTE_INDEX) {
+ degrees = 0;
+ }
+
+ int value = degrees / stepSize;
+ if (currentShowing == HOUR_INDEX && mIs24HourMode && !isInnerCircle && degrees != 0) {
+ value += 12;
+ }
+ return value;
+ }
+
+ private int getDegreesFromCoords(float pointX, float pointY, boolean forceLegal,
+ final Boolean[] isInnerCircle) {
+ int currentItem = getCurrentItemShowing();
+ if (currentItem == 0) {
+ return mHourRadialSelectorView.getDegreesFromCoords(
+ pointX, pointY, forceLegal, isInnerCircle);
+ } else if (currentItem == 1) {
+ return mMinuteRadialSelectorView.getDegreesFromCoords(
+ pointX, pointY, forceLegal, isInnerCircle);
+ } else {
+ return -1;
+ }
+ }
+
+ public int getCurrentItemShowing() {
+ if (mCurrentItemShowing != HOUR_INDEX && mCurrentItemShowing != MINUTE_INDEX) {
+ Log.e(TAG, "Current item showing was unfortunately set to "+mCurrentItemShowing);
+ return -1;
+ }
+ return mCurrentItemShowing;
+ }
+
+ 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);
+ return;
+ }
+
+ if (animate && (index != getCurrentItemShowing())) {
+ ObjectAnimator[] anims = new ObjectAnimator[4];
+ if (index == MINUTE_INDEX) {
+ anims[0] = mHourRadialTextsView.getDisappearAnimator();
+ anims[1] = mHourRadialSelectorView.getDisappearAnimator();
+ anims[2] = mMinuteRadialTextsView.getReappearAnimator();
+ anims[3] = mMinuteRadialSelectorView.getReappearAnimator();
+ } else if (index == HOUR_INDEX){
+ anims[0] = mHourRadialTextsView.getReappearAnimator();
+ anims[1] = mHourRadialSelectorView.getReappearAnimator();
+ anims[2] = mMinuteRadialTextsView.getDisappearAnimator();
+ anims[3] = mMinuteRadialSelectorView.getDisappearAnimator();
+ }
+
+ AnimatorSet transition = new AnimatorSet();
+ transition.playTogether(anims);
+ transition.start();
+ } else {
+ int hourAlpha = (index == 0) ? 255 : 0;
+ int minuteAlpha = (index == 1) ? 255 : 0;
+ mHourRadialTextsView.setAlpha(hourAlpha);
+ mHourRadialSelectorView.setAlpha(hourAlpha);
+ mMinuteRadialTextsView.setAlpha(minuteAlpha);
+ mMinuteRadialSelectorView.setAlpha(minuteAlpha);
+ }
+
+ mCurrentItemShowing = index;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ final float eventX = event.getX();
+ final float eventY = event.getY();
+ int degrees;
+ int value;
+ final int currentShowing = getCurrentItemShowing();
+ final Boolean[] isInnerCircle = new Boolean[1];
+ isInnerCircle[0] = false;
+
+ long millis = SystemClock.uptimeMillis();
+
+ switch(event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mDownX = eventX;
+ mDownY = eventY;
+
+ mLastValueSelected = -1;
+ mDoingMove = false;
+ if (!mIs24HourMode) {
+ mIsTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY);
+ } else {
+ mIsTouchingAmOrPm = -1;
+ }
+ if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) {
+ tryVibrate();
+ mDownDegrees = -1;
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mAmPmCirclesView.setAmOrPmPressed(mIsTouchingAmOrPm);
+ mAmPmCirclesView.invalidate();
+ }
+ }, TAP_TIMEOUT);
+ } else {
+ mDownDegrees = getDegreesFromCoords(eventX, eventY, false, isInnerCircle);
+ if (mDownDegrees != -1) {
+ tryTick();
+ mLastValueSelected = getCurrentlyShowingValue();
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mDoingMove = true;
+ int value = reselectSelector(currentShowing, mDownDegrees,
+ isInnerCircle[0], false, true, true);
+ mListener.onValueSelected(getCurrentItemShowing(), value, false);
+ }
+ }, TAP_TIMEOUT);
+ }
+ }
+ return true;
+ case MotionEvent.ACTION_MOVE:
+ float dY = Math.abs(eventY - mDownY);
+ float dX = Math.abs(eventX - mDownX);
+
+ if (!mDoingMove && dX <= TOUCH_SLOP && dY <= TOUCH_SLOP) {
+ // Hasn't registered down yet, just slight, accidental movement of finger.
+ break;
+ }
+
+ // If we're in the middle of touching down on AM or PM, check if we still are.
+ // If so, no-op. If not, remove its pressed state. Either way, no need to check
+ // for touches on the other circle.
+ if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) {
+ mHandler.removeCallbacksAndMessages(null);
+ int isTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY);
+ if (isTouchingAmOrPm != mIsTouchingAmOrPm) {
+ mAmPmCirclesView.setAmOrPmPressed(-1);
+ mAmPmCirclesView.invalidate();
+ mIsTouchingAmOrPm = -1;
+ }
+ break;
+ }
+
+ if (mDownDegrees == -1) {
+ // Original down was illegal, so no movement will register.
+ break;
+ }
+
+ mDoingMove = true;
+ mHandler.removeCallbacksAndMessages(null);
+ degrees = getDegreesFromCoords(eventX, eventY, true, isInnerCircle);
+ if (degrees != -1) {
+ value = reselectSelector(currentShowing, degrees,
+ isInnerCircle[0], false, true, true);
+ if (value != mLastValueSelected) {
+ tryTick();
+ mLastValueSelected = value;
+ }
+ mListener.onValueSelected(getCurrentItemShowing(), value, false);
+ }
+ return true;
+ case MotionEvent.ACTION_UP:
+ mHandler.removeCallbacksAndMessages(null);
+
+ if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) {
+ int isTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY);
+ mAmPmCirclesView.setAmOrPmPressed(-1);
+ mAmPmCirclesView.invalidate();
+
+ if (isTouchingAmOrPm == mIsTouchingAmOrPm) {
+ mAmPmCirclesView.setAmOrPm(isTouchingAmOrPm);
+ if (getIsCurrentlyAmOrPm() != isTouchingAmOrPm) {
+ mListener.onValueSelected(AMPM_INDEX, mIsTouchingAmOrPm, false);
+ setValueForItem(AMPM_INDEX, isTouchingAmOrPm);
+ }
+ }
+ mIsTouchingAmOrPm = -1;
+ break;
+ }
+
+ if (mDownDegrees != -1) {
+ degrees = getDegreesFromCoords(eventX, eventY, mDoingMove, isInnerCircle);
+ if (degrees != -1) {
+ value = reselectSelector(currentShowing, degrees, isInnerCircle[0],
+ !mDoingMove, true, false);
+ mListener.onValueSelected(getCurrentItemShowing(), value, true);
+
+ if (currentShowing == HOUR_INDEX && !mIs24HourMode) {
+ int amOrPm = getIsCurrentlyAmOrPm();
+ if (amOrPm == AM && value == 12) {
+ value = 0;
+ } else if (amOrPm == PM && value != 12) {
+ value += 12;
+ }
+ }
+ setValueForItem(getCurrentItemShowing(), value);
+ }
+ }
+ mDoingMove = false;
+ return true;
+ default:
+ break;
+ }
+ return false;
+ }
+
+ private class ReselectSelectorRunnable implements Runnable {
+ TimePicker mTimePicker;
+ private int mIndex;
+ private int mDegrees;
+ private boolean mIsInnerCircle;
+ private boolean mForceNotFineGrained;
+ private boolean mForceDrawLine;
+ private boolean mForceDrawDot;
+
+ public ReselectSelectorRunnable(TimePicker timePicker) {
+ mTimePicker = timePicker;
+ }
+
+ public void initializeValues(int index, int degrees, boolean isInnerCircle,
+ boolean forceNotFineGrained, boolean forceDrawLine, boolean forceDrawDot) {
+ mIndex = index;
+ mDegrees = degrees;
+ mIsInnerCircle = isInnerCircle;
+ mForceNotFineGrained = forceNotFineGrained;
+ mForceDrawDot = forceDrawDot;
+ }
+
+ @Override
+ public void run() {
+ mTimePicker.reselectSelector(mIndex, mDegrees, mIsInnerCircle, mForceNotFineGrained,
+ mForceDrawLine, mForceDrawDot);
+ }
+ }
+
+ public void tryVibrate() {
+ if (mVibrator != null) {
+ long now = SystemClock.uptimeMillis();
+ // We want to try to vibrate each individual tick discretely.
+ if (now - mLastVibrate >= 100) {
+ mVibrator.vibrate(5);
+ mLastVibrate = now;
+ }
+ }
+ }
+
+ public void tryTick() {
+ tryVibrate();
+ }
+}
diff --git a/src/com/android/datetimepicker/TimePickerDialog.java b/src/com/android/datetimepicker/TimePickerDialog.java
new file mode 100644
index 0000000..d76ebd4
--- /dev/null
+++ b/src/com/android/datetimepicker/TimePickerDialog.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.datetimepicker;
+
+import android.app.ActionBar.LayoutParams;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.text.style.AlignmentSpan;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.android.datetimepicker.R;
+
+import com.android.datetimepicker.TimePicker.OnValueSelectedListener;
+
+/**
+ * Dialog to set a time.
+ */
+public class TimePickerDialog extends DialogFragment implements OnValueSelectedListener{
+ private static final String TAG = "TimePickerDialog";
+
+ private static final String KEY_HOUR_OF_DAY = "hour_of_day";
+ private static final String KEY_MINUTE = "minute";
+ private static final String KEY_IS_24_HOUR_VIEW = "is_24_hour_view";
+ public static final int HOUR_INDEX = 0;
+ public static final int MINUTE_INDEX = 1;
+ public static final int AMPM_INDEX = 2; // NOT a real index for the purpose of what's showing.
+ public static final int AM = 0;
+ public static final int PM = 1;
+
+ private Handler mHandler = new Handler();
+
+ private OnTimeSetListener mCallback;
+
+ private Button mDoneButton;
+ private TextView mHourView;
+ private TextView mMinuteView;
+ private TextView mAmPmTextView;
+ private TimePicker mTimePicker;
+
+ private int mBlue;
+ private int mBlack;
+ private String mAmText;
+ private String mPmText;
+
+ private boolean mAllowAutoAdvance;
+ private int mInitialHourOfDay;
+ private int mInitialMinute;
+ private boolean mIs24HourMode;
+ private int mWidthPixels;
+
+ /**
+ * The callback interface used to indicate the user is done filling in
+ * the time (they clicked on the 'Set' button).
+ */
+ public interface OnTimeSetListener {
+
+ /**
+ * @param view The view associated with this listener.
+ * @param hourOfDay The hour that was set.
+ * @param minute The minute that was set.
+ */
+ void onTimeSet(TimePicker view, int hourOfDay, int minute);
+ }
+
+ public TimePickerDialog() {
+ // Empty constructor required for dialog fragment.
+ }
+
+ public TimePickerDialog(Context context, int theme, OnTimeSetListener callback,
+ int hourOfDay, int minute, boolean is24HourMode) {
+ // Empty constructor required for dialog fragment.
+ }
+
+ public static TimePickerDialog newInstance(OnTimeSetListener callback,
+ int hourOfDay, int minute, boolean is24HourMode) {
+ TimePickerDialog ret = new TimePickerDialog();
+ ret.initialize(callback, hourOfDay, minute, is24HourMode);
+ return ret;
+ }
+
+ public void initialize(OnTimeSetListener callback,
+ int hourOfDay, int minute, boolean is24HourMode) {
+ mCallback = callback;
+
+ mInitialHourOfDay = hourOfDay;
+ mInitialMinute = minute;
+ mIs24HourMode = is24HourMode;
+ }
+
+ public void setOnTimeSetListener(OnTimeSetListener callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null && savedInstanceState.containsKey(KEY_HOUR_OF_DAY)
+ && savedInstanceState.containsKey(KEY_MINUTE)
+ && savedInstanceState.containsKey(KEY_IS_24_HOUR_VIEW)) {
+ mInitialHourOfDay = savedInstanceState.getInt(KEY_HOUR_OF_DAY);
+ mInitialMinute = savedInstanceState.getInt(KEY_MINUTE);
+ mIs24HourMode = savedInstanceState.getBoolean(KEY_IS_24_HOUR_VIEW);
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
+
+ View view = inflater.inflate(R.layout.time_picker_dialog, null);
+ Resources res = getResources();
+
+ mBlue = res.getColor(R.color.blue);
+ mBlack = res.getColor(R.color.black);
+
+ mHourView = (TextView) view.findViewById(R.id.hours);
+ mMinuteView = (TextView) view.findViewById(R.id.minutes);
+ mAmPmTextView = (TextView) view.findViewById(R.id.ampm_label);
+ mAmText = res.getString(R.string.am_label);
+ mPmText = res.getString(R.string.pm_label);
+
+ mTimePicker = (TimePicker) view.findViewById(R.id.time_picker);
+ mTimePicker.setOnValueSelectedListener(this);
+ mTimePicker.initialize(getActivity(), mInitialHourOfDay, mInitialMinute, mIs24HourMode);
+ mTimePicker.invalidate();
+
+ mHourView.setTextColor(mBlue);
+ mHourView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setCurrentItemShowing(HOUR_INDEX, true);
+ mTimePicker.tryVibrate();
+ }
+ });
+ mMinuteView.setTextColor(mBlack);
+ mMinuteView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setCurrentItemShowing(MINUTE_INDEX, true);
+ mTimePicker.tryVibrate();
+ }
+ });
+
+ mDoneButton = (Button) view.findViewById(R.id.done_button);
+ mDoneButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mTimePicker.tryVibrate();
+ if (mCallback != null) {
+ mCallback.onTimeSet(mTimePicker,
+ mTimePicker.getHours(), mTimePicker.getMinutes());
+ }
+ dismiss();
+ }
+ });
+
+ DisplayMetrics metrics = new DisplayMetrics();
+ getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics);
+ mWidthPixels = metrics.widthPixels;
+
+ if (mIs24HourMode) {
+ mAmPmTextView.setVisibility(View.GONE);
+
+ RelativeLayout.LayoutParams paramsSeparator = new RelativeLayout.LayoutParams(
+ LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ paramsSeparator.addRule(RelativeLayout.CENTER_IN_PARENT);
+ TextView separatorView = (TextView) view.findViewById(R.id.separator);
+ separatorView.setLayoutParams(paramsSeparator);
+ } else {
+ mAmPmTextView.setVisibility(View.VISIBLE);
+ updateAmPmDisplay(mInitialHourOfDay < 12? AM : PM);
+ View amPmHitspace = view.findViewById(R.id.ampm_hitspace);
+ amPmHitspace.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mTimePicker.tryVibrate();
+ int amOrPm = mTimePicker.getIsCurrentlyAmOrPm();
+ if (amOrPm == AM) {
+ amOrPm = PM;
+ } else if (amOrPm == PM){
+ amOrPm = AM;
+ }
+ updateAmPmDisplay(amOrPm);
+ mTimePicker.setAmOrPm(amOrPm);
+ }
+ });
+ }
+
+ mAllowAutoAdvance = true;
+ setHour(mInitialHourOfDay);
+ setMinute(mInitialMinute);
+
+ return view;
+ }
+
+ private void updateAmPmDisplay(int amOrPm) {
+ if (amOrPm == AM) {
+ mAmPmTextView.setText(mAmText);
+ } else if (amOrPm == PM){
+ mAmPmTextView.setText(mPmText);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ if (mTimePicker != null) {
+ outState.putInt(KEY_HOUR_OF_DAY, mTimePicker.getHours());
+ outState.putInt(KEY_MINUTE, mTimePicker.getMinutes());
+ outState.putBoolean(KEY_IS_24_HOUR_VIEW, mIs24HourMode);
+ }
+ }
+
+ @Override
+ public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance) {
+ if (pickerIndex == HOUR_INDEX) {
+ setHour(newValue);
+ if (mAllowAutoAdvance && autoAdvance) {
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ setCurrentItemShowing(MINUTE_INDEX, true);
+ }
+ }, 150);
+ }
+ } else if (pickerIndex == MINUTE_INDEX){
+ setMinute(newValue);
+ } else if (pickerIndex == AMPM_INDEX) {
+ updateAmPmDisplay(newValue);
+ }
+ }
+
+ private void setHour(int value) {
+ String format;
+ if (mIs24HourMode) {
+ format = "%02d";
+ } else {
+ format = "%d";
+ value = value % 12;
+ if (value == 0) {
+ value = 12;
+ }
+ }
+
+ mHourView.setText(String.format(format, value));
+ }
+
+ private void setMinute(int value) {
+ if (value == 60) {
+ value = 0;
+ }
+ mMinuteView.setText(String.format("%02d", value));
+ }
+
+ private void setCurrentItemShowing(int index, boolean animate) {
+/*
+ if (mAllowAutoAdvance && index == 1) {
+ // Once we've seen the minutes, no need to auto-advance.
+ mAllowAutoAdvance = false;
+ }
+*/
+ mTimePicker.setCurrentItemShowing(index, animate);
+ int hourColor = (index == HOUR_INDEX)? mBlue : mBlack;
+ int minuteColor = (index == MINUTE_INDEX)? mBlue : mBlack;
+ mHourView.setTextColor(hourColor);
+ mMinuteView.setTextColor(minuteColor);
+ }
+}