From: Sunny Goyal Date: Mon, 4 Aug 2014 17:53:22 +0000 (-0700) Subject: Updating the virtual preloader UX. X-Git-Tag: android-x86-6.0-r1~137^2~86^2 X-Git-Url: http://git.osdn.net/view?a=commitdiff_plain;h=95abbb330ce9bbaf23594245f0f8a795c8118038;p=android-x86%2Fpackages-apps-Launcher3.git Updating the virtual preloader UX. > No click feedback when in preloader mode > No preloader UI when drawn in drag layer > The preloader consists of a background 9 patch image and a circular progress is drawn in the content region of the background. > The preloader is drawn in a slightly larget area than the actual bounds to make the circular progress more prominent compared to the icon. issue: 15835307 Change-Id: Ifec3d93ecf1fac994d1128b517da3797247e7ed6 --- diff --git a/res/drawable-xxhdpi/virtual_preload.9.png b/res/drawable-xxhdpi/virtual_preload.9.png new file mode 100644 index 000000000..0ec1740bf Binary files /dev/null and b/res/drawable-xxhdpi/virtual_preload.9.png differ diff --git a/res/drawable-xxhdpi/virtual_preload_folder.9.png b/res/drawable-xxhdpi/virtual_preload_folder.9.png new file mode 100644 index 000000000..ee80c768c Binary files /dev/null and b/res/drawable-xxhdpi/virtual_preload_folder.9.png differ diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 12fa3cd2b..552e84ca2 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -152,6 +152,12 @@ + + + + + + diff --git a/res/values/styles.xml b/res/values/styles.xml index 462c29239..6079eee3b 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -161,6 +161,18 @@ 2.0 + + + + diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index ab94814c3..5c2bb9946 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -19,6 +19,7 @@ package com.android.launcher3; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; +import android.content.res.Resources.Theme; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -28,6 +29,7 @@ import android.graphics.Region.Op; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; +import android.util.SparseArray; import android.util.TypedValue; import android.view.MotionEvent; import android.view.ViewConfiguration; @@ -39,6 +41,9 @@ import android.widget.TextView; * too aggressive. */ public class BubbleTextView extends TextView { + + private static SparseArray sPreloaderThemes = new SparseArray<>(2); + static final float SHADOW_LARGE_RADIUS = 4.0f; static final float SHADOW_SMALL_RADIUS = 1.75f; static final float SHADOW_Y_OFFSET = 2.0f; @@ -128,10 +133,7 @@ public class BubbleTextView extends TextView { LauncherAppState app = LauncherAppState.getInstance(); FastBitmapDrawable iconDrawable = Utilities.createIconDrawable(b); - if (info.isDisabled) { - iconDrawable.setSaturation(0); - iconDrawable.setBrightness(20); - } + iconDrawable.setGhostModeEnabled(info.isDisabled); setCompoundDrawables(null, iconDrawable, null, null); if (setDefaultPadding) { @@ -315,7 +317,9 @@ public class BubbleTextView extends TextView { } void setCellLayoutPressedOrFocusedIcon() { - if (getParent() instanceof ShortcutAndWidgetContainer) { + // Disable pressed state when the icon is in preloader state. + if ((getParent() instanceof ShortcutAndWidgetContainer) && + !(getCompoundDrawables()[1] instanceof PreloadIconDrawable)){ ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) getParent(); if (parent != null) { CellLayout layout = (CellLayout) parent.getParent(); @@ -385,7 +389,13 @@ public class BubbleTextView extends TextView { @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); + if (mBackground != null) mBackground.setCallback(this); + Drawable top = getCompoundDrawables()[1]; + + if (top instanceof PreloadIconDrawable) { + ((PreloadIconDrawable) top).applyTheme(getPreloaderTheme()); + } mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } @@ -466,7 +476,7 @@ public class BubbleTextView extends TextView { if (top instanceof PreloadIconDrawable) { preloadDrawable = (PreloadIconDrawable) top; } else { - preloadDrawable = new PreloadIconDrawable(top, getResources()); + preloadDrawable = new PreloadIconDrawable(top, getPreloaderTheme()); setCompoundDrawables(drawables[0], preloadDrawable, drawables[2], drawables[3]); } @@ -478,4 +488,18 @@ public class BubbleTextView extends TextView { } } } + + private Theme getPreloaderTheme() { + Object tag = getTag(); + int style = ((tag != null) && (tag instanceof ShortcutInfo) && + (((ShortcutInfo) tag).container >= 0)) ? R.style.PreloadIcon_Folder + : R.style.PreloadIcon; + Theme theme = sPreloaderThemes.get(style); + if (theme == null) { + theme = getResources().newTheme(); + theme.applyStyle(style, true); + sPreloaderThemes.put(style, theme); + } + return theme; + } } diff --git a/src/com/android/launcher3/DragLayer.java b/src/com/android/launcher3/DragLayer.java index 8bcc407d6..80f8dfce8 100644 --- a/src/com/android/launcher3/DragLayer.java +++ b/src/com/android/launcher3/DragLayer.java @@ -566,6 +566,10 @@ public class DragLayer extends FrameLayout implements ViewGroup.OnHierarchyChang // the drag view about the scaled child view. toY += Math.round(toScale * tv.getPaddingTop()); toY -= dragView.getMeasuredHeight() * (1 - toScale) / 2; + if (dragView.getDragVisualizeOffset() != null) { + toY -= Math.round(toScale * dragView.getDragVisualizeOffset().y); + } + toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2; } else if (child instanceof FolderIcon) { // Account for holographic blur padding on the drag view diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java index ef8d0973d..cf7c22eef 100644 --- a/src/com/android/launcher3/FastBitmapDrawable.java +++ b/src/com/android/launcher3/FastBitmapDrawable.java @@ -28,15 +28,17 @@ import android.graphics.drawable.Drawable; class FastBitmapDrawable extends Drawable { - private static final ColorMatrix sTempSaturationMatrix = new ColorMatrix(); - private static final ColorMatrix sTempBrightnessMatrix = new ColorMatrix(); + private static ColorMatrix sGhostModeMatrix; + private static final ColorMatrix sTempMatrix = new ColorMatrix(); + + private static final int GHOST_MODE_MIN_COLOR_RANGE = 130; private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); private Bitmap mBitmap; private int mAlpha; - private float mSatutation = 1; private int mBrightness = 0; + private boolean mGhostModeEnabled = false; FastBitmapDrawable(Bitmap b) { mAlpha = 255; @@ -101,13 +103,19 @@ class FastBitmapDrawable extends Drawable { return mBitmap; } - public float getSaturation() { - return mSatutation; + /** + * When enabled, the icon is grayed out and the contrast is increased to give it a 'ghost' + * appearance. + */ + public void setGhostModeEnabled(boolean enabled) { + if (mGhostModeEnabled != enabled) { + mGhostModeEnabled = enabled; + updateFilter(); + } } - public void setSaturation(float saturation) { - mSatutation = saturation; - updateFilter(); + public boolean isGhostModeEnabled() { + return mGhostModeEnabled; } public int getBrightness() { @@ -115,36 +123,58 @@ class FastBitmapDrawable extends Drawable { } public void addBrightness(int amount) { - mBrightness += amount; - updateFilter(); + setBrightness(mBrightness + amount); } public void setBrightness(int brightness) { - mBrightness = brightness; - updateFilter(); + if (mBrightness != brightness) { + mBrightness = brightness; + updateFilter(); + } } private void updateFilter() { - if (mSatutation != 1 || mBrightness != 0) { - sTempSaturationMatrix.setSaturation(mSatutation); - - if (mBrightness != 0) { - // Brightness: C-new = C-old*(1-amount) + amount - float scale = 1 - mBrightness / 255.0f; - sTempBrightnessMatrix.setScale(scale, scale, scale, 1); - float[] array = sTempBrightnessMatrix.getArray(); - - // Add the amount to RGB components of the matrix, as per the above formula. - // Fifth elements in the array correspond to the constant being added to - // red, blue, green, and alpha channel respectively. - array[4] = mBrightness; - array[9] = mBrightness; - array[14] = mBrightness; - sTempSaturationMatrix.preConcat(sTempBrightnessMatrix); + if (mGhostModeEnabled) { + if (sGhostModeMatrix == null) { + sGhostModeMatrix = new ColorMatrix(); + sGhostModeMatrix.setSaturation(0); + + // For ghost mode, set the color range to [GHOST_MODE_MIN_COLOR_RANGE, 255] + float range = (255 - GHOST_MODE_MIN_COLOR_RANGE) / 255.0f; + sTempMatrix.set(new float[] { + range, 0, 0, 0, GHOST_MODE_MIN_COLOR_RANGE, + 0, range, 0, 0, GHOST_MODE_MIN_COLOR_RANGE, + 0, 0, range, 0, GHOST_MODE_MIN_COLOR_RANGE, + 0, 0, 0, 1, 0 }); + sGhostModeMatrix.preConcat(sTempMatrix); + } + + if (mBrightness == 0) { + mPaint.setColorFilter(new ColorMatrixColorFilter(sGhostModeMatrix)); + } else { + setBrightnessMatrix(sTempMatrix, mBrightness); + sTempMatrix.postConcat(sGhostModeMatrix); + mPaint.setColorFilter(new ColorMatrixColorFilter(sTempMatrix)); } - mPaint.setColorFilter(new ColorMatrixColorFilter(sTempSaturationMatrix)); + } else if (mBrightness != 0) { + setBrightnessMatrix(sTempMatrix, mBrightness); + mPaint.setColorFilter(new ColorMatrixColorFilter(sTempMatrix)); } else { mPaint.setColorFilter(null); } } + + private static void setBrightnessMatrix(ColorMatrix matrix, int brightness) { + // Brightness: C-new = C-old*(1-amount) + amount + float scale = 1 - brightness / 255.0f; + matrix.setScale(scale, scale, scale, 1); + float[] array = matrix.getArray(); + + // Add the amount to RGB components of the matrix, as per the above formula. + // Fifth elements in the array correspond to the constant being added to + // red, blue, green, and alpha channel respectively. + array[4] = brightness; + array[9] = brightness; + array[14] = brightness; + } } diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java index 655e5c368..fcedaead3 100644 --- a/src/com/android/launcher3/Folder.java +++ b/src/com/android/launcher3/Folder.java @@ -1182,6 +1182,18 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList if (mIsExternalDrag) { si.cellX = mEmptyCell[0]; si.cellY = mEmptyCell[1]; + + // Actually move the item in the database if it was an external drag. Call this + // before creating the view, so that ShortcutInfo is updated appropriately. + LauncherModel.addOrMoveItemInDatabase( + mLauncher, si, mInfo.id, 0, si.cellX, si.cellY); + + // We only need to update the locations if it doesn't get handled in #onDropCompleted. + if (d.dragSource != this) { + updateItemLocationsInDatabaseBatch(); + } + mIsExternalDrag = false; + currentDragView = createAndAddShortcut(si); } else { currentDragView = mCurrentDragView; @@ -1209,18 +1221,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mItemsInvalidated = true; setupContentDimensions(getItemCount()); - // Actually move the item in the database if it was an external drag. - if (mIsExternalDrag) { - LauncherModel.addOrMoveItemInDatabase( - mLauncher, si, mInfo.id, 0, si.cellX, si.cellY); - - // We only need to update the locations if it doesn't get handled in #onDropCompleted. - if (d.dragSource != this) { - updateItemLocationsInDatabaseBatch(); - } - mIsExternalDrag = false; - } - // Temporarily suppress the listener, as we did all the work already here. mSuppressOnAdd = true; mInfo.add(si); diff --git a/src/com/android/launcher3/FolderIcon.java b/src/com/android/launcher3/FolderIcon.java index 5b49fb87a..c0b9da7d4 100644 --- a/src/com/android/launcher3/FolderIcon.java +++ b/src/com/android/launcher3/FolderIcon.java @@ -380,7 +380,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { float scaleRelativeToDragLayer, Runnable postAnimationRunnable) { // These correspond two the drawable and view that the icon was dropped _onto_ - Drawable animateDrawable = ((TextView) destView).getCompoundDrawables()[1]; + Drawable animateDrawable = getTopDrawable((TextView) destView); computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(), destView.getMeasuredWidth()); @@ -394,7 +394,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { } public void performDestroyAnimation(final View finalView, Runnable onCompleteRunnable) { - Drawable animateDrawable = ((TextView) finalView).getCompoundDrawables()[1]; + Drawable animateDrawable = getTopDrawable((TextView) finalView); computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(), finalView.getMeasuredWidth()); diff --git a/src/com/android/launcher3/PreloadIconDrawable.java b/src/com/android/launcher3/PreloadIconDrawable.java index d9365cc1f..2972c4f9b 100644 --- a/src/com/android/launcher3/PreloadIconDrawable.java +++ b/src/com/android/launcher3/PreloadIconDrawable.java @@ -1,96 +1,143 @@ package com.android.launcher3; import android.animation.ObjectAnimator; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; +import android.content.res.Resources.Theme; +import android.content.res.TypedArray; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; -import android.graphics.Path; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; class PreloadIconDrawable extends Drawable { + private static final float ANIMATION_PROGRESS_STOPPED = -1.0f; private static final float ANIMATION_PROGRESS_STARTED = 0f; private static final float ANIMATION_PROGRESS_COMPLETED = 1.0f; - private static final float ICON_SCALE_FACTOR = 0.6f; + private static final float MIN_SATUNATION = 0.2f; + private static final float MIN_LIGHTNESS = 0.6f; + + private static final float ICON_SCALE_FACTOR = 0.5f; + private static final int DEFAULT_COLOR = 0xFF009688; - private static Bitmap sProgressBg, sProgressFill; + private static final Rect sTempRect = new Rect(); - private final Rect mCanvasClipRect = new Rect(); - private final RectF mRect = new RectF(); - private final Path mProgressPath = new Path(); - private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + private final RectF mIndicatorRect = new RectF(); + private boolean mIndicatorRectDirty; + private final Paint mPaint; final Drawable mIcon; + private Drawable mBgDrawable; + private int mRingOutset; + + private int mIndicatorColor = 0; + /** * Indicates the progress of the preloader [0-100]. If it goes above 100, only the icon * is shown with no progress bar. */ private int mProgress = 0; - private boolean mPathChanged; private float mAnimationProgress = ANIMATION_PROGRESS_STOPPED; private ObjectAnimator mAnimator; - public PreloadIconDrawable(Drawable icon, Resources res) { + public PreloadIconDrawable(Drawable icon, Theme theme) { mIcon = icon; + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeCap(Paint.Cap.ROUND); + setBounds(icon.getBounds()); - mPathChanged = false; + applyTheme(theme); + onLevelChange(0); + } - if (sProgressBg == null) { - sProgressBg = BitmapFactory.decodeResource(res, R.drawable.bg_preloader); - } - if (sProgressFill == null) { - sProgressFill = BitmapFactory.decodeResource(res, R.drawable.bg_preloader_progress); + @Override + public void applyTheme(Theme t) { + TypedArray ta = t.obtainStyledAttributes(R.styleable.PreloadIconDrawable); + mBgDrawable = ta.getDrawable(R.styleable.PreloadIconDrawable_background); + mBgDrawable.setFilterBitmap(true); + mPaint.setStrokeWidth(ta.getDimension(R.styleable.PreloadIconDrawable_indicatorSize, 0)); + mRingOutset = ta.getDimensionPixelSize(R.styleable.PreloadIconDrawable_ringOutset, 0); + ta.recycle(); + onBoundsChange(getBounds()); + invalidateSelf(); + } + + @Override + protected void onBoundsChange(Rect bounds) { + mIcon.setBounds(bounds); + if (mBgDrawable != null) { + sTempRect.set(bounds); + sTempRect.inset(-mRingOutset, -mRingOutset); + mBgDrawable.setBounds(sTempRect); } + mIndicatorRectDirty = true; + } + + public int getOutset() { + return mRingOutset; + } + + /** + * The size of the indicator is same as the content region of the {@link #mBgDrawable} minus + * half the stroke size to accommodate the indicator. + */ + private void initIndicatorRect() { + Drawable d = mBgDrawable; + Rect bounds = d.getBounds(); + + d.getPadding(sTempRect); + // Amount by which padding has to be scaled + float paddingScaleX = ((float) bounds.width()) / d.getIntrinsicWidth(); + float paddingScaleY = ((float) bounds.height()) / d.getIntrinsicHeight(); + mIndicatorRect.set( + bounds.left + sTempRect.left * paddingScaleX, + bounds.top + sTempRect.top * paddingScaleY, + bounds.right - sTempRect.right * paddingScaleX, + bounds.bottom - sTempRect.bottom * paddingScaleY); + + float inset = mPaint.getStrokeWidth() / 2; + mIndicatorRect.inset(inset, inset); + mIndicatorRectDirty = false; } @Override public void draw(Canvas canvas) { - final Rect r = getBounds(); - if (canvas.getClipBounds(mCanvasClipRect) && !Rect.intersects(mCanvasClipRect, r)) { + final Rect r = new Rect(getBounds()); + if (canvas.getClipBounds(sTempRect) && !Rect.intersects(sTempRect, r)) { // The draw region has been clipped. return; } + if (mIndicatorRectDirty) { + initIndicatorRect(); + } final float iconScale; if ((mAnimationProgress >= ANIMATION_PROGRESS_STARTED) && (mAnimationProgress < ANIMATION_PROGRESS_COMPLETED)) { mPaint.setAlpha((int) ((1 - mAnimationProgress) * 255)); - canvas.drawBitmap(sProgressBg, null, r, mPaint); - canvas.drawBitmap(sProgressFill, null, r, mPaint); - iconScale = ICON_SCALE_FACTOR + (1 - ICON_SCALE_FACTOR) * mAnimationProgress; + mBgDrawable.setAlpha(mPaint.getAlpha()); + mBgDrawable.draw(canvas); + canvas.drawOval(mIndicatorRect, mPaint); + iconScale = ICON_SCALE_FACTOR + (1 - ICON_SCALE_FACTOR) * mAnimationProgress; } else if (mAnimationProgress == ANIMATION_PROGRESS_STOPPED) { mPaint.setAlpha(255); iconScale = ICON_SCALE_FACTOR; - canvas.drawBitmap(sProgressBg, null, r, mPaint); + mBgDrawable.setAlpha(255); + mBgDrawable.draw(canvas); if (mProgress >= 100) { - canvas.drawBitmap(sProgressFill, null, r, mPaint); + canvas.drawOval(mIndicatorRect, mPaint); } else if (mProgress > 0) { - if (mPathChanged) { - mProgressPath.reset(); - mProgressPath.moveTo(r.exactCenterX(), r.centerY()); - - mRect.set(r); - mProgressPath.arcTo(mRect, -90, mProgress * 3.6f); - mProgressPath.close(); - mPathChanged = false; - } - - canvas.save(); - canvas.clipPath(mProgressPath); - canvas.drawBitmap(sProgressFill, null, r, mPaint); - canvas.restore(); + canvas.drawArc(mIndicatorRect, -90, mProgress * 3.6f, false, mPaint); } } else { iconScale = 1; @@ -103,12 +150,6 @@ class PreloadIconDrawable extends Drawable { } @Override - protected void onBoundsChange(Rect bounds) { - mIcon.setBounds(bounds); - mPathChanged = true; - } - - @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } @@ -126,7 +167,6 @@ class PreloadIconDrawable extends Drawable { @Override protected boolean onLevelChange(int level) { mProgress = level; - mPathChanged = true; // Stop Animation if (mAnimator != null) { @@ -134,6 +174,14 @@ class PreloadIconDrawable extends Drawable { mAnimator = null; } mAnimationProgress = ANIMATION_PROGRESS_STOPPED; + if (level > 0) { + // Set the paint color only when the level changes, so that the dominant color + // is only calculated when needed. + mPaint.setColor(getIndicatorColor()); + } + if (mIcon instanceof FastBitmapDrawable) { + ((FastBitmapDrawable) mIcon).setGhostModeEnabled(level <= 0); + } invalidateSelf(); return true; @@ -165,4 +213,37 @@ class PreloadIconDrawable extends Drawable { public float getAnimationProgress() { return mAnimationProgress; } + + @Override + public int getIntrinsicHeight() { + return mIcon.getIntrinsicHeight(); + } + + @Override + public int getIntrinsicWidth() { + return mIcon.getIntrinsicWidth(); + } + + private int getIndicatorColor() { + if (mIndicatorColor != 0) { + return mIndicatorColor; + } + if (!(mIcon instanceof FastBitmapDrawable)) { + mIndicatorColor = DEFAULT_COLOR; + return mIndicatorColor; + } + mIndicatorColor = Utilities.findDominantColorByHue( + ((FastBitmapDrawable) mIcon).getBitmap(), 20); + + // Make sure that the dominant color has enough saturation to be visible properly. + float[] hsv = new float[3]; + Color.colorToHSV(mIndicatorColor, hsv); + if (hsv[1] < MIN_SATUNATION) { + mIndicatorColor = DEFAULT_COLOR; + return mIndicatorColor; + } + hsv[2] = Math.max(MIN_LIGHTNESS, hsv[2]); + mIndicatorColor = Color.HSVToColor(hsv); + return mIndicatorColor; + } } diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index 0a711c5dd..87c2d7515 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -29,6 +29,7 @@ import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PaintFlagsDrawFilter; @@ -39,6 +40,7 @@ import android.graphics.drawable.PaintDrawable; import android.os.Build; import android.util.DisplayMetrics; import android.util.Log; +import android.util.SparseArray; import android.view.View; import android.widget.Toast; @@ -393,4 +395,83 @@ public final class Utilities { return false; } } + + /** + * This picks a dominant color, looking for high-saturation, high-value, repeated hues. + * @param bitmap The bitmap to scan + * @param samples The approximate max number of samples to use. + */ + static int findDominantColorByHue(Bitmap bitmap, int samples) { + final int height = bitmap.getHeight(); + final int width = bitmap.getWidth(); + int sampleStride = (int) Math.sqrt((height * width) / samples); + if (sampleStride < 1) { + sampleStride = 1; + } + + // This is an out-param, for getting the hsv values for an rgb + float[] hsv = new float[3]; + + // First get the best hue, by creating a histogram over 360 hue buckets, + // where each pixel contributes a score weighted by saturation, value, and alpha. + float[] hueScoreHistogram = new float[360]; + float highScore = -1; + int bestHue = -1; + + for (int y = 0; y < height; y += sampleStride) { + for (int x = 0; x < width; x += sampleStride) { + int argb = bitmap.getPixel(x, y); + int alpha = 0xFF & (argb >> 24); + if (alpha < 0x80) { + // Drop mostly-transparent pixels. + continue; + } + // Remove the alpha channel. + int rgb = argb | 0xFF000000; + Color.colorToHSV(rgb, hsv); + // Bucket colors by the 360 integer hues. + int hue = (int) hsv[0]; + if (hue < 0 || hue >= hueScoreHistogram.length) { + // Defensively avoid array bounds violations. + continue; + } + float score = hsv[1] * hsv[2]; + hueScoreHistogram[hue] += score; + if (hueScoreHistogram[hue] > highScore) { + highScore = hueScoreHistogram[hue]; + bestHue = hue; + } + } + } + + SparseArray rgbScores = new SparseArray(); + int bestColor = 0xff000000; + highScore = -1; + // Go back over the RGB colors that match the winning hue, + // creating a histogram of weighted s*v scores, for up to 100*100 [s,v] buckets. + // The highest-scoring RGB color wins. + for (int y = 0; y < height; y += sampleStride) { + for (int x = 0; x < width; x += sampleStride) { + int rgb = bitmap.getPixel(x, y) | 0xff000000; + Color.colorToHSV(rgb, hsv); + int hue = (int) hsv[0]; + if (hue == bestHue) { + float s = hsv[1]; + float v = hsv[2]; + int bucket = (int) (s * 100) + (int) (v * 10000); + // Score by cumulative saturation * value. + float score = s * v; + Float oldTotal = rgbScores.get(bucket); + float newTotal = oldTotal == null ? score : oldTotal + score; + rgbScores.put(bucket, newTotal); + if (newTotal > highScore) { + highScore = newTotal; + // All the colors in the winning bucket are very similar. Last in wins. + bestColor = rgb; + } + } + } + } + return bestColor; + } } diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index c8f2f33fb..711133215 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -73,6 +73,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; /** * The workspace is a wide area with a wallpaper and a finite number of pages. @@ -1986,6 +1987,12 @@ public class Workspace extends SmoothPagedView d.copyBounds(bounds); if (bounds.width() == 0 || bounds.height() == 0) { bounds.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); + } else { + bounds.offsetTo(0, 0); + } + if (d instanceof PreloadIconDrawable) { + int inset = -((PreloadIconDrawable) d).getOutset(); + bounds.inset(inset, inset); } return bounds; } @@ -2013,7 +2020,7 @@ public class Workspace extends SmoothPagedView Bitmap b = Bitmap.createBitmap(bmpWidth, bmpHeight, Bitmap.Config.ARGB_8888); Canvas c = new Canvas(b); - drawDragView(v, c, 0, true); + drawDragView(v, c, 0); c.setBitmap(null); // The outline is used to visualize where the item will land if dropped @@ -2528,18 +2535,18 @@ public class Workspace extends SmoothPagedView * @param destCanvas the canvas to draw on * @param padding the horizontal and vertical padding to use when drawing */ - private void drawDragView(View v, Canvas destCanvas, int padding, boolean pruneToDrawable) { + private void drawDragView(View v, Canvas destCanvas, int padding) { final Rect clipRect = mTempRect; v.getDrawingRect(clipRect); boolean textVisible = false; destCanvas.save(); - if (v instanceof TextView && pruneToDrawable) { + if (v instanceof TextView) { Drawable d = ((TextView) v).getCompoundDrawables()[1]; Rect bounds = getDrawableBounds(d); clipRect.set(0, 0, bounds.width() + padding, bounds.height() + padding); - destCanvas.translate(padding / 2, padding / 2); + destCanvas.translate(padding / 2 - bounds.left, padding / 2 - bounds.top); d.draw(destCanvas); } else { if (v instanceof FolderIcon) { @@ -2549,14 +2556,6 @@ public class Workspace extends SmoothPagedView ((FolderIcon) v).setTextVisible(false); textVisible = true; } - } else if (v instanceof BubbleTextView) { - final BubbleTextView tv = (BubbleTextView) v; - clipRect.bottom = tv.getExtendedPaddingTop() - (int) BubbleTextView.PADDING_V + - tv.getLayout().getLineTop(0); - } else if (v instanceof TextView) { - final TextView tv = (TextView) v; - clipRect.bottom = tv.getExtendedPaddingTop() - tv.getCompoundDrawablePadding() + - tv.getLayout().getLineTop(0); } destCanvas.translate(-v.getScrollX() + padding / 2, -v.getScrollY() + padding / 2); destCanvas.clipRect(clipRect, Op.REPLACE); @@ -2573,22 +2572,26 @@ public class Workspace extends SmoothPagedView /** * Returns a new bitmap to show when the given View is being dragged around. * Responsibility for the bitmap is transferred to the caller. + * @param expectedPadding padding to add to the drag view. If a different padding was used + * its value will be changed */ - public Bitmap createDragBitmap(View v, Canvas canvas, int padding) { + public Bitmap createDragBitmap(View v, Canvas canvas, AtomicInteger expectedPadding) { Bitmap b; + int padding = expectedPadding.get(); if (v instanceof TextView) { Drawable d = ((TextView) v).getCompoundDrawables()[1]; Rect bounds = getDrawableBounds(d); b = Bitmap.createBitmap(bounds.width() + padding, bounds.height() + padding, Bitmap.Config.ARGB_8888); + expectedPadding.set(padding - bounds.left - bounds.top); } else { b = Bitmap.createBitmap( v.getWidth() + padding, v.getHeight() + padding, Bitmap.Config.ARGB_8888); } canvas.setBitmap(b); - drawDragView(v, canvas, padding, true); + drawDragView(v, canvas, padding); canvas.setBitmap(null); return b; @@ -2604,7 +2607,7 @@ public class Workspace extends SmoothPagedView v.getWidth() + padding, v.getHeight() + padding, Bitmap.Config.ARGB_8888); canvas.setBitmap(b); - drawDragView(v, canvas, padding, true); + drawDragView(v, canvas, padding); mOutlineHelper.applyMediumExpensiveOutlineWithBlur(b, canvas, outlineColor, outlineColor); canvas.setBitmap(null); return b; @@ -2664,7 +2667,8 @@ public class Workspace extends SmoothPagedView public void beginDragShared(View child, DragSource source) { mLauncher.onDragStarted(child); // The drag bitmap follows the touch point around on the screen - final Bitmap b = createDragBitmap(child, new Canvas(), DRAG_BITMAP_PADDING); + AtomicInteger padding = new AtomicInteger(DRAG_BITMAP_PADDING); + final Bitmap b = createDragBitmap(child, new Canvas(), padding); final int bmpWidth = b.getWidth(); final int bmpHeight = b.getHeight(); @@ -2672,7 +2676,7 @@ public class Workspace extends SmoothPagedView float scale = mLauncher.getDragLayer().getLocationInDragLayer(child, mTempXY); int dragLayerX = Math.round(mTempXY[0] - (bmpWidth - scale * child.getWidth()) / 2); int dragLayerY = Math.round(mTempXY[1] - (bmpHeight - scale * bmpHeight) / 2 - - DRAG_BITMAP_PADDING / 2); + - padding.get() / 2); LauncherAppState app = LauncherAppState.getInstance(); DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); @@ -2687,7 +2691,7 @@ public class Workspace extends SmoothPagedView dragLayerY += top; // Note: The drag region is used to calculate drag layer offsets, but the // dragVisualizeOffset in addition to the dragRect (the size) to position the outline. - dragVisualizeOffset = new Point(-DRAG_BITMAP_PADDING / 2, DRAG_BITMAP_PADDING / 2); + dragVisualizeOffset = new Point(-padding.get() / 2, padding.get() / 2); dragRect = new Rect(left, top, right, bottom); } else if (child instanceof FolderIcon) { int previewSize = grid.folderIconSizePx; @@ -2731,7 +2735,8 @@ public class Workspace extends SmoothPagedView mLauncher.onDragStarted(child); // Compose a new drag bitmap that is of the icon size - final Bitmap tmpB = createDragBitmap(child, new Canvas(), DRAG_BITMAP_PADDING); + AtomicInteger padding = new AtomicInteger(DRAG_BITMAP_PADDING); + final Bitmap tmpB = createDragBitmap(child, new Canvas(), padding); Bitmap b = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888); Paint p = new Paint(); p.setFilterBitmap(true); @@ -2749,7 +2754,7 @@ public class Workspace extends SmoothPagedView // Note: The drag region is used to calculate drag layer offsets, but the // dragVisualizeOffset in addition to the dragRect (the size) to position the outline. - Point dragVisualizeOffset = new Point(-DRAG_BITMAP_PADDING / 2, DRAG_BITMAP_PADDING / 2); + Point dragVisualizeOffset = new Point(-padding.get() / 2, padding.get() / 2); Rect dragRect = new Rect(0, 0, iconSize, iconSize); if (child.getTag() == null || !(child.getTag() instanceof ItemInfo)) { @@ -3981,15 +3986,16 @@ public class Workspace extends SmoothPagedView } else { cellLayout.findCellForSpan(mTargetCell, 1, 1); } + // Add the item to DB before adding to screen ensures that the container and other + // values of the info is properly updated. + LauncherModel.addOrMoveItemInDatabase(mLauncher, info, container, screenId, + mTargetCell[0], mTargetCell[1]); + addInScreen(view, container, screenId, mTargetCell[0], mTargetCell[1], info.spanX, info.spanY, insertAtFirst); cellLayout.onDropChild(view); - CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams(); cellLayout.getShortcutsAndWidgets().measureChild(view); - LauncherModel.addOrMoveItemInDatabase(mLauncher, info, container, screenId, - lp.cellX, lp.cellY); - if (d.dragView != null) { // We wrap the animation call in the temporary set and reset of the current // cellLayout to its final transform -- this means we animate the drag view to