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 package com.android.launcher3.graphics;
17 
18 import static android.graphics.Paint.DITHER_FLAG;
19 import static android.graphics.Paint.FILTER_BITMAP_FLAG;
20 
21 import static com.android.launcher3.config.FeatureFlags.KEYGUARD_ANIMATION;
22 
23 import android.animation.ObjectAnimator;
24 import android.graphics.Bitmap;
25 import android.graphics.Canvas;
26 import android.graphics.LinearGradient;
27 import android.graphics.Paint;
28 import android.graphics.Rect;
29 import android.graphics.RectF;
30 import android.graphics.Shader;
31 import android.util.DisplayMetrics;
32 import android.view.View;
33 
34 import androidx.annotation.ColorInt;
35 import androidx.annotation.VisibleForTesting;
36 
37 import com.android.launcher3.BaseDraggingActivity;
38 import com.android.launcher3.DeviceProfile;
39 import com.android.launcher3.R;
40 import com.android.launcher3.anim.AnimatedFloat;
41 import com.android.launcher3.testing.shared.ResourceUtils;
42 import com.android.launcher3.util.ScreenOnTracker;
43 import com.android.launcher3.util.ScreenOnTracker.ScreenOnListener;
44 import com.android.launcher3.util.Themes;
45 
46 /**
47  * View scrim which draws behind hotseat and workspace
48  */
49 public class SysUiScrim implements View.OnAttachStateChangeListener {
50 
51     /**
52      * Receiver used to get a signal that the user unlocked their device.
53      */
54     private final ScreenOnListener mScreenOnListener = new ScreenOnListener() {
55         @Override
56         public void onScreenOnChanged(boolean isOn) {
57             if (!isOn) {
58                 mAnimateScrimOnNextDraw = true;
59             }
60         }
61 
62         @Override
63         public void onUserPresent() {
64             // ACTION_USER_PRESENT is sent after onStart/onResume. This covers the case where
65             // the user unlocked and the Launcher is not in the foreground.
66             mAnimateScrimOnNextDraw = false;
67         }
68     };
69 
70     private static final int MAX_SYSUI_SCRIM_ALPHA = 255;
71     private static final int ALPHA_MASK_BITMAP_WIDTH_DP = 2;
72 
73     private static final int BOTTOM_MASK_HEIGHT_DP = 200;
74     private static final int TOP_MASK_HEIGHT_DP = 70;
75 
76     private boolean mDrawTopScrim, mDrawBottomScrim;
77 
78     private final RectF mTopMaskRect = new RectF();
79     private final Paint mTopMaskPaint = new Paint(FILTER_BITMAP_FLAG | DITHER_FLAG);
80     private final Bitmap mTopMaskBitmap;
81     private final int mTopMaskHeight;
82 
83     private final RectF mBottomMaskRect = new RectF();
84     private final Paint mBottomMaskPaint = new Paint(FILTER_BITMAP_FLAG | DITHER_FLAG);
85     private final Bitmap mBottomMaskBitmap;
86     private final int mBottomMaskHeight;
87 
88     private final View mRoot;
89     private final BaseDraggingActivity mActivity;
90     private final boolean mHideSysUiScrim;
91     private boolean mSkipScrimAnimationForTest = false;
92 
93     private boolean mAnimateScrimOnNextDraw = false;
94     private final AnimatedFloat mSysUiAnimMultiplier = new AnimatedFloat(this::reapplySysUiAlpha);
95     private final AnimatedFloat mSysUiProgress = new AnimatedFloat(this::reapplySysUiAlpha);
96 
SysUiScrim(View view)97     public SysUiScrim(View view) {
98         mRoot = view;
99         mActivity = BaseDraggingActivity.fromContext(view.getContext());
100         DisplayMetrics dm = mActivity.getResources().getDisplayMetrics();
101 
102         mTopMaskHeight = ResourceUtils.pxFromDp(TOP_MASK_HEIGHT_DP, dm);
103         mBottomMaskHeight = ResourceUtils.pxFromDp(BOTTOM_MASK_HEIGHT_DP, dm);
104         mHideSysUiScrim = Themes.getAttrBoolean(view.getContext(), R.attr.isWorkspaceDarkText);
105 
106         mTopMaskBitmap = mHideSysUiScrim ? null : createDitheredAlphaMask(mTopMaskHeight,
107                 new int[]{0x3DFFFFFF, 0x0AFFFFFF, 0x00FFFFFF},
108                 new float[]{0f, 0.7f, 1f});
109         mTopMaskPaint.setColor(0xFF222222);
110         mBottomMaskBitmap = mHideSysUiScrim ? null : createDitheredAlphaMask(mBottomMaskHeight,
111                 new int[]{0x00FFFFFF, 0x2FFFFFFF},
112                 new float[]{0f, 1f});
113 
114         if (!KEYGUARD_ANIMATION.get() && !mHideSysUiScrim) {
115             view.addOnAttachStateChangeListener(this);
116         }
117     }
118 
119     /**
120      * Draw the top and bottom scrims
121      */
draw(Canvas canvas)122     public void draw(Canvas canvas) {
123         if (!mHideSysUiScrim) {
124             if (mSysUiProgress.value <= 0) {
125                 mAnimateScrimOnNextDraw = false;
126                 return;
127             }
128 
129             if (mAnimateScrimOnNextDraw) {
130                 mSysUiAnimMultiplier.value = 0;
131                 reapplySysUiAlphaNoInvalidate();
132 
133                 ObjectAnimator oa = mSysUiAnimMultiplier.animateToValue(1);
134                 oa.setDuration(600);
135                 oa.setStartDelay(mActivity.getWindow().getTransitionBackgroundFadeDuration());
136                 oa.start();
137                 mAnimateScrimOnNextDraw = false;
138             }
139 
140             if (mDrawTopScrim) {
141                 canvas.drawBitmap(mTopMaskBitmap, null, mTopMaskRect, mTopMaskPaint);
142             }
143             if (mDrawBottomScrim) {
144                 canvas.drawBitmap(mBottomMaskBitmap, null, mBottomMaskRect, mBottomMaskPaint);
145             }
146         }
147     }
148 
149     /**
150      * Returns the sysui multiplier property for controlling fade in/out of the scrim
151      */
getSysUIMultiplier()152     public AnimatedFloat getSysUIMultiplier() {
153         return mSysUiAnimMultiplier;
154     }
155 
156     /**
157      * Returns the sysui progress property for controlling fade in/out of the scrim
158      */
getSysUIProgress()159     public AnimatedFloat getSysUIProgress() {
160         return mSysUiProgress;
161     }
162 
163     /**
164      * Determines whether to draw the top and/or bottom scrim based on new insets.
165      *
166      * In order for the bottom scrim to be drawn this 3 condition should be meet at the same time:
167      * the device is in 3 button navigation, the taskbar is not present and the Hotseat is
168      * horizontal
169      */
onInsetsChanged(Rect insets)170     public void onInsetsChanged(Rect insets) {
171         DeviceProfile dp = mActivity.getDeviceProfile();
172         mDrawTopScrim = insets.top > 0;
173         mDrawBottomScrim = !dp.isVerticalBarLayout() && !dp.isGestureMode && !dp.isTaskbarPresent;
174     }
175 
176     @Override
onViewAttachedToWindow(View view)177     public void onViewAttachedToWindow(View view) {
178         ScreenOnTracker.INSTANCE.get(mActivity).addListener(mScreenOnListener);
179     }
180 
181     @Override
onViewDetachedFromWindow(View view)182     public void onViewDetachedFromWindow(View view) {
183         ScreenOnTracker.INSTANCE.get(mActivity).removeListener(mScreenOnListener);
184     }
185 
186     /**
187      * Set the width and height of the view being scrimmed
188      */
setSize(int w, int h)189     public void setSize(int w, int h) {
190         mTopMaskRect.set(0, 0, w, mTopMaskHeight);
191         mBottomMaskRect.set(0, h - mBottomMaskHeight, w, h);
192     }
193 
194     /**
195      * Sets whether the SysUiScrim should hide for testing.
196      */
197     @VisibleForTesting
skipScrimAnimation()198     public void skipScrimAnimation() {
199         mSkipScrimAnimationForTest = true;
200         reapplySysUiAlpha();
201     }
202 
reapplySysUiAlpha()203     private void reapplySysUiAlpha() {
204         reapplySysUiAlphaNoInvalidate();
205         if (!mHideSysUiScrim) {
206             mRoot.invalidate();
207         }
208     }
209 
reapplySysUiAlphaNoInvalidate()210     private void reapplySysUiAlphaNoInvalidate() {
211         float factor = mSysUiProgress.value * mSysUiAnimMultiplier.value;
212         if (mSkipScrimAnimationForTest) factor = 1f;
213         mBottomMaskPaint.setAlpha(Math.round(MAX_SYSUI_SCRIM_ALPHA * factor));
214         mTopMaskPaint.setAlpha(Math.round(MAX_SYSUI_SCRIM_ALPHA * factor));
215     }
216 
createDitheredAlphaMask(int height, @ColorInt int[] colors, float[] positions)217     private Bitmap createDitheredAlphaMask(int height, @ColorInt int[] colors, float[] positions) {
218         DisplayMetrics dm = mActivity.getResources().getDisplayMetrics();
219         int width = ResourceUtils.pxFromDp(ALPHA_MASK_BITMAP_WIDTH_DP, dm);
220         Bitmap dst = Bitmap.createBitmap(width, height, Bitmap.Config.ALPHA_8);
221         Canvas c = new Canvas(dst);
222         Paint paint = new Paint(DITHER_FLAG);
223         LinearGradient lg = new LinearGradient(0, 0, 0, height,
224                 colors, positions, Shader.TileMode.CLAMP);
225         paint.setShader(lg);
226         c.drawPaint(paint);
227         return dst;
228     }
229 }
230