Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1e2d004d authored by Samuel Fufa's avatar Samuel Fufa
Browse files

Hybrid hotseat predicted icon visuals

Bug:142753423
Test: Manual
Change-Id: I6f056aaec905c8ca357b7cf78a657cdaac84e2f1
parent 0cdb388a
Loading
Loading
Loading
Loading
+17 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2019 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.
-->

<com.android.launcher3.uioverrides.PredictedAppIcon style="@style/BaseIcon.Workspace" />
+74 −48
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.launcher3;
import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;

import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.prediction.AppPredictionContext;
import android.app.prediction.AppPredictionManager;
@@ -40,10 +41,9 @@ import com.android.launcher3.appprediction.DynamicItemCache;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.popup.PopupContainerWithArrow;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.shortcuts.ShortcutKey;
import com.android.launcher3.touch.ItemLongClickListener;
import com.android.launcher3.uioverrides.PredictedAppIcon;
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.launcher3.util.ComponentKey;

@@ -81,12 +81,14 @@ public class HotseatPredictionController implements DragController.DragListener,
    private AppPredictor mAppPredictor;
    private AllAppsStore mAllAppsStore;

    private List<PredictedAppIcon.PredictedIconOutlineDrawing> mOutlineDrawings = new ArrayList<>();

    public HotseatPredictionController(Launcher launcher) {
        mLauncher = launcher;
        mHotseat = launcher.getHotseat();
        mAllAppsStore = mLauncher.getAppsView().getAppsStore();
        mAllAppsStore.addUpdateListener(this);
        mDynamicItemCache = new DynamicItemCache(mLauncher, () -> fillGapsWithPrediction(false));
        mDynamicItemCache = new DynamicItemCache(mLauncher, this::fillGapsWithPrediction);
        mHotSeatItemsCount = mLauncher.getDeviceProfile().inv.numHotseatIcons;
        launcher.getDeviceProfile().inv.addOnChangeListener(this);
        mHotseat.addOnAttachStateChangeListener(this);
@@ -102,16 +104,17 @@ public class HotseatPredictionController implements DragController.DragListener,
        mLauncher.getDragController().removeDragListener(this);
    }

    /**
     * Fills gaps in the hotseat with predictions
     */
    public void fillGapsWithPrediction(boolean animate) {
    private void fillGapsWithPrediction() {
        fillGapsWithPrediction(false, null);
    }

    private void fillGapsWithPrediction(boolean animate, Runnable callback) {
        if (mDragObject != null) {
            return;
        }
        List<WorkspaceItemInfo> predictedApps = mapToWorkspaceItemInfo(mComponentKeyMappers);
        int predictionIndex = 0;
        ArrayList<ItemInfo> newItemsToAdd = new ArrayList<>();
        ArrayList<WorkspaceItemInfo> newItems = new ArrayList<>();
        for (int rank = 0; rank < mHotSeatItemsCount; rank++) {
            View child = mHotseat.getChildAt(
                    mHotseat.getCellXFromOrder(rank),
@@ -130,21 +133,37 @@ public class HotseatPredictionController implements DragController.DragListener,

            WorkspaceItemInfo predictedItem = predictedApps.get(predictionIndex++);
            if (isPredictedIcon(child)) {
                BubbleTextView icon = (BubbleTextView) child;
                PredictedAppIcon icon = (PredictedAppIcon) child;
                icon.applyFromWorkspaceItem(predictedItem);
                icon.finishBinding();
            } else {
                newItemsToAdd.add(predictedItem);
                newItems.add(predictedItem);
            }
            preparePredictionInfo(predictedItem, rank);
        }
        mLauncher.bindItems(newItemsToAdd, animate);
        for (BubbleTextView icon : getPredictedIcons()) {
            icon.verifyHighRes();
            icon.setOnLongClickListener((v) -> {
                PopupContainerWithArrow.showForIcon((BubbleTextView) v);
                return true;
        bindItems(newItems, animate, callback);
    }

    private void bindItems(List<WorkspaceItemInfo> itemsToAdd, boolean animate, Runnable callback) {
        AnimatorSet animationSet = new AnimatorSet();
        for (WorkspaceItemInfo item : itemsToAdd) {
            PredictedAppIcon icon = PredictedAppIcon.createIcon(mHotseat, item);
            mLauncher.getWorkspace().addInScreenFromBind(icon, item);
            icon.finishBinding();
            if (animate) {
                animationSet.play(ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 0.2f, 1));
            }
        }
        if (animate) {
            animationSet.addListener(new AnimationSuccessListener() {
                @Override
                public void onAnimationSuccess(Animator animator) {
                    if (callback != null) callback.run();
                }
            });
            icon.setBackgroundResource(R.drawable.predicted_icon_background);
            animationSet.start();
        } else {
            if (callback != null) callback.run();
        }
    }

@@ -179,6 +198,7 @@ public class HotseatPredictionController implements DragController.DragListener,
                        .build());
        mAppPredictor.registerPredictionUpdates(mLauncher.getMainExecutor(),
                this::setPredictedApps);

        mAppPredictor.requestPredictionUpdate();
    }

@@ -210,7 +230,7 @@ public class HotseatPredictionController implements DragController.DragListener,
            mComponentKeyMappers.add(new ComponentKeyMapper(key, mDynamicItemCache));
        }
        updateDependencies();
        fillGapsWithPrediction(false);
        fillGapsWithPrediction();
    }

    private void updateDependencies() {
@@ -219,7 +239,7 @@ public class HotseatPredictionController implements DragController.DragListener,
    }

    private void pinPrediction(ItemInfo info) {
        BubbleTextView icon = (BubbleTextView) mHotseat.getChildAt(
        PredictedAppIcon icon = (PredictedAppIcon) mHotseat.getChildAt(
                mHotseat.getCellXFromOrder(info.rank),
                mHotseat.getCellYFromOrder(info.rank));
        if (icon == null) {
@@ -230,9 +250,7 @@ public class HotseatPredictionController implements DragController.DragListener,
                LauncherSettings.Favorites.CONTAINER_HOTSEAT, workspaceItemInfo.screenId,
                workspaceItemInfo.cellX, workspaceItemInfo.cellY);
        ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 1, 0.8f, 1).start();
        icon.reset();
        icon.applyFromWorkspaceItem(workspaceItemInfo);
        icon.setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
        icon.pin(workspaceItemInfo);
        AppTarget appTarget = getAppTargetFromItemInfo(workspaceItemInfo);
        notifyItemAction(appTarget, AppTargetEvent.ACTION_PIN);
    }
