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 */
16package com.android.mail.bitmap;
17
18import android.animation.ValueAnimator;
19import android.animation.ValueAnimator.AnimatorUpdateListener;
20import android.graphics.Canvas;
21import android.graphics.ColorFilter;
22import android.graphics.Rect;
23import android.graphics.drawable.Drawable;
24
25import 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 */
40public 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     */
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
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
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
167    public void setAlpha(final int alpha) {
168        mFront.setAlpha(alpha);
169        mBack.setAlpha(alpha);
170    }
171
172    @Override
173    public void setColorFilter(final ColorFilter cf) {
174        mFront.setColorFilter(cf);
175        mBack.setColorFilter(cf);
176    }
177
178    @Override
179    public int getOpacity() {
180        return resolveOpacity(mFront.getOpacity(), mBack.getOpacity());
181    }
182
183    @Override
184    protected boolean onLevelChange(final int level) {
185        return mFront.setLevel(level) || mBack.setLevel(level);
186    }
187
188    @Override
189    public void invalidateDrawable(final Drawable who) {
190        invalidateSelf();
191    }
192
193    @Override
194    public void scheduleDrawable(final Drawable who, final Runnable what, final long when) {
195        scheduleSelf(what, when);
196    }
197
198    @Override
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     */
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     */
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     */
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     */
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     */
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     */
266    public boolean isFlipping() {
267        return mFlipAnimator.isStarted();
268    }
269}
270