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 */ 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