OSDN Git Service

Updating the virtual preloader UX.
authorSunny Goyal <sunnygoyal@google.com>
Mon, 4 Aug 2014 17:53:22 +0000 (10:53 -0700)
committerSunny Goyal <sunnygoyal@google.com>
Mon, 11 Aug 2014 19:32:04 +0000 (12:32 -0700)
> 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

12 files changed:
res/drawable-xxhdpi/virtual_preload.9.png [new file with mode: 0644]
res/drawable-xxhdpi/virtual_preload_folder.9.png [new file with mode: 0644]
res/values/attrs.xml
res/values/styles.xml
src/com/android/launcher3/BubbleTextView.java
src/com/android/launcher3/DragLayer.java
src/com/android/launcher3/FastBitmapDrawable.java
src/com/android/launcher3/Folder.java
src/com/android/launcher3/FolderIcon.java
src/com/android/launcher3/PreloadIconDrawable.java
src/com/android/launcher3/Utilities.java
src/com/android/launcher3/Workspace.java

diff --git a/res/drawable-xxhdpi/virtual_preload.9.png b/res/drawable-xxhdpi/virtual_preload.9.png
new file mode 100644 (file)
index 0000000..0ec1740
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 (file)
index 0000000..ee80c76
Binary files /dev/null and b/res/drawable-xxhdpi/virtual_preload_folder.9.png differ
index 12fa3cd..552e84c 100644 (file)
         <attr name="workspace" format="reference" />
     </declare-styleable>
 
+    <declare-styleable name="PreloadIconDrawable">
+        <attr name="background" format="reference" />
+        <attr name="ringOutset" format="dimension" />
+        <attr name="indicatorSize" format="dimension" />
+    </declare-styleable>
+
     <!-- Only used in the device overlays -->
     <declare-styleable name="CustomClingTitleText">
     </declare-styleable>
index 462c292..6079eee 100644 (file)
         <item name="android:shadowRadius">2.0</item>
     </style>
 
+    <style name="PreloadIcon">
+        <item name="background">@drawable/virtual_preload</item>
+        <item name="indicatorSize">4dp</item>
+        <item name="ringOutset">4dp</item>
+    </style>
+
+    <style name="PreloadIcon.Folder">
+        <item name="background">@drawable/virtual_preload_folder</item>
+        <item name="indicatorSize">4dp</item>
+        <item name="ringOutset">4dp</item>
+    </style>
+
     <!-- Overridden in device overlays -->
     <style name="CustomClingTitleText">
     </style>
index ab94814..5c2bb99 100644 (file)
@@ -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<Theme> 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;
+    }
 }
index 8bcc407..80f8dfc 100644 (file)
@@ -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
index ef8d097..cf7c22e 100644 (file)
@@ -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;
+    }
 }
index 655e5c3..fcedaea 100644 (file)
@@ -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);
index 5b49fb8..c0b9da7 100644 (file)
@@ -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());
 
index d9365cc..2972c4f 100644 (file)
 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;
+    }
 }
index 0a711c5..87c2d75 100644 (file)
@@ -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<Float> rgbScores = new SparseArray<Float>();
+        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;
+    }
 }
index c8f2f33..7111332 100644 (file)
@@ -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