• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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