/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.mail.bitmap; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Rect; import android.graphics.drawable.Drawable; import com.android.mail.utils.LogUtils; /** * A drawable that wraps two other drawables and allows flipping between them. The flipping * animation is a 2D rotation around the y axis. * *
* The 3 durations are: (best viewed in documentation form) ** <pre>[_][]|[][_]<post> * | | | * V V V * <pre>< flip ><post> **/ public class FlipDrawable extends Drawable implements Drawable.Callback { /** * The inner drawables. */ protected final Drawable mFront; protected final Drawable mBack; protected final int mFlipDurationMs; protected final int mPreFlipDurationMs; protected final int mPostFlipDurationMs; private final ValueAnimator mFlipAnimator; private static final float END_VALUE = 2f; /** * From 0f to END_VALUE. Determines the flip progress between mFront and mBack. 0f means * mFront is fully shown, while END_VALUE means mBack is fully shown. */ private float mFlipFraction = 0f; /** * True if flipping towards front, false if flipping towards back. */ private boolean mFlipToSide = true; /** * Create a new FlipDrawable. The front is fully shown by default. * * * The 3 durations are: (best viewed in documentation form) *
* <pre>[_][]|[][_]<post>
* | | |
* V V V
* <pre>< flip ><post>
*
*
* @param front The front drawable.
* @param back The back drawable.
* @param flipDurationMs The duration of the actual flip. This duration includes both
* animating away one side and showing the other.
* @param preFlipDurationMs The duration before the actual flip begins. Subclasses can use this
* to add flourish.
* @param postFlipDurationMs The duration after the actual flip begins. Subclasses can use this
* to add flourish.
*/
public FlipDrawable(final Drawable front, final Drawable back, final int flipDurationMs,
final int preFlipDurationMs, final int postFlipDurationMs) {
if (front == null || back == null) {
throw new IllegalArgumentException("Front and back drawables must not be null.");
}
mFront = front;
mBack = back;
mFront.setCallback(this);
mBack.setCallback(this);
mFlipDurationMs = flipDurationMs;
mPreFlipDurationMs = preFlipDurationMs;
mPostFlipDurationMs = postFlipDurationMs;
mFlipAnimator = ValueAnimator.ofFloat(0f, END_VALUE)
.setDuration(mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs);
mFlipAnimator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(final ValueAnimator animation) {
final float old = mFlipFraction;
//noinspection ConstantConditions
mFlipFraction = (Float) animation.getAnimatedValue();
if (old != mFlipFraction) {
invalidateSelf();
}
}
});
reset(true);
}
@Override
protected void onBoundsChange(final Rect bounds) {
super.onBoundsChange(bounds);
if (bounds.isEmpty()) {
mFront.setBounds(0, 0, 0, 0);
mBack.setBounds(0, 0, 0, 0);
} else {
mFront.setBounds(bounds);
mBack.setBounds(bounds);
}
}
@Override
public void draw(final Canvas canvas) {
final Rect bounds = getBounds();
if (!isVisible() || bounds.isEmpty()) {
return;
}
final Drawable inner = getSideShown() /* == front */ ? mFront : mBack;
final float totalDurationMs = mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs;
final float scaleX;
if (mFlipFraction / 2 <= mPreFlipDurationMs / totalDurationMs) {
// During pre-flip.
scaleX = 1;
} else if (mFlipFraction / 2 >= (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) {
// During post-flip.
scaleX = 1;
} else {
// During flip.
final float flipFraction = mFlipFraction / 2;
final float flipMiddle = (mPreFlipDurationMs / totalDurationMs
+ (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) / 2;
final float distFraction = Math.abs(flipFraction - flipMiddle);
final float multiplier = 1 / (flipMiddle - (mPreFlipDurationMs / totalDurationMs));
scaleX = distFraction * multiplier;
}
canvas.save();
// The flip is a simple 1 dimensional scale.
canvas.scale(scaleX, 1, bounds.exactCenterX(), bounds.exactCenterY());
inner.draw(canvas);
canvas.restore();
}
@Override
public void setAlpha(final int alpha) {
mFront.setAlpha(alpha);
mBack.setAlpha(alpha);
}
@Override
public void setColorFilter(final ColorFilter cf) {
mFront.setColorFilter(cf);
mBack.setColorFilter(cf);
}
@Override
public int getOpacity() {
return resolveOpacity(mFront.getOpacity(), mBack.getOpacity());
}
@Override
protected boolean onLevelChange(final int level) {
return mFront.setLevel(level) || mBack.setLevel(level);
}
@Override
public void invalidateDrawable(final Drawable who) {
invalidateSelf();
}
@Override
public void scheduleDrawable(final Drawable who, final Runnable what, final long when) {
scheduleSelf(what, when);
}
@Override
public void unscheduleDrawable(final Drawable who, final Runnable what) {
unscheduleSelf(what);
}
/**
* Stop animating the flip and reset to one side.
* @param side Pass true if reset to front, false if reset to back.
*/
public void reset(final boolean side) {
final float old = mFlipFraction;
mFlipAnimator.cancel();
mFlipFraction = side ? 0f : 2f;
mFlipToSide = side;
if (mFlipFraction != old) {
invalidateSelf();
}
}
/**
* Returns true if the front is shown. Returns false if the back is shown.
*/
public boolean getSideShown() {
final float totalDurationMs = mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs;
final float middleFraction = (mPreFlipDurationMs / totalDurationMs
+ (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) / 2;
return mFlipFraction / 2 < middleFraction;
}
/**
* Returns true if the front is being flipped towards. Returns false if the back is being
* flipped towards.
*/
public boolean getSideFlippingTowards() {
return mFlipToSide;
}
/**
* Starts an animated flip to the other side. If a flip animation is currently started,
* it will be reversed.
*/
public void flip() {
mFlipToSide = !mFlipToSide;
if (mFlipAnimator.isStarted()) {
mFlipAnimator.reverse();
} else {
if (!mFlipToSide /* front to back */) {
mFlipAnimator.start();
} else /* back to front */ {
mFlipAnimator.reverse();
}
}
}
/**
* Start an animated flip to a side. This works regardless of whether a flip animation is
* currently started.
* @param side Pass true if flip to front, false if flip to back.
*/
public void flipTo(final boolean side) {
if (mFlipToSide != side) {
flip();
}
}
/**
* Returns whether flipping is in progress.
*/
public boolean isFlipping() {
return mFlipAnimator.isStarted();
}
}