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         // Call mutate, so that the pixel allocation by the underlying vector drawable is cleared.
124         Drawable d = mState.mChildState.newDrawable().mutate();
125         d.setBounds(mState.mShadowSize, mState.mShadowSize,
126                 mState.mIntrinsicWidth - mState.mShadowSize,
127                 mState.mIntrinsicHeight - mState.mShadowSize);
128         d.setTint(mState.mIsDark ? mState.mDarkTintColor : Color.WHITE);
129 
130         if (mState.mIsDark) {
131             // Dark text do not have any shadow, but just the bitmap
132             mState.mLastDrawnBitmap = BitmapRenderer.createHardwareBitmap(
133                     mState.mIntrinsicWidth, mState.mIntrinsicHeight, d::draw);
134         } else {
135             Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
136             paint.setMaskFilter(new BlurMaskFilter(mState.mShadowSize, BlurMaskFilter.Blur.NORMAL));
137 
138             // Generate the shadow bitmap
139             int[] offset = new int[2];
140             Bitmap shadow = BitmapRenderer.createSoftwareBitmap(
141                     mState.mIntrinsicWidth, mState.mIntrinsicHeight, d::draw)
142                     .extractAlpha(paint, offset);
143 
144             paint.setMaskFilter(null);
145             paint.setColor(mState.mShadowColor);
146             mState.mLastDrawnBitmap = BitmapRenderer.createHardwareBitmap(
147                     mState.mIntrinsicWidth, mState.mIntrinsicHeight, c -> {
148                         c.drawBitmap(shadow, offset[0], offset[1], paint);
149                         d.draw(c);
150                     });
151         }
152     }
153 
154     @Override
inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Resources.Theme theme)155     public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs,
156             Resources.Theme theme) throws XmlPullParserException, IOException {
157         super.inflate(r, parser, attrs, theme);
158 
159         final TypedArray a = theme == null
160                 ? r.obtainAttributes(attrs, R.styleable.ShadowDrawable)
161                 : theme.obtainStyledAttributes(attrs, R.styleable.ShadowDrawable, 0, 0);
162         try {
163             Drawable d = a.getDrawable(R.styleable.ShadowDrawable_android_src);
164             if (d == null) {
165                 throw new XmlPullParserException("missing src attribute");
166             }
167             mState.mShadowColor = a.getColor(
168                     R.styleable.ShadowDrawable_android_shadowColor, Color.BLACK);
169             mState.mShadowSize = a.getDimensionPixelSize(
170                     R.styleable.ShadowDrawable_android_elevation, 0);
171             mState.mDarkTintColor = a.getColor(
172                     R.styleable.ShadowDrawable_darkTintColor, Color.BLACK);
173 
174             mState.mIntrinsicHeight = d.getIntrinsicHeight() + 2 * mState.mShadowSize;
175             mState.mIntrinsicWidth = d.getIntrinsicWidth() + 2 * mState.mShadowSize;
176             mState.mChangingConfigurations = d.getChangingConfigurations();
177 
178             mState.mChildState = d.getConstantState();
179         } finally {
180             a.recycle();
181         }
182     }
183 
184     private static class ShadowDrawableState extends ConstantState {
185 
186         int mChangingConfigurations;
187         int mIntrinsicWidth;
188         int mIntrinsicHeight;
189 
190         int mShadowColor;
191         int mShadowSize;
192         int mDarkTintColor;
193 
194         boolean mIsDark;
195         Bitmap mLastDrawnBitmap;
196         ConstantState mChildState;
197 
198         @Override
newDrawable()199         public Drawable newDrawable() {
200             return new ShadowDrawable(this);
201         }
202 
203         @Override
getChangingConfigurations()204         public int getChangingConfigurations() {
205             return mChangingConfigurations;
206         }
207 
208         @Override
canApplyTheme()209         public boolean canApplyTheme() {
210             return true;
211         }
212     }
213 }
214