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