@@ -265,21 +283,23 @@ public class HotseatPredictionController implements DragController.DragListener,
        return predictedApps;
    }

    private List<BubbleTextView> getPredictedIcons() {
        List<BubbleTextView> icons = new ArrayList<>();
    private List<PredictedAppIcon> getPredictedIcons() {
        List<PredictedAppIcon> icons = new ArrayList<>();
        ViewGroup vg = mHotseat.getShortcutsAndWidgets();
        for (int i = 0; i < vg.getChildCount(); i++) {
            View child = vg.getChildAt(i);
            if (isPredictedIcon(child)) {
                icons.add((BubbleTextView) child);
                icons.add((PredictedAppIcon) child);
            }
        }
        return icons;
    }

    private void removePredictedApps(boolean animate) {
        for (BubbleTextView icon : getPredictedIcons()) {
            if (animate) {
    private void removePredictedApps(List<PredictedAppIcon.PredictedIconOutlineDrawing> outlines) {
        for (PredictedAppIcon icon : getPredictedIcons()) {
            int rank = ((WorkspaceItemInfo) icon.getTag()).rank;
            outlines.add(new PredictedAppIcon.PredictedIconOutlineDrawing(
                    mHotseat.getCellXFromOrder(rank), mHotseat.getCellYFromOrder(rank), icon));
            icon.animate().scaleY(0).scaleX(0).setListener(new AnimationSuccessListener() {
                @Override
                public void onAnimationSuccess(Animator animator) {
@@ -288,14 +308,10 @@ public class HotseatPredictionController implements DragController.DragListener,
                    }
                }
            });
            } else {
                if (icon.getParent() != null) {
                    mHotseat.removeView(icon);
                }
            }
        }
    }


    private void notifyItemAction(AppTarget target, int action) {
        if (mAppPredictor != null) {
            mAppPredictor.notifyAppTargetEvent(new AppTargetEvent.Builder(target, action).build());
@@ -304,8 +320,13 @@ public class HotseatPredictionController implements DragController.DragListener,

    @Override
    public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
        removePredictedApps(true);
        removePredictedApps(mOutlineDrawings);
        mDragObject = dragObject;
        if (mOutlineDrawings.isEmpty()) return;
        for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
            mHotseat.addDelegatedCellDrawing(outlineDrawing);
        }
        mHotseat.invalidate();
    }

    @Override
@@ -322,7 +343,14 @@ public class HotseatPredictionController implements DragController.DragListener,
            }
        }
        mDragObject = null;
        fillGapsWithPrediction(true);
        fillGapsWithPrediction(true, () -> {
            if (mOutlineDrawings.isEmpty()) return;
            for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
                mHotseat.removeDelegatedCellDrawing(outlineDrawing);
            }
            mHotseat.invalidate();
            mOutlineDrawings.clear();
        });
    }

    @Nullable
