• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3.uioverrides;
17 
18 import android.content.Context;
19 import android.graphics.BlurMaskFilter;
20 import android.graphics.Canvas;
21 import android.graphics.Color;
22 import android.graphics.Matrix;
23 import android.graphics.Paint;
24 import android.graphics.Path;
25 import android.graphics.Rect;
26 import android.os.Process;
27 import android.util.AttributeSet;
28 import android.view.LayoutInflater;
29 import android.view.ViewGroup;
30 
31 import androidx.core.graphics.ColorUtils;
32 
33 import com.android.launcher3.CellLayout;
34 import com.android.launcher3.DeviceProfile;
35 import com.android.launcher3.Launcher;
36 import com.android.launcher3.LauncherSettings;
37 import com.android.launcher3.R;
38 import com.android.launcher3.icons.GraphicsUtils;
39 import com.android.launcher3.icons.IconNormalizer;
40 import com.android.launcher3.icons.LauncherIcons;
41 import com.android.launcher3.model.data.WorkspaceItemInfo;
42 import com.android.launcher3.touch.ItemClickHandler;
43 import com.android.launcher3.touch.ItemLongClickListener;
44 import com.android.launcher3.util.SafeCloseable;
45 import com.android.launcher3.views.ActivityContext;
46 import com.android.launcher3.views.DoubleShadowBubbleTextView;
47 
48 /**
49  * A BubbleTextView with a ring around it's drawable
50  */
51 public class PredictedAppIcon extends DoubleShadowBubbleTextView {
52 
53     private static final int RING_SHADOW_COLOR = 0x99000000;
54     private static final float RING_EFFECT_RATIO = 0.095f;
55 
56     boolean mIsDrawingDot = false;
57     private final DeviceProfile mDeviceProfile;
58     private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
59     private final Path mRingPath = new Path();
60     private final int mNormalizedIconSize;
61     private final Path mShapePath;
62     private final Matrix mTmpMatrix = new Matrix();
63 
64     private final BlurMaskFilter mShadowFilter;
65 
66     private boolean mIsPinned = false;
67     private int mPlateColor;
68     boolean mDrawForDrag = false;
69 
PredictedAppIcon(Context context)70     public PredictedAppIcon(Context context) {
71         this(context, null, 0);
72     }
73 
PredictedAppIcon(Context context, AttributeSet attrs)74     public PredictedAppIcon(Context context, AttributeSet attrs) {
75         this(context, attrs, 0);
76     }
77 
PredictedAppIcon(Context context, AttributeSet attrs, int defStyle)78     public PredictedAppIcon(Context context, AttributeSet attrs, int defStyle) {
79         super(context, attrs, defStyle);
80         mDeviceProfile = ActivityContext.lookupContext(context).getDeviceProfile();
81         mNormalizedIconSize = IconNormalizer.getNormalizedCircleSize(getIconSize());
82         int shadowSize = context.getResources().getDimensionPixelSize(
83                 R.dimen.blur_size_thin_outline);
84         mShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.OUTER);
85         mShapePath = GraphicsUtils.getShapePath(mNormalizedIconSize);
86     }
87 
88     @Override
onDraw(Canvas canvas)89     public void onDraw(Canvas canvas) {
90         int count = canvas.save();
91         if (!mIsPinned) {
92             drawEffect(canvas);
93             canvas.translate(getWidth() * RING_EFFECT_RATIO, getHeight() * RING_EFFECT_RATIO);
94             canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO);
95         }
96         super.onDraw(canvas);
97         canvas.restoreToCount(count);
98     }
99 
100     @Override
drawDotIfNecessary(Canvas canvas)101     protected void drawDotIfNecessary(Canvas canvas) {
102         mIsDrawingDot = true;
103         int count = canvas.save();
104         canvas.translate(-getWidth() * RING_EFFECT_RATIO, -getHeight() * RING_EFFECT_RATIO);
105         canvas.scale(1 + 2 * RING_EFFECT_RATIO, 1 + 2 * RING_EFFECT_RATIO);
106         super.drawDotIfNecessary(canvas);
107         canvas.restoreToCount(count);
108         mIsDrawingDot = false;
109     }
110 
111     @Override
applyFromWorkspaceItem(WorkspaceItemInfo info)112     public void applyFromWorkspaceItem(WorkspaceItemInfo info) {
113         super.applyFromWorkspaceItem(info);
114         mPlateColor = ColorUtils.setAlphaComponent(mDotParams.color, 200);
115         if (mIsPinned) {
116             setContentDescription(info.contentDescription);
117         } else {
118             setContentDescription(
119                     getContext().getString(R.string.hotseat_prediction_content_description,
120                             info.contentDescription));
121         }
122     }
123 
124     /**
125      * Removes prediction ring from app icon
126      */
pin(WorkspaceItemInfo info)127     public void pin(WorkspaceItemInfo info) {
128         if (mIsPinned) return;
129         mIsPinned = true;
130         applyFromWorkspaceItem(info);
131         setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
132         ((CellLayout.LayoutParams) getLayoutParams()).canReorder = true;
133         invalidate();
134     }
135 
136     /**
137      * prepares prediction icon for usage after bind
138      */
finishBinding(OnLongClickListener longClickListener)139     public void finishBinding(OnLongClickListener longClickListener) {
140         setOnLongClickListener(longClickListener);
141         ((CellLayout.LayoutParams) getLayoutParams()).canReorder = false;
142         setTextVisibility(false);
143         verifyHighRes();
144     }
145 
146     @Override
getIconBounds(Rect outBounds)147     public void getIconBounds(Rect outBounds) {
148         super.getIconBounds(outBounds);
149         if (!mIsPinned && !mIsDrawingDot) {
150             int predictionInset = (int) (getIconSize() * RING_EFFECT_RATIO);
151             outBounds.inset(predictionInset, predictionInset);
152         }
153     }
154 
isPinned()155     public boolean isPinned() {
156         return mIsPinned;
157     }
158 
getOutlineOffsetX()159     private int getOutlineOffsetX() {
160         return (getMeasuredWidth() - mNormalizedIconSize) / 2;
161     }
162 
getOutlineOffsetY()163     private int getOutlineOffsetY() {
164         if (mDisplay != DISPLAY_TASKBAR) {
165             return getPaddingTop() + mDeviceProfile.folderIconOffsetYPx;
166         }
167         return (getMeasuredHeight() - mNormalizedIconSize) / 2;
168     }
169 
170     @Override
onSizeChanged(int w, int h, int oldw, int oldh)171     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
172         super.onSizeChanged(w, h, oldw, oldh);
173         updateRingPath();
174     }
175 
176     @Override
setTag(Object tag)177     public void setTag(Object tag) {
178         super.setTag(tag);
179         updateRingPath();
180     }
181 
updateRingPath()182     private void updateRingPath() {
183         boolean isBadged = false;
184         if (getTag() instanceof WorkspaceItemInfo) {
185             WorkspaceItemInfo info = (WorkspaceItemInfo) getTag();
186             isBadged = !Process.myUserHandle().equals(info.user)
187                     || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT
188                     || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
189         }
190 
191         mRingPath.reset();
192         mTmpMatrix.setTranslate(getOutlineOffsetX(), getOutlineOffsetY());
193 
194         mRingPath.addPath(mShapePath, mTmpMatrix);
195         if (isBadged) {
196             float outlineSize = mNormalizedIconSize * RING_EFFECT_RATIO;
197             float iconSize = getIconSize() * (1 - 2 * RING_EFFECT_RATIO);
198             float badgeSize = LauncherIcons.getBadgeSizeForIconSize((int) iconSize) + outlineSize;
199             float scale = badgeSize / mNormalizedIconSize;
200             mTmpMatrix.postTranslate(mNormalizedIconSize, mNormalizedIconSize);
201             mTmpMatrix.preScale(scale, scale);
202             mTmpMatrix.preTranslate(-mNormalizedIconSize, -mNormalizedIconSize);
203             mRingPath.addPath(mShapePath, mTmpMatrix);
204         }
205     }
206 
drawEffect(Canvas canvas)207     private void drawEffect(Canvas canvas) {
208         // Don't draw ring effect if item is about to be dragged.
209         if (mDrawForDrag) {
210             return;
211         }
212         mIconRingPaint.setColor(RING_SHADOW_COLOR);
213         mIconRingPaint.setMaskFilter(mShadowFilter);
214         canvas.drawPath(mRingPath, mIconRingPaint);
215         mIconRingPaint.setColor(mPlateColor);
216         mIconRingPaint.setMaskFilter(null);
217         canvas.drawPath(mRingPath, mIconRingPaint);
218     }
219 
220     @Override
getSourceVisualDragBounds(Rect bounds)221     public void getSourceVisualDragBounds(Rect bounds) {
222         super.getSourceVisualDragBounds(bounds);
223         if (!mIsPinned) {
224             int internalSize = (int) (bounds.width() * RING_EFFECT_RATIO);
225             bounds.inset(internalSize, internalSize);
226         }
227     }
228 
229     @Override
prepareDrawDragView()230     public SafeCloseable prepareDrawDragView() {
231         mDrawForDrag = true;
232         invalidate();
233         SafeCloseable r = super.prepareDrawDragView();
234         return () -> {
235             r.close();
236             mDrawForDrag = false;
237         };
238     }
239 
240     /**
241      * Creates and returns a new instance of PredictedAppIcon from WorkspaceItemInfo
242      */
createIcon(ViewGroup parent, WorkspaceItemInfo info)243     public static PredictedAppIcon createIcon(ViewGroup parent, WorkspaceItemInfo info) {
244         PredictedAppIcon icon = (PredictedAppIcon) LayoutInflater.from(parent.getContext())
245                 .inflate(R.layout.predicted_app_icon, parent, false);
246         icon.applyFromWorkspaceItem(info);
247         icon.setOnClickListener(ItemClickHandler.INSTANCE);
248         icon.setOnFocusChangeListener(Launcher.getLauncher(parent.getContext()).getFocusHandler());
249         return icon;
250     }
251 
252     /**
253      * Draws Predicted Icon outline on cell layout
254      */
255     public static class PredictedIconOutlineDrawing extends CellLayout.DelegatedCellDrawing {
256 
257         private final PredictedAppIcon mIcon;
258         private final Paint mOutlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
259 
PredictedIconOutlineDrawing(int cellX, int cellY, PredictedAppIcon icon)260         public PredictedIconOutlineDrawing(int cellX, int cellY, PredictedAppIcon icon) {
261             mDelegateCellX = cellX;
262             mDelegateCellY = cellY;
263             mIcon = icon;
264             mOutlinePaint.setStyle(Paint.Style.FILL);
265             mOutlinePaint.setColor(Color.argb(24, 245, 245, 245));
266         }
267 
268         /**
269          * Draws predicted app icon outline under CellLayout
270          */
271         @Override
drawUnderItem(Canvas canvas)272         public void drawUnderItem(Canvas canvas) {
273             canvas.save();
274             canvas.translate(mIcon.getOutlineOffsetX(), mIcon.getOutlineOffsetY());
275             canvas.drawPath(mIcon.mShapePath, mOutlinePaint);
276             canvas.restore();
277         }
278 
279         /**
280          * Draws PredictedAppIcon outline over CellLayout
281          */
282         @Override
drawOverItem(Canvas canvas)283         public void drawOverItem(Canvas canvas) {
284             // Does nothing
285         }
286     }
287 }
288