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 * <pre>[_][]|[][_]<post> 35 * | | | 36 * V V V 37 * <pre>< flip ><post> 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 * <pre>[_][]|[][_]<post> 73 * | | | 74 * V V V 75 * <pre>< flip ><post> 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