Move PreviewBackground into its own class file.

This is done in preparation for new PreviewBackground animation
where we will be animating in the first page preview items when
closing the Folder from a non-first page.

Bug: 36022592
Bug: 35064148
Change-Id: Id4d54da16ddfa852ca639a2dc8d328307d4d443a
diff --git a/src/com/android/launcher3/folder/PreviewBackground.java b/src/com/android/launcher3/folder/PreviewBackground.java
new file mode 100644
index 0000000..44ebbcd
--- /dev/null
+++ b/src/com/android/launcher3/folder/PreviewBackground.java
@@ -0,0 +1,430 @@
+/*
+ * 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.folder;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.RadialGradient;
+import android.graphics.Region;
+import android.graphics.Shader;
+import android.support.v4.graphics.ColorUtils;
+import android.util.Property;
+import android.view.View;
+
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAnimUtils;
+import com.android.launcher3.util.Themes;
+
+/**
+ * This object represents a FolderIcon preview background. It stores drawing / measurement
+ * information, handles drawing, and animation (accept state <--> rest state).
+ */
+public class PreviewBackground {
+
+    private static final int CONSUMPTION_ANIMATION_DURATION = 100;
+
+    private final PorterDuffXfermode mClipPorterDuffXfermode
+            = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
+    // Create a RadialGradient such that it draws a black circle and then extends with
+    // transparent. To achieve this, we keep the gradient to black for the range [0, 1) and
+    // just at the edge quickly change it to transparent.
+    private final RadialGradient mClipShader = new RadialGradient(0, 0, 1,
+            new int[] {Color.BLACK, Color.BLACK, Color.TRANSPARENT },
+            new float[] {0, 0.999f, 1},
+            Shader.TileMode.CLAMP);
+
+    private final PorterDuffXfermode mShadowPorterDuffXfermode
+            = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
+    private RadialGradient mShadowShader = null;
+
+    private final Matrix mShaderMatrix = new Matrix();
+    private final Path mPath = new Path();
+
+    private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+    float mScale = 1f;
+    private float mColorMultiplier = 1f;
+    private int mBgColor;
+    private float mStrokeWidth;
+    private int mStrokeAlpha = MAX_BG_OPACITY;
+    private int mShadowAlpha = 255;
+    private View mInvalidateDelegate;
+
+    int previewSize;
+    int basePreviewOffsetX;
+    int basePreviewOffsetY;
+
+    private CellLayout mDrawingDelegate;
+    public int delegateCellX;
+    public int delegateCellY;
+
+    // When the PreviewBackground is drawn under an icon (for creating a folder) the border
+    // should not occlude the icon
+    public boolean isClipping = true;
+
+    // Drawing / animation configurations
+    private static final float ACCEPT_SCALE_FACTOR = 1.25f;
+    private static final float ACCEPT_COLOR_MULTIPLIER = 1.5f;
+
+    // Expressed on a scale from 0 to 255.
+    private static final int BG_OPACITY = 160;
+    private static final int MAX_BG_OPACITY = 225;
+    private static final int SHADOW_OPACITY = 40;
+
+    private ValueAnimator mScaleAnimator;
+    private ObjectAnimator mStrokeAlphaAnimator;
+    private ObjectAnimator mShadowAnimator;
+
+    private static final Property<PreviewBackground, Integer> STROKE_ALPHA =
+            new Property<PreviewBackground, Integer>(Integer.class, "strokeAlpha") {
+                @Override
+                public Integer get(PreviewBackground previewBackground) {
+                    return previewBackground.mStrokeAlpha;
+                }
+
+                @Override
+                public void set(PreviewBackground previewBackground, Integer alpha) {
+                    previewBackground.mStrokeAlpha = alpha;
+                    previewBackground.invalidate();
+                }
+            };
+
+    private static final Property<PreviewBackground, Integer> SHADOW_ALPHA =
+            new Property<PreviewBackground, Integer>(Integer.class, "shadowAlpha") {
+                @Override
+                public Integer get(PreviewBackground previewBackground) {
+                    return previewBackground.mShadowAlpha;
+                }
+
+                @Override
+                public void set(PreviewBackground previewBackground, Integer alpha) {
+                    previewBackground.mShadowAlpha = alpha;
+                    previewBackground.invalidate();
+                }
+            };
+
+    public void setup(Launcher launcher, View invalidateDelegate,
+                      int availableSpace, int topPadding) {
+        mInvalidateDelegate = invalidateDelegate;
+        mBgColor = Themes.getAttrColor(launcher, android.R.attr.colorPrimary);
+
+        DeviceProfile grid = launcher.getDeviceProfile();
+        final int previewSize = grid.folderIconSizePx;
+        final int previewPadding = grid.folderIconPreviewPadding;
+
+        this.previewSize = (previewSize - 2 * previewPadding);
+
+        basePreviewOffsetX = (availableSpace - this.previewSize) / 2;
+        basePreviewOffsetY = previewPadding + grid.folderBackgroundOffset + topPadding;
+
+        // Stroke width is 1dp
+        mStrokeWidth = launcher.getResources().getDisplayMetrics().density;
+
+        float radius = getScaledRadius();
+        float shadowRadius = radius + mStrokeWidth;
+        int shadowColor = Color.argb(SHADOW_OPACITY, 0, 0, 0);
+        mShadowShader = new RadialGradient(0, 0, 1,
+                new int[] {shadowColor, Color.TRANSPARENT},
+                new float[] {radius / shadowRadius, 1},
+                Shader.TileMode.CLAMP);
+
+        invalidate();
+    }
+
+    int getRadius() {
+        return previewSize / 2;
+    }
+
+    int getScaledRadius() {
+        return (int) (mScale * getRadius());
+    }
+
+    int getOffsetX() {
+        return basePreviewOffsetX - (getScaledRadius() - getRadius());
+    }
+
+    int getOffsetY() {
+        return basePreviewOffsetY - (getScaledRadius() - getRadius());
+    }
+
+    /**
+     * Returns the progress of the scale animation, where 0 means the scale is at 1f
+     * and 1 means the scale is at ACCEPT_SCALE_FACTOR.
+     */
+    float getScaleProgress() {
+        return (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f);
+    }
+
+    void invalidate() {
+        if (mInvalidateDelegate != null) {
+            mInvalidateDelegate.invalidate();
+        }
+
+        if (mDrawingDelegate != null) {
+            mDrawingDelegate.invalidate();
+        }
+    }
+
+    void setInvalidateDelegate(View invalidateDelegate) {
+        mInvalidateDelegate = invalidateDelegate;
+        invalidate();
+    }
+
+    public void drawBackground(Canvas canvas) {
+        mPaint.setStyle(Paint.Style.FILL);
+        int alpha = (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
+        mPaint.setColor(ColorUtils.setAlphaComponent(mBgColor, alpha));
+
+        drawCircle(canvas, 0 /* deltaRadius */);
+
+        // Draw shadow.
+        if (mShadowShader == null) {
+            return;
+        }
+        float radius = getScaledRadius();
+        float shadowRadius = radius + mStrokeWidth;
+        mPaint.setColor(Color.BLACK);
+        int offsetX = getOffsetX();
+        int offsetY = getOffsetY();
+        final int saveCount;
+        if (canvas.isHardwareAccelerated()) {
+            saveCount = canvas.saveLayer(offsetX - mStrokeWidth, offsetY,
+                    offsetX + radius + shadowRadius, offsetY + shadowRadius + shadowRadius,
+                    null, Canvas.CLIP_TO_LAYER_SAVE_FLAG | Canvas.HAS_ALPHA_LAYER_SAVE_FLAG);
+
+        } else {
+            saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
+            clipCanvasSoftware(canvas, Region.Op.DIFFERENCE);
+        }
+
+        mShaderMatrix.setScale(shadowRadius, shadowRadius);
+        mShaderMatrix.postTranslate(radius + offsetX, shadowRadius + offsetY);
+        mShadowShader.setLocalMatrix(mShaderMatrix);
+        mPaint.setAlpha(mShadowAlpha);
+        mPaint.setShader(mShadowShader);
+        canvas.drawPaint(mPaint);
+        mPaint.setAlpha(255);
+        mPaint.setShader(null);
+        if (canvas.isHardwareAccelerated()) {
+            mPaint.setXfermode(mShadowPorterDuffXfermode);
+            canvas.drawCircle(radius + offsetX, radius + offsetY, radius, mPaint);
+            mPaint.setXfermode(null);
+        }
+
+        canvas.restoreToCount(saveCount);
+    }
+
+    public void fadeInBackgroundShadow() {
+        if (mShadowAnimator != null) {
+            mShadowAnimator.cancel();
+        }
+        mShadowAnimator = ObjectAnimator
+                .ofInt(this, SHADOW_ALPHA, 0, 255)
+                .setDuration(100);
+        mShadowAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mShadowAnimator = null;
+            }
+        });
+        mShadowAnimator.start();
+    }
+
+    public void animateBackgroundStroke() {
+        if (mStrokeAlphaAnimator != null) {
+            mStrokeAlphaAnimator.cancel();
+        }
+        mStrokeAlphaAnimator = ObjectAnimator
+                .ofInt(this, STROKE_ALPHA, MAX_BG_OPACITY / 2, MAX_BG_OPACITY)
+                .setDuration(100);
+        mStrokeAlphaAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mStrokeAlphaAnimator = null;
+            }
+        });
+        mStrokeAlphaAnimator.start();
+    }
+
+    public void drawBackgroundStroke(Canvas canvas) {
+        mPaint.setColor(ColorUtils.setAlphaComponent(mBgColor, mStrokeAlpha));
+        mPaint.setStyle(Paint.Style.STROKE);
+        mPaint.setStrokeWidth(mStrokeWidth);
+        drawCircle(canvas, 1 /* deltaRadius */);
+    }
+
+    public void drawLeaveBehind(Canvas canvas) {
+        float originalScale = mScale;
+        mScale = 0.5f;
+
+        mPaint.setStyle(Paint.Style.FILL);
+        mPaint.setColor(Color.argb(160, 245, 245, 245));
+        drawCircle(canvas, 0 /* deltaRadius */);
+
+        mScale = originalScale;
+    }
+
+    private void drawCircle(Canvas canvas,float deltaRadius) {
+        float radius = getScaledRadius();
+        canvas.drawCircle(radius + getOffsetX(), radius + getOffsetY(),
+                radius - deltaRadius, mPaint);
+    }
+
+    // It is the callers responsibility to save and restore the canvas layers.
+    void clipCanvasSoftware(Canvas canvas, Region.Op op) {
+        mPath.reset();
+        float r = getScaledRadius();
+        mPath.addCircle(r + getOffsetX(), r + getOffsetY(), r, Path.Direction.CW);
+        canvas.clipPath(mPath, op);
+    }
+
+    // It is the callers responsibility to save and restore the canvas layers.
+    void clipCanvasHardware(Canvas canvas) {
+        mPaint.setColor(Color.BLACK);
+        mPaint.setXfermode(mClipPorterDuffXfermode);
+
+        float radius = getScaledRadius();
+        mShaderMatrix.setScale(radius, radius);
+        mShaderMatrix.postTranslate(radius + getOffsetX(), radius + getOffsetY());
+        mClipShader.setLocalMatrix(mShaderMatrix);
+        mPaint.setShader(mClipShader);
+        canvas.drawPaint(mPaint);
+        mPaint.setXfermode(null);
+        mPaint.setShader(null);
+    }
+
+    private void delegateDrawing(CellLayout delegate, int cellX, int cellY) {
+        if (mDrawingDelegate != delegate) {
+            delegate.addFolderBackground(this);
+        }
+
+        mDrawingDelegate = delegate;
+        delegateCellX = cellX;
+        delegateCellY = cellY;
+
+        invalidate();
+    }
+
+    private void clearDrawingDelegate() {
+        if (mDrawingDelegate != null) {
+            mDrawingDelegate.removeFolderBackground(this);
+        }
+
+        mDrawingDelegate = null;
+        invalidate();
+    }
+
+    boolean drawingDelegated() {
+        return mDrawingDelegate != null;
+    }
+
+    private void animateScale(float finalScale, float finalMultiplier,
+                              final Runnable onStart, final Runnable onEnd) {
+        final float scale0 = mScale;
+        final float scale1 = finalScale;
+
+        final float bgMultiplier0 = mColorMultiplier;
+        final float bgMultiplier1 = finalMultiplier;
+
+        if (mScaleAnimator != null) {
+            mScaleAnimator.cancel();
+        }
+
+        mScaleAnimator = LauncherAnimUtils.ofFloat(0f, 1.0f);
+
+        mScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                float prog = animation.getAnimatedFraction();
+                mScale = prog * scale1 + (1 - prog) * scale0;
+                mColorMultiplier = prog * bgMultiplier1 + (1 - prog) * bgMultiplier0;
+                invalidate();
+            }
+        });
+        mScaleAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                if (onStart != null) {
+                    onStart.run();
+                }
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (onEnd != null) {
+                    onEnd.run();
+                }
+                mScaleAnimator = null;
+            }
+        });
+
+        mScaleAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
+        mScaleAnimator.start();
+    }
+
+    public void animateToAccept(final CellLayout cl, final int cellX, final int cellY) {
+        Runnable onStart = new Runnable() {
+            @Override
+            public void run() {
+                delegateDrawing(cl, cellX, cellY);
+            }
+        };
+        animateScale(ACCEPT_SCALE_FACTOR, ACCEPT_COLOR_MULTIPLIER, onStart, null);
+    }
+
+    public void animateToRest() {
+        // This can be called multiple times -- we need to make sure the drawing delegate
+        // is saved and restored at the beginning of the animation, since cancelling the
+        // existing animation can clear the delgate.
+        final CellLayout cl = mDrawingDelegate;
+        final int cellX = delegateCellX;
+        final int cellY = delegateCellY;
+
+        Runnable onStart = new Runnable() {
+            @Override
+            public void run() {
+                delegateDrawing(cl, cellX, cellY);
+            }
+        };
+        Runnable onEnd = new Runnable() {
+            @Override
+            public void run() {
+                clearDrawingDelegate();
+            }
+        };
+        animateScale(1f, 1f, onStart, onEnd);
+    }
+
+    public int getBackgroundAlpha() {
+        return (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
+    }
+
+    public float getStrokeWidth() {
+        return mStrokeWidth;
+    }
+}