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