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