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 static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.PIN_PREDICTION; 19 import static com.android.launcher3.graphics.IconShape.getShape; 20 21 import android.content.Context; 22 import android.graphics.BlurMaskFilter; 23 import android.graphics.Canvas; 24 import android.graphics.Color; 25 import android.graphics.Paint; 26 import android.graphics.Path; 27 import android.graphics.Rect; 28 import android.os.Process; 29 import android.util.AttributeSet; 30 import android.view.LayoutInflater; 31 import android.view.ViewGroup; 32 import android.view.accessibility.AccessibilityNodeInfo; 33 34 import androidx.core.graphics.ColorUtils; 35 36 import com.android.launcher3.CellLayout; 37 import com.android.launcher3.DeviceProfile; 38 import com.android.launcher3.Launcher; 39 import com.android.launcher3.R; 40 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; 41 import com.android.launcher3.graphics.IconPalette; 42 import com.android.launcher3.hybridhotseat.HotseatPredictionController; 43 import com.android.launcher3.icons.IconNormalizer; 44 import com.android.launcher3.icons.LauncherIcons; 45 import com.android.launcher3.model.data.ItemInfo; 46 import com.android.launcher3.model.data.WorkspaceItemInfo; 47 import com.android.launcher3.touch.ItemClickHandler; 48 import com.android.launcher3.touch.ItemLongClickListener; 49 import com.android.launcher3.util.SafeCloseable; 50 import com.android.launcher3.views.ActivityContext; 51 import com.android.launcher3.views.DoubleShadowBubbleTextView; 52 53 /** 54 * A BubbleTextView with a ring around it's drawable 55 */ 56 public class PredictedAppIcon extends DoubleShadowBubbleTextView implements 57 LauncherAccessibilityDelegate.AccessibilityActionHandler { 58 59 private static final int RING_SHADOW_COLOR = 0x99000000; 60 private static final float RING_EFFECT_RATIO = 0.095f; 61 62 boolean mIsDrawingDot = false; 63 private final DeviceProfile mDeviceProfile; 64 private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 65 private final Path mRingPath = new Path(); 66 private boolean mIsPinned = false; 67 private final int mNormalizedIconRadius; 68 private final BlurMaskFilter mShadowFilter; 69 private int mPlateColor; 70 boolean mDrawForDrag = false; 71 PredictedAppIcon(Context context)72 public PredictedAppIcon(Context context) { 73 this(context, null, 0); 74 } 75 PredictedAppIcon(Context context, AttributeSet attrs)76 public PredictedAppIcon(Context context, AttributeSet attrs) { 77 this(context, attrs, 0); 78 } 79 PredictedAppIcon(Context context, AttributeSet attrs, int defStyle)80 public PredictedAppIcon(Context context, AttributeSet attrs, int defStyle) { 81 super(context, attrs, defStyle); 82 mDeviceProfile = ActivityContext.lookupContext(context).getDeviceProfile(); 83 mNormalizedIconRadius = IconNormalizer.getNormalizedCircleSize(getIconSize()) / 2; 84 int shadowSize = context.getResources().getDimensionPixelSize( 85 R.dimen.blur_size_thin_outline); 86 mShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.OUTER); 87 } 88 89 @Override onDraw(Canvas canvas)90 public void onDraw(Canvas canvas) { 91 int count = canvas.save(); 92 if (!mIsPinned) { 93 boolean isBadged = getTag() instanceof WorkspaceItemInfo 94 && !Process.myUserHandle().equals(((ItemInfo) getTag()).user); 95 drawEffect(canvas, isBadged); 96 canvas.translate(getWidth() * RING_EFFECT_RATIO, getHeight() * RING_EFFECT_RATIO); 97 canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO); 98 } 99 super.onDraw(canvas); 100 canvas.restoreToCount(count); 101 } 102 103 @Override drawDotIfNecessary(Canvas canvas)104 protected void drawDotIfNecessary(Canvas canvas) { 105 mIsDrawingDot = true; 106 int count = canvas.save(); 107 canvas.translate(-getWidth() * RING_EFFECT_RATIO, -getHeight() * RING_EFFECT_RATIO); 108 canvas.scale(1 + 2 * RING_EFFECT_RATIO, 1 + 2 * RING_EFFECT_RATIO); 109 super.drawDotIfNecessary(canvas); 110 canvas.restoreToCount(count); 111 mIsDrawingDot = false; 112 } 113 114 @Override applyFromWorkspaceItem(WorkspaceItemInfo info)115 public void applyFromWorkspaceItem(WorkspaceItemInfo info) { 116 super.applyFromWorkspaceItem(info); 117 int color = IconPalette.getMutedColor(info.bitmap.color, 0.54f); 118 mPlateColor = ColorUtils.setAlphaComponent(color, 200); 119 if (mIsPinned) { 120 setContentDescription(info.contentDescription); 121 } else { 122 setContentDescription( 123 getContext().getString(R.string.hotseat_prediction_content_description, 124 info.contentDescription)); 125 } 126 } 127 128 /** 129 * Removes prediction ring from app icon 130 */ pin(WorkspaceItemInfo info)131 public void pin(WorkspaceItemInfo info) { 132 if (mIsPinned) return; 133 mIsPinned = true; 134 applyFromWorkspaceItem(info); 135 setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE); 136 ((CellLayout.LayoutParams) getLayoutParams()).canReorder = true; 137 invalidate(); 138 } 139 140 /** 141 * prepares prediction icon for usage after bind 142 */ finishBinding(OnLongClickListener longClickListener)143 public void finishBinding(OnLongClickListener longClickListener) { 144 setOnLongClickListener(longClickListener); 145 ((CellLayout.LayoutParams) getLayoutParams()).canReorder = false; 146 setTextVisibility(false); 147 verifyHighRes(); 148 } 149 150 @Override addSupportedAccessibilityActions(AccessibilityNodeInfo accessibilityNodeInfo)151 public void addSupportedAccessibilityActions(AccessibilityNodeInfo accessibilityNodeInfo) { 152 if (!mIsPinned) { 153 accessibilityNodeInfo.addAction( 154 new AccessibilityNodeInfo.AccessibilityAction(PIN_PREDICTION, 155 getContext().getText(R.string.pin_prediction))); 156 } 157 } 158 159 @Override performAccessibilityAction(int action, ItemInfo info)160 public boolean performAccessibilityAction(int action, ItemInfo info) { 161 QuickstepLauncher launcher = Launcher.cast(Launcher.getLauncher(getContext())); 162 if (action == PIN_PREDICTION) { 163 if (launcher == null || launcher.getHotseatPredictionController() == null) { 164 return false; 165 } 166 HotseatPredictionController controller = launcher.getHotseatPredictionController(); 167 controller.pinPrediction(info); 168 return true; 169 } 170 return false; 171 } 172 173 @Override getIconBounds(Rect outBounds)174 public void getIconBounds(Rect outBounds) { 175 super.getIconBounds(outBounds); 176 if (!mIsPinned && !mIsDrawingDot) { 177 int predictionInset = (int) (getIconSize() * RING_EFFECT_RATIO); 178 outBounds.inset(predictionInset, predictionInset); 179 } 180 } 181 getOutlineOffsetX()182 private int getOutlineOffsetX() { 183 return (getMeasuredWidth() / 2) - mNormalizedIconRadius; 184 } 185 getOutlineOffsetY()186 private int getOutlineOffsetY() { 187 return getPaddingTop() + mDeviceProfile.folderIconOffsetYPx; 188 } 189 drawEffect(Canvas canvas, boolean isBadged)190 private void drawEffect(Canvas canvas, boolean isBadged) { 191 // Don't draw ring effect if item is about to be dragged. 192 if (mDrawForDrag) { 193 return; 194 } 195 mRingPath.reset(); 196 getShape().addToPath(mRingPath, getOutlineOffsetX(), getOutlineOffsetY(), 197 mNormalizedIconRadius); 198 if (isBadged) { 199 float outlineSize = mNormalizedIconRadius * RING_EFFECT_RATIO * 2; 200 float iconSize = getIconSize() * (1 - 2 * RING_EFFECT_RATIO); 201 float badgeSize = LauncherIcons.getBadgeSizeForIconSize((int) iconSize) + outlineSize; 202 float badgeInset = mNormalizedIconRadius * 2 - badgeSize; 203 getShape().addToPath(mRingPath, getOutlineOffsetX() + badgeInset, 204 getOutlineOffsetY() + badgeInset, badgeSize / 2); 205 206 } 207 mIconRingPaint.setColor(RING_SHADOW_COLOR); 208 mIconRingPaint.setMaskFilter(mShadowFilter); 209 canvas.drawPath(mRingPath, mIconRingPaint); 210 mIconRingPaint.setColor(mPlateColor); 211 mIconRingPaint.setMaskFilter(null); 212 canvas.drawPath(mRingPath, mIconRingPaint); 213 } 214 215 @Override getSourceVisualDragBounds(Rect bounds)216 public void getSourceVisualDragBounds(Rect bounds) { 217 super.getSourceVisualDragBounds(bounds); 218 if (!mIsPinned) { 219 int internalSize = (int) (bounds.width() * RING_EFFECT_RATIO); 220 bounds.inset(internalSize, internalSize); 221 } 222 } 223 224 @Override prepareDrawDragView()225 public SafeCloseable prepareDrawDragView() { 226 mDrawForDrag = true; 227 invalidate(); 228 SafeCloseable r = super.prepareDrawDragView(); 229 return () -> { 230 r.close(); 231 mDrawForDrag = false; 232 }; 233 } 234 235 /** 236 * Creates and returns a new instance of PredictedAppIcon from WorkspaceItemInfo 237 */ createIcon(ViewGroup parent, WorkspaceItemInfo info)238 public static PredictedAppIcon createIcon(ViewGroup parent, WorkspaceItemInfo info) { 239 PredictedAppIcon icon = (PredictedAppIcon) LayoutInflater.from(parent.getContext()) 240 .inflate(R.layout.predicted_app_icon, parent, false); 241 icon.applyFromWorkspaceItem(info); 242 icon.setOnClickListener(ItemClickHandler.INSTANCE); 243 icon.setOnFocusChangeListener(Launcher.getLauncher(parent.getContext()).getFocusHandler()); 244 return icon; 245 } 246 247 /** 248 * Draws Predicted Icon outline on cell layout 249 */ 250 public static class PredictedIconOutlineDrawing extends CellLayout.DelegatedCellDrawing { 251 252 private int mOffsetX; 253 private int mOffsetY; 254 private int mIconRadius; 255 private Paint mOutlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 256 PredictedIconOutlineDrawing(int cellX, int cellY, PredictedAppIcon icon)257 public PredictedIconOutlineDrawing(int cellX, int cellY, PredictedAppIcon icon) { 258 mDelegateCellX = cellX; 259 mDelegateCellY = cellY; 260 mOffsetX = icon.getOutlineOffsetX(); 261 mOffsetY = icon.getOutlineOffsetY(); 262 mIconRadius = icon.mNormalizedIconRadius; 263 mOutlinePaint.setStyle(Paint.Style.FILL); 264 mOutlinePaint.setColor(Color.argb(24, 245, 245, 245)); 265 } 266 267 /** 268 * Draws predicted app icon outline under CellLayout 269 */ 270 @Override drawUnderItem(Canvas canvas)271 public void drawUnderItem(Canvas canvas) { 272 getShape().drawShape(canvas, mOffsetX, mOffsetY, mIconRadius, mOutlinePaint); 273 } 274 275 /** 276 * Draws PredictedAppIcon outline over CellLayout 277 */ 278 @Override drawOverItem(Canvas canvas)279 public void drawOverItem(Canvas canvas) { 280 // Does nothing 281 } 282 } 283 } 284