@@ -351,8 +379,7 @@ public class HotseatPredictionController implements DragController.DragListener,

    @Override
    public void onAppsUpdated() {
        updateDependencies();
        fillGapsWithPrediction(false);
        fillGapsWithPrediction();
    }

    @Override
@@ -375,7 +402,7 @@ public class HotseatPredictionController implements DragController.DragListener,
    }

    private static boolean isPredictedIcon(View view) {
        return view instanceof BubbleTextView && view.getTag() instanceof WorkspaceItemInfo
        return view instanceof PredictedAppIcon && view.getTag() instanceof WorkspaceItemInfo
                && ((WorkspaceItemInfo) view.getTag()).container
                == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
    }
@@ -396,9 +423,8 @@ public class HotseatPredictionController implements DragController.DragListener,

    private static AppTarget getAppTargetFromItemInfo(ItemInfo info) {
        if (info.getTargetComponent() == null) return null;
        return new AppTarget.Builder(
                new AppTargetId("app:" + info.getTargetComponent().getPackageName()),
                info.getTargetComponent().getPackageName(), info.user).setClassName(
                info.getTargetComponent().getClassName()).build();
        ComponentName cn = info.getTargetComponent();
        return new AppTarget.Builder(new AppTargetId("app:" + cn.getPackageName()),
                cn.getPackageName(), info.user).setClassName(cn.getClassName()).build();
    }
}
+188 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.uioverrides;

import static com.android.launcher3.graphics.IconShape.getShape;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.ViewGroup;

import androidx.core.graphics.ColorUtils;

import com.android.launcher3.BubbleTextView;
import com.android.launcher3.CellLayout;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.graphics.IconPalette;
import com.android.launcher3.icons.IconNormalizer;
import com.android.launcher3.popup.PopupContainerWithArrow;
import com.android.launcher3.touch.ItemClickHandler;
import com.android.launcher3.touch.ItemLongClickListener;

/**
 * A BubbleTextView with a ring around it's drawable
 */
public class PredictedAppIcon extends BubbleTextView {

    private static final float RING_EFFECT_RATIO = 0.12f;

    private DeviceProfile mDeviceProfile;
    private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private boolean mIsPinned = false;
    private int mNormalizedIconRadius;


    public PredictedAppIcon(Context context) {
        this(context, null, 0);
    }

    public PredictedAppIcon(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PredictedAppIcon(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mDeviceProfile = Launcher.getLauncher(context).getDeviceProfile();
        mNormalizedIconRadius = IconNormalizer.getNormalizedCircleSize(getIconSize()) / 2;
        setOnClickListener(ItemClickHandler.INSTANCE);
        setOnFocusChangeListener(Launcher.getLauncher(context).mFocusHandler);
    }

    @Override
    public void onDraw(Canvas canvas) {
        int count = canvas.save();
        if (!mIsPinned) {
            drawEffect(canvas);
            canvas.translate(getWidth() * RING_EFFECT_RATIO, getHeight() * RING_EFFECT_RATIO);
            canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO);
        }
        super.onDraw(canvas);
        canvas.restoreToCount(count);
    }

