OSDN Git Service

Animate badges when they are added or removed
authorTony Wickham <twickham@google.com>
Thu, 2 Feb 2017 20:57:18 +0000 (12:57 -0800)
committerTony Wickham <twickham@google.com>
Tue, 7 Feb 2017 22:10:09 +0000 (14:10 -0800)
- Scale the badge and text or icon up or down, respectively.
- Only animate if the badge is visible, and don't animate when
  applying shortcut or app info.
- Animate folder badge out when folder enters accepting state.

Bug: 34838365
Bug: 32410600
Change-Id: Ie60cb1fc54fe60d72734d833040545d27660d645

src/com/android/launcher3/BubbleTextView.java
src/com/android/launcher3/FastBitmapDrawable.java
src/com/android/launcher3/Workspace.java
src/com/android/launcher3/badge/BadgeRenderer.java
src/com/android/launcher3/folder/FolderIcon.java

index 8043eac..bbf2cb8 100644 (file)
@@ -167,7 +167,7 @@ public class BubbleTextView extends TextView
             applyPromiseState(promiseStateChanged);
         }
 
-        applyBadgeState(info);
+        applyBadgeState(info, false /* animate */);
     }
 
     public void applyFromApplicationInfo(AppInfo info) {
@@ -179,7 +179,7 @@ public class BubbleTextView extends TextView
         // Verify high res immediately
         verifyHighRes();
 
-        applyBadgeState(info);
+        applyBadgeState(info, false /* animate */);
     }
 
     public void applyFromPackageItemInfo(PackageItemInfo info) {
@@ -501,11 +501,11 @@ public class BubbleTextView extends TextView
         }
     }
 
