Home | History | Annotate | Download | only in folder
      1 /*
      2  * Copyright (C) 2017 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.folder;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.ObjectAnimator;
     22 import android.animation.ValueAnimator;
     23 import android.graphics.Canvas;
     24 import android.graphics.Color;
     25 import android.graphics.Matrix;
     26 import android.graphics.Paint;
     27 import android.graphics.Path;
     28 import android.graphics.PorterDuff;
     29 import android.graphics.PorterDuffXfermode;
     30 import android.graphics.RadialGradient;
     31 import android.graphics.Region;
     32 import android.graphics.Shader;
     33 import android.support.v4.graphics.ColorUtils;
     34 import android.util.Property;
     35 import android.view.View;
     36 
     37 import com.android.launcher3.CellLayout;
     38 import com.android.launcher3.DeviceProfile;
     39 import com.android.launcher3.Launcher;
     40 import com.android.launcher3.LauncherAnimUtils;
     41 import com.android.launcher3.util.Themes;
     42 
     43 /**
     44  * This object represents a FolderIcon preview background. It stores drawing / measurement
     45  * information, handles drawing, and animation (accept state <--> rest state).
     46  */
     47 public class PreviewBackground {
     48 
     49     private static final int CONSUMPTION_ANIMATION_DURATION = 100;
     50 
     51     private final PorterDuffXfermode mClipPorterDuffXfermode
     52             = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
     53     // Create a RadialGradient such that it draws a black circle and then extends with
     54     // transparent. To achieve this, we keep the gradient to black for the range [0, 1) and
     55     // just at the edge quickly change it to transparent.
     56     private final RadialGradient mClipShader = new RadialGradient(0, 0, 1,
     57             new int[] {Color.BLACK, Color.BLACK, Color.TRANSPARENT },
     58             new float[] {0, 0.999f, 1},
     59             Shader.TileMode.CLAMP);
     60 
     61     private final PorterDuffXfermode mShadowPorterDuffXfermode
     62             = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
     63     private RadialGradient mShadowShader = null;
     64 
     65     private final Matrix mShaderMatrix = new Matrix();
     66     private final Path mPath = new Path();
     67 
     68     private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
     69 
     70     float mScale = 1f;
     71     private float mColorMultiplier = 1f;
     72     private int mBgColor;
     73     private float mStrokeWidth;
     74     private int mStrokeAlpha = MAX_BG_OPACITY;
     75     private int mShadowAlpha = 255;
     76     private View mInvalidateDelegate;
     77 
     78     int previewSize;
     79     int basePreviewOffsetX;
     80     int basePreviewOffsetY;
     81 
     82     private CellLayout mDrawingDelegate;
     83     public int delegateCellX;
     84     public int delegateCellY;
     85 
     86     // When the PreviewBackground is drawn under an icon (for creating a folder) the border
     87     // should not occlude the icon
     88     public boolean isClipping = true;
     89 
     90     // Drawing / animation configurations
     91     private static final float ACCEPT_SCALE_FACTOR = 1.20f;
     92     private static final float ACCEPT_COLOR_MULTIPLIER = 1.5f;
     93 
     94     // Expressed on a scale from 0 to 255.
     95     private static final int BG_OPACITY = 160;
     96     private static final int MAX_BG_OPACITY = 225;
     97     private static final int SHADOW_OPACITY = 40;
     98 
     99     private ValueAnimator mScaleAnimator;
    100     private ObjectAnimator mStrokeAlphaAnimator;
    101     private ObjectAnimator mShadowAnimator;
    102 
    103     private static final Property<PreviewBackground, Integer> STROKE_ALPHA =
    104             new Property<PreviewBackground, Integer>(Integer.class, "strokeAlpha") {
    105                 @Override
    106                 public Integer get(PreviewBackground previewBackground) {
    107                     return previewBackground.mStrokeAlpha;
    108                 }
    109 
    110                 @Override
    111                 public void set(PreviewBackground previewBackground, Integer alpha) {
    112                     previewBackground.mStrokeAlpha = alpha;
    113                     previewBackground.invalidate();
    114                 }
    115             };
    116 
    117     private static final Property<PreviewBackground, Integer> SHADOW_ALPHA =
    118             new Property<PreviewBackground, Integer>(Integer.class, "shadowAlpha") {
    119                 @Override
    120                 public Integer get(PreviewBackground previewBackground) {
    121                     return previewBackground.mShadowAlpha;
    122                 }
    123 
    124                 @Override
    125                 public void set(PreviewBackground previewBackground, Integer alpha) {
    126                     previewBackground.mShadowAlpha = alpha;
    127                     previewBackground.invalidate();
    128                 }
    129             };
    130 
    131     public void setup(Launcher launcher, View invalidateDelegate,
    132                       int availableSpaceX, int topPadding) {
    133         mInvalidateDelegate = invalidateDelegate;
    134         mBgColor = Themes.getAttrColor(launcher, android.R.attr.colorPrimary);
    135 
    136         DeviceProfile grid = launcher.getDeviceProfile();
    137         previewSize = grid.folderIconSizePx;
    138 
    139         basePreviewOffsetX = (availableSpaceX - previewSize) / 2;
    140         basePreviewOffsetY = topPadding + grid.folderIconOffsetYPx;
    141 
    142         // Stroke width is 1dp
    143         mStrokeWidth = launcher.getResources().getDisplayMetrics().density;
    144 
    145         float radius = getScaledRadius();
    146         float shadowRadius = radius + mStrokeWidth;
    147         int shadowColor = Color.argb(SHADOW_OPACITY, 0, 0, 0);
    148         mShadowShader = new RadialGradient(0, 0, 1,
    149                 new int[] {shadowColor, Color.TRANSPARENT},
    150                 new float[] {radius / shadowRadius, 1},
    151                 Shader.TileMode.CLAMP);
    152 
    153         invalidate();
    154     }
    155 
    156     int getRadius() {
    157         return previewSize / 2;
    158     }
    159 
    160     int getScaledRadius() {
    161         return (int) (mScale * getRadius());
    162     }
    163 
    164     int getOffsetX() {
    165         return basePreviewOffsetX - (getScaledRadius() - getRadius());
    166     }
    167 
    168     int getOffsetY() {
    169         return basePreviewOffsetY - (getScaledRadius() - getRadius());
    170     }
    171 
    172     /**
    173      * Returns the progress of the scale animation, where 0 means the scale is at 1f
    174      * and 1 means the scale is at ACCEPT_SCALE_FACTOR.
    175      */
    176     float getScaleProgress() {
    177         return (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f);
    178     }
    179 
    180     void invalidate() {
    181         if (mInvalidateDelegate != null) {
    182             mInvalidateDelegate.invalidate();
    183         }
    184 
    185         if (mDrawingDelegate != null) {
    186             mDrawingDelegate.invalidate();
    187         }
    188     }
    189 
    190     void setInvalidateDelegate(View invalidateDelegate) {
    191         mInvalidateDelegate = invalidateDelegate;
    192         invalidate();
    193     }
    194 
    195     public int getBgColor() {
    196         int alpha = (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
    197         return ColorUtils.setAlphaComponent(mBgColor, alpha);
    198     }
    199 
    200     public int getBadgeColor() {
    201         return mBgColor;
    202     }
    203 
    204     public void drawBackground(Canvas canvas) {
    205         mPaint.setStyle(Paint.Style.FILL);
    206         mPaint.setColor(getBgColor());
    207 
    208         drawCircle(canvas, 0 /* deltaRadius */);
    209 
    210         drawShadow(canvas);
    211     }
    212 
    213     public void drawShadow(Canvas canvas) {
    214         if (mShadowShader == null) {
    215             return;
    216         }
    217 
    218         float radius = getScaledRadius();
    219         float shadowRadius = radius + mStrokeWidth;
    220         mPaint.setStyle(Paint.Style.FILL);
    221         mPaint.setColor(Color.BLACK);
    222         int offsetX = getOffsetX();
    223         int offsetY = getOffsetY();
    224         final int saveCount;
    225         if (canvas.isHardwareAccelerated()) {
    226             saveCount = canvas.saveLayer(offsetX - mStrokeWidth, offsetY,
    227                     offsetX + radius + shadowRadius, offsetY + shadowRadius + shadowRadius, null);
    228 
    229         } else {
    230             saveCount = canvas.save();
    231             canvas.clipPath(getClipPath(), Region.Op.DIFFERENCE);
    232         }
    233 
    234         mShaderMatrix.setScale(shadowRadius, shadowRadius);
    235         mShaderMatrix.postTranslate(radius + offsetX, shadowRadius + offsetY);
    236         mShadowShader.setLocalMatrix(mShaderMatrix);
    237         mPaint.setAlpha(mShadowAlpha);
    238         mPaint.setShader(mShadowShader);
    239         canvas.drawPaint(mPaint);
    240         mPaint.setAlpha(255);
    241         mPaint.setShader(null);
    242         if (canvas.isHardwareAccelerated()) {
    243             mPaint.setXfermode(mShadowPorterDuffXfermode);
    244             canvas.drawCircle(radius + offsetX, radius + offsetY, radius, mPaint);
    245             mPaint.setXfermode(null);
    246         }
    247 
    248         canvas.restoreToCount(saveCount);
    249     }
    250 
    251     public void fadeInBackgroundShadow() {
    252         if (mShadowAnimator != null) {
    253             mShadowAnimator.cancel();
    254         }
    255         mShadowAnimator = ObjectAnimator
    256                 .ofInt(this, SHADOW_ALPHA, 0, 255)
    257                 .setDuration(100);
    258         mShadowAnimator.addListener(new AnimatorListenerAdapter() {
    259             @Override
    260             public void onAnimationEnd(Animator animation) {
    261                 mShadowAnimator = null;
    262             }
    263         });
    264         mShadowAnimator.start();
    265     }
    266 
    267     public void animateBackgroundStroke() {
    268         if (mStrokeAlphaAnimator != null) {
    269             mStrokeAlphaAnimator.cancel();
    270         }
    271         mStrokeAlphaAnimator = ObjectAnimator
    272                 .ofInt(this, STROKE_ALPHA, MAX_BG_OPACITY / 2, MAX_BG_OPACITY)
    273                 .setDuration(100);
    274         mStrokeAlphaAnimator.addListener(new AnimatorListenerAdapter() {
    275             @Override
    276             public void onAnimationEnd(Animator animation) {
    277                 mStrokeAlphaAnimator = null;
    278             }
    279         });
    280         mStrokeAlphaAnimator.start();
    281     }
    282 
    283     public void drawBackgroundStroke(Canvas canvas) {
    284         mPaint.setColor(ColorUtils.setAlphaComponent(mBgColor, mStrokeAlpha));
    285         mPaint.setStyle(Paint.Style.STROKE);
    286         mPaint.setStrokeWidth(mStrokeWidth);
    287         drawCircle(canvas, 1 /* deltaRadius */);
    288     }
    289 
    290     public void drawLeaveBehind(Canvas canvas) {
    291         float originalScale = mScale;
    292         mScale = 0.5f;
    293 
    294         mPaint.setStyle(Paint.Style.FILL);
    295         mPaint.setColor(Color.argb(160, 245, 245, 245));
    296         drawCircle(canvas, 0 /* deltaRadius */);
    297 
    298         mScale = originalScale;
    299     }
    300 
    301     private void drawCircle(Canvas canvas,float deltaRadius) {
    302         float radius = getScaledRadius();
    303         canvas.drawCircle(radius + getOffsetX(), radius + getOffsetY(),
    304                 radius - deltaRadius, mPaint);
    305     }
    306 
    307     public Path getClipPath() {
    308         mPath.reset();
    309         float r = getScaledRadius();
    310         mPath.addCircle(r + getOffsetX(), r + getOffsetY(), r, Path.Direction.CW);
    311         return mPath;
    312     }
    313 
    314     // It is the callers responsibility to save and restore the canvas layers.
    315     void clipCanvasHardware(Canvas canvas) {
    316         mPaint.setColor(Color.BLACK);
    317         mPaint.setStyle(Paint.Style.FILL);
    318         mPaint.setXfermode(mClipPorterDuffXfermode);
    319 
    320         float radius = getScaledRadius();
    321         mShaderMatrix.setScale(radius, radius);
    322         mShaderMatrix.postTranslate(radius + getOffsetX(), radius + getOffsetY());
    323         mClipShader.setLocalMatrix(mShaderMatrix);
    324         mPaint.setShader(mClipShader);
    325         canvas.drawPaint(mPaint);
    326         mPaint.setXfermode(null);
    327         mPaint.setShader(null);
    328     }
    329 
    330     private void delegateDrawing(CellLayout delegate, int cellX, int cellY) {
    331         if (mDrawingDelegate != delegate) {
    332             delegate.addFolderBackground(this);
    333         }
    334 
    335         mDrawingDelegate = delegate;
    336         delegateCellX = cellX;
    337         delegateCellY = cellY;
    338 
    339         invalidate();
    340     }
    341 
    342     private void clearDrawingDelegate() {
    343         if (mDrawingDelegate != null) {
    344             mDrawingDelegate.removeFolderBackground(this);
    345         }
    346 
    347         mDrawingDelegate = null;
    348         isClipping = true;
    349         invalidate();
    350     }
    351 
    352     boolean drawingDelegated() {
    353         return mDrawingDelegate != null;
    354     }
    355 
    356     private void animateScale(float finalScale, float finalMultiplier,
    357                               final Runnable onStart, final Runnable onEnd) {
    358         final float scale0 = mScale;
    359         final float scale1 = finalScale;
    360 
    361         final float bgMultiplier0 = mColorMultiplier;
    362         final float bgMultiplier1 = finalMultiplier;
    363 
    364         if (mScaleAnimator != null) {
    365             mScaleAnimator.cancel();
    366         }
    367 
    368         mScaleAnimator = LauncherAnimUtils.ofFloat(0f, 1.0f);
    369 
    370         mScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    371             @Override
    372             public void onAnimationUpdate(ValueAnimator animation) {
    373                 float prog = animation.getAnimatedFraction();
    374                 mScale = prog * scale1 + (1 - prog) * scale0;
    375                 mColorMultiplier = prog * bgMultiplier1 + (1 - prog) * bgMultiplier0;
    376                 invalidate();
    377             }
    378         });
    379         mScaleAnimator.addListener(new AnimatorListenerAdapter() {
    380             @Override
    381             public void onAnimationStart(Animator animation) {
    382                 if (onStart != null) {
    383                     onStart.run();
    384                 }
    385             }
    386 
    387             @Override
    388             public void onAnimationEnd(Animator animation) {
    389                 if (onEnd != null) {
    390                     onEnd.run();
    391                 }
    392                 mScaleAnimator = null;
    393             }
    394         });
    395 
    396         mScaleAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
    397         mScaleAnimator.start();
    398     }
    399 
    400     public void animateToAccept(final CellLayout cl, final int cellX, final int cellY) {
    401         Runnable onStart = new Runnable() {
    402             @Override
    403             public void run() {
    404                 delegateDrawing(cl, cellX, cellY);
    405             }
    406         };
    407         animateScale(ACCEPT_SCALE_FACTOR, ACCEPT_COLOR_MULTIPLIER, onStart, null);
    408     }
    409 
    410     public void animateToRest() {
    411         // This can be called multiple times -- we need to make sure the drawing delegate
    412         // is saved and restored at the beginning of the animation, since cancelling the
    413         // existing animation can clear the delgate.
    414         final CellLayout cl = mDrawingDelegate;
    415         final int cellX = delegateCellX;
    416         final int cellY = delegateCellY;
    417 
    418         Runnable onStart = new Runnable() {
    419             @Override
    420             public void run() {
    421                 delegateDrawing(cl, cellX, cellY);
    422             }
    423         };
    424         Runnable onEnd = new Runnable() {
    425             @Override
    426             public void run() {
    427                 clearDrawingDelegate();
    428             }
    429         };
    430         animateScale(1f, 1f, onStart, onEnd);
    431     }
    432 
    433     public int getBackgroundAlpha() {
    434         return (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
    435     }
    436 
    437     public float getStrokeWidth() {
    438         return mStrokeWidth;
    439     }
    440 }
    441