First pass at new FolderIcon visual treatment

-> Modeled as a set of items around a circle
-> Modulate the radius and icon size as number of items grow
-> Clip the icons by a circular clip aligned to the background drawable

Remaining issues
-> Probably want to move to a programmaticly drawn circle + shadow
-> Anti-aliasing of the clipped region will need more attention
-> Need to animate all items in the preview as it changes (this
   wasn't required before)

Change-Id: I678ec605f6c8a34e9d7e4aec4e9583e36a9ef394
diff --git a/res/layout/user_folder_icon_normalized.xml b/res/layout/user_folder_icon_normalized.xml
index 75b5c48..d445a7a 100644
--- a/res/layout/user_folder_icon_normalized.xml
+++ b/res/layout/user_folder_icon_normalized.xml
@@ -14,7 +14,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.launcher3.Folder xmlns:android="http://schemas.android.com/apk/res/android"
+<com.android.launcher3.folder.Folder xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
@@ -34,7 +34,7 @@
             android:layout_width="20dp"
             android:layout_height="20dp" />
 
-        <com.android.launcher3.FolderPagedView
+        <com.android.launcher3.folder.FolderPagedView
             android:id="@+id/folder_content"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
@@ -82,4 +82,4 @@
 
     </LinearLayout>
 
-</com.android.launcher3.Folder>
+</com.android.launcher3.folder.Folder>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 468beaa..fee62dc 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -138,8 +138,8 @@
     <dimen name="quantum_panel_outer_padding">4dp</dimen>
 
 <!-- Folders -->
-    <!-- The amount that the preview contents are inset from the preview background -->
-    <dimen name="folder_preview_padding">4dp</dimen>
+    <!-- The size of the padding on the preview background drawable -->
+    <dimen name="folder_preview_padding">6dp</dimen>
 
 <!-- Sizes for managed profile badges -->
     <dimen name="profile_badge_size">24dp</dimen>
diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java
index d7f1d86..3870080 100644
--- a/src/com/android/launcher3/FastBitmapDrawable.java
+++ b/src/com/android/launcher3/FastBitmapDrawable.java
@@ -93,7 +93,7 @@
     private static final ColorMatrix sTempBrightnessMatrix = new ColorMatrix();
     private static final ColorMatrix sTempFilterMatrix = new ColorMatrix();
 
-    private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
+    private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
     private final Bitmap mBitmap;
     private State mState = State.NORMAL;
 
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 63c03e5..dcc4b29 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -40,13 +40,13 @@
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget;
 import com.android.launcher3.ExtendedEditText;
+import com.android.launcher3.folder.Folder;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherTransitionable;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.folder.Folder;
 import com.android.launcher3.util.ComponentKey;
 
 import java.nio.charset.Charset;
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index b0b602c..e8e15c2 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -31,7 +31,8 @@
     public static boolean IS_RELEASE_BUILD = true;
 
     // Custom flags go below this
-    public static boolean LAUNCHER3_ICON_NORMALIZATION = false;
     // As opposed to the new spring-loaded workspace.
     public static boolean LAUNCHER3_LEGACY_WORKSPACE_DND = false;
+    public static boolean LAUNCHER3_ICON_NORMALIZATION = true;
+    public static boolean LAUNCHER3_CLIPPED_FOLDER_ICON = false;
 }
