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