/* * 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 org.holoeverywhere.widget.datetimepicker.time; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.util.AttributeSet; import android.util.Log; import android.view.View; import com.nineoldandroids.animation.Keyframe; import com.nineoldandroids.animation.ObjectAnimator; import com.nineoldandroids.animation.PropertyValuesHolder; import com.nineoldandroids.animation.ValueAnimator; import com.nineoldandroids.view.animation.AnimatorProxy; import org.holoeverywhere.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"; private final Paint mPaint = new Paint(); private final AnimatorProxy mProxy = AnimatorProxy.NEEDS_PROXY ? AnimatorProxy.wrap(this) : null; 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 int mLineLength; private int mSelectionRadius; private InvalidateUpdateListener mInvalidateUpdateListener; private int mSelectionDegrees; private double mSelectionRadians; private boolean mForceDrawDot; public RadialSelectorView(Context context) { this(context, null); } public RadialSelectorView(Context context, AttributeSet attrs) { this(context, attrs, R.attr.dateTimePickerStyle); } public RadialSelectorView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mIsInitialized = false; TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DateTimePicker, defStyle, R.style.Holo_DateTimePicker); mPaint.setColor(a.getColor(R.styleable.DateTimePicker_timePickerSelectorColor, 0)); a.recycle(); } @Override public float getAlpha() { if (mProxy != null) { return mProxy.getAlpha(); } else { return super.getAlpha(); } } @Override public void setAlpha(float alpha) { if (mProxy != null) { mProxy.setAlpha(alpha); } else { super.setAlpha(alpha); } } /** * 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(); mPaint.setAntiAlias(true); // Calculate values for the circle radius size. mIs24HourMode = is24HourMode; if (is24HourMode) { mCircleRadiusMultiplier = Float.parseFloat( res.getString(R.string.time_circle_radius_multiplier_24HourMode)); } else { mCircleRadiusMultiplier = Float.parseFloat( res.getString(R.string.time_circle_radius_multiplier)); mAmPmCircleRadiusMultiplier = Float.parseFloat(res.getString(R.string.time_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.time_numbers_radius_multiplier_inner)); mOuterNumbersRadiusMultiplier = Float.parseFloat(res.getString(R.string.time_numbers_radius_multiplier_outer)); } else { mNumbersRadiusMultiplier = Float.parseFloat(res.getString(R.string.time_time_numbers_radius_multiplier_normal)); } mSelectionRadiusMultiplier = Float.parseFloat(res.getString(R.string.time_selection_radius_multiplier)); // Calculate values for the transition mid-way states. mAnimationRadiusMultiplier = 1; mInvalidateUpdateListener = new InvalidateUpdateListener(); setSelection(selectionDegrees, isInnerCircle, false); mIsInitialized = true; } /** * 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; } 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) { degrees = 90 + degrees; } else if (!topSide) { degrees = 270 - degrees; } else { 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(51); 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(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)); } // 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; kf0 = Keyframe.ofFloat(0f, 1f); kf1 = Keyframe.ofFloat(1f, 0f); PropertyValuesHolder fade = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1); ObjectAnimator disappearAnimator = ObjectAnimator.ofPropertyValuesHolder(this, fade).setDuration(300); 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; kf0 = Keyframe.ofFloat(0f, 0f); kf1 = Keyframe.ofFloat(1f, 1f); PropertyValuesHolder fade = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1); ObjectAnimator reappearAnimator = ObjectAnimator.ofPropertyValuesHolder(this, fade).setDuration(300); reappearAnimator.addUpdateListener(mInvalidateUpdateListener); return reappearAnimator; } /** * We'll need to invalidate during the animation. */ private class InvalidateUpdateListener implements ValueAnimator.AnimatorUpdateListener { @Override public void onAnimationUpdate(ValueAnimator animation) { RadialSelectorView.this.invalidate(); } } }