diff --git a/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java b/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
new file mode 100644
index 0000000..44d7ac6
--- /dev/null
+++ b/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
@@ -0,0 +1,128 @@
+package com.android.launcher3.folder;
+
+import android.graphics.Path;
+import android.graphics.Point;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.Utilities;
+
+public class ClippedFolderIconLayoutRule implements FolderIcon.PreviewLayoutRule {
+
+    static final int MAX_NUM_ITEMS_IN_PREVIEW = 4;
+    private static final int MIN_NUM_ITEMS_IN_PREVIEW = 2;
+
+    final float MIN_SCALE = 0.48f;
+    final float MAX_SCALE = 0.58f;
+    final float MAX_RADIUS_DILATION = 0.15f;
+
+    private float[] mTmpPoint = new float[2];
+
+    private float mAvailableSpace;
+    private float mRadius;
+    private float mIconSize;
+    private boolean mIsRtl;
+    private Path mClipPath = new Path();
+
+    @Override
+    public void init(int availableSpace, int intrinsicIconSize, boolean rtl) {
+        mAvailableSpace = availableSpace;
+        mRadius = 0.66f * availableSpace;
+        mIconSize = intrinsicIconSize;
+        mIsRtl = rtl;
+
+        // We make the clip radius just slightly smaller than the background drawable
+        // TODO(adamcohen): this is hacky, needs cleanup (likely through programmatic drawing).
+        int clipRadius = (int) mAvailableSpace / 2 - 1;
+
+        mClipPath.addCircle(mAvailableSpace / 2, mAvailableSpace / 2, clipRadius, Path.Direction.CW);
+    }
+
+    @Override
+    public FolderIcon.PreviewItemDrawingParams computePreviewItemDrawingParams(int index,
+            int curNumItems, FolderIcon.PreviewItemDrawingParams params) {
+
+        getPosition(index, curNumItems, mTmpPoint);
+
+        float transX = mTmpPoint[0];
+        float transY = mTmpPoint[1];
+        float totalScale = scaleForNumItems(curNumItems);
+        float overlayAlpha = 0;
+
+        if (params == null) {
+            params = new FolderIcon.PreviewItemDrawingParams(transX, transY, totalScale, overlayAlpha);
+        } else {
+            params.transX = transX;
+            params.transY = transY;
+            params.scale = totalScale;
+            params.overlayAlpha = overlayAlpha;
+        }
+
+        return params;
+    }
+
+    private void getPosition(int index, int curNumItems, float[] result) {
+        // The case of two items is homomorphic to the case of one.
+        curNumItems = Math.max(curNumItems, 2);
+
+
+        // We model the preview as a circle of items starting in the appropriate piece of the
+        // upper left quadrant (to achieve horizontal and vertical symmetry).
+        double theta0 = mIsRtl ? 0 : Math.PI;
+
+        // In RTL we go counterclockwise
+        int direction = mIsRtl ? 1 : -1;
+
+        double thetaShift = 0;
+        if (curNumItems == 3) {
+            thetaShift = Math.PI / 6;
+        } else if (curNumItems == 4) {
+            thetaShift = Math.PI / 4;
+        }
+        theta0 += direction * thetaShift;
+
+        // We want the items to appear in reading order. For the case of 1, 2 and 3 items, this
+        // is natural for the circular model. With 4 items, however, we need to swap the 3rd and
+        // 4th indices to achieve reading order.
+        if (curNumItems == 4 && index == 3) {
+            index = 2;
+        } else if (curNumItems == 4 && index == 2) {
+            index = 3;
+        }
+
+        // We bump the radius up between 0 and MAX_RADIUS_DILATION % as the number of items increase
+        float radius = mRadius * (1 + MAX_RADIUS_DILATION * (curNumItems -
+                MIN_NUM_ITEMS_IN_PREVIEW) / (MAX_NUM_ITEMS_IN_PREVIEW - MIN_NUM_ITEMS_IN_PREVIEW));
+        double theta = theta0 + index * (2 * Math.PI / curNumItems) * direction;
+
+        float halfIconSize = (mIconSize * scaleForNumItems(curNumItems)) / 2;
+
+        // Map the location along the circle, and offset the coordinates to represent the center
+        // of the icon, and to be based from the top / left of the preview area. The y component
+        // is inverted to match the coordinate system.
+        result[0] = mAvailableSpace / 2 + (float) (radius * Math.cos(theta) / 2) - halfIconSize;
+        result[1] = mAvailableSpace / 2 + (float) (- radius * Math.sin(theta) / 2) - halfIconSize;
+
+    }
+
+    private float scaleForNumItems(int numItems) {
+        if (numItems <= 2) {
+            return MAX_SCALE;
+        } else if (numItems == 3) {
+            return (MAX_SCALE + MIN_SCALE) / 2;
+        } else {
+            return MIN_SCALE;
+        }
+    }
+
+    @Override
+    public int numItems() {
+        return MAX_NUM_ITEMS_IN_PREVIEW;
+    }
+
+    @Override
+    public Path getClipPath() {
+        return mClipPath;
+    }
+
+}
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 8b95e76..5c084d9 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -24,6 +24,7 @@
 import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.Path;
 import android.graphics.PorterDuff;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
@@ -65,6 +66,7 @@
 import com.android.launcher3.StylusEventHelper;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.util.Thunk;
