1 /* 2 * Copyright (C) 2016 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 17 package com.android.launcher3.graphics; 18 19 import android.content.Context; 20 import android.graphics.Bitmap; 21 import android.graphics.BlurMaskFilter; 22 import android.graphics.Canvas; 23 import android.graphics.Paint; 24 import android.graphics.PorterDuff; 25 import android.graphics.PorterDuffXfermode; 26 import android.graphics.Rect; 27 import android.graphics.drawable.Drawable; 28 import android.os.Handler; 29 import android.view.View; 30 31 import com.android.launcher3.BubbleTextView; 32 import com.android.launcher3.Launcher; 33 import com.android.launcher3.R; 34 import com.android.launcher3.config.FeatureFlags; 35 import com.android.launcher3.folder.FolderIcon; 36 import com.android.launcher3.icons.BitmapRenderer; 37 import com.android.launcher3.util.UiThreadHelper; 38 import com.android.launcher3.widget.LauncherAppWidgetHostView; 39 import com.android.launcher3.widget.PendingAppWidgetHostView; 40 41 import java.nio.ByteBuffer; 42 43 /** 44 * A utility class to generate preview bitmap for dragging. 45 */ 46 public class DragPreviewProvider { 47 48 private final Rect mTempRect = new Rect(); 49 50 protected final View mView; 51 52 // The padding added to the drag view during the preview generation. 53 public final int previewPadding; 54 55 protected final int blurSizeOutline; 56 57 private OutlineGeneratorCallback mOutlineGeneratorCallback; 58 public Bitmap generatedDragOutline; 59 DragPreviewProvider(View view)60 public DragPreviewProvider(View view) { 61 this(view, view.getContext()); 62 } 63 DragPreviewProvider(View view, Context context)64 public DragPreviewProvider(View view, Context context) { 65 mView = view; 66 blurSizeOutline = 67 context.getResources().getDimensionPixelSize(R.dimen.blur_size_medium_outline); 68 69 if (mView instanceof BubbleTextView) { 70 Drawable d = ((BubbleTextView) mView).getIcon(); 71 Rect bounds = getDrawableBounds(d); 72 previewPadding = blurSizeOutline - bounds.left - bounds.top; 73 } else { 74 previewPadding = blurSizeOutline; 75 } 76 } 77 78 /** 79 * Draws the {@link #mView} into the given {@param destCanvas}. 80 */ drawDragView(Canvas destCanvas, float scale)81 protected void drawDragView(Canvas destCanvas, float scale) { 82 destCanvas.save(); 83 destCanvas.scale(scale, scale); 84 85 if (mView instanceof BubbleTextView) { 86 Drawable d = ((BubbleTextView) mView).getIcon(); 87 Rect bounds = getDrawableBounds(d); 88 destCanvas.translate(blurSizeOutline / 2 - bounds.left, 89 blurSizeOutline / 2 - bounds.top); 90 d.draw(destCanvas); 91 } else { 92 final Rect clipRect = mTempRect; 93 mView.getDrawingRect(clipRect); 94 95 boolean textVisible = false; 96 if (mView instanceof FolderIcon) { 97 // For FolderIcons the text can bleed into the icon area, and so we need to 98 // hide the text completely (which can't be achieved by clipping). 99 if (((FolderIcon) mView).getTextVisible()) { 100 ((FolderIcon) mView).setTextVisible(false); 101 textVisible = true; 102 } 103 } 104 destCanvas.translate(-mView.getScrollX() + blurSizeOutline / 2, 105 -mView.getScrollY() + blurSizeOutline / 2); 106 destCanvas.clipRect(clipRect); 107 mView.draw(destCanvas); 108 109 // Restore text visibility of FolderIcon if necessary 110 if (textVisible) { 111 ((FolderIcon) mView).setTextVisible(true); 112 } 113 } 114 destCanvas.restore(); 115 } 116 117 /** 118 * Returns a new bitmap to show when the {@link #mView} is being dragged around. 119 * Responsibility for the bitmap is transferred to the caller. 120 */ createDragBitmap()121 public Bitmap createDragBitmap() { 122 int width = mView.getWidth(); 123 int height = mView.getHeight(); 124 125 if (mView instanceof BubbleTextView) { 126 Drawable d = ((BubbleTextView) mView).getIcon(); 127 Rect bounds = getDrawableBounds(d); 128 width = bounds.width(); 129 height = bounds.height(); 130 } else if (mView instanceof LauncherAppWidgetHostView) { 131 float scale = ((LauncherAppWidgetHostView) mView).getScaleToFit(); 132 width = (int) (mView.getWidth() * scale); 133 height = (int) (mView.getHeight() * scale); 134 135 if (mView instanceof PendingAppWidgetHostView) { 136 // Use hardware renderer as the icon for the pending app widget may be a hw bitmap 137 return BitmapRenderer.createHardwareBitmap(width + blurSizeOutline, 138 height + blurSizeOutline, (c) -> drawDragView(c, scale)); 139 } else { 140 // Use software renderer for widgets as we know that they already work 141 return BitmapRenderer.createSoftwareBitmap(width + blurSizeOutline, 142 height + blurSizeOutline, (c) -> drawDragView(c, scale)); 143 } 144 } 145 146 return BitmapRenderer.createHardwareBitmap(width + blurSizeOutline, 147 height + blurSizeOutline, (c) -> drawDragView(c, 1)); 148 } 149 generateDragOutline(Bitmap preview)150 public final void generateDragOutline(Bitmap preview) { 151 if (FeatureFlags.IS_DOGFOOD_BUILD && mOutlineGeneratorCallback != null) { 152 throw new RuntimeException("Drag outline generated twice"); 153 } 154 155 mOutlineGeneratorCallback = new OutlineGeneratorCallback(preview); 156 new Handler(UiThreadHelper.getBackgroundLooper()).post(mOutlineGeneratorCallback); 157 } 158 getDrawableBounds(Drawable d)159 protected static Rect getDrawableBounds(Drawable d) { 160 Rect bounds = new Rect(); 161 d.copyBounds(bounds); 162 if (bounds.width() == 0 || bounds.height() == 0) { 163 bounds.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); 164 } else { 165 bounds.offsetTo(0, 0); 166 } 167 return bounds; 168 } 169 getScaleAndPosition(Bitmap preview, int[] outPos)170 public float getScaleAndPosition(Bitmap preview, int[] outPos) { 171 float scale = Launcher.getLauncher(mView.getContext()) 172 .getDragLayer().getLocationInDragLayer(mView, outPos); 173 if (mView instanceof LauncherAppWidgetHostView) { 174 // App widgets are technically scaled, but are drawn at their expected size -- so the 175 // app widget scale should not affect the scale of the preview. 176 scale /= ((LauncherAppWidgetHostView) mView).getScaleToFit(); 177 } 178 179 outPos[0] = Math.round(outPos[0] - 180 (preview.getWidth() - scale * mView.getWidth() * mView.getScaleX()) / 2); 181 outPos[1] = Math.round(outPos[1] - (1 - scale) * preview.getHeight() / 2 182 - previewPadding / 2); 183 return scale; 184 } 185 convertPreviewToAlphaBitmap(Bitmap preview)186 protected Bitmap convertPreviewToAlphaBitmap(Bitmap preview) { 187 return preview.copy(Bitmap.Config.ALPHA_8, true); 188 } 189 190 private class OutlineGeneratorCallback implements Runnable { 191 192 private final Bitmap mPreviewSnapshot; 193 private final Context mContext; 194 OutlineGeneratorCallback(Bitmap preview)195 OutlineGeneratorCallback(Bitmap preview) { 196 mPreviewSnapshot = preview; 197 mContext = mView.getContext(); 198 } 199 200 @Override run()201 public void run() { 202 Bitmap preview = convertPreviewToAlphaBitmap(mPreviewSnapshot); 203 204 // We start by removing most of the alpha channel so as to ignore shadows, and 205 // other types of partial transparency when defining the shape of the object 206 byte[] pixels = new byte[preview.getWidth() * preview.getHeight()]; 207 ByteBuffer buffer = ByteBuffer.wrap(pixels); 208 buffer.rewind(); 209 preview.copyPixelsToBuffer(buffer); 210 211 for (int i = 0; i < pixels.length; i++) { 212 if ((pixels[i] & 0xFF) < 188) { 213 pixels[i] = 0; 214 } 215 } 216 217 buffer.rewind(); 218 preview.copyPixelsFromBuffer(buffer); 219 220 final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 221 Canvas canvas = new Canvas(); 222 223 // calculate the outer blur first 224 paint.setMaskFilter(new BlurMaskFilter(blurSizeOutline, BlurMaskFilter.Blur.OUTER)); 225 int[] outerBlurOffset = new int[2]; 226 Bitmap thickOuterBlur = preview.extractAlpha(paint, outerBlurOffset); 227 228 paint.setMaskFilter(new BlurMaskFilter( 229 mContext.getResources().getDimension(R.dimen.blur_size_thin_outline), 230 BlurMaskFilter.Blur.OUTER)); 231 int[] brightOutlineOffset = new int[2]; 232 Bitmap brightOutline = preview.extractAlpha(paint, brightOutlineOffset); 233 234 // calculate the inner blur 235 canvas.setBitmap(preview); 236 canvas.drawColor(0xFF000000, PorterDuff.Mode.SRC_OUT); 237 paint.setMaskFilter(new BlurMaskFilter(blurSizeOutline, BlurMaskFilter.Blur.NORMAL)); 238 int[] thickInnerBlurOffset = new int[2]; 239 Bitmap thickInnerBlur = preview.extractAlpha(paint, thickInnerBlurOffset); 240 241 // mask out the inner blur 242 paint.setMaskFilter(null); 243 paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); 244 canvas.setBitmap(thickInnerBlur); 245 canvas.drawBitmap(preview, -thickInnerBlurOffset[0], 246 -thickInnerBlurOffset[1], paint); 247 canvas.drawRect(0, 0, -thickInnerBlurOffset[0], thickInnerBlur.getHeight(), paint); 248 canvas.drawRect(0, 0, thickInnerBlur.getWidth(), -thickInnerBlurOffset[1], paint); 249 250 // draw the inner and outer blur 251 paint.setXfermode(null); 252 canvas.setBitmap(preview); 253 canvas.drawColor(0, PorterDuff.Mode.CLEAR); 254 canvas.drawBitmap(thickInnerBlur, thickInnerBlurOffset[0], thickInnerBlurOffset[1], 255 paint); 256 canvas.drawBitmap(thickOuterBlur, outerBlurOffset[0], outerBlurOffset[1], paint); 257 258 // draw the bright outline 259 canvas.drawBitmap(brightOutline, brightOutlineOffset[0], brightOutlineOffset[1], paint); 260 261 // cleanup 262 canvas.setBitmap(null); 263 brightOutline.recycle(); 264 thickOuterBlur.recycle(); 265 thickInnerBlur.recycle(); 266 267 generatedDragOutline = preview; 268 } 269 } 270 } 271