1 /*
2  * Copyright (C) 2018 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 android.content.Intent.ACTION_SCREEN_OFF;
20 import static android.content.Intent.ACTION_USER_PRESENT;
21 
22 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
23 
24 import android.animation.ObjectAnimator;
25 import android.content.BroadcastReceiver;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 import android.graphics.Bitmap;
30 import android.graphics.Canvas;
31 import android.graphics.Color;
32 import android.graphics.LinearGradient;
33 import android.graphics.Paint;
34 import android.graphics.Rect;
35 import android.graphics.RectF;
36 import android.graphics.Region;
37 import android.graphics.Shader;
38 import android.graphics.drawable.Drawable;
39 import android.util.DisplayMetrics;
40 import android.util.Property;
41 import android.view.View;
42 
43 import androidx.core.graphics.ColorUtils;
44 
45 import com.android.launcher3.CellLayout;
46 import com.android.launcher3.R;
47 import com.android.launcher3.ResourceUtils;
48 import com.android.launcher3.Workspace;
49 import com.android.launcher3.uioverrides.WallpaperColorInfo;
50 import com.android.launcher3.util.Themes;
51 
52 /**
53  * View scrim which draws behind hotseat and workspace
54  */
55 public class WorkspaceAndHotseatScrim extends Scrim {
56 
57     public static Property<WorkspaceAndHotseatScrim, Float> SYSUI_PROGRESS =
58             new Property<WorkspaceAndHotseatScrim, Float>(Float.TYPE, "sysUiProgress") {
59                 @Override
60                 public Float get(WorkspaceAndHotseatScrim scrim) {
61                     return scrim.mSysUiProgress;
62                 }
63 
64                 @Override
65                 public void set(WorkspaceAndHotseatScrim scrim, Float value) {
66                     scrim.setSysUiProgress(value);
67                 }
68             };
69 
70     private static Property<WorkspaceAndHotseatScrim, Float> SYSUI_ANIM_MULTIPLIER =
71             new Property<WorkspaceAndHotseatScrim, Float>(Float.TYPE, "sysUiAnimMultiplier") {
72                 @Override
73                 public Float get(WorkspaceAndHotseatScrim scrim) {
74                     return scrim.mSysUiAnimMultiplier;
75                 }
76 
77                 @Override
78                 public void set(WorkspaceAndHotseatScrim scrim, Float value) {
79                     scrim.mSysUiAnimMultiplier = value;
80                     scrim.reapplySysUiAlpha();
81                 }
82             };
83 
84     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
85         @Override
86         public void onReceive(Context context, Intent intent) {
87             final String action = intent.getAction();
88             if (ACTION_SCREEN_OFF.equals(action)) {
89                 mAnimateScrimOnNextDraw = true;
90             } else if (ACTION_USER_PRESENT.equals(action)) {
91                 // ACTION_USER_PRESENT is sent after onStart/onResume. This covers the case where
92                 // the user unlocked and the Launcher is not in the foreground.
93                 mAnimateScrimOnNextDraw = false;
94             }
95         }
96     };
97 
98     private static final int DARK_SCRIM_COLOR = 0x55000000;
99     private static final int MAX_HOTSEAT_SCRIM_ALPHA = 100;
100     private static final int ALPHA_MASK_HEIGHT_DP = 500;
101     private static final int ALPHA_MASK_BITMAP_DP = 200;
102     private static final int ALPHA_MASK_WIDTH_DP = 2;
103 
104     private final Rect mHighlightRect = new Rect();
105 
106     private Workspace mWorkspace;
107 
108     private boolean mDrawTopScrim, mDrawBottomScrim;
109 
110     private final RectF mFinalMaskRect = new RectF();
111     private final Paint mBottomMaskPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
112     private final Bitmap mBottomMask;
113     private final int mMaskHeight;
114 
115     private final Drawable mTopScrim;
116 
117     private float mSysUiProgress = 1;
118     private boolean mHideSysUiScrim;
119 
120     private boolean mAnimateScrimOnNextDraw = false;
121     private float mSysUiAnimMultiplier = 1;
122 
WorkspaceAndHotseatScrim(View view)123     public WorkspaceAndHotseatScrim(View view) {
124         super(view);
125 
126         mMaskHeight = ResourceUtils.pxFromDp(ALPHA_MASK_BITMAP_DP,
127                 view.getResources().getDisplayMetrics());
128         mTopScrim = Themes.getAttrDrawable(view.getContext(), R.attr.workspaceStatusBarScrim);
129         mBottomMask = mTopScrim == null ? null : createDitheredAlphaMask();
130         mHideSysUiScrim = mTopScrim == null;
131 
132         onExtractedColorsChanged(mWallpaperColorInfo);
133     }
134 
setWorkspace(Workspace workspace)135     public void setWorkspace(Workspace workspace)  {
136         mWorkspace = workspace;
137     }
138 
draw(Canvas canvas)139     public void draw(Canvas canvas) {
140         // Draw the background below children.
141         if (mScrimAlpha > 0) {
142             // Update the scroll position first to ensure scrim cutout is in the right place.
143             mWorkspace.computeScrollWithoutInvalidation();
144             CellLayout currCellLayout = mWorkspace.getCurrentDragOverlappingLayout();
145             canvas.save();
146             if (currCellLayout != null && currCellLayout != mLauncher.getHotseat()) {
147                 // Cut a hole in the darkening scrim on the page that should be highlighted, if any.
148                 mLauncher.getDragLayer()
149                         .getDescendantRectRelativeToSelf(currCellLayout, mHighlightRect);
150                 canvas.clipRect(mHighlightRect, Region.Op.DIFFERENCE);
151             }
152 
153             super.draw(canvas);
154             canvas.restore();
155         }
156 
157         if (!mHideSysUiScrim) {
158             if (mSysUiProgress <= 0) {
159                 mAnimateScrimOnNextDraw = false;
160                 return;
161             }
162 
163             if (mAnimateScrimOnNextDraw) {
164                 mSysUiAnimMultiplier = 0;
165                 reapplySysUiAlphaNoInvalidate();
166 
167                 animateToSysuiMultiplier(1, 600,
168                         mLauncher.getWindow().getTransitionBackgroundFadeDuration());
169                 mAnimateScrimOnNextDraw = false;
170             }
171 
172             if (mDrawTopScrim) {
173                 mTopScrim.draw(canvas);
174             }
175             if (mDrawBottomScrim) {
176                 canvas.drawBitmap(mBottomMask, null, mFinalMaskRect, mBottomMaskPaint);
177             }
178         }
179     }
180 
animateToSysuiMultiplier(float toMultiplier, long duration, long startDelay)181     public void animateToSysuiMultiplier(float toMultiplier, long duration,
182             long startDelay) {
183         ObjectAnimator anim = ObjectAnimator.ofFloat(this, SYSUI_ANIM_MULTIPLIER, toMultiplier);
184         anim.setAutoCancel(true);
185         anim.setDuration(duration);
186         anim.setStartDelay(startDelay);
187         anim.start();
188     }
189 
190     /**
191      * Determines whether to draw the top and/or bottom scrim based on new insets.
192      */
onInsetsChanged(Rect insets, boolean allowSysuiScrims)193     public void onInsetsChanged(Rect insets, boolean allowSysuiScrims) {
194         mDrawTopScrim = allowSysuiScrims && mTopScrim != null && insets.top > 0;
195         mDrawBottomScrim = allowSysuiScrims && mBottomMask != null
196                 && !mLauncher.getDeviceProfile().isVerticalBarLayout();
197     }
198 
199     @Override
onViewAttachedToWindow(View view)200     public void onViewAttachedToWindow(View view) {
201         super.onViewAttachedToWindow(view);
202 
203         if (mTopScrim != null) {
204             IntentFilter filter = new IntentFilter(ACTION_SCREEN_OFF);
205             filter.addAction(ACTION_USER_PRESENT); // When the device wakes up + keyguard is gone
206             mRoot.getContext().registerReceiver(mReceiver, filter);
207         }
208     }
209 
210     @Override
onViewDetachedFromWindow(View view)211     public void onViewDetachedFromWindow(View view) {
212         super.onViewDetachedFromWindow(view);
213         if (mTopScrim != null) {
214             mRoot.getContext().unregisterReceiver(mReceiver);
215         }
216     }
217 
218     @Override
onExtractedColorsChanged(WallpaperColorInfo wallpaperColorInfo)219     public void onExtractedColorsChanged(WallpaperColorInfo wallpaperColorInfo) {
220         // for super light wallpaper it needs to be darken for contrast to workspace
221         // for dark wallpapers the text is white so darkening works as well
222         mBottomMaskPaint.setColor(ColorUtils.compositeColors(DARK_SCRIM_COLOR,
223                 wallpaperColorInfo.getMainColor()));
224         reapplySysUiAlpha();
225         super.onExtractedColorsChanged(wallpaperColorInfo);
226     }
227 
setSize(int w, int h)228     public void setSize(int w, int h) {
229         if (mTopScrim != null) {
230             mTopScrim.setBounds(0, 0, w, h);
231             mFinalMaskRect.set(0, h - mMaskHeight, w, h);
232         }
233     }
234 
hideSysUiScrim(boolean hideSysUiScrim)235     public void hideSysUiScrim(boolean hideSysUiScrim) {
236         mHideSysUiScrim = hideSysUiScrim || (mTopScrim == null);
237         if (!hideSysUiScrim) {
238             mAnimateScrimOnNextDraw = true;
239         }
240         invalidate();
241     }
242 
setSysUiProgress(float progress)243     private void setSysUiProgress(float progress) {
244         if (progress != mSysUiProgress) {
245             mSysUiProgress = progress;
246             reapplySysUiAlpha();
247         }
248     }
249 
reapplySysUiAlpha()250     private void reapplySysUiAlpha() {
251         reapplySysUiAlphaNoInvalidate();
252         if (!mHideSysUiScrim) {
253             invalidate();
254         }
255     }
256 
reapplySysUiAlphaNoInvalidate()257     private void reapplySysUiAlphaNoInvalidate() {
258         float factor = mSysUiProgress * mSysUiAnimMultiplier;
259         mBottomMaskPaint.setAlpha(Math.round(MAX_HOTSEAT_SCRIM_ALPHA * factor));
260         if (mTopScrim != null) {
261             mTopScrim.setAlpha(Math.round(255 * factor));
262         }
263     }
264 
createDitheredAlphaMask()265     public Bitmap createDitheredAlphaMask() {
266         DisplayMetrics dm = mLauncher.getResources().getDisplayMetrics();
267         int width = ResourceUtils.pxFromDp(ALPHA_MASK_WIDTH_DP, dm);
268         int gradientHeight = ResourceUtils.pxFromDp(ALPHA_MASK_HEIGHT_DP, dm);
269         Bitmap dst = Bitmap.createBitmap(width, mMaskHeight, Bitmap.Config.ALPHA_8);
270         Canvas c = new Canvas(dst);
271         Paint paint = new Paint(Paint.DITHER_FLAG);
272         LinearGradient lg = new LinearGradient(0, 0, 0, gradientHeight,
273                 new int[]{
274                         0x00FFFFFF,
275                         setColorAlphaBound(Color.WHITE, (int) (0xFF * 0.95)),
276                         0xFFFFFFFF},
277                 new float[]{0f, 0.8f, 1f},
278                 Shader.TileMode.CLAMP);
279         paint.setShader(lg);
280         c.drawRect(0, 0, width, gradientHeight, paint);
281         return dst;
282     }
283 }
284