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 package com.android.mail.bitmap;
17 
18 import android.animation.ValueAnimator;
19 import android.animation.ValueAnimator.AnimatorUpdateListener;
20 import android.content.res.Resources;
21 import android.graphics.Bitmap;
22 import android.graphics.BitmapFactory;
23 import android.graphics.Canvas;
24 import android.graphics.ColorFilter;
25 import android.graphics.Matrix;
26 import android.graphics.Paint;
27 import android.graphics.PixelFormat;
28 import android.graphics.Rect;
29 import android.graphics.drawable.Drawable;
30 
31 import com.android.mail.R;
32 
33 /**
34  * Custom FlipDrawable which has a {@link ContactDrawable} on the front,
35  * and a {@link CheckmarkDrawable} on the back.
36  */
37 public class CheckableContactFlipDrawable extends FlipDrawable implements AnimatorUpdateListener {
38 
39     private final ContactDrawable mContactDrawable;
40     private final CheckmarkDrawable mCheckmarkDrawable;
41 
42     private final ValueAnimator mCheckmarkScaleAnimator;
43     private final ValueAnimator mCheckmarkAlphaAnimator;
44 
45     private static final int POST_FLIP_DURATION_MS = 150;
46 
47     private static final float CHECKMARK_SCALE_BEGIN_VALUE = 0.2f;
48     private static final float CHECKMARK_ALPHA_BEGIN_VALUE = 0f;
49 
50     /** Must be <= 1f since the animation value is used as a percentage. */
51     private static final float END_VALUE = 1f;
52 
CheckableContactFlipDrawable(final Resources res, final int flipDurationMs)53     public CheckableContactFlipDrawable(final Resources res, final int flipDurationMs) {
54         super(new ContactDrawable(res), new CheckmarkDrawable(res), flipDurationMs,
55                 0 /* preFlipDurationMs */, POST_FLIP_DURATION_MS);
56 
57         mContactDrawable = (ContactDrawable) mFront;
58         mCheckmarkDrawable = (CheckmarkDrawable) mBack;
59 
60         // We will create checkmark animations that are synchronized with the flipping animation.
61         // The entire delay + duration of the checkmark animation needs to equal the entire
62         // duration of the flip animation (where delay is 0).
63 
64         // The checkmark animation is in effect only when the back drawable is being shown.
65         // For the flip animation duration    <pre>[_][]|[][_]<post>
66         // The checkmark animation will be    |--delay--|-duration-|
67 
68         // Need delay to skip the first half of the flip duration.
69         final long animationDelay = mPreFlipDurationMs + mFlipDurationMs / 2;
70         // Actual duration is the second half of the flip duration.
71         final long animationDuration = mFlipDurationMs / 2 + mPostFlipDurationMs;
72 
73         mCheckmarkScaleAnimator = ValueAnimator.ofFloat(CHECKMARK_SCALE_BEGIN_VALUE, END_VALUE)
74                 .setDuration(animationDuration);
75         mCheckmarkScaleAnimator.setStartDelay(animationDelay);
76         mCheckmarkScaleAnimator.addUpdateListener(this);
77 
78         mCheckmarkAlphaAnimator = ValueAnimator.ofFloat(CHECKMARK_ALPHA_BEGIN_VALUE, END_VALUE)
79                 .setDuration(animationDuration);
80         mCheckmarkAlphaAnimator.setStartDelay(animationDelay);
81         mCheckmarkAlphaAnimator.addUpdateListener(this);
82     }
83 
84     @Override
reset(final boolean side)85     public void reset(final boolean side) {
86         super.reset(side);
87         if (mCheckmarkScaleAnimator == null) {
88             // Call from super's constructor. Not yet initialized.
89             return;
90         }
91         mCheckmarkScaleAnimator.cancel();
92         mCheckmarkAlphaAnimator.cancel();
93         mCheckmarkDrawable.setScaleAnimatorValue(side ? CHECKMARK_SCALE_BEGIN_VALUE : END_VALUE);
94         mCheckmarkDrawable.setAlphaAnimatorValue(side ? CHECKMARK_ALPHA_BEGIN_VALUE : END_VALUE);
95     }
96 
97     @Override
flip()98     public void flip() {
99         super.flip();
100         // Keep the checkmark animators in sync with the flip animator.
101         if (mCheckmarkScaleAnimator.isStarted()) {
102             mCheckmarkScaleAnimator.reverse();
103             mCheckmarkAlphaAnimator.reverse();
104         } else {
105             if (!getSideFlippingTowards() /* front to back */) {
106                 mCheckmarkScaleAnimator.start();
107                 mCheckmarkAlphaAnimator.start();
108             } else /* back to front */ {
109                 mCheckmarkScaleAnimator.reverse();
110                 mCheckmarkAlphaAnimator.reverse();
111             }
112         }
113     }
114 
getContactDrawable()115     public ContactDrawable getContactDrawable() {
116         return mContactDrawable;
117     }
118 
119     @Override
onAnimationUpdate(final ValueAnimator animation)120     public void onAnimationUpdate(final ValueAnimator animation) {
121         //noinspection ConstantConditions
122         final float value = (Float) animation.getAnimatedValue();
123 
124         if (animation == mCheckmarkScaleAnimator) {
125             mCheckmarkDrawable.setScaleAnimatorValue(value);
126         } else if (animation == mCheckmarkAlphaAnimator) {
127             mCheckmarkDrawable.setAlphaAnimatorValue(value);
128         }
129     }
130 
131     /**
132      * Meant to be used as the with a FlipDrawable. The animator driving this Drawable should be
133      * more or less in sync with the containing FlipDrawable's flip animator.
134      */
135     private static class CheckmarkDrawable extends Drawable {
136 
137         private static Bitmap CHECKMARK;
138         private static int sBackgroundColor;
139 
140         private final Paint mPaint;
141 
142         private float mScaleFraction;
143         private float mAlphaFraction;
144 
145         private static final Matrix sMatrix = new Matrix();
146 
CheckmarkDrawable(final Resources res)147         public CheckmarkDrawable(final Resources res) {
148             if (CHECKMARK == null) {
149                 CHECKMARK = BitmapFactory.decodeResource(res, R.drawable.ic_check_wht_24dp);
150                 sBackgroundColor = res.getColor(R.color.checkmark_tile_background_color);
151             }
152             mPaint = new Paint();
153             mPaint.setAntiAlias(true);
154             mPaint.setFilterBitmap(true);
155             mPaint.setColor(sBackgroundColor);
156         }
157 
158         @Override
draw(final Canvas canvas)159         public void draw(final Canvas canvas) {
160             final Rect bounds = getBounds();
161             if (!isVisible() || bounds.isEmpty()) {
162                 return;
163             }
164 
165             canvas.drawCircle(bounds.centerX(), bounds.centerY(), bounds.width() / 2, mPaint);
166 
167             // Scale the checkmark.
168             sMatrix.reset();
169             sMatrix.setScale(mScaleFraction, mScaleFraction, CHECKMARK.getWidth() / 2,
170                     CHECKMARK.getHeight() / 2);
171             sMatrix.postTranslate(bounds.centerX() - CHECKMARK.getWidth() / 2,
172                     bounds.centerY() - CHECKMARK.getHeight() / 2);
173 
174             // Fade the checkmark.
175             final int oldAlpha = mPaint.getAlpha();
176             // Interpolate the alpha.
177             mPaint.setAlpha((int) (oldAlpha * mAlphaFraction));
178             canvas.drawBitmap(CHECKMARK, sMatrix, mPaint);
179             // Restore the alpha.
180             mPaint.setAlpha(oldAlpha);
181         }
182 
183         @Override
setAlpha(final int alpha)184         public void setAlpha(final int alpha) {
185             mPaint.setAlpha(alpha);
186         }
187 
188         @Override
setColorFilter(final ColorFilter cf)189         public void setColorFilter(final ColorFilter cf) {
190             mPaint.setColorFilter(cf);
191         }
192 
193         @Override
getOpacity()194         public int getOpacity() {
195             // Always a gray background.
196             return PixelFormat.OPAQUE;
197         }
198 
199         /**
200          * Set value as a fraction from 0f to 1f.
201          */
setScaleAnimatorValue(final float value)202         public void setScaleAnimatorValue(final float value) {
203             final float old = mScaleFraction;
204             mScaleFraction = value;
205             if (old != mScaleFraction) {
206                 invalidateSelf();
207             }
208         }
209 
210         /**
211          * Set value as a fraction from 0f to 1f.
212          */
setAlphaAnimatorValue(final float value)213         public void setAlphaAnimatorValue(final float value) {
214             final float old = mAlphaFraction;
215             mAlphaFraction = value;
216             if (old != mAlphaFraction) {
217                 invalidateSelf();
218             }
219         }
220     }
221 }
222