Home | History | Annotate | Download | only in time
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.datetimepicker.time;
     18 
     19 import android.animation.Keyframe;
     20 import android.animation.ObjectAnimator;
     21 import android.animation.PropertyValuesHolder;
     22 import android.animation.ValueAnimator;
     23 import android.animation.ValueAnimator.AnimatorUpdateListener;
     24 import android.content.Context;
     25 import android.content.res.Resources;
     26 import android.graphics.Canvas;
     27 import android.graphics.Paint;
     28 import android.graphics.Typeface;
     29 import android.graphics.Paint.Align;
     30 import android.util.Log;
     31 import android.view.View;
     32 
     33 import com.android.datetimepicker.R;
     34 
     35 /**
     36  * A view to show a series of numbers in a circular pattern.
     37  */
     38 public class RadialTextsView extends View {
     39     private final static String TAG = "RadialTextsView";
     40 
     41     private final Paint mPaint = new Paint();
     42 
     43     private boolean mDrawValuesReady;
     44     private boolean mIsInitialized;
     45 
     46     private Typeface mTypefaceLight;
     47     private Typeface mTypefaceRegular;
     48     private String[] mTexts;
     49     private String[] mInnerTexts;
     50     private boolean mIs24HourMode;
     51     private boolean mHasInnerCircle;
     52     private float mCircleRadiusMultiplier;
     53     private float mAmPmCircleRadiusMultiplier;
     54     private float mNumbersRadiusMultiplier;
     55     private float mInnerNumbersRadiusMultiplier;
     56     private float mTextSizeMultiplier;
     57     private float mInnerTextSizeMultiplier;
     58 
     59     private int mXCenter;
     60     private int mYCenter;
     61     private float mCircleRadius;
     62     private boolean mTextGridValuesDirty;
     63     private float mTextSize;
     64     private float mInnerTextSize;
     65     private float[] mTextGridHeights;
     66     private float[] mTextGridWidths;
     67     private float[] mInnerTextGridHeights;
     68     private float[] mInnerTextGridWidths;
     69 
     70     private float mAnimationRadiusMultiplier;
     71     private float mTransitionMidRadiusMultiplier;
     72     private float mTransitionEndRadiusMultiplier;
     73     ObjectAnimator mDisappearAnimator;
     74     ObjectAnimator mReappearAnimator;
     75     private InvalidateUpdateListener mInvalidateUpdateListener;
     76 
     77     public RadialTextsView(Context context) {
     78         super(context);
     79         mIsInitialized = false;
     80     }
     81 
     82     public void initialize(Resources res, String[] texts, String[] innerTexts,
     83             boolean is24HourMode, boolean disappearsOut) {
     84         if (mIsInitialized) {
     85             Log.e(TAG, "This RadialTextsView may only be initialized once.");
     86             return;
     87         }
     88 
     89         // Set up the paint.
     90         int numbersTextColor = res.getColor(R.color.numbers_text_color);
     91         mPaint.setColor(numbersTextColor);
     92         String typefaceFamily = res.getString(R.string.radial_numbers_typeface);
     93         mTypefaceLight = Typeface.create(typefaceFamily, Typeface.NORMAL);
     94         String typefaceFamilyRegular = res.getString(R.string.sans_serif);
     95         mTypefaceRegular = Typeface.create(typefaceFamilyRegular, Typeface.NORMAL);
     96         mPaint.setAntiAlias(true);
     97         mPaint.setTextAlign(Align.CENTER);
     98 
     99         mTexts = texts;
    100         mInnerTexts = innerTexts;
    101         mIs24HourMode = is24HourMode;
    102         mHasInnerCircle = (innerTexts != null);
    103 
    104         // Calculate the radius for the main circle.
    105         if (is24HourMode) {
    106             mCircleRadiusMultiplier = Float.parseFloat(
    107                     res.getString(R.string.circle_radius_multiplier_24HourMode));
    108         } else {
    109             mCircleRadiusMultiplier = Float.parseFloat(
    110                     res.getString(R.string.circle_radius_multiplier));
    111             mAmPmCircleRadiusMultiplier =
    112                     Float.parseFloat(res.getString(R.string.ampm_circle_radius_multiplier));
    113         }
    114 
    115         // Initialize the widths and heights of the grid, and calculate the values for the numbers.
    116         mTextGridHeights = new float[7];
    117         mTextGridWidths = new float[7];
    118         if (mHasInnerCircle) {
    119             mNumbersRadiusMultiplier = Float.parseFloat(
    120                     res.getString(R.string.numbers_radius_multiplier_outer));
    121             mTextSizeMultiplier = Float.parseFloat(
    122                     res.getString(R.string.text_size_multiplier_outer));
    123             mInnerNumbersRadiusMultiplier = Float.parseFloat(
    124                     res.getString(R.string.numbers_radius_multiplier_inner));
    125             mInnerTextSizeMultiplier = Float.parseFloat(
    126                     res.getString(R.string.text_size_multiplier_inner));
    127 
    128             mInnerTextGridHeights = new float[7];
    129             mInnerTextGridWidths = new float[7];
    130         } else {
    131             mNumbersRadiusMultiplier = Float.parseFloat(
    132                     res.getString(R.string.numbers_radius_multiplier_normal));
    133             mTextSizeMultiplier = Float.parseFloat(
    134                     res.getString(R.string.text_size_multiplier_normal));
    135         }
    136 
    137         mAnimationRadiusMultiplier = 1;
    138         mTransitionMidRadiusMultiplier = 1f + (0.05f * (disappearsOut? -1 : 1));
    139         mTransitionEndRadiusMultiplier = 1f + (0.3f * (disappearsOut? 1 : -1));
    140         mInvalidateUpdateListener = new InvalidateUpdateListener();
    141 
    142         mTextGridValuesDirty = true;
    143         mIsInitialized = true;
    144     }
    145 
    146     /* package */ void setTheme(Context context, boolean themeDark) {
    147         Resources res = context.getResources();
    148         int textColor;
    149         if (themeDark) {
    150             textColor = res.getColor(R.color.white);
    151         } else {
    152             textColor = res.getColor(R.color.numbers_text_color);
    153         }
    154         mPaint.setColor(textColor);
    155     }
    156 
    157     /**
    158      * Allows for smoother animation.
    159      */
    160     @Override
    161     public boolean hasOverlappingRendering() {
    162         return false;
    163     }
    164 
    165     /**
    166      * Used by the animation to move the numbers in and out.
    167      */
    168     public void setAnimationRadiusMultiplier(float animationRadiusMultiplier) {
    169         mAnimationRadiusMultiplier = animationRadiusMultiplier;
    170         mTextGridValuesDirty = true;
    171     }
    172 
    173     @Override
    174     public void onDraw(Canvas canvas) {
    175         int viewWidth = getWidth();
    176         if (viewWidth == 0 || !mIsInitialized) {
    177             return;
    178         }
    179 
    180         if (!mDrawValuesReady) {
    181             mXCenter = getWidth() / 2;
    182             mYCenter = getHeight() / 2;
    183             mCircleRadius = Math.min(mXCenter, mYCenter) * mCircleRadiusMultiplier;
    184             if (!mIs24HourMode) {
    185                 // We'll need to draw the AM/PM circles, so the main circle will need to have
    186                 // a slightly higher center. To keep the entire view centered vertically, we'll
    187                 // have to push it up by half the radius of the AM/PM circles.
    188                 float amPmCircleRadius = mCircleRadius * mAmPmCircleRadiusMultiplier;
    189                 mYCenter -= amPmCircleRadius / 2;
    190             }
    191 
    192             mTextSize = mCircleRadius * mTextSizeMultiplier;
    193             if (mHasInnerCircle) {
    194                 mInnerTextSize = mCircleRadius * mInnerTextSizeMultiplier;
    195             }
    196 
    197             // Because the text positions will be static, pre-render the animations.
    198             renderAnimations();
    199 
    200             mTextGridValuesDirty = true;
    201             mDrawValuesReady = true;
    202         }
    203 
    204         // Calculate the text positions, but only if they've changed since the last onDraw.
    205         if (mTextGridValuesDirty) {
    206             float numbersRadius =
    207                     mCircleRadius * mNumbersRadiusMultiplier * mAnimationRadiusMultiplier;
    208 
    209             // Calculate the positions for the 12 numbers in the main circle.
    210             calculateGridSizes(numbersRadius, mXCenter, mYCenter,
    211                     mTextSize, mTextGridHeights, mTextGridWidths);
    212             if (mHasInnerCircle) {
    213                 // If we have an inner circle, calculate those positions too.
    214                 float innerNumbersRadius =
    215                         mCircleRadius * mInnerNumbersRadiusMultiplier * mAnimationRadiusMultiplier;
    216                 calculateGridSizes(innerNumbersRadius, mXCenter, mYCenter,
    217                         mInnerTextSize, mInnerTextGridHeights, mInnerTextGridWidths);
    218             }
    219             mTextGridValuesDirty = false;
    220         }
    221 
    222         // Draw the texts in the pre-calculated positions.
    223         drawTexts(canvas, mTextSize, mTypefaceLight, mTexts, mTextGridWidths, mTextGridHeights);
    224         if (mHasInnerCircle) {
    225             drawTexts(canvas, mInnerTextSize, mTypefaceRegular, mInnerTexts,
    226                     mInnerTextGridWidths, mInnerTextGridHeights);
    227         }
    228     }
    229 
    230     /**
    231      * Using the trigonometric Unit Circle, calculate the positions that the text will need to be
    232      * drawn at based on the specified circle radius. Place the values in the textGridHeights and
    233      * textGridWidths parameters.
    234      */
    235     private void calculateGridSizes(float numbersRadius, float xCenter, float yCenter,
    236             float textSize, float[] textGridHeights, float[] textGridWidths) {
    237         /*
    238          * The numbers need to be drawn in a 7x7 grid, representing the points on the Unit Circle.
    239          */
    240         float offset1 = numbersRadius;
    241         // cos(30) = a / r => r * cos(30) = a => r * 3/2 = a
    242         float offset2 = numbersRadius * ((float) Math.sqrt(3)) / 2f;
    243         // sin(30) = o / r => r * sin(30) = o => r / 2 = a
    244         float offset3 = numbersRadius / 2f;
    245         mPaint.setTextSize(textSize);
    246         // We'll need yTextBase to be slightly lower to account for the text's baseline.
    247         yCenter -= (mPaint.descent() + mPaint.ascent()) / 2;
    248 
    249         textGridHeights[0] = yCenter - offset1;
    250         textGridWidths[0] = xCenter - offset1;
    251         textGridHeights[1] = yCenter - offset2;
    252         textGridWidths[1] = xCenter - offset2;
    253         textGridHeights[2] = yCenter - offset3;
    254         textGridWidths[2] = xCenter - offset3;
    255         textGridHeights[3] = yCenter;
    256         textGridWidths[3] = xCenter;
    257         textGridHeights[4] = yCenter + offset3;
    258         textGridWidths[4] = xCenter + offset3;
    259         textGridHeights[5] = yCenter + offset2;
    260         textGridWidths[5] = xCenter + offset2;
    261         textGridHeights[6] = yCenter + offset1;
    262         textGridWidths[6] = xCenter + offset1;
    263     }
    264 
    265     /**
    266      * Draw the 12 text values at the positions specified by the textGrid parameters.
    267      */
    268     private void drawTexts(Canvas canvas, float textSize, Typeface typeface, String[] texts,
    269             float[] textGridWidths, float[] textGridHeights) {
    270         mPaint.setTextSize(textSize);
    271         mPaint.setTypeface(typeface);
    272         canvas.drawText(texts[0], textGridWidths[3], textGridHeights[0], mPaint);
    273         canvas.drawText(texts[1], textGridWidths[4], textGridHeights[1], mPaint);
    274         canvas.drawText(texts[2], textGridWidths[5], textGridHeights[2], mPaint);
    275         canvas.drawText(texts[3], textGridWidths[6], textGridHeights[3], mPaint);
    276         canvas.drawText(texts[4], textGridWidths[5], textGridHeights[4], mPaint);
    277         canvas.drawText(texts[5], textGridWidths[4], textGridHeights[5], mPaint);
    278         canvas.drawText(texts[6], textGridWidths[3], textGridHeights[6], mPaint);
    279         canvas.drawText(texts[7], textGridWidths[2], textGridHeights[5], mPaint);
    280         canvas.drawText(texts[8], textGridWidths[1], textGridHeights[4], mPaint);
    281         canvas.drawText(texts[9], textGridWidths[0], textGridHeights[3], mPaint);
    282         canvas.drawText(texts[10], textGridWidths[1], textGridHeights[2], mPaint);
    283         canvas.drawText(texts[11], textGridWidths[2], textGridHeights[1], mPaint);
    284     }
    285 
    286     /**
    287      * Render the animations for appearing and disappearing.
    288      */
    289     private void renderAnimations() {
    290         Keyframe kf0, kf1, kf2, kf3;
    291         float midwayPoint = 0.2f;
    292         int duration = 500;
    293 
    294         // Set up animator for disappearing.
    295         kf0 = Keyframe.ofFloat(0f, 1);
    296         kf1 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier);
    297         kf2 = Keyframe.ofFloat(1f, mTransitionEndRadiusMultiplier);
    298         PropertyValuesHolder radiusDisappear = PropertyValuesHolder.ofKeyframe(
    299                 "animationRadiusMultiplier", kf0, kf1, kf2);
    300 
    301         kf0 = Keyframe.ofFloat(0f, 1f);
    302         kf1 = Keyframe.ofFloat(1f, 0f);
    303         PropertyValuesHolder fadeOut = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1);
    304 
    305         mDisappearAnimator = ObjectAnimator.ofPropertyValuesHolder(
    306                 this, radiusDisappear, fadeOut).setDuration(duration);
    307         mDisappearAnimator.addUpdateListener(mInvalidateUpdateListener);
    308 
    309 
    310         // Set up animator for reappearing.
    311         float delayMultiplier = 0.25f;
    312         float transitionDurationMultiplier = 1f;
    313         float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier;
    314         int totalDuration = (int) (duration * totalDurationMultiplier);
    315         float delayPoint = (delayMultiplier * duration) / totalDuration;
    316         midwayPoint = 1 - (midwayPoint * (1 - delayPoint));
    317 
    318         kf0 = Keyframe.ofFloat(0f, mTransitionEndRadiusMultiplier);
    319         kf1 = Keyframe.ofFloat(delayPoint, mTransitionEndRadiusMultiplier);
    320         kf2 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier);
    321         kf3 = Keyframe.ofFloat(1f, 1);
    322         PropertyValuesHolder radiusReappear = PropertyValuesHolder.ofKeyframe(
    323                 "animationRadiusMultiplier", kf0, kf1, kf2, kf3);
    324 
    325         kf0 = Keyframe.ofFloat(0f, 0f);
    326         kf1 = Keyframe.ofFloat(delayPoint, 0f);
    327         kf2 = Keyframe.ofFloat(1f, 1f);
    328         PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1, kf2);
    329 
    330         mReappearAnimator = ObjectAnimator.ofPropertyValuesHolder(
    331                 this, radiusReappear, fadeIn).setDuration(totalDuration);
    332         mReappearAnimator.addUpdateListener(mInvalidateUpdateListener);
    333     }
    334 
    335     public ObjectAnimator getDisappearAnimator() {
    336         if (!mIsInitialized || !mDrawValuesReady || mDisappearAnimator == null) {
    337             Log.e(TAG, "RadialTextView was not ready for animation.");
    338             return null;
    339         }
    340 
    341         return mDisappearAnimator;
    342     }
    343 
    344     public ObjectAnimator getReappearAnimator() {
    345         if (!mIsInitialized || !mDrawValuesReady || mReappearAnimator == null) {
    346             Log.e(TAG, "RadialTextView was not ready for animation.");
    347             return null;
    348         }
    349 
    350         return mReappearAnimator;
    351     }
    352 
    353     private class InvalidateUpdateListener implements AnimatorUpdateListener {
    354         @Override
    355         public void onAnimationUpdate(ValueAnimator animation) {
    356             RadialTextsView.this.invalidate();
    357         }
    358     }
    359 }
    360