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