Home | History | Annotate | Download | only in bitmap
      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      */
     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