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.graphics;
18 
19 import android.annotation.TargetApi;
20 import android.content.res.Resources;
21 import android.content.res.TypedArray;
22 import android.graphics.Bitmap;
23 import android.graphics.BlurMaskFilter;
24 import android.graphics.Canvas;
25 import android.graphics.Color;
26 import android.graphics.ColorFilter;
27 import android.graphics.Paint;
28 import android.graphics.PixelFormat;
29 import android.graphics.Rect;
30 import android.graphics.drawable.Drawable;
31 import android.os.Build;
32 import android.util.AttributeSet;
33 
34 import com.android.launcher3.R;
35 import com.android.launcher3.icons.BitmapRenderer;
36 
37 import org.xmlpull.v1.XmlPullParser;
38 import org.xmlpull.v1.XmlPullParserException;
39 
40 import java.io.IOException;
41 
42 /**
43  * A drawable which adds shadow around a child drawable.
44  */
45 @TargetApi(Build.VERSION_CODES.O)
46 public class ShadowDrawable extends Drawable {
47 
48     private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
49 
50     private final ShadowDrawableState mState;
51 
52     @SuppressWarnings("unused")
ShadowDrawable()53     public ShadowDrawable() {
54         this(new ShadowDrawableState());
55     }
56 
ShadowDrawable(ShadowDrawableState state)57     private ShadowDrawable(ShadowDrawableState state) {
58         mState = state;
59     }
60 
61     @Override
draw(Canvas canvas)62     public void draw(Canvas canvas) {
63         Rect bounds = getBounds();
64         if (bounds.isEmpty()) {
65             return;
66         }
67         if (mState.mLastDrawnBitmap == null) {
68             regenerateBitmapCache();
69         }
70         canvas.drawBitmap(mState.mLastDrawnBitmap, null, bounds, mPaint);
71     }
72 
73     @Override
setAlpha(int alpha)74     public void setAlpha(int alpha) {
75         mPaint.setAlpha(alpha);
76         invalidateSelf();
77     }
78 
79     @Override
setColorFilter(ColorFilter colorFilter)80     public void setColorFilter(ColorFilter colorFilter) {
81         mPaint.setColorFilter(colorFilter);
82         invalidateSelf();
83     }
84 
85     @Override
getConstantState()86     public ConstantState getConstantState() {
87         return mState;
88     }
89 
90     @Override
getOpacity()91     public int getOpacity() {
92         return PixelFormat.TRANSLUCENT;
93     }
94 
95     @Override
getIntrinsicHeight()96     public int getIntrinsicHeight() {
97         return mState.mIntrinsicHeight;
98     }
99 
100     @Override
getIntrinsicWidth()101     public int getIntrinsicWidth() {
102         return mState.mIntrinsicWidth;
103     }
104 
105     @Override
canApplyTheme()106     public boolean canApplyTheme() {
107         return mState.canApplyTheme();
108     }
109 
110     @Override
applyTheme(Resources.Theme t)111     public void applyTheme(Resources.Theme t) {
112         TypedArray ta = t.obtainStyledAttributes(new int[] {R.attr.isWorkspaceDarkText});
113         boolean isDark = ta.getBoolean(0, false);
114         ta.recycle();
115         if (mState.mIsDark != isDark) {
116             mState.mIsDark = isDark;
117             mState.mLastDrawnBitmap = null;
118             invalidateSelf();
119         }
120     }
121 
regenerateBitmapCache()122     private void regenerateBitmapCache() {
123         Bitmap bitmap = Bitmap.createBitmap(mState.mIntrinsicWidth, mState.mIntrinsicHeight,
124                 Bitmap.Config.ARGB_8888);
125         Canvas canvas = new Canvas(bitmap);
126 
127         // Call mutate, so that the pixel allocation by the underlying vector drawable is cleared.
128         Drawable d = mState.mChildState.newDrawable().mutate();
129         d.setBounds(mState.mShadowSize, mState.mShadowSize,
130                 mState.mIntrinsicWidth - mState.mShadowSize,
131                 mState.mIntrinsicHeight - mState.mShadowSize);
132         d.setTint(mState.mIsDark ? mState.mDarkTintColor : Color.WHITE);
133         d.draw(canvas);
134 
135         // Do not draw shadow on dark theme
136         if (!mState.mIsDark) {
137             Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
138             paint.setMaskFilter(new BlurMaskFilter(mState.mShadowSize, BlurMaskFilter.Blur.NORMAL));
139             int[] offset = new int[2];
140             Bitmap shadow = bitmap.extractAlpha(paint, offset);
141 
142             paint.setMaskFilter(null);
143             paint.setColor(mState.mShadowColor);
144             bitmap.eraseColor(Color.TRANSPARENT);
145             canvas.drawBitmap(shadow, offset[0], offset[1], paint);
146             d.draw(canvas);
147         }
148 
149         if (BitmapRenderer.USE_HARDWARE_BITMAP) {
150             bitmap = bitmap.copy(Bitmap.Config.HARDWARE, false);
151         }
152         mState.mLastDrawnBitmap = bitmap;
153     }
154 
155     @Override
inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Resources.Theme theme)156     public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs,
157             Resources.Theme theme) throws XmlPullParserException, IOException {
158         super.inflate(r, parser, attrs, theme);
159 
160         final TypedArray a = theme == null
161                 ? r.obtainAttributes(attrs, R.styleable.ShadowDrawable)
162                 : theme.obtainStyledAttributes(attrs, R.styleable.ShadowDrawable, 0, 0);
163         try {
164             Drawable d = a.getDrawable(R.styleable.ShadowDrawable_android_src);
165             if (d == null) {
166                 throw new XmlPullParserException("missing src attribute");
167             }
168             mState.mShadowColor = a.getColor(
169                     R.styleable.ShadowDrawable_android_shadowColor, Color.BLACK);
170             mState.mShadowSize = a.getDimensionPixelSize(
171                     R.styleable.ShadowDrawable_android_elevation, 0);
172             mState.mDarkTintColor = a.getColor(
173                     R.styleable.ShadowDrawable_darkTintColor, Color.BLACK);
174 
175             mState.mIntrinsicHeight = d.getIntrinsicHeight() + 2 * mState.mShadowSize;
176             mState.mIntrinsicWidth = d.getIntrinsicWidth() + 2 * mState.mShadowSize;
177             mState.mChangingConfigurations = d.getChangingConfigurations();
178 
179             mState.mChildState = d.getConstantState();
180         } finally {
181             a.recycle();
182         }
183     }
184 
185     private static class ShadowDrawableState extends ConstantState {
186 
187         int mChangingConfigurations;
188         int mIntrinsicWidth;
189         int mIntrinsicHeight;
190 
191         int mShadowColor;
192         int mShadowSize;
193         int mDarkTintColor;
194 
195         boolean mIsDark;
196         Bitmap mLastDrawnBitmap;
197         ConstantState mChildState;
198 
199         @Override
newDrawable()200         public Drawable newDrawable() {
201             return new ShadowDrawable(this);
202         }
203 
204         @Override
getChangingConfigurations()205         public int getChangingConfigurations() {
206             return mChangingConfigurations;
207         }
208 
209         @Override
canApplyTheme()210         public boolean canApplyTheme() {
211             return true;
212         }
213     }
214 }
215