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.views;
17 
18 import static android.content.Context.ACCESSIBILITY_SERVICE;
19 import static android.view.MotionEvent.ACTION_DOWN;
20 
21 import static androidx.core.graphics.ColorUtils.compositeColors;
22 
23 import static com.android.launcher3.LauncherState.ALL_APPS;
24 import static com.android.launcher3.LauncherState.NORMAL;
25 import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
26 import static com.android.launcher3.anim.Interpolators.DEACCEL;
27 import static com.android.launcher3.anim.Interpolators.LINEAR;
28 import static com.android.launcher3.anim.Interpolators.clampToProgress;
29 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
30 import static com.android.launcher3.util.SystemUiController.UI_STATE_SCRIM_VIEW;
31 
32 import android.animation.Animator;
33 import android.animation.AnimatorListenerAdapter;
34 import android.animation.Keyframe;
35 import android.animation.ObjectAnimator;
36 import android.animation.PropertyValuesHolder;
37 import android.animation.RectEvaluator;
38 import android.content.Context;
39 import android.content.res.Resources;
40 import android.graphics.Canvas;
41 import android.graphics.Color;
42 import android.graphics.Point;
43 import android.graphics.Rect;
44 import android.graphics.RectF;
45 import android.graphics.drawable.Drawable;
46 import android.os.Bundle;
47 import android.util.AttributeSet;
48 import android.util.IntProperty;
49 import android.view.KeyEvent;
50 import android.view.MotionEvent;
51 import android.view.View;
52 import android.view.accessibility.AccessibilityManager;
53 import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
54 
55 import androidx.annotation.NonNull;
56 import androidx.annotation.Nullable;
57 import androidx.core.graphics.ColorUtils;
58 import androidx.core.view.ViewCompat;
59 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
60 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
61 import androidx.customview.widget.ExploreByTouchHelper;
62 
63 import com.android.launcher3.DeviceProfile;
64 import com.android.launcher3.Insettable;
65 import com.android.launcher3.Launcher;
66 import com.android.launcher3.LauncherState;
67 import com.android.launcher3.R;
68 import com.android.launcher3.Utilities;
69 import com.android.launcher3.statemanager.StateManager;
70 import com.android.launcher3.statemanager.StateManager.StateListener;
71 import com.android.launcher3.uioverrides.WallpaperColorInfo;
72 import com.android.launcher3.uioverrides.WallpaperColorInfo.OnChangeListener;
73 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
74 import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
75 import com.android.launcher3.util.MultiValueAlpha;
76 import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
77 import com.android.launcher3.util.Themes;
78 import com.android.launcher3.widget.WidgetsFullSheet;
79 
80 import java.util.List;
81 
82 /**
83  * Simple scrim which draws a flat color
84  */
85 public class ScrimView<T extends Launcher> extends View implements Insettable, OnChangeListener,
86         AccessibilityStateChangeListener {
87 
88     public static final IntProperty<ScrimView> DRAG_HANDLE_ALPHA =
89             new IntProperty<ScrimView>("dragHandleAlpha") {
90 
91                 @Override
92                 public Integer get(ScrimView scrimView) {
93                     return scrimView.mDragHandleAlpha;
94                 }
95 
96                 @Override
97                 public void setValue(ScrimView scrimView, int value) {
98                     scrimView.setDragHandleAlpha(value);
99                 }
100             };
101     private static final int WALLPAPERS = R.string.wallpaper_button_text;
102     private static final int WIDGETS = R.string.widget_button_text;
103     private static final int SETTINGS = R.string.settings_button_text;
104     private static final int ALPHA_CHANNEL_COUNT = 1;
105 
106     private static final long DRAG_HANDLE_BOUNCE_DURATION_MS = 300;
107     // How much to delay before repeating the bounce.
108     private static final long DRAG_HANDLE_BOUNCE_DELAY_MS = 200;
109     // Repeat this many times (i.e. total number of bounces is 1 + this).
110     private static final int DRAG_HANDLE_BOUNCE_REPEAT_COUNT = 2;
111 
112     private final Rect mTempRect = new Rect();
113     private final int[] mTempPos = new int[2];
114 
115     protected final T mLauncher;
116     private final WallpaperColorInfo mWallpaperColorInfo;
117     private final AccessibilityManager mAM;
118     protected final int mEndScrim;
119     protected final boolean mIsScrimDark;
120 
121     private final StateListener<LauncherState> mAccessibilityLauncherStateListener =
122             new StateListener<LauncherState>() {
123         @Override
124         public void onStateTransitionComplete(LauncherState finalState) {
125             setImportantForAccessibility(finalState == ALL_APPS
126                     ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
127                     : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
128         }
129     };
130 
131     protected float mMaxScrimAlpha;
132 
133     protected float mProgress = 1;
134     protected int mScrimColor;
135 
136     protected int mCurrentFlatColor;
137     protected int mEndFlatColor;
138     protected int mEndFlatColorAlpha;
139 
140     protected final Point mDragHandleSize;
141     private final int mDragHandleTouchSize;
142     private final int mDragHandlePaddingInVerticalBarLayout;
143     protected float mDragHandleOffset;
144     private final Rect mDragHandleBounds;
145     private final RectF mHitRect = new RectF();
146     private ObjectAnimator mDragHandleAnim;
147 
148     private final MultiValueAlpha mMultiValueAlpha;
149 
150     private final AccessibilityHelper mAccessibilityHelper;
151     @Nullable
152     protected Drawable mDragHandle;
153 
154     private int mDragHandleAlpha = 255;
155 
ScrimView(Context context, AttributeSet attrs)156     public ScrimView(Context context, AttributeSet attrs) {
157         super(context, attrs);
158         mLauncher = Launcher.cast(Launcher.getLauncher(context));
159         mWallpaperColorInfo = WallpaperColorInfo.INSTANCE.get(context);
160         mEndScrim = Themes.getAttrColor(context, R.attr.allAppsScrimColor);
161         mIsScrimDark = ColorUtils.calculateLuminance(mEndScrim) < 0.5f;
162 
163         mMaxScrimAlpha = 0.7f;
164 
165         Resources res = context.getResources();
166         mDragHandleSize = new Point(res.getDimensionPixelSize(R.dimen.vertical_drag_handle_width),
167                 res.getDimensionPixelSize(R.dimen.vertical_drag_handle_height));
168         mDragHandleBounds = new Rect(0, 0, mDragHandleSize.x, mDragHandleSize.y);
169         mDragHandleTouchSize = res.getDimensionPixelSize(R.dimen.vertical_drag_handle_touch_size);
170         mDragHandlePaddingInVerticalBarLayout = context.getResources()
171                 .getDimensionPixelSize(R.dimen.vertical_drag_handle_padding_in_vertical_bar_layout);
172 
173         mAccessibilityHelper = createAccessibilityHelper();
174         ViewCompat.setAccessibilityDelegate(this, mAccessibilityHelper);
175 
176         mAM = (AccessibilityManager) context.getSystemService(ACCESSIBILITY_SERVICE);
177         setFocusable(false);
178         mMultiValueAlpha = new MultiValueAlpha(this, ALPHA_CHANNEL_COUNT);
179     }
180 
181     public AlphaProperty getAlphaProperty(int index) {
182         return mMultiValueAlpha.getProperty(index);
183     }
184 
185     @NonNull
186     protected AccessibilityHelper createAccessibilityHelper() {
187         return new AccessibilityHelper();
188     }
189 
190     @Override
191     public void setInsets(Rect insets) {
192         updateDragHandleBounds();
193         updateDragHandleVisibility();
194     }
195 
196     @Override
197     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
198         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
199         updateDragHandleBounds();
200     }
201 
202     @Override
203     protected void onAttachedToWindow() {
204         super.onAttachedToWindow();
205         mWallpaperColorInfo.addOnChangeListener(this);
206         onExtractedColorsChanged(mWallpaperColorInfo);
207 
208         mAM.addAccessibilityStateChangeListener(this);
209         onAccessibilityStateChanged(mAM.isEnabled());
210     }
211 
212     @Override
213     protected void onDetachedFromWindow() {
214         super.onDetachedFromWindow();
215         mWallpaperColorInfo.removeOnChangeListener(this);
216         mAM.removeAccessibilityStateChangeListener(this);
217     }
218 
219     @Override
220     public boolean hasOverlappingRendering() {
221         return false;
222     }
223 
224     @Override
225     public void onExtractedColorsChanged(WallpaperColorInfo wallpaperColorInfo) {
226         mScrimColor = wallpaperColorInfo.getMainColor();
227         mEndFlatColor = compositeColors(mEndScrim, setColorAlphaBound(
228                 mScrimColor, Math.round(mMaxScrimAlpha * 255)));
229         mEndFlatColorAlpha = Color.alpha(mEndFlatColor);
230         updateColors();
231         invalidate();
232     }
233 
234     public void setProgress(float progress) {
235         if (mProgress != progress) {
236             mProgress = progress;
237             stopDragHandleEducationAnim();
238             updateColors();
239             updateSysUiColors();
240             updateDragHandleAlpha();
241             invalidate();
242         }
243     }
244 
245     public void reInitUi() { }
246 
247     protected void updateColors() {
248         mCurrentFlatColor = mProgress >= 1 ? 0 : setColorAlphaBound(
249                 mEndFlatColor, Math.round((1 - mProgress) * mEndFlatColorAlpha));
250     }
251 
updateSysUiColors()252     protected void updateSysUiColors() {
253         // Use a light system UI (dark icons) if all apps is behind at least half of the
254         // status bar.
255         boolean forceChange = mProgress <= 0.1f;
256         if (forceChange) {
257             mLauncher.getSystemUiController().updateUiState(UI_STATE_SCRIM_VIEW, !mIsScrimDark);
258         } else {
259             mLauncher.getSystemUiController().updateUiState(UI_STATE_SCRIM_VIEW, 0);
260         }
261     }
262 
updateDragHandleAlpha()263     protected void updateDragHandleAlpha() {
264         if (mDragHandle != null) {
265             mDragHandle.setAlpha(mDragHandleAlpha);
266         }
267     }
268 
setDragHandleAlpha(int alpha)269     private void setDragHandleAlpha(int alpha) {
270         if (alpha != mDragHandleAlpha) {
271             mDragHandleAlpha = alpha;
272             if (mDragHandle != null) {
273                 mDragHandle.setAlpha(mDragHandleAlpha);
274                 invalidate();
275             }
276         }
277     }
278 
279     @Override
onDraw(Canvas canvas)280     protected void onDraw(Canvas canvas) {
281         if (mCurrentFlatColor != 0) {
282             canvas.drawColor(mCurrentFlatColor);
283         }
284         drawDragHandle(canvas);
285     }
286 
drawDragHandle(Canvas canvas)287     protected void drawDragHandle(Canvas canvas) {
288         if (mDragHandle != null) {
289             canvas.translate(0, -mDragHandleOffset);
290             mDragHandle.draw(canvas);
291             canvas.translate(0, mDragHandleOffset);
292         }
293     }
294 
295     @Override
onTouchEvent(MotionEvent event)296     public boolean onTouchEvent(MotionEvent event) {
297         boolean superHandledTouch = super.onTouchEvent(event);
298         if (event.getAction() == ACTION_DOWN) {
299             if (!superHandledTouch && mHitRect.contains(event.getX(), event.getY())) {
300                 if (startDragHandleEducationAnim()) {
301                     return true;
302                 }
303             }
304             stopDragHandleEducationAnim();
305         }
306         return superHandledTouch;
307     }
308 
309     /**
310      * Animates the drag handle to demonstrate how to get to all apps.
311      * @return Whether the animation was started (false if drag handle is invisible).
312      */
startDragHandleEducationAnim()313     public boolean startDragHandleEducationAnim() {
314         stopDragHandleEducationAnim();
315 
316         if (mDragHandle == null || mDragHandle.getAlpha() != 255) {
317             return false;
318         }
319 
320         final Drawable drawable = mDragHandle;
321         mDragHandle = null;
322 
323         Rect bounds = new Rect(mDragHandleBounds);
324         bounds.offset(0, -(int) mDragHandleOffset);
325         drawable.setBounds(bounds);
326 
327         Rect topBounds = new Rect(bounds);
328         topBounds.offset(0, -bounds.height());
329 
330         Rect invalidateRegion = new Rect(bounds);
331         invalidateRegion.top = topBounds.top;
332 
333         final float progressToReachTop = 0.6f;
334         Keyframe frameTop = Keyframe.ofObject(progressToReachTop, topBounds);
335         frameTop.setInterpolator(DEACCEL);
336         Keyframe frameBot = Keyframe.ofObject(1, bounds);
337         frameBot.setInterpolator(ACCEL_DEACCEL);
338         PropertyValuesHolder holder = PropertyValuesHolder.ofKeyframe("bounds",
339                 Keyframe.ofObject(0, bounds), frameTop, frameBot);
340         holder.setEvaluator(new RectEvaluator());
341 
342         mDragHandleAnim = ObjectAnimator.ofPropertyValuesHolder(drawable, holder);
343         long totalBounceDuration = DRAG_HANDLE_BOUNCE_DURATION_MS + DRAG_HANDLE_BOUNCE_DELAY_MS;
344         // The bounce finishes by this progress, the rest of the duration just delays next bounce.
345         float delayStartProgress = 1f - (float) DRAG_HANDLE_BOUNCE_DELAY_MS / totalBounceDuration;
346         mDragHandleAnim.addUpdateListener((v) -> invalidate(invalidateRegion));
347         mDragHandleAnim.setDuration(totalBounceDuration);
348         mDragHandleAnim.setInterpolator(clampToProgress(LINEAR, 0, delayStartProgress));
349         mDragHandleAnim.setRepeatCount(DRAG_HANDLE_BOUNCE_REPEAT_COUNT);
350         getOverlay().add(drawable);
351 
352         mDragHandleAnim.addListener(new AnimatorListenerAdapter() {
353             @Override
354             public void onAnimationEnd(Animator animation) {
355                 mDragHandleAnim = null;
356                 getOverlay().remove(drawable);
357                 updateDragHandleVisibility(drawable);
358             }
359         });
360         mDragHandleAnim.start();
361         return true;
362     }
363 
stopDragHandleEducationAnim()364     private void stopDragHandleEducationAnim() {
365         if (mDragHandleAnim != null) {
366             mDragHandleAnim.end();
367         }
368     }
369 
updateDragHandleBounds()370     protected void updateDragHandleBounds() {
371         DeviceProfile grid = mLauncher.getDeviceProfile();
372         final int left;
373         final int width = getMeasuredWidth();
374         final int top = getMeasuredHeight() - mDragHandleSize.y - grid.getInsets().bottom;
375         final int topMargin;
376 
377         if (grid.isVerticalBarLayout()) {
378             topMargin = grid.workspacePadding.bottom + mDragHandlePaddingInVerticalBarLayout;
379             if (grid.isSeascape()) {
380                 left = width - grid.getInsets().right - mDragHandleSize.x
381                         - mDragHandlePaddingInVerticalBarLayout;
382             } else {
383                 left = grid.getInsets().left + mDragHandlePaddingInVerticalBarLayout;
384             }
385         } else {
386             left = Math.round((width - mDragHandleSize.x) / 2f);
387             topMargin = grid.hotseatBarSizePx;
388         }
389         mDragHandleBounds.offsetTo(left, top - topMargin);
390         mHitRect.set(mDragHandleBounds);
391         // Inset outwards to increase touch size.
392         mHitRect.inset((mDragHandleSize.x - mDragHandleTouchSize) / 2f,
393                 (mDragHandleSize.y - mDragHandleTouchSize) / 2f);
394 
395         if (mDragHandle != null) {
396             mDragHandle.setBounds(mDragHandleBounds);
397         }
398     }
399 
400     @Override
onAccessibilityStateChanged(boolean enabled)401     public void onAccessibilityStateChanged(boolean enabled) {
402         StateManager<LauncherState> stateManager = mLauncher.getStateManager();
403         stateManager.removeStateListener(mAccessibilityLauncherStateListener);
404 
405         if (enabled) {
406             stateManager.addStateListener(mAccessibilityLauncherStateListener);
407             mAccessibilityLauncherStateListener.onStateTransitionComplete(stateManager.getState());
408         } else {
409             setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
410         }
411         updateDragHandleVisibility();
412     }
413 
updateDragHandleVisibility()414     public void updateDragHandleVisibility() {
415         updateDragHandleVisibility(null);
416     }
417 
updateDragHandleVisibility(@ullable Drawable recycle)418     private void updateDragHandleVisibility(@Nullable Drawable recycle) {
419         boolean visible = shouldDragHandleBeVisible();
420         boolean wasVisible = mDragHandle != null;
421         if (visible != wasVisible) {
422             if (visible) {
423                 mDragHandle = recycle != null ? recycle :
424                         mLauncher.getDrawable(R.drawable.drag_handle_indicator_shadow);
425                 mDragHandle.setBounds(mDragHandleBounds);
426 
427                 updateDragHandleAlpha();
428             } else {
429                 mDragHandle = null;
430             }
431             invalidate();
432         }
433     }
434 
shouldDragHandleBeVisible()435     protected boolean shouldDragHandleBeVisible() {
436         return mLauncher.getDeviceProfile().isVerticalBarLayout() || mAM.isEnabled();
437     }
438 
439     @Override
dispatchHoverEvent(MotionEvent event)440     public boolean dispatchHoverEvent(MotionEvent event) {
441         return mAccessibilityHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
442     }
443 
444     @Override
dispatchKeyEvent(KeyEvent event)445     public boolean dispatchKeyEvent(KeyEvent event) {
446         return mAccessibilityHelper.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
447     }
448 
449     @Override
onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)450     public void onFocusChanged(boolean gainFocus, int direction,
451             Rect previouslyFocusedRect) {
452         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
453         mAccessibilityHelper.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
454     }
455 
456     protected class AccessibilityHelper extends ExploreByTouchHelper {
457 
458         private static final int DRAG_HANDLE_ID = 1;
459 
AccessibilityHelper()460         public AccessibilityHelper() {
461             super(ScrimView.this);
462         }
463 
464         @Override
getVirtualViewAt(float x, float y)465         protected int getVirtualViewAt(float x, float y) {
466             return  mHitRect.contains((int) x, (int) y)
467                     ? DRAG_HANDLE_ID : INVALID_ID;
468         }
469 
470         @Override
getVisibleVirtualViews(List<Integer> virtualViewIds)471         protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
472             virtualViewIds.add(DRAG_HANDLE_ID);
473         }
474 
475         @Override
onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfoCompat node)476         protected void onPopulateNodeForVirtualView(int virtualViewId,
477                 AccessibilityNodeInfoCompat node) {
478             node.setContentDescription(getContext().getString(R.string.all_apps_button_label));
479             node.setBoundsInParent(mDragHandleBounds);
480 
481             getLocationOnScreen(mTempPos);
482             mTempRect.set(mDragHandleBounds);
483             mTempRect.offset(mTempPos[0], mTempPos[1]);
484             node.setBoundsInScreen(mTempRect);
485 
486             node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
487             node.setClickable(true);
488             node.setFocusable(true);
489 
490             if (mLauncher.isInState(NORMAL)) {
491                 Context context = getContext();
492                 if (Utilities.isWallpaperAllowed(context)) {
493                     node.addAction(
494                             new AccessibilityActionCompat(WALLPAPERS, context.getText(WALLPAPERS)));
495                 }
496                 node.addAction(new AccessibilityActionCompat(WIDGETS, context.getText(WIDGETS)));
497                 node.addAction(new AccessibilityActionCompat(SETTINGS, context.getText(SETTINGS)));
498             }
499         }
500 
501         @Override
onPerformActionForVirtualView( int virtualViewId, int action, Bundle arguments)502         protected boolean onPerformActionForVirtualView(
503                 int virtualViewId, int action, Bundle arguments) {
504             if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
505                 mLauncher.getUserEventDispatcher().logActionOnControl(
506                         Action.Touch.TAP, ControlType.ALL_APPS_BUTTON,
507                         mLauncher.getStateManager().getState().containerType);
508                 mLauncher.getStateManager().goToState(ALL_APPS);
509                 return true;
510             } else if (action == WALLPAPERS) {
511                 return OptionsPopupView.startWallpaperPicker(ScrimView.this);
512             } else if (action == WIDGETS) {
513                 int originalImportanceForAccessibility = getImportantForAccessibility();
514                 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
515                 WidgetsFullSheet widgetsFullSheet = OptionsPopupView.openWidgets(mLauncher);
516                 if (widgetsFullSheet == null) {
517                     setImportantForAccessibility(originalImportanceForAccessibility);
518                     return false;
519                 }
520                 widgetsFullSheet.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
521                     @Override
522                     public void onViewAttachedToWindow(View view) {}
523 
524                     @Override
525                     public void onViewDetachedFromWindow(View view) {
526                         setImportantForAccessibility(originalImportanceForAccessibility);
527                         widgetsFullSheet.removeOnAttachStateChangeListener(this);
528                     }
529                 });
530                 return true;
531             } else if (action == SETTINGS) {
532                 return OptionsPopupView.startSettings(ScrimView.this);
533             }
534 
535             return false;
536         }
537     }
538 
539     /**
540      * @return The top of this scrim view, or {@link Float#MAX_VALUE} if there's no distinct top.
541      */
getVisualTop()542     public float getVisualTop() {
543         return Float.MAX_VALUE;
544     }
545 }
546