From 96ac68a481ce5b794b5227e09ace7c30d6dd5e7b Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Thu, 2 Feb 2017 16:37:21 -0800 Subject: Updating the PreloadIconDrawable > The drawable gets the path from MaskIconDrawable path, instead of using a circle > The progress changes are animated as well Bug: 34831873 Change-Id: I4e7f0b610f4fd94de8e0cfcf8b179b775cf0b4d8 --- .../launcher3/graphics/DragPreviewProvider.java | 5 - .../launcher3/graphics/DrawableFactory.java | 41 ++- .../launcher3/graphics/FixedScaleDrawable.java | 41 +++ .../android/launcher3/graphics/IconPalette.java | 42 ++- .../android/launcher3/graphics/LauncherIcons.java | 20 +- .../launcher3/graphics/PreloadIconDrawable.java | 289 +++++++++++++++++++++ 6 files changed, 409 insertions(+), 29 deletions(-) create mode 100644 src/com/android/launcher3/graphics/FixedScaleDrawable.java create mode 100644 src/com/android/launcher3/graphics/PreloadIconDrawable.java (limited to 'src/com/android/launcher3/graphics') diff --git a/src/com/android/launcher3/graphics/DragPreviewProvider.java b/src/com/android/launcher3/graphics/DragPreviewProvider.java index 1a470ff11..bb136f7a3 100644 --- a/src/com/android/launcher3/graphics/DragPreviewProvider.java +++ b/src/com/android/launcher3/graphics/DragPreviewProvider.java @@ -27,7 +27,6 @@ import android.widget.TextView; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppWidgetHostView; -import com.android.launcher3.PreloadIconDrawable; import com.android.launcher3.R; import com.android.launcher3.Workspace; import com.android.launcher3.config.ProviderConfig; @@ -185,10 +184,6 @@ public class DragPreviewProvider { } else { bounds.offsetTo(0, 0); } - if (d instanceof PreloadIconDrawable) { - int inset = -((PreloadIconDrawable) d).getOutset(); - bounds.inset(inset, inset); - } return bounds; } diff --git a/src/com/android/launcher3/graphics/DrawableFactory.java b/src/com/android/launcher3/graphics/DrawableFactory.java index 4d4d5087f..249344792 100644 --- a/src/com/android/launcher3/graphics/DrawableFactory.java +++ b/src/com/android/launcher3/graphics/DrawableFactory.java @@ -21,12 +21,14 @@ import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Path; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Process; import android.os.UserHandle; import android.support.annotation.UiThread; +import android.util.Log; import com.android.launcher3.FastBitmapDrawable; import com.android.launcher3.ItemInfo; @@ -41,9 +43,13 @@ import java.util.HashMap; */ public class DrawableFactory { + private static final String TAG = "DrawableFactory"; + private static DrawableFactory sInstance; private static final Object LOCK = new Object(); + private Path mPreloadProgressPath; + public static DrawableFactory get(Context context) { synchronized (LOCK) { if (sInstance == null) { @@ -61,9 +67,38 @@ public class DrawableFactory { * Returns a FastBitmapDrawable with the icon. */ public FastBitmapDrawable newIcon(Bitmap icon, ItemInfo info) { - FastBitmapDrawable d = new FastBitmapDrawable(icon); - d.setFilterBitmap(true); - return d; + return new FastBitmapDrawable(icon); + } + + /** + * Returns a FastBitmapDrawable with the icon. + */ + public PreloadIconDrawable newPendingIcon(Bitmap icon, Context context) { + if (mPreloadProgressPath == null) { + mPreloadProgressPath = getPreloadProgressPath(context); + } + return new PreloadIconDrawable(icon, mPreloadProgressPath); + } + + + protected Path getPreloadProgressPath(Context context) { + if (Utilities.isAtLeastO()) { + try { + // Try to load the path from Mask Icon + Drawable maskIcon = context.getDrawable(R.drawable.mask_drawable_wrapper); + maskIcon.setBounds(0, 0, + PreloadIconDrawable.PATH_SIZE, PreloadIconDrawable.PATH_SIZE); + return (Path) maskIcon.getClass().getMethod("getIconMask").invoke(maskIcon); + } catch (Exception e) { + Log.e(TAG, "Error loading mask icon", e); + } + } + + // Create a circle static from top center and going clockwise. + Path p = new Path(); + p.moveTo(PreloadIconDrawable.PATH_SIZE / 2, 0); + p.addArc(0, 0, PreloadIconDrawable.PATH_SIZE, PreloadIconDrawable.PATH_SIZE, -90, 360); + return p; } public AllAppsBackgroundDrawable getAllAppsBackground(Context context) { diff --git a/src/com/android/launcher3/graphics/FixedScaleDrawable.java b/src/com/android/launcher3/graphics/FixedScaleDrawable.java new file mode 100644 index 000000000..4be4bd552 --- /dev/null +++ b/src/com/android/launcher3/graphics/FixedScaleDrawable.java @@ -0,0 +1,41 @@ +package com.android.launcher3.graphics; + +import android.annotation.TargetApi; +import android.content.res.Resources; +import android.content.res.Resources.Theme; +import android.graphics.Canvas; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.DrawableWrapper; +import android.os.Build; +import android.util.AttributeSet; + +import org.xmlpull.v1.XmlPullParser; + +/** + * Extension of {@link DrawableWrapper} which scales the child drawables by a fixed amount. + */ +@TargetApi(Build.VERSION_CODES.N) +public class FixedScaleDrawable extends DrawableWrapper { + + // TODO b/33553066 use the constant defined in MaskableIconDrawable + private static final float LEGACY_ICON_SCALE = .7f * .6667f; + + public FixedScaleDrawable() { + super(new ColorDrawable()); + } + + @Override + public void draw(Canvas canvas) { + int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); + canvas.scale(LEGACY_ICON_SCALE, LEGACY_ICON_SCALE, + getBounds().exactCenterX(), getBounds().exactCenterY()); + super.draw(canvas); + canvas.restoreToCount(saveCount); + } + + @Override + public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) { } + + @Override + public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) { } +} diff --git a/src/com/android/launcher3/graphics/IconPalette.java b/src/com/android/launcher3/graphics/IconPalette.java index 27aeaba34..991038cca 100644 --- a/src/com/android/launcher3/graphics/IconPalette.java +++ b/src/com/android/launcher3/graphics/IconPalette.java @@ -32,16 +32,42 @@ public class IconPalette { private static final boolean DEBUG = false; private static final String TAG = "IconPalette"; - public int backgroundColor; - public int textColor; - public int secondaryColor; + private static final float MIN_PRELOAD_COLOR_SATURATION = 0.2f; + private static final float MIN_PRELOAD_COLOR_LIGHTNESS = 0.6f; + private static final int DEFAULT_PRELOAD_COLOR = 0xFF009688; + + public final int dominantColor; + public final int backgroundColor; + public final int textColor; + public final int secondaryColor; + + private IconPalette(int color) { + dominantColor = color; + backgroundColor = getMutedColor(dominantColor); + textColor = getTextColorForBackground(backgroundColor); + secondaryColor = getLowContrastColor(backgroundColor); + } + + /** + * Returns a color suitable for the progress bar color of preload icon. + */ + public int getPreloadProgressColor() { + int result = dominantColor; + + // Make sure that the dominant color has enough saturation to be visible properly. + float[] hsv = new float[3]; + Color.colorToHSV(result, hsv); + if (hsv[1] < MIN_PRELOAD_COLOR_SATURATION) { + result = DEFAULT_PRELOAD_COLOR; + } else { + hsv[2] = Math.max(MIN_PRELOAD_COLOR_LIGHTNESS, hsv[2]); + result = Color.HSVToColor(hsv); + } + return result; + } public static IconPalette fromDominantColor(int dominantColor) { - IconPalette palette = new IconPalette(); - palette.backgroundColor = getMutedColor(dominantColor); - palette.textColor = getTextColorForBackground(palette.backgroundColor); - palette.secondaryColor = getLowContrastColor(palette.backgroundColor); - return palette; + return new IconPalette(dominantColor); } /** diff --git a/src/com/android/launcher3/graphics/LauncherIcons.java b/src/com/android/launcher3/graphics/LauncherIcons.java index 472b9135b..3fffb5bb5 100644 --- a/src/com/android/launcher3/graphics/LauncherIcons.java +++ b/src/com/android/launcher3/graphics/LauncherIcons.java @@ -29,13 +29,10 @@ import android.graphics.PaintFlagsDrawFilter; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.PaintDrawable; -import android.graphics.drawable.ScaleDrawable; import android.os.Process; import android.os.UserHandle; -import android.view.Gravity; import com.android.launcher3.AppInfo; import com.android.launcher3.IconCache; @@ -52,8 +49,6 @@ import com.android.launcher3.shortcuts.ShortcutInfoCompat; * Helper methods for generating various launcher icons */ public class LauncherIcons { - // TODO b/33553066 use the constant defined in MaskableIconDrawable - private static final float LEGACY_ICON_SCALE = .7f * .6667f; private static final Rect sOldBounds = new Rect(); private static final Canvas sCanvas = new Canvas(); @@ -236,17 +231,16 @@ public class LauncherIcons { if (!(ProviderConfig.IS_DOGFOOD_BUILD && Utilities.isAtLeastO())) { return drawable; } - int color = context.getResources().getColor(R.color.legacy_icon_background); - ColorDrawable colorDrawable = new ColorDrawable(color); - ScaleDrawable scaleDrawable = new ScaleDrawable(drawable, - Gravity.CENTER, LEGACY_ICON_SCALE, LEGACY_ICON_SCALE); - scaleDrawable.setLevel(1); + try { Class clazz = Class.forName("android.graphics.drawable.MaskableIconDrawable"); - if (!clazz.isAssignableFrom(drawable.getClass())){ + if (!clazz.isAssignableFrom(drawable.getClass())) { + Drawable maskWrapper = + context.getDrawable(R.drawable.mask_drawable_wrapper).mutate(); + ((FixedScaleDrawable) clazz.getMethod("getForeground").invoke(maskWrapper)) + .setDrawable(drawable); - return (Drawable) clazz.getConstructor(Drawable.class, Drawable.class) - .newInstance(colorDrawable, scaleDrawable); + return maskWrapper; } } catch (Exception e) { return drawable; diff --git a/src/com/android/launcher3/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java new file mode 100644 index 000000000..bc07ce1a5 --- /dev/null +++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2017 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.launcher3.graphics; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.graphics.Rect; +import android.util.Property; +import android.util.SparseArray; +import android.view.animation.LinearInterpolator; + +import com.android.launcher3.FastBitmapDrawable; + +import java.lang.ref.WeakReference; + +/** + * Extension of {@link FastBitmapDrawable} which shows a progress bar around the icon. + */ +public class PreloadIconDrawable extends FastBitmapDrawable { + + private static final Property INTERNAL_STATE = + new Property(Float.TYPE, "internalStateProgress") { + @Override + public Float get(PreloadIconDrawable object) { + return object.mInternalStateProgress; + } + + @Override + public void set(PreloadIconDrawable object, Float value) { + object.setInternalProgress(value); + } + }; + + public static final int PATH_SIZE = 100; + + private static final float PROGRESS_WIDTH = 7; + private static final float PROGRESS_GAP = 2; + private static final int MAX_PAINT_ALPHA = 255; + + private static final long DURATION_SCALE = 500; + + // The smaller the number, the faster the animation would be. + // Duration = COMPLETE_ANIM_FRACTION * DURATION_SCALE + private static final float COMPLETE_ANIM_FRACTION = 0.3f; + + private static final int COLOR_TRACK = 0x77EEEEEE; + private static final int COLOR_SHADOW = 0x55000000; + + private static final float SMALL_SCALE = 0.75f; + + private static final SparseArray> sShadowCache = new SparseArray<>(); + + private final Matrix mTmpMatrix = new Matrix(); + private final PathMeasure mPathMeasure = new PathMeasure(); + + // Path in [0, 100] bounds. + private final Path mProgressPath; + + private final Path mScaledTrackPath; + private final Path mScaledProgressPath; + private final Paint mProgressPaint; + + private Bitmap mShadowBitmap; + private int mIndicatorColor = 0; + + private int mTrackAlpha; + private float mTrackLength; + private float mIconScale; + + private boolean mRanFinishAnimation; + + // Progress of the internal state. [0, 1] indicates the fraction of completed progress, + // [1, (1 + COMPLETE_ANIM_FRACTION)] indicates the progress of zoom animation. + private float mInternalStateProgress; + + private ObjectAnimator mCurrentAnim; + + /** + * @param progressPath fixed path in the bounds [0, 0, 100, 100] representing a progress bar. + */ + public PreloadIconDrawable(Bitmap b, Path progressPath) { + super(b); + mProgressPath = progressPath; + mScaledTrackPath = new Path(); + mScaledProgressPath = new Path(); + + mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + mProgressPaint.setStyle(Paint.Style.STROKE); + mProgressPaint.setStrokeCap(Paint.Cap.ROUND); + + setInternalProgress(0); + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + mTmpMatrix.setScale( + (bounds.width() - PROGRESS_WIDTH - 2 * PROGRESS_GAP) / PATH_SIZE, + (bounds.height() - PROGRESS_WIDTH - 2 * PROGRESS_GAP) / PATH_SIZE); + mTmpMatrix.postTranslate( + bounds.left + PROGRESS_WIDTH / 2 + PROGRESS_GAP, + bounds.top + PROGRESS_WIDTH / 2 + PROGRESS_GAP); + + mProgressPath.transform(mTmpMatrix, mScaledTrackPath); + float scale = bounds.width() / PATH_SIZE; + mProgressPaint.setStrokeWidth(PROGRESS_WIDTH * scale); + + mShadowBitmap = getShadowBitmap(bounds.width(), bounds.height(), + (PROGRESS_GAP ) * scale); + mPathMeasure.setPath(mScaledTrackPath, true); + mTrackLength = mPathMeasure.getLength(); + + setInternalProgress(mInternalStateProgress); + } + + private Bitmap getShadowBitmap(int width, int height, float shadowRadius) { + int key = (width << 16) | height; + WeakReference shadowRef = sShadowCache.get(key); + Bitmap shadow = shadowRef != null ? shadowRef.get() : null; + if (shadow != null) { + return shadow; + } + shadow = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(shadow); + mProgressPaint.setShadowLayer(shadowRadius, 0, 0, COLOR_SHADOW); + mProgressPaint.setColor(COLOR_TRACK); + mProgressPaint.setAlpha(MAX_PAINT_ALPHA); + c.drawPath(mScaledTrackPath, mProgressPaint); + mProgressPaint.clearShadowLayer(); + c.setBitmap(null); + + sShadowCache.put(key, new WeakReference<>(shadow)); + return shadow; + } + + @Override + public void draw(Canvas canvas) { + if (mRanFinishAnimation) { + super.draw(canvas); + return; + } + + // Draw track. + mProgressPaint.setColor(mIndicatorColor); + mProgressPaint.setAlpha(mTrackAlpha); + if (mShadowBitmap != null) { + canvas.drawBitmap(mShadowBitmap, getBounds().left, getBounds().top, mProgressPaint); + } + canvas.drawPath(mScaledProgressPath, mProgressPaint); + + int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); + Rect bounds = getBounds(); + + canvas.scale(mIconScale, mIconScale, bounds.exactCenterX(), bounds.exactCenterY()); + drawInternal(canvas); + canvas.restoreToCount(saveCount); + } + + /** + * Updates the install progress based on the level + */ + @Override + protected boolean onLevelChange(int level) { + // Run the animation if we have already been bound. + updateInternalState(level * 0.01f, getBounds().width() > 0, false); + return true; + } + + /** + * Runs the finish animation if it is has not been run after last call to + * {@link #onLevelChange} + */ + public void maybePerformFinishedAnimation() { + // If the drawable was recently initialized, skip the progress animation. + if (mInternalStateProgress == 0) { + mInternalStateProgress = 1; + } + updateInternalState(1 + COMPLETE_ANIM_FRACTION, true, true); + } + + public boolean hasNotCompleted() { + return !mRanFinishAnimation; + } + + private void updateInternalState(float finalProgress, boolean shouldAnimate, boolean isFinish) { + if (mCurrentAnim != null) { + mCurrentAnim.cancel(); + mCurrentAnim = null; + } + + if (Float.compare(finalProgress, mInternalStateProgress) == 0) { + return; + } + if (!shouldAnimate || mRanFinishAnimation) { + setInternalProgress(finalProgress); + } else { + mCurrentAnim = ObjectAnimator.ofFloat(this, INTERNAL_STATE, finalProgress); + mCurrentAnim.setDuration( + (long) ((finalProgress - mInternalStateProgress) * DURATION_SCALE)); + mCurrentAnim.setInterpolator(new LinearInterpolator()); + if (isFinish) { + mCurrentAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mRanFinishAnimation = true; + } + }); + } + mCurrentAnim.start(); + } + + } + + /** + * Sets the internal progress and updates the UI accordingly + * for progress <= 0: + * - icon in the small scale and disabled state + * - progress track is visible + * - progress bar is not visible + * for 0 < progress < 1 + * - icon in the small scale and disabled state + * - progress track is visible + * - progress bar is visible with dominant color. Progress bar is drawn as a fraction of + * {@link #mScaledTrackPath}. + * @see PathMeasure#getSegment(float, float, Path, boolean) + * for 1 <= progress < (1 + COMPLETE_ANIM_FRACTION) + * - we calculate fraction of progress in the above range + * - progress track is drawn with alpha based on fraction + * - progress bar is drawn at 100% with alpha based on fraction + * - icon is scaled up based on fraction and is drawn in enabled state + * for progress >= (1 + COMPLETE_ANIM_FRACTION) + * - only icon is drawn in normal state + */ + private void setInternalProgress(float progress) { + mInternalStateProgress = progress; + if (progress <= 0) { + mIconScale = SMALL_SCALE; + mScaledTrackPath.reset(); + mTrackAlpha = MAX_PAINT_ALPHA; + setIsDisabled(true); + } else if (mIndicatorColor == 0) { + // Update the indicator color + mIndicatorColor = getIconPalette().getPreloadProgressColor(); + } + + if (progress < 1 && progress > 0) { + mPathMeasure.getSegment(0, progress * mTrackLength, mScaledProgressPath, true); + mIconScale = SMALL_SCALE; + mTrackAlpha = MAX_PAINT_ALPHA; + setIsDisabled(true); + } else if (progress >= 1) { + setIsDisabled(false); + mScaledTrackPath.set(mScaledProgressPath); + float fraction = (progress - 1) / COMPLETE_ANIM_FRACTION; + + if (fraction >= 1) { + // Animation has completed + mIconScale = 1; + mTrackAlpha = 0; + } else { + mTrackAlpha = Math.round((1 - fraction) * MAX_PAINT_ALPHA); + mIconScale = SMALL_SCALE + (1 - SMALL_SCALE) * fraction; + } + } + invalidateSelf(); + } +} -- cgit v1.2.3