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