@@ -81,8 +83,9 @@
     private FolderInfo mInfo;
     @Thunk static boolean sStaticValuesDirty = true;
 
-    // TODO(adamcohen): remove this
-    public static final int NUM_ITEMS_IN_PREVIEW = 3;
+    public static final int NUM_ITEMS_IN_PREVIEW = FeatureFlags.LAUNCHER3_CLIPPED_FOLDER_ICON ?
+            ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW :
+            StackFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
 
     private CheckLongPressHelper mLongPressHelper;
     private StylusEventHelper mStylusEventHelper;
@@ -152,7 +155,10 @@
     private void init() {
         mLongPressHelper = new CheckLongPressHelper(this);
         mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this);
-        mPreviewLayoutRule = new StackFolderIconLayoutRule();
+        mPreviewLayoutRule = FeatureFlags.LAUNCHER3_CLIPPED_FOLDER_ICON ?
+                new ClippedFolderIconLayoutRule() :
+                new StackFolderIconLayoutRule();
+
         setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate());
     }
 
@@ -468,7 +474,7 @@
             }
 
             int[] center = new int[2];
-            float scale = getLocalCenterForIndex(index, center);
+            float scale = getLocalCenterForIndex(index, index + 1, center);
             center[0] = (int) Math.round(scaleRelativeToDragLayer * center[0]);
             center[1] = (int) Math.round(scaleRelativeToDragLayer * center[1]);
 
@@ -516,7 +522,7 @@
             mIntrinsicIconSize = drawableSize;
             mTotalWidth = totalSize;
 
-            final int previewSize = mPreviewBackground.getLayoutParams().height;
+            final int previewSize = FolderRingAnimator.sPreviewSize;
             final int previewPadding = FolderRingAnimator.sPreviewPadding;
 
             mAvailableSpaceInPreview = (previewSize - 2 * previewPadding);
@@ -524,8 +530,8 @@
             mPreviewOffsetX = (mTotalWidth - mAvailableSpaceInPreview) / 2;
             mPreviewOffsetY = previewPadding + grid.folderBackgroundOffset + getPaddingTop();
 
-            // Initialize the preview layout rule
-            mPreviewLayoutRule.init(mAvailableSpaceInPreview, mIntrinsicIconSize);
+            mPreviewLayoutRule.init(mAvailableSpaceInPreview, mIntrinsicIconSize,
+                    Utilities.isRtl(getResources()));
         }
     }
 
@@ -547,9 +553,9 @@
         Drawable drawable;
     }
 
-    private float getLocalCenterForIndex(int index, int[] center) {
+    private float getLocalCenterForIndex(int index, int curNumItems, int[] center) {
         mParams = computePreviewItemDrawingParams(Math.min(mPreviewLayoutRule.numItems(), index),
-                mParams);
+                curNumItems, mParams);
 
         mParams.transX += mPreviewOffsetX;
         mParams.transY += mPreviewOffsetY;
@@ -561,14 +567,14 @@
         return mParams.scale;
     }
 
-    private PreviewItemDrawingParams computePreviewItemDrawingParams(int index,
+    private PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems,
             PreviewItemDrawingParams params) {
-        return mPreviewLayoutRule.computePreviewItemDrawingParams(index, params);
+        return mPreviewLayoutRule.computePreviewItemDrawingParams(index, curNumItems, params);
     }
 
     private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params) {
         canvas.save();
-        canvas.translate(params.transX + mPreviewOffsetX, params.transY + mPreviewOffsetY);
+        canvas.translate(params.transX, params.transY);
         canvas.scale(params.scale, params.scale);
         Drawable d = params.drawable;
 
@@ -612,13 +618,20 @@
             computePreviewDrawingParams(d);
         }
 