-    public void applyBadgeState(ItemInfo itemInfo) {
+    public void applyBadgeState(ItemInfo itemInfo, boolean animate) {
         if (mIcon instanceof FastBitmapDrawable) {
             BadgeInfo badgeInfo = mLauncher.getPopupDataProvider().getBadgeInfoForItem(itemInfo);
             BadgeRenderer badgeRenderer = mLauncher.getDeviceProfile().mBadgeRenderer;
-            ((FastBitmapDrawable) mIcon).applyIconBadge(badgeInfo, badgeRenderer);
+            ((FastBitmapDrawable) mIcon).applyIconBadge(badgeInfo, badgeRenderer, animate);
         }
     }
 
index 95d2daf..1f74c88 100644 (file)
@@ -30,7 +30,7 @@ import android.graphics.PixelFormat;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffColorFilter;
 import android.graphics.drawable.Drawable;
-import android.util.Log;
+import android.util.Property;
 import android.util.SparseArray;
 import android.view.animation.DecelerateInterpolator;
 
@@ -107,6 +107,21 @@ public class FastBitmapDrawable extends Drawable {
     private BadgeInfo mBadgeInfo;
     private BadgeRenderer mBadgeRenderer;
     private IconPalette mIconPalette;
+    private float mBadgeScale;
+
+    private static final Property<FastBitmapDrawable, Float> BADGE_SCALE_PROPERTY
+            = new Property<FastBitmapDrawable, Float>(Float.TYPE, "badgeScale") {
+        @Override
+        public Float get(FastBitmapDrawable fastBitmapDrawable) {
+            return fastBitmapDrawable.mBadgeScale;
+        }
+
+        @Override
+        public void set(FastBitmapDrawable fastBitmapDrawable, Float value) {
+            fastBitmapDrawable.mBadgeScale = value;
+            fastBitmapDrawable.invalidateSelf();
+        }
+    };
 
     // The saturation and brightness are values that are mapped to REDUCED_FILTER_VALUE_SPACE and
     // as a result, can be used to compose the key for the cached ColorMatrixColorFilters
@@ -123,14 +138,22 @@ public class FastBitmapDrawable extends Drawable {
         setFilterBitmap(true);
     }
 
-    public void applyIconBadge(BadgeInfo badgeInfo, BadgeRenderer badgeRenderer) {
+    public void applyIconBadge(final BadgeInfo badgeInfo, BadgeRenderer badgeRenderer,
+            boolean animate) {
         boolean wasBadged = mBadgeInfo != null;
         boolean isBadged = badgeInfo != null;
+        float newBadgeScale = isBadged ? 1f : 0;
         mBadgeInfo = badgeInfo;
         mBadgeRenderer = badgeRenderer;
         if (wasBadged || isBadged) {
             mIconPalette = getIconPalette();
-            invalidateSelf();
+            // Animate when a badge is first added or when it is removed.
+            if (animate && (wasBadged ^ isBadged) && isVisible()) {
+                ObjectAnimator.ofFloat(this, BADGE_SCALE_PROPERTY, newBadgeScale).start();
+            } else {
+                mBadgeScale = newBadgeScale;
+                invalidateSelf();
+            }
         }
     }
 
@@ -154,7 +177,7 @@ public class FastBitmapDrawable extends Drawable {
 
     protected void drawBadgeIfNecessary(Canvas canvas) {
         if (hasBadge()) {
-            mBadgeRenderer.draw(canvas, mIconPalette, mBadgeInfo, getBounds());
+            mBadgeRenderer.draw(canvas, mIconPalette, mBadgeInfo, getBounds(), mBadgeScale);
         }
     }
 
@@ -167,7 +190,7 @@ public class FastBitmapDrawable extends Drawable {
     }
 
     private boolean hasBadge() {
-        return mBadgeInfo != null && mBadgeInfo.getNotificationCount() != 0;
+        return (mBadgeInfo != null && mBadgeInfo.getNotificationCount() > 0) || mBadgeScale > 0;
     }
 
     @Override
index cd74ed9..3aa8825 100644 (file)
@@ -3993,7 +3993,7 @@ public class Workspace extends PagedView
                 if (info instanceof ShortcutInfo && v instanceof BubbleTextView
                         && packageUserKey.updateFromItemInfo(info)) {
                     if (updatedBadges.contains(packageUserKey)) {
-                        ((BubbleTextView) v).applyBadgeState(info);
+                        ((BubbleTextView) v).applyBadgeState(info, true /* animate */);
                         folderIds.add(info.container);
                     }
                 }
index 4bb0954..8bbc2af 100644 (file)
@@ -24,6 +24,7 @@ import android.graphics.Paint;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.Shader;
+import android.support.annotation.Nullable;
 
 import com.android.launcher3.R;
 import com.android.launcher3.graphics.IconPalette;
@@ -34,62 +35,59 @@ import com.android.launcher3.graphics.IconPalette;
  */
 public class BadgeRenderer {
 
-    public int size;
-    public int textSize;
-    public IconDrawer largeIconDrawer;
-    public IconDrawer smallIconDrawer;
-
     private final Context mContext;
-    private final RectF mBackgroundRect = new RectF();
+    private final int mSize;
+    private final int mTextHeight;
+    private final IconDrawer mLargeIconDrawer;
+    private final IconDrawer mSmallIconDrawer;
     private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
     private final Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-    private final int mTextHeight;
 
     public BadgeRenderer(Context context) {
         mContext = context;
         Resources res = context.getResources();
-        size = res.getDimensionPixelSize(R.dimen.badge_size);
-        textSize = res.getDimensionPixelSize(R.dimen.badge_text_size);
-        largeIconDrawer = new IconDrawer(res.getDimensionPixelSize(R.dimen.badge_small_padding));
-        smallIconDrawer = new IconDrawer(res.getDimensionPixelSize(R.dimen.badge_large_padding));
+        mSize = res.getDimensionPixelSize(R.dimen.badge_size);
+        mLargeIconDrawer = new IconDrawer(res.getDimensionPixelSize(R.dimen.badge_small_padding));
+        mSmallIconDrawer = new IconDrawer(res.getDimensionPixelSize(R.dimen.badge_large_padding));
         mTextPaint.setTextAlign(Paint.Align.CENTER);
-        mTextPaint.setTextSize(textSize);
+        mTextPaint.setTextSize(res.getDimensionPixelSize(R.dimen.badge_text_size));
         // Measure the text height.
-        Rect temp = new Rect();
-        mTextPaint.getTextBounds("0", 0, 1, temp);
-        mTextHeight = temp.height();
+        Rect tempTextHeight = new Rect();
+        mTextPaint.getTextBounds("0", 0, 1, tempTextHeight);
+        mTextHeight = tempTextHeight.height();
     }
 
     /**
      * Draw a circle in the top right corner of the given bounds, and draw
      * {@link BadgeInfo#getNotificationCount()} on top of the circle.
      * @param palette The colors (based on the icon) to use for the badge.
-     * @param badgeInfo Contains data to draw on the badge.
+     * @param badgeInfo Contains data to draw on the badge. Could be null if we are animating out.
      * @param iconBounds The bounds of the icon being badged.
+     * @param badgeScale The progress of the animation, from 0 to 1.
      */
-    public void draw(Canvas canvas, IconPalette palette, BadgeInfo badgeInfo, Rect iconBounds) {
+    public void draw(Canvas canvas, IconPalette palette, @Nullable BadgeInfo badgeInfo,
+            Rect iconBounds, float badgeScale) {
         mBackgroundPaint.setColor(palette.backgroundColor);
         mTextPaint.setColor(palette.textColor);
-        mBackgroundRect.set(iconBounds.right - size, iconBounds.top, iconBounds.right,
-                iconBounds.top + size);
-        canvas.drawOval(mBackgroundRect, mBackgroundPaint);
-        IconDrawer iconDrawer = badgeInfo.isIconLarge() ? largeIconDrawer : smallIconDrawer;
-        Shader icon = badgeInfo.getNotificationIconForBadge(mContext, palette.backgroundColor, size,
-                iconDrawer.mPadding);
+        canvas.save(Canvas.MATRIX_SAVE_FLAG);
+        // We draw the badge relative to its center.
+        canvas.translate(iconBounds.right - mSize / 2, iconBounds.top + mSize / 2);
+        canvas.scale(badgeScale, badgeScale);
+        canvas.drawCircle(0, 0, mSize / 2, mBackgroundPaint);
+        IconDrawer iconDrawer = badgeInfo != null && badgeInfo.isIconLarge()
+                ? mLargeIconDrawer : mSmallIconDrawer;
+        Shader icon = badgeInfo == null ? null : badgeInfo.getNotificationIconForBadge(
+                mContext, palette.backgroundColor, mSize, iconDrawer.mPadding);
         if (icon != null) {
             // Draw the notification icon with padding.
-            canvas.save();
-            canvas.translate(mBackgroundRect.left, mBackgroundRect.top);
             iconDrawer.drawIcon(icon, canvas);
-            canvas.restore();
         } else {
             // Draw the notification count.
-            String notificationCount = String.valueOf(badgeInfo.getNotificationCount());
-            canvas.drawText(notificationCount,
-                    mBackgroundRect.centerX(),
-                    mBackgroundRect.centerY() + mTextHeight / 2,
-                    mTextPaint);
+            String notificationCount = badgeInfo == null ? "0"
+                    : String.valueOf(badgeInfo.getNotificationCount());
+            canvas.drawText(notificationCount, 0, mTextHeight / 2, mTextPaint);
         }
+        canvas.restore();
     }
 
     /** Draws the notification icon with padding of a given size. */
@@ -102,15 +100,15 @@ public class BadgeRenderer {
 
         public IconDrawer(int padding) {
             mPadding = padding;
-            mCircleClipBitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ALPHA_8);
+            mCircleClipBitmap = Bitmap.createBitmap(mSize, mSize, Bitmap.Config.ALPHA_8);
             Canvas canvas = new Canvas();
             canvas.setBitmap(mCircleClipBitmap);
-            canvas.drawCircle(size / 2, size / 2, size / 2 - padding, mPaint);
+            canvas.drawCircle(mSize / 2, mSize / 2, mSize / 2 - padding, mPaint);
         }
 
         public void drawIcon(Shader icon, Canvas canvas) {
             mPaint.setShader(icon);
-            canvas.drawBitmap(mCircleClipBitmap, 0f, 0f, mPaint);
+            canvas.drawBitmap(mCircleClipBitmap, -mSize / 2, -mSize / 2, mPaint);
             mPaint.setShader(null);
         }
     }
index 74dc48c..c802559 100644 (file)
@@ -33,6 +33,7 @@ import android.graphics.drawable.Drawable;
 import android.os.Parcelable;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
+import android.util.Property;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
@@ -129,6 +130,21 @@ public class FolderIcon extends FrameLayout implements FolderListener {
 
     private FolderBadgeInfo mBadgeInfo = new FolderBadgeInfo();
     private BadgeRenderer mBadgeRenderer;
+    private float mBadgeScale;
+
+    private static final Property<FolderIcon, Float> BADGE_SCALE_PROPERTY
+            = new Property<FolderIcon, Float>(Float.TYPE, "badgeScale") {
+        @Override
+        public Float get(FolderIcon folderIcon) {
+            return folderIcon.mBadgeScale;
+        }
+
+        @Override
+        public void set(FolderIcon folderIcon, Float value) {
+            folderIcon.mBadgeScale = value;
+            folderIcon.invalidate();
+        }
+    };
 
     public FolderIcon(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -387,8 +403,25 @@ public class FolderIcon extends FrameLayout implements FolderListener {
     }
 
     public void setBadgeInfo(FolderBadgeInfo badgeInfo) {
+        updateBadgeScale(mBadgeInfo.getNotificationCount(), badgeInfo.getNotificationCount());
         mBadgeInfo = badgeInfo;
-        invalidate();
+    }
+
+    /**
+     * Sets mBadgeScale to 1 or 0, animating if oldCount or newCount is 0
+     * (the badge is being added or removed).
+     */
+    private void updateBadgeScale(int oldCount, int newCount) {
+        boolean wasBadged = oldCount > 0;
+        boolean isBadged = newCount > 0;
+        float newBadgeScale = isBadged ? 1f : 0f;
+        // Animate when a badge is first added or when it is removed.
+        if ((wasBadged ^ isBadged) && isShown()) {
+            ObjectAnimator.ofFloat(this, BADGE_SCALE_PROPERTY, newBadgeScale).start();
+        } else {
+            mBadgeScale = newBadgeScale;
+            invalidate();
+        }
     }
 
     static class PreviewItemDrawingParams {
@@ -549,6 +582,14 @@ public class FolderIcon extends FrameLayout implements FolderListener {
             return basePreviewOffsetY - (getScaledRadius() - getRadius());
         }
 
+        /**
+         * Returns the progress of the scale animation, where 0 means the scale is at 1f
+         * and 1 means the scale is at ACCEPT_SCALE_FACTOR.
+         */
+        float getScaleProgress() {
+            return (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f);
+        }
+
         void invalidate() {
             int radius = getScaledRadius();
             mClipPath.reset();
@@ -784,8 +825,10 @@ public class FolderIcon extends FrameLayout implements FolderListener {
         int offsetY = mBackground.getOffsetY();
         int previewSize = (int) (mBackground.previewSize * mBackground.mScale);
         Rect bounds = new Rect(offsetX, offsetY, offsetX + previewSize, offsetY + previewSize);
-        if (mBadgeInfo != null && mBadgeInfo.getNotificationCount() > 0) {
-            mBadgeRenderer.draw(canvas, IconPalette.FOLDER_ICON_PALETTE, mBadgeInfo, bounds);
+        if ((mBadgeInfo != null && mBadgeInfo.getNotificationCount() > 0) || mBadgeScale > 0) {
+            // If we are animating to the accepting state, animate the badge out.
+            float badgeScale = Math.max(0, mBadgeScale - mBackground.getScaleProgress());
+            mBadgeRenderer.draw(canvas, IconPalette.FOLDER_ICON_PALETTE, mBadgeInfo, bounds, badgeScale);
         }
     }
 
@@ -938,14 +981,20 @@ public class FolderIcon extends FrameLayout implements FolderListener {
 
     @Override
     public void onAdd(ShortcutInfo item) {
+        int oldCount = mBadgeInfo.getNotificationCount();
         mBadgeInfo.addBadgeInfo(mLauncher.getPopupDataProvider().getBadgeInfoForItem(item));
+        int newCount = mBadgeInfo.getNotificationCount();
+        updateBadgeScale(oldCount, newCount);
         invalidate();
         requestLayout();
     }
 
     @Override
     public void onRemove(ShortcutInfo item) {
+        int oldCount = mBadgeInfo.getNotificationCount();
         mBadgeInfo.subtractBadgeInfo(mLauncher.getPopupDataProvider().getBadgeInfoForItem(item));
+        int newCount = mBadgeInfo.getNotificationCount();
+        updateBadgeScale(oldCount, newCount);
         invalidate();
         requestLayout();
     }