Home | History | Annotate | Download | only in graphics
      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