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.graphics.Canvas;
21 import android.graphics.ColorFilter;
22 import android.graphics.Rect;
23 import android.graphics.drawable.Drawable;
24 
25 import com.android.mail.utils.LogUtils;
26 
27 /**
28  * A drawable that wraps two other drawables and allows flipping between them. The flipping
29  * animation is a 2D rotation around the y axis.
30  *
31  * <p/>
32  * The 3 durations are: (best viewed in documentation form)
33  * <pre>
34  * &lt;pre&gt;[_][]|[][_]&lt;post&gt;
35  *   |       |       |
36  *   V       V       V
37  * &lt;pre>&lt;   flip  &gt;&lt;post&gt;
38  * </pre>
39  */
40 public class FlipDrawable extends Drawable implements Drawable.Callback {
41 
42     /**
43      * The inner drawables.
44      */
45     protected final Drawable mFront;
46     protected final Drawable mBack;
47 
48     protected final int mFlipDurationMs;
49     protected final int mPreFlipDurationMs;
50     protected final int mPostFlipDurationMs;
51     private final ValueAnimator mFlipAnimator;
52 
53     private static final float END_VALUE = 2f;
54 
55     /**
56      * From 0f to END_VALUE. Determines the flip progress between mFront and mBack. 0f means
57      * mFront is fully shown, while END_VALUE means mBack is fully shown.
58      */
59     private float mFlipFraction = 0f;
60 
61     /**
62      * True if flipping towards front, false if flipping towards back.
63      */
64     private boolean mFlipToSide = true;
65 
66     /**
67      * Create a new FlipDrawable. The front is fully shown by default.
68      *
69      * <p/>
70      * The 3 durations are: (best viewed in documentation form)
71      * <pre>
72      * &lt;pre&gt;[_][]|[][_]&lt;post&gt;
73      *   |       |       |
74      *   V       V       V
75      * &lt;pre>&lt;   flip  &gt;&lt;post&gt;
76      * </pre>
77      *
78      * @param front              The front drawable.
79      * @param back               The back drawable.
80      * @param flipDurationMs     The duration of the actual flip. This duration includes both
81      *                           animating away one side and showing the other.
82      * @param preFlipDurationMs  The duration before the actual flip begins. Subclasses can use this
83      *                           to add flourish.
84      * @param postFlipDurationMs The duration after the actual flip begins. Subclasses can use this
85      *                           to add flourish.
86      */
FlipDrawable(final Drawable front, final Drawable back, final int flipDurationMs, final int preFlipDurationMs, final int postFlipDurationMs)87     public FlipDrawable(final Drawable front, final Drawable back, final int flipDurationMs,
88             final int preFlipDurationMs, final int postFlipDurationMs) {
89         if (front == null || back == null) {
90             throw new IllegalArgumentException("Front and back drawables must not be null.");
91         }
92         mFront = front;
93         mBack = back;
94 
95         mFront.setCallback(this);
96         mBack.setCallback(this);
97 
98         mFlipDurationMs = flipDurationMs;
99         mPreFlipDurationMs = preFlipDurationMs;
100         mPostFlipDurationMs = postFlipDurationMs;
101 
102         mFlipAnimator = ValueAnimator.ofFloat(0f, END_VALUE)
103                 .setDuration(mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs);
104         mFlipAnimator.addUpdateListener(new AnimatorUpdateListener() {
105             @Override
106             public void onAnimationUpdate(final ValueAnimator animation) {
107                 final float old = mFlipFraction;
108                 //noinspection ConstantConditions
109                 mFlipFraction = (Float) animation.getAnimatedValue();
110                 if (old != mFlipFraction) {
111                     invalidateSelf();
112                 }
113             }
114         });
115 
116         reset(true);
117     }
118 
119     @Override
onBoundsChange(final Rect bounds)120     protected void onBoundsChange(final Rect bounds) {
121         super.onBoundsChange(bounds);
122         if (bounds.isEmpty()) {
123             mFront.setBounds(0, 0, 0, 0);
124             mBack.setBounds(0, 0, 0, 0);
125         } else {
126             mFront.setBounds(bounds);
127             mBack.setBounds(bounds);
128         }
129     }
130 
131     @Override
draw(final Canvas canvas)132     public void draw(final Canvas canvas) {
133         final Rect bounds = getBounds();
134         if (!isVisible() || bounds.isEmpty()) {
135             return;
136         }
137 
138         final Drawable inner = getSideShown() /* == front */ ? mFront : mBack;
139 
140         final float totalDurationMs = mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs;
141 
142         final float scaleX;
143         if (mFlipFraction / 2 <= mPreFlipDurationMs / totalDurationMs) {
144             // During pre-flip.
145             scaleX = 1;
146         } else if (mFlipFraction / 2 >= (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) {
147             // During post-flip.
148             scaleX = 1;
149         } else {
150             // During flip.
151             final float flipFraction = mFlipFraction / 2;
152             final float flipMiddle = (mPreFlipDurationMs / totalDurationMs
153                     + (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) / 2;
154             final float distFraction = Math.abs(flipFraction - flipMiddle);
155             final float multiplier = 1 / (flipMiddle - (mPreFlipDurationMs / totalDurationMs));
156             scaleX = distFraction * multiplier;
157         }
158 
159         canvas.save();
160         // The flip is a simple 1 dimensional scale.
161         canvas.scale(scaleX, 1, bounds.exactCenterX(), bounds.exactCenterY());
162         inner.draw(canvas);
163         canvas.restore();
164     }
165 
166     @Override
setAlpha(final int alpha)167     public void setAlpha(final int alpha) {
168         mFront.setAlpha(alpha);
169         mBack.setAlpha(alpha);
170     }
171 
172     @Override
setColorFilter(final ColorFilter cf)173     public void setColorFilter(final ColorFilter cf) {
174         mFront.setColorFilter(cf);
175         mBack.setColorFilter(cf);
176     }
177 
178     @Override
getOpacity()179     public int getOpacity() {
180         return resolveOpacity(mFront.getOpacity(), mBack.getOpacity());
181     }
182 
183     @Override
onLevelChange(final int level)184     protected boolean onLevelChange(final int level) {
185         return mFront.setLevel(level) || mBack.setLevel(level);
186     }
187 
188     @Override
invalidateDrawable(final Drawable who)189     public void invalidateDrawable(final Drawable who) {
190         invalidateSelf();
191     }
192 
193     @Override
scheduleDrawable(final Drawable who, final Runnable what, final long when)194     public void scheduleDrawable(final Drawable who, final Runnable what, final long when) {
195         scheduleSelf(what, when);
196     }
197 
198     @Override
unscheduleDrawable(final Drawable who, final Runnable what)199     public void unscheduleDrawable(final Drawable who, final Runnable what) {
200         unscheduleSelf(what);
201     }
202 
203     /**
204      * Stop animating the flip and reset to one side.
205      * @param side Pass true if reset to front, false if reset to back.
206      */
reset(final boolean side)207     public void reset(final boolean side) {
208         final float old = mFlipFraction;
209         mFlipAnimator.cancel();
210         mFlipFraction = side ? 0f : 2f;
211         mFlipToSide = side;
212         if (mFlipFraction != old) {
213             invalidateSelf();
214         }
215     }
216 
217     /**
218      * Returns true if the front is shown. Returns false if the back is shown.
219      */
getSideShown()220     public boolean getSideShown() {
221         final float totalDurationMs = mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs;
222         final float middleFraction = (mPreFlipDurationMs / totalDurationMs
223                 + (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) / 2;
224         return mFlipFraction / 2 < middleFraction;
225     }
226 
227     /**
228      * Returns true if the front is being flipped towards. Returns false if the back is being
229      * flipped towards.
230      */
getSideFlippingTowards()231     public boolean getSideFlippingTowards() {
232         return mFlipToSide;
233     }
234 
235     /**
236      * Starts an animated flip to the other side. If a flip animation is currently started,
237      * it will be reversed.
238      */
flip()239     public void flip() {
240         mFlipToSide = !mFlipToSide;
241         if (mFlipAnimator.isStarted()) {
242             mFlipAnimator.reverse();
243         } else {
244             if (!mFlipToSide /* front to back */) {
245                 mFlipAnimator.start();
246             } else /* back to front */ {
247                 mFlipAnimator.reverse();
248             }
249         }
250     }
251 
252     /**
253      * Start an animated flip to a side. This works regardless of whether a flip animation is
254      * currently started.
255      * @param side Pass true if flip to front, false if flip to back.
256      */
flipTo(final boolean side)257     public void flipTo(final boolean side) {
258         if (mFlipToSide != side) {
259             flip();
260         }
261     }
262 
263     /**
264      * Returns whether flipping is in progress.
265      */
isFlipping()266     public boolean isFlipping() {
267         return mFlipAnimator.isStarted();
268     }
269 }
270