summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanny Baumann <dannybaumann@web.de>2014-12-08 09:18:06 +0100
committerGerrit Code Review <gerrit@cyanogenmod.org>2014-12-10 22:07:16 +0000
commit5d3fe79d9ce4800084d5826041b13f0ae01408a6 (patch)
treeb9781677c2059dfe0c5bcf1a8a585640f84b0821
parent735ed91ec5c35024969c57153c8bba69315ae291 (diff)
downloadandroid_packages_apps_ContactsCommon-5d3fe79d9ce4800084d5826041b13f0ae01408a6.tar.gz
android_packages_apps_ContactsCommon-5d3fe79d9ce4800084d5826041b13f0ae01408a6.tar.bz2
android_packages_apps_ContactsCommon-5d3fe79d9ce4800084d5826041b13f0ae01408a6.zip
Add a checkable QuickContactBadge.
To be used from multi picker UI. FlipDrawable and CheckableFlipDrawable were kanged from UnifiedEmail. Change-Id: Ic910071da9314526c3d75ab354b62645399824ce
-rw-r--r--res/drawable-hdpi/ic_check_wht_24dp.pngbin0 -> 341 bytes
-rw-r--r--res/drawable-mdpi/ic_check_wht_24dp.pngbin0 -> 295 bytes
-rw-r--r--res/drawable-xhdpi/ic_check_wht_24dp.pngbin0 -> 402 bytes
-rw-r--r--res/drawable-xxhdpi/ic_check_wht_24dp.pngbin0 -> 449 bytes
-rw-r--r--res/values/attrs.xml1
-rwxr-xr-xsrc/com/android/contacts/common/list/ContactListItemView.java19
-rw-r--r--src/com/android/contacts/common/widget/CheckableFlipDrawable.java220
-rw-r--r--src/com/android/contacts/common/widget/CheckableImageView.java103
-rw-r--r--src/com/android/contacts/common/widget/CheckableQuickContactBadge.java103
-rw-r--r--src/com/android/contacts/common/widget/FlipDrawable.java276
10 files changed, 718 insertions, 4 deletions
diff --git a/res/drawable-hdpi/ic_check_wht_24dp.png b/res/drawable-hdpi/ic_check_wht_24dp.png
new file mode 100644
index 00000000..12ce8e0d
--- /dev/null
+++ b/res/drawable-hdpi/ic_check_wht_24dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_check_wht_24dp.png b/res/drawable-mdpi/ic_check_wht_24dp.png
new file mode 100644
index 00000000..c7de7050
--- /dev/null
+++ b/res/drawable-mdpi/ic_check_wht_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_check_wht_24dp.png b/res/drawable-xhdpi/ic_check_wht_24dp.png
new file mode 100644
index 00000000..e34b73e5
--- /dev/null
+++ b/res/drawable-xhdpi/ic_check_wht_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_check_wht_24dp.png b/res/drawable-xxhdpi/ic_check_wht_24dp.png
new file mode 100644
index 00000000..4c6a653f
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_check_wht_24dp.png
Binary files differ
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index b098c0b5..921469ff 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -17,6 +17,7 @@
<resources>
<declare-styleable name="Theme">
<attr name="android:textColorSecondary" />
+ <attr name="android:colorPrimary" />
</declare-styleable>
<declare-styleable name="ContactsDataKind">
diff --git a/src/com/android/contacts/common/list/ContactListItemView.java b/src/com/android/contacts/common/list/ContactListItemView.java
index 146a4a29..5f57376e 100755
--- a/src/com/android/contacts/common/list/ContactListItemView.java
+++ b/src/com/android/contacts/common/list/ContactListItemView.java
@@ -52,6 +52,8 @@ import com.android.contacts.common.R;
import com.android.contacts.common.format.TextHighlighter;
import com.android.contacts.common.util.SearchUtil;
import com.android.contacts.common.util.ViewUtil;
+import com.android.contacts.common.widget.CheckableImageView;
+import com.android.contacts.common.widget.CheckableQuickContactBadge;
import com.google.common.collect.Lists;
@@ -152,9 +154,9 @@ public class ContactListItemView extends ViewGroup
// The views inside the contact view
private boolean mQuickContactEnabled = true;
private boolean mQuickCallButtonEnabled = false;
- private QuickContactBadge mQuickContact;
+ private CheckableQuickContactBadge mQuickContact;
private ImageView mQuickCallView;
- private ImageView mPhotoView;
+ private CheckableImageView mPhotoView;
private TextView mNameTextView;
private TextView mPhoneticNameTextView;
private TextView mLabelView;
@@ -853,7 +855,7 @@ public class ContactListItemView extends ViewGroup
throw new IllegalStateException("QuickContact is disabled for this view");
}
if (mQuickContact == null) {
- mQuickContact = new QuickContactBadge(getContext());
+ mQuickContact = new CheckableQuickContactBadge(getContext());
mQuickContact.setOverlay(null);
mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams());
if (mNameTextView != null) {
@@ -867,12 +869,21 @@ public class ContactListItemView extends ViewGroup
return mQuickContact;
}
+ public void setChecked(boolean checked, boolean animate) {
+ if (mQuickContact != null) {
+ mQuickContact.setChecked(checked, animate);
+ }
+ if (mPhotoView != null) {
+ mPhotoView.setChecked(checked, animate);
+ }
+ }
+
/**
* Returns the photo view, creating it if necessary.
*/
public ImageView getPhotoView() {
if (mPhotoView == null) {
- mPhotoView = new ImageView(getContext());
+ mPhotoView = new CheckableImageView(getContext());
mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams());
// Quick contact style used above will set a background - remove it
mPhotoView.setBackground(null);
diff --git a/src/com/android/contacts/common/widget/CheckableFlipDrawable.java b/src/com/android/contacts/common/widget/CheckableFlipDrawable.java
new file mode 100644
index 00000000..cca82886
--- /dev/null
+++ b/src/com/android/contacts/common/widget/CheckableFlipDrawable.java
@@ -0,0 +1,220 @@
+package com.android.contacts.common.widget;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.Checkable;
+import android.widget.QuickContactBadge;
+
+import com.android.contacts.common.R;
+
+public class CheckableFlipDrawable extends FlipDrawable implements
+ ValueAnimator.AnimatorUpdateListener {
+
+ private final CheckmarkDrawable mCheckmarkDrawable;
+
+ private final ValueAnimator mCheckmarkScaleAnimator;
+ private final ValueAnimator mCheckmarkAlphaAnimator;
+
+ private static final int POST_FLIP_DURATION_MS = 150;
+
+ private static final float CHECKMARK_SCALE_BEGIN_VALUE = 0.2f;
+ private static final float CHECKMARK_ALPHA_BEGIN_VALUE = 0f;
+
+ /** Must be <= 1f since the animation value is used as a percentage. */
+ private static final float END_VALUE = 1f;
+
+ public CheckableFlipDrawable(Drawable front, final Resources res,
+ final int checkBackgroundColor, final int flipDurationMs) {
+ super(front, new CheckmarkDrawable(res, checkBackgroundColor),
+ flipDurationMs, 0 /* preFlipDurationMs */, POST_FLIP_DURATION_MS);
+
+ mCheckmarkDrawable = (CheckmarkDrawable) mBack;
+
+ // We will create checkmark animations that are synchronized with the
+ // flipping animation. The entire delay + duration of the checkmark animation
+ // needs to equal the entire duration of the flip animation (where delay is 0).
+
+ // The checkmark animation is in effect only when the back drawable is being shown.
+ // For the flip animation duration <pre>[_][]|[][_]<post>
+ // The checkmark animation will be |--delay--|-duration-|
+
+ // Need delay to skip the first half of the flip duration.
+ final long animationDelay = mPreFlipDurationMs + mFlipDurationMs / 2;
+ // Actual duration is the second half of the flip duration.
+ final long animationDuration = mFlipDurationMs / 2 + mPostFlipDurationMs;
+
+ mCheckmarkScaleAnimator = ValueAnimator.ofFloat(CHECKMARK_SCALE_BEGIN_VALUE, END_VALUE)
+ .setDuration(animationDuration);
+ mCheckmarkScaleAnimator.setStartDelay(animationDelay);
+ mCheckmarkScaleAnimator.addUpdateListener(this);
+
+ mCheckmarkAlphaAnimator = ValueAnimator.ofFloat(CHECKMARK_ALPHA_BEGIN_VALUE, END_VALUE)
+ .setDuration(animationDuration);
+ mCheckmarkAlphaAnimator.setStartDelay(animationDelay);
+ mCheckmarkAlphaAnimator.addUpdateListener(this);
+ }
+
+ public void setFront(Drawable front) {
+ mFront.setCallback(null);
+
+ mFront = front;
+
+ mFront.setCallback(this);
+ mFront.setBounds(getBounds());
+ mFront.setAlpha(getAlpha());
+ mFront.setColorFilter(getColorFilter());
+ mFront.setLevel(getLevel());
+
+ reset();
+ invalidateSelf();
+ }
+
+ public void setCheckMarkBackgroundColor(int color) {
+ mCheckmarkDrawable.setBackgroundColor(color);
+ invalidateSelf();
+ }
+
+ @Override
+ public void reset() {
+ super.reset();
+ if (mCheckmarkScaleAnimator == null) {
+ // Call from super's constructor. Not yet initialized.
+ return;
+ }
+ mCheckmarkScaleAnimator.cancel();
+ mCheckmarkAlphaAnimator.cancel();
+ boolean side = getSideFlippingTowards();
+ mCheckmarkDrawable.setScaleAnimatorValue(side ? CHECKMARK_SCALE_BEGIN_VALUE : END_VALUE);
+ mCheckmarkDrawable.setAlphaAnimatorValue(side ? CHECKMARK_ALPHA_BEGIN_VALUE : END_VALUE);
+ }
+
+ @Override
+ public void flip() {
+ super.flip();
+ // Keep the checkmark animators in sync with the flip animator.
+ if (mCheckmarkScaleAnimator.isStarted()) {
+ mCheckmarkScaleAnimator.reverse();
+ mCheckmarkAlphaAnimator.reverse();
+ } else {
+ if (!getSideFlippingTowards() /* front to back */) {
+ mCheckmarkScaleAnimator.start();
+ mCheckmarkAlphaAnimator.start();
+ } else /* back to front */ {
+ mCheckmarkScaleAnimator.reverse();
+ mCheckmarkAlphaAnimator.reverse();
+ }
+ }
+ }
+
+ @Override
+ public void onAnimationUpdate(final ValueAnimator animation) {
+ //noinspection ConstantConditions
+ final float value = (Float) animation.getAnimatedValue();
+
+ if (animation == mCheckmarkScaleAnimator) {
+ mCheckmarkDrawable.setScaleAnimatorValue(value);
+ } else if (animation == mCheckmarkAlphaAnimator) {
+ mCheckmarkDrawable.setAlphaAnimatorValue(value);
+ }
+ }
+
+ private static class CheckmarkDrawable extends Drawable {
+ private static Bitmap sCheckMark;
+
+ private final Paint mPaint;
+
+ private float mScaleFraction;
+ private float mAlphaFraction;
+
+ private static final Matrix sMatrix = new Matrix();
+
+ public CheckmarkDrawable(final Resources res, int backgroundColor) {
+ if (sCheckMark == null) {
+ sCheckMark = BitmapFactory.decodeResource(res, R.drawable.ic_check_wht_24dp);
+ }
+ mPaint = new Paint();
+ mPaint.setAntiAlias(true);
+ mPaint.setFilterBitmap(true);
+ mPaint.setColor(backgroundColor);
+ }
+
+ public void setBackgroundColor(int color) {
+ mPaint.setColor(color);
+ }
+
+ @Override
+ public void draw(final Canvas canvas) {
+ final Rect bounds = getBounds();
+ if (!isVisible() || bounds.isEmpty()) {
+ return;
+ }
+
+ canvas.drawCircle(bounds.centerX(), bounds.centerY(), bounds.width() / 2, mPaint);
+
+ // Scale the checkmark.
+ sMatrix.reset();
+ sMatrix.setScale(mScaleFraction, mScaleFraction, sCheckMark.getWidth() / 2,
+ sCheckMark.getHeight() / 2);
+ sMatrix.postTranslate(bounds.centerX() - sCheckMark.getWidth() / 2,
+ bounds.centerY() - sCheckMark.getHeight() / 2);
+
+ // Fade the checkmark.
+ final int oldAlpha = mPaint.getAlpha();
+ // Interpolate the alpha.
+ mPaint.setAlpha((int) (oldAlpha * mAlphaFraction));
+ canvas.drawBitmap(sCheckMark, sMatrix, mPaint);
+ // Restore the alpha.
+ mPaint.setAlpha(oldAlpha);
+ }
+
+ @Override
+ public void setAlpha(final int alpha) {
+ mPaint.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(final ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ }
+
+ @Override
+ public int getOpacity() {
+ // Always a gray background.
+ return PixelFormat.OPAQUE;
+ }
+
+ /**
+ * Set value as a fraction from 0f to 1f.
+ */
+ public void setScaleAnimatorValue(final float value) {
+ final float old = mScaleFraction;
+ mScaleFraction = value;
+ if (old != mScaleFraction) {
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * Set value as a fraction from 0f to 1f.
+ */
+ public void setAlphaAnimatorValue(final float value) {
+ final float old = mAlphaFraction;
+ mAlphaFraction = value;
+ if (old != mAlphaFraction) {
+ invalidateSelf();
+ }
+ }
+ }
+}
diff --git a/src/com/android/contacts/common/widget/CheckableImageView.java b/src/com/android/contacts/common/widget/CheckableImageView.java
new file mode 100644
index 00000000..914d4eaf
--- /dev/null
+++ b/src/com/android/contacts/common/widget/CheckableImageView.java
@@ -0,0 +1,103 @@
+package com.android.contacts.common.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.Checkable;
+import android.widget.ImageView;
+
+import com.android.contacts.common.R;
+
+public class CheckableImageView extends ImageView implements Checkable {
+ private boolean mChecked = false;
+ private int mCheckMarkBackgroundColor;
+ private CheckableFlipDrawable mDrawable;
+
+ public CheckableImageView(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public CheckableImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public CheckableImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context);
+ }
+
+ public CheckableImageView(Context context, AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init(context);
+ }
+
+ private void init(Context context) {
+ TypedArray a = context.obtainStyledAttributes(android.R.styleable.Theme);
+ setCheckMarkBackgroundColor(a.getColor(android.R.styleable.Theme_colorPrimary,
+ context.getResources().getColor(R.color.people_app_theme_color)));
+ a.recycle();
+ }
+
+ public void setCheckMarkBackgroundColor(int color) {
+ mCheckMarkBackgroundColor = color;
+ if (mDrawable != null) {
+ mDrawable.setCheckMarkBackgroundColor(color);
+ }
+ }
+
+ public void toggle() {
+ setChecked(!mChecked);
+ }
+
+ @Override
+ public boolean isChecked() {
+ return mChecked;
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ setChecked(checked, true);
+ }
+
+ public void setChecked(boolean checked, boolean animate) {
+ if (mChecked == checked) {
+ return;
+ }
+
+ mChecked = checked;
+
+ Drawable d = getDrawable();
+ if (d instanceof CheckableFlipDrawable) {
+ CheckableFlipDrawable cfd = (CheckableFlipDrawable) d;
+ cfd.flipTo(!mChecked);
+ if (!animate) {
+ cfd.reset();
+ }
+ }
+ }
+
+ @Override
+ public void setImageDrawable(Drawable d) {
+ if (d != null) {
+ if (mDrawable == null) {
+ mDrawable = new CheckableFlipDrawable(d, getResources(),
+ mCheckMarkBackgroundColor, 150);
+ } else {
+ int oldWidth = mDrawable.getIntrinsicWidth();
+ int oldHeight = mDrawable.getIntrinsicHeight();
+ mDrawable.setFront(d);
+ if (oldWidth != mDrawable.getIntrinsicWidth()
+ || oldHeight != mDrawable.getIntrinsicHeight()) {
+ // enforce drawable size update + layout
+ super.setImageDrawable(null);
+ }
+ }
+ d = mDrawable;
+ }
+ super.setImageDrawable(d);
+ }
+}
diff --git a/src/com/android/contacts/common/widget/CheckableQuickContactBadge.java b/src/com/android/contacts/common/widget/CheckableQuickContactBadge.java
new file mode 100644
index 00000000..85160569
--- /dev/null
+++ b/src/com/android/contacts/common/widget/CheckableQuickContactBadge.java
@@ -0,0 +1,103 @@
+package com.android.contacts.common.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.Checkable;
+import android.widget.QuickContactBadge;
+
+import com.android.contacts.common.R;
+
+public class CheckableQuickContactBadge extends QuickContactBadge implements Checkable {
+ private boolean mChecked = false;
+ private int mCheckMarkBackgroundColor;
+ private CheckableFlipDrawable mDrawable;
+
+ public CheckableQuickContactBadge(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public CheckableQuickContactBadge(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public CheckableQuickContactBadge(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context);
+ }
+
+ public CheckableQuickContactBadge(Context context, AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init(context);
+ }
+
+ private void init(Context context) {
+ TypedArray a = context.obtainStyledAttributes(android.R.styleable.Theme);
+ setCheckMarkBackgroundColor(a.getColor(android.R.styleable.Theme_colorPrimary,
+ context.getResources().getColor(R.color.people_app_theme_color)));
+ a.recycle();
+ }
+
+ public void setCheckMarkBackgroundColor(int color) {
+ mCheckMarkBackgroundColor = color;
+ if (mDrawable != null) {
+ mDrawable.setCheckMarkBackgroundColor(color);
+ }
+ }
+
+ public void toggle() {
+ setChecked(!mChecked);
+ }
+
+ @Override
+ public boolean isChecked() {
+ return mChecked;
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ setChecked(checked, true);
+ }
+
+ public void setChecked(boolean checked, boolean animate) {
+ if (mChecked == checked) {
+ return;
+ }
+
+ mChecked = checked;
+
+ Drawable d = getDrawable();
+ if (d instanceof CheckableFlipDrawable) {
+ CheckableFlipDrawable cfd = (CheckableFlipDrawable) d;
+ cfd.flipTo(!mChecked);
+ if (!animate) {
+ cfd.reset();
+ }
+ }
+ }
+
+ @Override
+ public void setImageDrawable(Drawable d) {
+ if (d != null) {
+ if (mDrawable == null) {
+ mDrawable = new CheckableFlipDrawable(d, getResources(),
+ mCheckMarkBackgroundColor, 150);
+ } else {
+ int oldWidth = mDrawable.getIntrinsicWidth();
+ int oldHeight = mDrawable.getIntrinsicHeight();
+ mDrawable.setFront(d);
+ if (oldWidth != mDrawable.getIntrinsicWidth()
+ || oldHeight != mDrawable.getIntrinsicHeight()) {
+ // enforce drawable size update + layout
+ super.setImageDrawable(null);
+ }
+ }
+ d = mDrawable;
+ }
+ super.setImageDrawable(d);
+ }
+}
diff --git a/src/com/android/contacts/common/widget/FlipDrawable.java b/src/com/android/contacts/common/widget/FlipDrawable.java
new file mode 100644
index 00000000..ff14b508
--- /dev/null
+++ b/src/com/android/contacts/common/widget/FlipDrawable.java
@@ -0,0 +1,276 @@
+/*
+ * 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.contacts.common.widget;
+
+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;
+
+/**
+ * A drawable that wraps two other drawables and allows flipping between them. The flipping
+ * animation is a 2D rotation around the y axis.
+ *
+ * <p/>
+ * The 3 durations are: (best viewed in documentation form)
+ * <pre>
+ * &lt;pre&gt;[_][]|[][_]&lt;post&gt;
+ * | | |
+ * V V V
+ * &lt;pre>&lt; flip &gt;&lt;post&gt;
+ * </pre>
+ */
+public class FlipDrawable extends Drawable implements Drawable.Callback {
+
+ /**
+ * The inner drawables.
+ */
+ protected 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.
+ *
+ * <p/>
+ * The 3 durations are: (best viewed in documentation form)
+ * <pre>
+ * &lt;pre&gt;[_][]|[][_]&lt;post&gt;
+ * | | |
+ * V V V
+ * &lt;pre>&lt; flip &gt;&lt;post&gt;
+ * </pre>
+ *
+ * @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();
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mFront.getIntrinsicWidth();
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mFront.getIntrinsicHeight();
+ }
+
+ @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 float old = mFlipFraction;
+ mFlipAnimator.cancel();
+ mFlipFraction = mFlipToSide ? 0f : 2f;
+ 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();
+ }
+}