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;
26 import static com.android.launcher3.anim.Interpolators.DEACCEL;
27 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
28 
29 import android.animation.Animator;
30 import android.animation.AnimatorListenerAdapter;
31 import android.animation.Keyframe;
32 import android.animation.ObjectAnimator;
33 import android.animation.PropertyValuesHolder;
34 import android.animation.RectEvaluator;
35 import android.content.Context;
36 import android.graphics.Canvas;
37 import android.graphics.Color;
38 import android.graphics.Rect;
39 import android.graphics.RectF;
40 import android.graphics.drawable.Drawable;
41 import android.os.Bundle;
42 import android.util.AttributeSet;
43 import android.util.Property;
44 import android.view.KeyEvent;
45 import android.view.MotionEvent;
46 import android.view.View;
47 import android.view.accessibility.AccessibilityManager;
48 import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
49 
50 import androidx.annotation.NonNull;
51 import androidx.annotation.Nullable;
52 import androidx.core.view.ViewCompat;
53 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
54 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
55 import androidx.customview.widget.ExploreByTouchHelper;
56 
57 import com.android.launcher3.DeviceProfile;
58 import com.android.launcher3.Insettable;
59 import com.android.launcher3.Launcher;
60 import com.android.launcher3.LauncherState;
61 import com.android.launcher3.LauncherStateManager;
62 import com.android.launcher3.LauncherStateManager.StateListener;
63 import com.android.launcher3.R;
64 import com.android.launcher3.Utilities;
65 import com.android.launcher3.uioverrides.WallpaperColorInfo;
66 import com.android.launcher3.uioverrides.WallpaperColorInfo.OnChangeListener;
67 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
68 import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
69 import com.android.launcher3.util.MultiValueAlpha;
70 import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
71 import com.android.launcher3.util.Themes;
72 import com.android.launcher3.widget.WidgetsFullSheet;
73 
74 import java.util.List;
75 
76 
77 /**
78  * Simple scrim which draws a flat color
79  */
80 public class ScrimView extends View implements Insettable, OnChangeListener,
81         AccessibilityStateChangeListener, StateListener {
82 
83     public static final Property<ScrimView, Integer> DRAG_HANDLE_ALPHA =
84             new Property<ScrimView, Integer>(Integer.TYPE, "dragHandleAlpha") {
85 
86                 @Override
87                 public Integer get(ScrimView scrimView) {
88                     return scrimView.mDragHandleAlpha;
89                 }
90 
91                 @Override
92                 public void set(ScrimView scrimView, Integer value) {
93                     scrimView.setDragHandleAlpha(value);
94                 }
95             };
96     private static final int WALLPAPERS = R.string.wallpaper_button_text;
97     private static final int WIDGETS = R.string.widget_button_text;
98     private static final int SETTINGS = R.string.settings_button_text;
99     private static final int ALPHA_CHANNEL_COUNT = 1;
100 
101     private final Rect mTempRect = new Rect();
102     private final int[] mTempPos = new int[2];
103 
104     protected final Launcher mLauncher;
105     private final WallpaperColorInfo mWallpaperColorInfo;
106     private final AccessibilityManager mAM;
107     protected final int mEndScrim;
108 
109     protected float mMaxScrimAlpha;
110 
111     protected float mProgress = 1;
112     protected int mScrimColor;
113 
114     protected int mCurrentFlatColor;
115     protected int mEndFlatColor;
116     protected int mEndFlatColorAlpha;
117 
118     protected final int mDragHandleSize;
119     protected float mDragHandleOffset;
120     private final Rect mDragHandleBounds;
121     private final RectF mHitRect = new RectF();
122 
123     private final MultiValueAlpha mMultiValueAlpha;
124 
125     private final AccessibilityHelper mAccessibilityHelper;
126     @Nullable
127     protected Drawable mDragHandle;
128 
129     private int mDragHandleAlpha = 255;
130 
ScrimView(Context context, AttributeSet attrs)131     public ScrimView(Context context, AttributeSet attrs) {
132         super(context, attrs);
133         mLauncher = Launcher.getLauncher(context);
134         mWallpaperColorInfo = WallpaperColorInfo.getInstance(context);
135         mEndScrim = Themes.getAttrColor(context, R.attr.allAppsScrimColor);
136 
137         mMaxScrimAlpha = 0.7f;
138 
139         mDragHandleSize = context.getResources()
140                 .getDimensionPixelSize(R.dimen.vertical_drag_handle_size);
141         mDragHandleBounds = new Rect(0, 0, mDragHandleSize, mDragHandleSize);
142 
143         mAccessibilityHelper = createAccessibilityHelper();
144         ViewCompat.setAccessibilityDelegate(this, mAccessibilityHelper);
145 
146         mAM = (AccessibilityManager) context.getSystemService(ACCESSIBILITY_SERVICE);
147         setFocusable(false);
148         mMultiValueAlpha = new MultiValueAlpha(this, ALPHA_CHANNEL_COUNT);
149     }
150 
getAlphaProperty(int index)151     public AlphaProperty getAlphaProperty(int index) {
152         return mMultiValueAlpha.getProperty(index);
153     }
154 
155     @NonNull
createAccessibilityHelper()156     protected AccessibilityHelper createAccessibilityHelper() {
157         return new AccessibilityHelper();
158     }
159 
160     @Override
setInsets(Rect insets)161     public void setInsets(Rect insets) {
162         updateDragHandleBounds();
163         updateDragHandleVisibility(null);
164     }
165 
166     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)167     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
168         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
169         updateDragHandleBounds();
170     }
171 
172     @Override
onAttachedToWindow()173     protected void onAttachedToWindow() {
174         super.onAttachedToWindow();
175         mWallpaperColorInfo.addOnChangeListener(this);
176         onExtractedColorsChanged(mWallpaperColorInfo);
177 
178         mAM.addAccessibilityStateChangeListener(this);
179         onAccessibilityStateChanged(mAM.isEnabled());
180     }
181 
182     @Override
onDetachedFromWindow()183     protected void onDetachedFromWindow() {
184         super.onDetachedFromWindow();
185         mWallpaperColorInfo.removeOnChangeListener(this);
186         mAM.removeAccessibilityStateChangeListener(this);
187     }
188 
189     @Override
hasOverlappingRendering()190     public boolean hasOverlappingRendering() {
191         return false;
192     }
193 
194     @Override
onExtractedColorsChanged(WallpaperColorInfo wallpaperColorInfo)195     public void onExtractedColorsChanged(WallpaperColorInfo wallpaperColorInfo) {
196         mScrimColor = wallpaperColorInfo.getMainColor();
197         mEndFlatColor = compositeColors(mEndScrim, setColorAlphaBound(
198                 mScrimColor, Math.round(mMaxScrimAlpha * 255)));
199         mEndFlatColorAlpha = Color.alpha(mEndFlatColor);
200         updateColors();
201         invalidate();
202     }
203 
setProgress(float progress)204     public void setProgress(float progress) {
205         if (mProgress != progress) {
206             mProgress = progress;
207             updateColors();
208             updateDragHandleAlpha();
209             invalidate();
210         }
211     }
212 
reInitUi()213     public void reInitUi() { }
214 
updateColors()215     protected void updateColors() {
216         mCurrentFlatColor = mProgress >= 1 ? 0 : setColorAlphaBound(
217                 mEndFlatColor, Math.round((1 - mProgress) * mEndFlatColorAlpha));
218     }
219 
updateDragHandleAlpha()220     protected void updateDragHandleAlpha() {
221         if (mDragHandle != null) {
222             mDragHandle.setAlpha(mDragHandleAlpha);
223         }
224     }
225 
setDragHandleAlpha(int alpha)226     private void setDragHandleAlpha(int alpha) {
227         if (alpha != mDragHandleAlpha) {
228             mDragHandleAlpha = alpha;
229             if (mDragHandle != null) {
230                 mDragHandle.setAlpha(mDragHandleAlpha);
231                 invalidate();
232             }
233         }
234     }
235 
236     @Override
onDraw(Canvas canvas)237     protected void onDraw(Canvas canvas) {
238         if (mCurrentFlatColor != 0) {
239             canvas.drawColor(mCurrentFlatColor);
240         }
241         drawDragHandle(canvas);
242     }
243 
drawDragHandle(Canvas canvas)244     protected void drawDragHandle(Canvas canvas) {
245         if (mDragHandle != null) {
246             canvas.translate(0, -mDragHandleOffset);
247             mDragHandle.draw(canvas);
248             canvas.translate(0, mDragHandleOffset);
249         }
250     }
251 
252     @Override
onTouchEvent(MotionEvent event)253     public boolean onTouchEvent(MotionEvent event) {
254         boolean value = super.onTouchEvent(event);
255         if (!value && mDragHandle != null && event.getAction() == ACTION_DOWN
256                 && mDragHandle.getAlpha() == 255
257                 && mHitRect.contains(event.getX(), event.getY())) {
258 
259             final Drawable drawable = mDragHandle;
260             mDragHandle = null;
261 
262             Rect bounds = new Rect(mDragHandleBounds);
263             bounds.offset(0, -(int) mDragHandleOffset);
264             drawable.setBounds(bounds);
265 
266             Rect topBounds = new Rect(bounds);
267             topBounds.offset(0, -bounds.height() / 2);
268 
269             Rect invalidateRegion = new Rect(bounds);
270             invalidateRegion.top = topBounds.top;
271 
272             Keyframe frameTop = Keyframe.ofObject(0.6f, topBounds);
273             frameTop.setInterpolator(DEACCEL);
274             Keyframe frameBot = Keyframe.ofObject(1, bounds);
275             frameBot.setInterpolator(ACCEL);
276             PropertyValuesHolder holder = PropertyValuesHolder .ofKeyframe("bounds",
277                     Keyframe.ofObject(0, bounds), frameTop, frameBot);
278             holder.setEvaluator(new RectEvaluator());
279 
280             ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(drawable, holder);
281             anim.addListener(new AnimatorListenerAdapter() {
282                 @Override
283                 public void onAnimationEnd(Animator animation) {
284                     getOverlay().remove(drawable);
285                     updateDragHandleVisibility(drawable);
286                 }
287             });
288             anim.addUpdateListener((v) -> invalidate(invalidateRegion));
289             getOverlay().add(drawable);
290             anim.start();
291             return true;
292         }
293         return value;
294     }
295 
updateDragHandleBounds()296     protected void updateDragHandleBounds() {
297         DeviceProfile grid = mLauncher.getDeviceProfile();
298         final int left;
299         final int width = getMeasuredWidth();
300         final int top = getMeasuredHeight() - mDragHandleSize - grid.getInsets().bottom;
301         final int topMargin;
302 
303         if (grid.isVerticalBarLayout()) {
304             topMargin = grid.workspacePadding.bottom;
305             if (grid.isSeascape()) {
306                 left = width - grid.getInsets().right - mDragHandleSize;
307             } else {
308                 left = mDragHandleSize + grid.getInsets().left;
309             }
310         } else {
311             left = (width - mDragHandleSize) / 2;
312             topMargin = grid.hotseatBarSizePx;
313         }
314         mDragHandleBounds.offsetTo(left, top - topMargin);
315         mHitRect.set(mDragHandleBounds);
316         float inset = -mDragHandleSize / 2;
317         mHitRect.inset(inset, inset);
318 
319         if (mDragHandle != null) {
320             mDragHandle.setBounds(mDragHandleBounds);
321         }
322     }
323 
324     @Override
onAccessibilityStateChanged(boolean enabled)325     public void onAccessibilityStateChanged(boolean enabled) {
326         LauncherStateManager stateManager = mLauncher.getStateManager();
327         stateManager.removeStateListener(this);
328 
329         if (enabled) {
330             stateManager.addStateListener(this);
331             handleStateChangedComplete(stateManager.getState());
332         } else {
333             setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
334         }
335         updateDragHandleVisibility(null);
336     }
337 
updateDragHandleVisibility(Drawable recycle)338     private void updateDragHandleVisibility(Drawable recycle) {
339         boolean visible = mLauncher.getDeviceProfile().isVerticalBarLayout() || mAM.isEnabled();
340         boolean wasVisible = mDragHandle != null;
341         if (visible != wasVisible) {
342             if (visible) {
343                 mDragHandle = recycle != null ? recycle :
344                         mLauncher.getDrawable(R.drawable.drag_handle_indicator);
345                 mDragHandle.setBounds(mDragHandleBounds);
346 
347                 updateDragHandleAlpha();
348             } else {
349                 mDragHandle = null;
350             }
351             invalidate();
352         }
353     }
354 
355     @Override
dispatchHoverEvent(MotionEvent event)356     public boolean dispatchHoverEvent(MotionEvent event) {
357         return mAccessibilityHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
358     }
359 
360     @Override
dispatchKeyEvent(KeyEvent event)361     public boolean dispatchKeyEvent(KeyEvent event) {
362         return mAccessibilityHelper.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
363     }
364 
365     @Override
onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)366     public void onFocusChanged(boolean gainFocus, int direction,
367             Rect previouslyFocusedRect) {
368         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
369         mAccessibilityHelper.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
370     }
371 
372     @Override
onStateTransitionStart(LauncherState toState)373     public void onStateTransitionStart(LauncherState toState) {}
374 
375     @Override
onStateTransitionComplete(LauncherState finalState)376     public void onStateTransitionComplete(LauncherState finalState) {
377         handleStateChangedComplete(finalState);
378     }
379 
handleStateChangedComplete(LauncherState finalState)380     private void handleStateChangedComplete(LauncherState finalState) {
381         setImportantForAccessibility(finalState == ALL_APPS
382                 ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
383                 : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
384     }
385 
386     protected class AccessibilityHelper extends ExploreByTouchHelper {
387 
388         private static final int DRAG_HANDLE_ID = 1;
389 
AccessibilityHelper()390         public AccessibilityHelper() {
391             super(ScrimView.this);
392         }
393 
394         @Override
getVirtualViewAt(float x, float y)395         protected int getVirtualViewAt(float x, float y) {
396             return  mDragHandleBounds.contains((int) x, (int) y)
397                     ? DRAG_HANDLE_ID : INVALID_ID;
398         }
399 
400         @Override
getVisibleVirtualViews(List<Integer> virtualViewIds)401         protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
402             virtualViewIds.add(DRAG_HANDLE_ID);
403         }
404 
405         @Override
onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfoCompat node)406         protected void onPopulateNodeForVirtualView(int virtualViewId,
407                 AccessibilityNodeInfoCompat node) {
408             node.setContentDescription(getContext().getString(R.string.all_apps_button_label));
409             node.setBoundsInParent(mDragHandleBounds);
410 
411             getLocationOnScreen(mTempPos);
412             mTempRect.set(mDragHandleBounds);
413             mTempRect.offset(mTempPos[0], mTempPos[1]);
414             node.setBoundsInScreen(mTempRect);
415 
416             node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
417             node.setClickable(true);
418             node.setFocusable(true);
419 
420             if (mLauncher.isInState(NORMAL)) {
421                 Context context = getContext();
422                 if (Utilities.isWallpaperAllowed(context)) {
423                     node.addAction(
424                             new AccessibilityActionCompat(WALLPAPERS, context.getText(WALLPAPERS)));
425                 }
426                 node.addAction(new AccessibilityActionCompat(WIDGETS, context.getText(WIDGETS)));
427                 node.addAction(new AccessibilityActionCompat(SETTINGS, context.getText(SETTINGS)));
428             }
429         }
430 
431         @Override
onPerformActionForVirtualView( int virtualViewId, int action, Bundle arguments)432         protected boolean onPerformActionForVirtualView(
433                 int virtualViewId, int action, Bundle arguments) {
434             if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
435                 mLauncher.getUserEventDispatcher().logActionOnControl(
436                         Action.Touch.TAP, ControlType.ALL_APPS_BUTTON,
437                         mLauncher.getStateManager().getState().containerType);
438                 mLauncher.getStateManager().goToState(ALL_APPS);
439                 return true;
440             } else if (action == WALLPAPERS) {
441                 return OptionsPopupView.startWallpaperPicker(ScrimView.this);
442             } else if (action == WIDGETS) {
443                 int originalImportanceForAccessibility = getImportantForAccessibility();
444                 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
445                 WidgetsFullSheet widgetsFullSheet = OptionsPopupView.openWidgets(mLauncher);
446                 if (widgetsFullSheet == null) {
447                     setImportantForAccessibility(originalImportanceForAccessibility);
448                     return false;
449                 }
450                 widgetsFullSheet.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
451                     @Override
452                     public void onViewAttachedToWindow(View view) {}
453 
454                     @Override
455                     public void onViewDetachedFromWindow(View view) {
456                         setImportantForAccessibility(originalImportanceForAccessibility);
457                         widgetsFullSheet.removeOnAttachStateChangeListener(this);
458                     }
459                 });
460                 return true;
461             } else if (action == SETTINGS) {
462                 return OptionsPopupView.startSettings(ScrimView.this);
463             }
464 
465             return false;
466         }
467     }
468 
getDragHandleSize()469     public int getDragHandleSize() {
470         return mDragHandleSize;
471     }
472 }
473