    @Override
    public void applyFromWorkspaceItem(WorkspaceItemInfo info) {
        super.applyFromWorkspaceItem(info);
        int color = IconPalette.getMutedColor(info.bitmap.color, 0.54f);
        mIconRingPaint.setColor(ColorUtils.setAlphaComponent(color, 200));
    }

    /**
     * Removes prediction ring from app icon
     */
    public void pin(WorkspaceItemInfo info) {
        if (mIsPinned) return;
        applyFromWorkspaceItem(info);
        setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
        mIsPinned = true;
        invalidate();
    }

    /**
     * prepares prediction icon for usage after bind
     */
    public void finishBinding() {
        setOnLongClickListener((v) -> {
            PopupContainerWithArrow.showForIcon((BubbleTextView) v);
            if (getParent() != null) {
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            return true;
        });
        setTextVisibility(false);
        verifyHighRes();
    }

    @Override
    public void getIconBounds(Rect outBounds) {
        super.getIconBounds(outBounds);
        if (!mIsPinned) {
            int predictionInset = (int) (getIconSize() * RING_EFFECT_RATIO);
            outBounds.inset(predictionInset, predictionInset);
        }
    }

    private int getOutlineOffsetX() {
        return (getMeasuredWidth() / 2) - mNormalizedIconRadius;
    }

    private int getOutlineOffsetY() {
        return getPaddingTop() + mDeviceProfile.folderIconOffsetYPx;
    }

    private void drawEffect(Canvas canvas) {
        getShape().drawShape(canvas, getOutlineOffsetX(), getOutlineOffsetY(),
                mNormalizedIconRadius, mIconRingPaint);
    }

    /**
     * Creates and returns a new instance of PredictedAppIcon from WorkspaceItemInfo
     */
    public static PredictedAppIcon createIcon(ViewGroup parent, WorkspaceItemInfo info) {
        PredictedAppIcon icon = (PredictedAppIcon) LayoutInflater.from(parent.getContext())
                .inflate(R.layout.predicted_app_icon, parent, false);
        icon.applyFromWorkspaceItem(info);
        return icon;
    }

    /**
     * Draws Predicted Icon outline on cell layout
     */
    public static class PredictedIconOutlineDrawing extends CellLayout.DelegatedCellDrawing {

        private int mOffsetX;
        private int mOffsetY;
        private int mIconRadius;
        private Paint mOutlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);

        public PredictedIconOutlineDrawing(int cellX, int cellY, PredictedAppIcon icon) {
            mDelegateCellX = cellX;
            mDelegateCellY = cellY;
            mOffsetX = icon.getOutlineOffsetX();
            mOffsetY = icon.getOutlineOffsetY();
            mIconRadius = icon.mNormalizedIconRadius;
            mOutlinePaint.setStyle(Paint.Style.STROKE);
            mOutlinePaint.setStrokeWidth(5);
            mOutlinePaint.setPathEffect(new DashPathEffect(new float[]{15, 15}, 0));
            mOutlinePaint.setColor(Color.argb(100, 245, 245, 245));
        }

        /**
         * Draws predicted app icon outline under CellLayout
         */
        @Override
        public void drawUnderItem(Canvas canvas) {
            getShape().drawShape(canvas, mOffsetX, mOffsetY, mIconRadius, mOutlinePaint);
        }

        /**
         * Draws PredictedAppIcon outline over CellLayout
         */
        @Override
        public void drawOverItem(Canvas canvas) {
            // Does nothing
        }
    }
}
+50 −30
Original line number Diff line number Diff line
@@ -110,7 +110,7 @@ public class CellLayout extends ViewGroup implements Transposable {

    private OnTouchListener mInterceptTouchListener;

    private final ArrayList<PreviewBackground> mFolderBackgrounds = new ArrayList<>();
    private final ArrayList<DelegatedCellDrawing> mDelegatedCellDrawings = new ArrayList<>();
    final PreviewBackground mFolderLeaveBehind = new PreviewBackground();

    private static final int[] BACKGROUND_STATE_ACTIVE = new int[] { android.R.attr.state_active };
@@ -219,8 +219,8 @@ public class CellLayout extends ViewGroup implements Transposable {
        mPreviousReorderDirection[0] = INVALID_DIRECTION;
        mPreviousReorderDirection[1] = INVALID_DIRECTION;

        mFolderLeaveBehind.delegateCellX = -1;
        mFolderLeaveBehind.delegateCellY = -1;
        mFolderLeaveBehind.mDelegateCellX = -1;
        mFolderLeaveBehind.mDelegateCellY = -1;

        setAlwaysDrawnWithCacheEnabled(false);
        final Resources res = getResources();
@@ -466,21 +466,18 @@ public class CellLayout extends ViewGroup implements Transposable {
            }
        }

        for (int i = 0; i < mFolderBackgrounds.size(); i++) {
            PreviewBackground bg = mFolderBackgrounds.get(i);
            cellToPoint(bg.delegateCellX, bg.delegateCellY, mTempLocation);
        for (int i = 0; i < mDelegatedCellDrawings.size(); i++) {
            DelegatedCellDrawing cellDrawing = mDelegatedCellDrawings.get(i);
            cellToPoint(cellDrawing.mDelegateCellX, cellDrawing.mDelegateCellY, mTempLocation);
            canvas.save();
            canvas.translate(mTempLocation[0], mTempLocation[1]);
            bg.drawBackground(canvas);
            if (!bg.isClipping) {
                bg.drawBackgroundStroke(canvas);
            }
            cellDrawing.drawUnderItem(canvas);
            canvas.restore();
        }

        if (mFolderLeaveBehind.delegateCellX >= 0 && mFolderLeaveBehind.delegateCellY >= 0) {
            cellToPoint(mFolderLeaveBehind.delegateCellX,
                    mFolderLeaveBehind.delegateCellY, mTempLocation);
        if (mFolderLeaveBehind.mDelegateCellX >= 0 && mFolderLeaveBehind.mDelegateCellY >= 0) {
            cellToPoint(mFolderLeaveBehind.mDelegateCellX,
                    mFolderLeaveBehind.mDelegateCellY, mTempLocation);
            canvas.save();
            canvas.translate(mTempLocation[0], mTempLocation[1]);
            mFolderLeaveBehind.drawLeaveBehind(canvas);
@@ -492,23 +489,28 @@ public class CellLayout extends ViewGroup implements Transposable {
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);

        for (int i = 0; i < mFolderBackgrounds.size(); i++) {
            PreviewBackground bg = mFolderBackgrounds.get(i);
            if (bg.isClipping) {
                cellToPoint(bg.delegateCellX, bg.delegateCellY, mTempLocation);
        for (int i = 0; i < mDelegatedCellDrawings.size(); i++) {
            DelegatedCellDrawing bg = mDelegatedCellDrawings.get(i);
            cellToPoint(bg.mDelegateCellX, bg.mDelegateCellY, mTempLocation);
            canvas.save();
            canvas.translate(mTempLocation[0], mTempLocation[1]);
                bg.drawBackgroundStroke(canvas);
            bg.drawOverItem(canvas);
            canvas.restore();
        }
    }
    }

    public void addFolderBackground(PreviewBackground bg) {
        mFolderBackgrounds.add(bg);
    /**
     * Add Delegated cell drawing
     */
    public void addDelegatedCellDrawing(DelegatedCellDrawing bg) {
        mDelegatedCellDrawings.add(bg);
    }
    public void removeFolderBackground(PreviewBackground bg) {
        mFolderBackgrounds.remove(bg);

    /**
     * Remove item from DelegatedCellDrawings
     */
    public void removeDelegatedCellDrawing(DelegatedCellDrawing bg) {
        mDelegatedCellDrawings.remove(bg);
    }

    public void setFolderLeaveBehindCell(int x, int y) {
@@ -516,14 +518,14 @@ public class CellLayout extends ViewGroup implements Transposable {
        mFolderLeaveBehind.setup(getContext(), mActivity, null,
                child.getMeasuredWidth(), child.getPaddingTop());

        mFolderLeaveBehind.delegateCellX = x;
        mFolderLeaveBehind.delegateCellY = y;
        mFolderLeaveBehind.mDelegateCellX = x;
        mFolderLeaveBehind.mDelegateCellY = y;
        invalidate();
    }

    public void clearFolderLeaveBehind() {
        mFolderLeaveBehind.delegateCellX = -1;
        mFolderLeaveBehind.delegateCellY = -1;
        mFolderLeaveBehind.mDelegateCellX = -1;
        mFolderLeaveBehind.mDelegateCellY = -1;
        invalidate();
    }

@@ -2743,6 +2745,24 @@ public class CellLayout extends ViewGroup implements Transposable {
        }
    }

    /**
     * A Delegated cell Drawing for drawing on CellLayout
     */
    public abstract static class DelegatedCellDrawing {
        public int mDelegateCellX;
        public int mDelegateCellY;

        /**
         * Draw under CellLayout
         */
        public abstract void drawUnderItem(Canvas canvas);

        /**
         * Draw over CellLayout
         */
        public abstract void drawOverItem(Canvas canvas);
    }

    /**
     * Returns whether an item can be placed in this CellLayout (after rearranging and/or resizing
     * if necessary).
+28 −9
Original line number Diff line number Diff line
@@ -48,7 +48,7 @@ import com.android.launcher3.views.ActivityContext;
 * This object represents a FolderIcon preview background. It stores drawing / measurement
 * information, handles drawing, and animation (accept state <--> rest state).
 */
public class PreviewBackground {
public class PreviewBackground extends CellLayout.DelegatedCellDrawing {

    private static final int CONSUMPTION_ANIMATION_DURATION = 100;

@@ -76,8 +76,6 @@ public class PreviewBackground {
    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
@@ -124,6 +122,27 @@ public class PreviewBackground {
                }
            };

    /**
     * Draws folder background under cell layout
     */
    @Override
    public void drawUnderItem(Canvas canvas) {
        drawBackground(canvas);
        if (!isClipping) {
            drawBackgroundStroke(canvas);
        }
    }

    /**
     * Draws folder background on cell layout
     */
    @Override
    public void drawOverItem(Canvas canvas) {
        if (isClipping) {
            drawBackgroundStroke(canvas);
        }
    }

    public void setup(Context context, ActivityContext activity, View invalidateDelegate,
                      int availableSpaceX, int topPadding) {
        mInvalidateDelegate = invalidateDelegate;
@@ -317,19 +336,19 @@ public class PreviewBackground {

    private void delegateDrawing(CellLayout delegate, int cellX, int cellY) {
        if (mDrawingDelegate != delegate) {
            delegate.addFolderBackground(this);
            delegate.addDelegatedCellDrawing(this);
        }

        mDrawingDelegate = delegate;
        delegateCellX = cellX;
        delegateCellY = cellY;
        mDelegateCellX = cellX;
        mDelegateCellY = cellY;

        invalidate();
    }

    private void clearDrawingDelegate() {
        if (mDrawingDelegate != null) {
            mDrawingDelegate.removeFolderBackground(this);
            mDrawingDelegate.removeDelegatedCellDrawing(this);
        }

        mDrawingDelegate = null;
@@ -395,8 +414,8 @@ public class PreviewBackground {
        // is saved and restored at the beginning of the animation, since cancelling the
        // existing animation can clear the delgate.
        CellLayout cl = mDrawingDelegate;
        int cellX = delegateCellX;
        int cellY = delegateCellY;
        int cellX = mDelegateCellX;
        int cellY = mDelegateCellY;
        animateScale(1f, 1f, () -> delegateDrawing(cl, cellX, cellY), this::clearDrawingDelegate);
    }