+        canvas.save();
+        canvas.translate(mPreviewOffsetX, mPreviewOffsetY);
+        Path clipPath = mPreviewLayoutRule.getClipPath();
+        if (clipPath != null) {
+            canvas.clipPath(clipPath);
+        }
+
         int nItemsInPreview = Math.min(items.size(), mPreviewLayoutRule.numItems());
         if (!mAnimating) {
             for (int i = nItemsInPreview - 1; i >= 0; i--) {
                 v = (TextView) items.get(i);
                 if (!mHiddenItems.contains(v.getTag())) {
                     d = getTopDrawable(v);
-                    mParams = computePreviewItemDrawingParams(i, mParams);
+                    mParams = computePreviewItemDrawingParams(i, nItemsInPreview, mParams);
                     mParams.drawable = d;
                     drawPreviewItem(canvas, mParams);
                 }
@@ -626,6 +639,7 @@
         } else {
             drawPreviewItem(canvas, mAnimParams);
         }
+        canvas.restore();
     }
 
     private Drawable getTopDrawable(TextView v) {
@@ -635,7 +649,9 @@
 
     private void animateFirstItem(final Drawable d, int duration, final boolean reverse,
             final Runnable onCompleteRunnable) {
-        final PreviewItemDrawingParams finalParams = computePreviewItemDrawingParams(0, null);
+
+        final PreviewItemDrawingParams finalParams =
+            computePreviewItemDrawingParams(0, reverse ? 1 : 2, null);
 
         float iconSize = mLauncher.getDeviceProfile().iconSizePx;
         final float scale0 = iconSize / d.getIntrinsicWidth() ;
@@ -749,11 +765,12 @@
     }
 
     public interface PreviewLayoutRule {
-        public PreviewItemDrawingParams computePreviewItemDrawingParams(int index,
+        public PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems,
             PreviewItemDrawingParams params);
 
-        public void init(int availableSpace, int intrinsicIconSize);
+        public void init(int availableSpace, int intrinsicIconSize, boolean rtl);
 
         public int numItems();
+        public Path getClipPath();
     }
 }
diff --git a/src/com/android/launcher3/folder/StackFolderIconLayoutRule.java b/src/com/android/launcher3/folder/StackFolderIconLayoutRule.java
index 1405b40..87f5f89 100644
--- a/src/com/android/launcher3/folder/StackFolderIconLayoutRule.java
+++ b/src/com/android/launcher3/folder/StackFolderIconLayoutRule.java
@@ -16,11 +16,13 @@
 
 package com.android.launcher3.folder;
 
+import android.graphics.Path;
+
 import com.android.launcher3.folder.FolderIcon.PreviewItemDrawingParams;
 
 public class StackFolderIconLayoutRule implements FolderIcon.PreviewLayoutRule {
 
-    public static final int NUM_ITEMS_IN_PREVIEW = 3;
+    static final int MAX_NUM_ITEMS_IN_PREVIEW = 3;
 
     // The degree to which the item in the back of the stack is scaled [0...1]
     // (0 means it's not scaled at all, 1 means it's scaled to nothing)
@@ -29,13 +31,13 @@
     // The amount of vertical spread between items in the stack [0...1]
     private static final float PERSPECTIVE_SHIFT_FACTOR = 0.18f;
 
-    //private int mIntrinsicIconSize;
     private float mBaselineIconScale;
     private int mBaselineIconSize;
     private int mAvailableSpaceInPreview;
     private float mMaxPerspectiveShift;
 
-    public void init(int availableSpace, int intrinsicIconSize) {
+    @Override
+    public void init(int availableSpace, int intrinsicIconSize, boolean rtl) {
         mAvailableSpaceInPreview = availableSpace;
 
         // cos(45) = 0.707  + ~= 0.1) = 0.8f
@@ -50,11 +52,11 @@
     }
 
     @Override
-    public PreviewItemDrawingParams computePreviewItemDrawingParams(int index,
+    public PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems,
             PreviewItemDrawingParams params) {
 
-        index = NUM_ITEMS_IN_PREVIEW - index - 1;
-        float r = (index * 1.0f) / (NUM_ITEMS_IN_PREVIEW - 1);
+        index = MAX_NUM_ITEMS_IN_PREVIEW - index - 1;
+        float r = (index * 1.0f) / (MAX_NUM_ITEMS_IN_PREVIEW - 1);
         float scale = (1 - PERSPECTIVE_SCALE_FACTOR * (1 - r));
 
         float offset = (1 - r) * mMaxPerspectiveShift;
@@ -81,6 +83,11 @@
 
     @Override
     public int numItems() {
-        return NUM_ITEMS_IN_PREVIEW;
+        return MAX_NUM_ITEMS_IN_PREVIEW;
+    }
+
+    @Override
+    public Path getClipPath() {
+        return null;
     }
 }