| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.datetimepicker.time; |
| |
| import android.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.util.Log; |
| import android.view.View; |
| |
| import com.android.datetimepicker.R; |
| import com.android.datetimepicker.Utils; |
| |
| /** |
| * 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. |
| */ |
| class RadialSelectorView extends View { |
| private static final String TAG = "RadialSelectorView"; |
| |
| // Alpha level for selected circle. |
| private static final int SELECTED_ALPHA = Utils.SELECTED_ALPHA; |
| private static final int SELECTED_ALPHA_THEME_DARK = Utils.SELECTED_ALPHA_THEME_DARK; |
| // Alpha level for the line. |
| private static final int FULL_ALPHA = Utils.FULL_ALPHA; |
| |
| 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 mSelectionAlpha; |
| |
| 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 mForceDrawDot; |
| |
| public RadialSelectorView(Context context) { |
| super(context); |
| 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) { |
| 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); |
| mSelectionAlpha = SELECTED_ALPHA; |
| |
| // Calculate values for the circle radius size. |
| 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)); |
| } |
| |
| // Calculate values for the radius size(s) of the numbers circle(s). |
| 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)); |
| |
| // 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); |
| mIsInitialized = true; |
| } |
| |
| /* package */ void setTheme(Context context, boolean themeDark) { |
| Resources res = context.getResources(); |
| int color; |
| if (themeDark) { |
| color = res.getColor(R.color.red); |
| mSelectionAlpha = SELECTED_ALPHA_THEME_DARK; |
| } else { |
| color = res.getColor(R.color.blue); |
| mSelectionAlpha = SELECTED_ALPHA; |
| } |
| mPaint.setColor(color); |
| } |
| |
| /** |
| * 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; |
| mForceDrawDot = forceDrawDot; |
| |
| if (mHasInnerCircle) { |
| if (isInnerCircle) { |
| mNumbersRadiusMultiplier = mInnerNumbersRadiusMultiplier; |
| } else { |
| mNumbersRadiusMultiplier = mOuterNumbersRadiusMultiplier; |
| } |
| } |
| } |
| |
| /** |
| * 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; |
| } |
| |
| 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; |
| } |
| |
| // Calculate the current radius at which to place the selection circle. |
| mLineLength = (int) (mCircleRadius * mNumbersRadiusMultiplier * mAnimationRadiusMultiplier); |
| int pointX = mXCenter + (int) (mLineLength * Math.sin(mSelectionRadians)); |
| int pointY = mYCenter - (int) (mLineLength * Math.cos(mSelectionRadians)); |
| |
| // Draw the selection circle. |
| mPaint.setAlpha(mSelectionAlpha); |
| canvas.drawCircle(pointX, pointY, mSelectionRadius, mPaint); |
| |
| if (mForceDrawDot | mSelectionDegrees % 30 != 0) { |
| // We're not on a direct tick (or we've been told to draw the dot anyway). |
| mPaint.setAlpha(FULL_ALPHA); |
| 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)); |
| } |
| |
| // 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() { |
| 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.25f; |
| float transitionDurationMultiplier = 1f; |
| 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; |
| } |
| |
| /** |
| * We'll need to invalidate during the animation. |
| */ |
| private class InvalidateUpdateListener implements AnimatorUpdateListener { |
| @Override |
| public void onAnimationUpdate(ValueAnimator animation) { |
| RadialSelectorView.this.invalidate(); |
| } |
| } |
| } |