1 /* 2 * Copyright (C) 2017 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.view.ViewGroup.LayoutParams.MATCH_PARENT; 19 20 import static com.android.app.animation.Interpolators.LINEAR; 21 import static com.android.app.animation.Interpolators.scrollInterpolatorForVelocity; 22 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; 23 import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS; 24 import static com.android.launcher3.LauncherAnimUtils.TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS; 25 import static com.android.launcher3.allapps.AllAppsTransitionController.REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS; 26 import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE; 27 28 import android.animation.Animator; 29 import android.animation.ObjectAnimator; 30 import android.animation.ValueAnimator; 31 import android.content.Context; 32 import android.graphics.Canvas; 33 import android.graphics.Outline; 34 import android.graphics.drawable.Drawable; 35 import android.os.Build; 36 import android.util.AttributeSet; 37 import android.util.FloatProperty; 38 import android.view.MotionEvent; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.view.ViewOutlineProvider; 42 import android.view.animation.Interpolator; 43 import android.window.BackEvent; 44 45 import androidx.annotation.NonNull; 46 import androidx.annotation.Nullable; 47 import androidx.annotation.Px; 48 import androidx.annotation.RequiresApi; 49 50 import com.android.app.animation.Interpolators; 51 import com.android.launcher3.AbstractFloatingView; 52 import com.android.launcher3.Utilities; 53 import com.android.launcher3.anim.AnimatedFloat; 54 import com.android.launcher3.anim.AnimatorListeners; 55 import com.android.launcher3.anim.AnimatorPlaybackController; 56 import com.android.launcher3.anim.PendingAnimation; 57 import com.android.launcher3.touch.BaseSwipeDetector; 58 import com.android.launcher3.touch.SingleAxisSwipeDetector; 59 60 import java.util.ArrayList; 61 import java.util.List; 62 import java.util.Optional; 63 64 /** 65 * Extension of {@link AbstractFloatingView} with common methods for sliding in from bottom. 66 * 67 * @param <T> Type of ActivityContext inflating this view. 68 */ 69 public abstract class AbstractSlideInView<T extends Context & ActivityContext> 70 extends AbstractFloatingView implements SingleAxisSwipeDetector.Listener { 71 72 protected static final FloatProperty<AbstractSlideInView<?>> TRANSLATION_SHIFT = 73 new FloatProperty<>("translationShift") { 74 75 @Override 76 public Float get(AbstractSlideInView view) { 77 return view.mTranslationShift; 78 } 79 80 @Override 81 public void setValue(AbstractSlideInView view, float value) { 82 view.setTranslationShift(value); 83 } 84 }; 85 protected static final float TRANSLATION_SHIFT_CLOSED = 1f; 86 protected static final float TRANSLATION_SHIFT_OPENED = 0f; 87 private static final int DEFAULT_DURATION = 300; 88 89 protected final T mActivityContext; 90 91 protected final SingleAxisSwipeDetector mSwipeDetector; 92 protected @NonNull AnimatorPlaybackController mOpenCloseAnimation; 93 94 protected ViewGroup mContent; 95 protected final @Nullable View mColorScrim; 96 97 /** 98 * Interpolator for {@link #mOpenCloseAnimation} when we are closing due to dragging downwards. 99 */ 100 private Interpolator mScrollInterpolator; 101 private long mScrollDuration; 102 /** 103 * End progress for {@link #mOpenCloseAnimation} when we are closing due to dragging downloads. 104 * <p> 105 * There are two cases that determine this value: 106 * <ol> 107 * <li> 108 * If the drag interrupts the opening transition (i.e. {@link #mToTranslationShift} 109 * is {@link #TRANSLATION_SHIFT_OPENED}), we need to animate back to {@code 0} to 110 * reverse the animation that was paused at {@link #onDragStart(boolean, float)}. 111 * </li> 112 * <li> 113 * If the drag started after the view is fully opened (i.e. 114 * {@link #mToTranslationShift} is {@link #TRANSLATION_SHIFT_CLOSED}), the animation 115 * that was set up at {@link #onDragStart(boolean, float)} for closing the view 116 * should go forward to {@code 1}. 117 * </li> 118 * </ol> 119 */ 120 private float mScrollEndProgress; 121 122 // range [0, 1], 0=> completely open, 1=> completely closed 123 protected float mTranslationShift = TRANSLATION_SHIFT_CLOSED; 124 protected float mFromTranslationShift; 125 protected float mToTranslationShift; 126 /** {@link #mOpenCloseAnimation} progress at {@link #onDragStart(boolean, float)}. */ 127 private float mDragStartProgress; 128 129 protected boolean mNoIntercept; 130 protected @Nullable OnCloseListener mOnCloseBeginListener; 131 protected List<OnCloseListener> mOnCloseListeners = new ArrayList<>(); 132 133 /** 134 * How far through a "user initiated dismissal" the UI is. e.g. Predictive back, swipe to home, 135 * 0 is regular state, 1 is fully dismissed. 136 */ 137 protected final AnimatedFloat mSwipeToDismissProgress = 138 new AnimatedFloat(this::onUserSwipeToDismissProgressChanged, 0f); 139 protected boolean mIsDismissInProgress; 140 private View mViewToAnimateInSwipeToDismiss = this; 141 private @Nullable Drawable mContentBackground; 142 private @Nullable View mContentBackgroundParentView; 143 144 protected final ViewOutlineProvider mViewOutlineProvider = new ViewOutlineProvider() { 145 @Override 146 public void getOutline(View view, Outline outline) { 147 outline.setRect( 148 0, 149 0, 150 view.getMeasuredWidth(), 151 view.getMeasuredHeight() + getBottomOffsetPx() 152 ); 153 } 154 }; 155 AbstractSlideInView(Context context, AttributeSet attrs, int defStyleAttr)156 public AbstractSlideInView(Context context, AttributeSet attrs, int defStyleAttr) { 157 super(context, attrs, defStyleAttr); 158 mActivityContext = ActivityContext.lookupContext(context); 159 160 mScrollInterpolator = Interpolators.SCROLL_CUBIC; 161 mScrollDuration = DEFAULT_DURATION; 162 mSwipeDetector = new SingleAxisSwipeDetector(context, this, 163 SingleAxisSwipeDetector.VERTICAL); 164 165 mOpenCloseAnimation = new PendingAnimation(0).createPlaybackController(); 166 167 int scrimColor = getScrimColor(context); 168 mColorScrim = scrimColor != -1 ? createColorScrim(context, scrimColor) : null; 169 } 170 171 /** 172 * Sets up a {@link #mOpenCloseAnimation} for opening with default parameters. 173 * 174 * @see #setUpOpenCloseAnimation(float, float, long) 175 */ setUpDefaultOpenAnimation()176 protected final AnimatorPlaybackController setUpDefaultOpenAnimation() { 177 AnimatorPlaybackController animation = setUpOpenCloseAnimation( 178 TRANSLATION_SHIFT_CLOSED, TRANSLATION_SHIFT_OPENED, DEFAULT_DURATION); 179 animation.getAnimationPlayer().setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 180 return animation; 181 } 182 183 /** 184 * Sets up a {@link #mOpenCloseAnimation} for opening with a given duration. 185 * 186 * @see #setUpOpenCloseAnimation(float, float, long) 187 */ setUpOpenAnimation(long duration)188 protected final AnimatorPlaybackController setUpOpenAnimation(long duration) { 189 return setUpOpenCloseAnimation( 190 TRANSLATION_SHIFT_CLOSED, TRANSLATION_SHIFT_OPENED, duration); 191 } 192 setUpCloseAnimation(long duration)193 private AnimatorPlaybackController setUpCloseAnimation(long duration) { 194 return setUpOpenCloseAnimation( 195 TRANSLATION_SHIFT_OPENED, TRANSLATION_SHIFT_CLOSED, duration); 196 } 197 198 /** 199 * Initializes a new {@link #mOpenCloseAnimation}. 200 * 201 * @param fromTranslationShift translation shift to animate from. 202 * @param toTranslationShift translation shift to animate to. 203 * @param duration animation duration. 204 * @return {@link #mOpenCloseAnimation} 205 */ setUpOpenCloseAnimation( float fromTranslationShift, float toTranslationShift, long duration)206 private AnimatorPlaybackController setUpOpenCloseAnimation( 207 float fromTranslationShift, float toTranslationShift, long duration) { 208 mFromTranslationShift = fromTranslationShift; 209 mToTranslationShift = toTranslationShift; 210 211 PendingAnimation animation = new PendingAnimation(duration); 212 animation.addEndListener(b -> { 213 mSwipeDetector.finishedScrolling(); 214 announceAccessibilityChanges(); 215 }); 216 217 animation.addFloat( 218 this, TRANSLATION_SHIFT, fromTranslationShift, toTranslationShift, LINEAR); 219 if (mColorScrim != null) { 220 animation.setViewAlpha(mColorScrim, 1 - toTranslationShift, getScrimInterpolator()); 221 } 222 onOpenCloseAnimationPending(animation); 223 224 mOpenCloseAnimation = animation.createPlaybackController(); 225 return mOpenCloseAnimation; 226 } 227 228 /** 229 * Invoked when a {@link #mOpenCloseAnimation} is being set up. 230 * <p> 231 * Subclasses can override this method to modify the animation before it's used to create a 232 * {@link AnimatorPlaybackController}. 233 */ onOpenCloseAnimationPending(PendingAnimation animation)234 protected void onOpenCloseAnimationPending(PendingAnimation animation) {} 235 attachToContainer()236 protected void attachToContainer() { 237 if (mColorScrim != null) { 238 getPopupContainer().addView(mColorScrim); 239 } 240 getPopupContainer().addView(this); 241 } 242 243 /** 244 * Returns a scrim color for a sliding view. if returned value is -1, no scrim is added. 245 */ getScrimColor(Context context)246 protected int getScrimColor(Context context) { 247 return -1; 248 } 249 250 /** 251 * Returns the range in height that the slide in view can be dragged. 252 */ getShiftRange()253 protected float getShiftRange() { 254 return mContent.getHeight(); 255 } 256 setTranslationShift(float translationShift)257 protected void setTranslationShift(float translationShift) { 258 mTranslationShift = translationShift; 259 mContent.setTranslationY(mTranslationShift * getShiftRange()); 260 invalidate(); 261 } 262 263 @Override onControllerInterceptTouchEvent(MotionEvent ev)264 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 265 if (mNoIntercept) { 266 return false; 267 } 268 269 int directionsToDetectScroll = mSwipeDetector.isIdleState() 270 ? SingleAxisSwipeDetector.DIRECTION_NEGATIVE : 0; 271 mSwipeDetector.setDetectableScrollConditions( 272 directionsToDetectScroll, false); 273 mSwipeDetector.onTouchEvent(ev); 274 return mSwipeDetector.isDraggingOrSettling() || !isEventOverContent(ev); 275 } 276 277 @Override onControllerTouchEvent(MotionEvent ev)278 public boolean onControllerTouchEvent(MotionEvent ev) { 279 mSwipeDetector.onTouchEvent(ev); 280 if (ev.getAction() == MotionEvent.ACTION_UP && mSwipeDetector.isIdleState() 281 && !isOpeningAnimationRunning()) { 282 // If we got ACTION_UP without ever starting swipe, close the panel. 283 if (!isEventOverContent(ev)) { 284 close(true); 285 } 286 } 287 return true; 288 } 289 290 @Override 291 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) onBackStarted(BackEvent backEvent)292 public void onBackStarted(BackEvent backEvent) { 293 super.onBackStarted(backEvent); 294 mViewToAnimateInSwipeToDismiss = shouldAnimateContentViewInBackSwipe() ? mContent : this; 295 } 296 297 @Override 298 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) onBackProgressed(BackEvent backEvent)299 public void onBackProgressed(BackEvent backEvent) { 300 final float progress = backEvent.getProgress(); 301 mSwipeToDismissProgress.updateValue(progress); 302 } 303 304 /** 305 * During predictive back swipe, the default behavior is to scale {@link AbstractSlideInView} 306 * during back swipe. This method allow subclass to scale {@link #mContent}, typically to exit 307 * search mode. 308 * 309 * <p>Note that this method can be expensive, and should only be called from 310 * {@link #onBackStarted(BackEvent)}, not from {@link #onBackProgressed(BackEvent)}. 311 */ shouldAnimateContentViewInBackSwipe()312 protected boolean shouldAnimateContentViewInBackSwipe() { 313 return false; 314 } 315 onUserSwipeToDismissProgressChanged()316 protected void onUserSwipeToDismissProgressChanged() { 317 float progress = mSwipeToDismissProgress.value; 318 mIsDismissInProgress = progress > 0f; 319 320 float scale = PREDICTIVE_BACK_MIN_SCALE + (1 - PREDICTIVE_BACK_MIN_SCALE) * (1f - progress); 321 SCALE_PROPERTY.set(mViewToAnimateInSwipeToDismiss, scale); 322 setClipChildren(!mIsDismissInProgress); 323 setClipToPadding(!mIsDismissInProgress); 324 mContent.setClipChildren(!mIsDismissInProgress); 325 mContent.setClipToPadding(!mIsDismissInProgress); 326 invalidate(); 327 } 328 329 @Override onBackCancelled()330 public void onBackCancelled() { 331 super.onBackCancelled(); 332 animateSwipeToDismissProgressToStart(); 333 } 334 animateSwipeToDismissProgressToStart()335 protected void animateSwipeToDismissProgressToStart() { 336 ObjectAnimator objectAnimator = mSwipeToDismissProgress.animateToValue(0f) 337 .setDuration(REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS); 338 339 // If we are animating a different view, we should reset the animating view back to 340 // AbstractSlideInView as it is the default view to animate. 341 if (this != mViewToAnimateInSwipeToDismiss) { 342 objectAnimator.addListener(new Animator.AnimatorListener() { 343 @Override 344 public void onAnimationCancel(Animator animator) { 345 mViewToAnimateInSwipeToDismiss = AbstractSlideInView.this; 346 } 347 348 @Override 349 public void onAnimationEnd(Animator animator) { 350 mViewToAnimateInSwipeToDismiss = AbstractSlideInView.this; 351 } 352 353 @Override 354 public void onAnimationRepeat(Animator animator) {} 355 356 @Override 357 public void onAnimationStart(Animator animator) {} 358 }); 359 } 360 361 objectAnimator.start(); 362 } 363 364 @Override dispatchDraw(Canvas canvas)365 protected void dispatchDraw(Canvas canvas) { 366 drawScaledBackground(canvas); 367 super.dispatchDraw(canvas); 368 } 369 370 /** 371 * Set slide in view's background {@link Drawable} which will be draw onto a parent view in 372 * {@link #dispatchDraw(Canvas)} 373 */ setContentBackgroundWithParent( @onNull Drawable drawable, @NonNull View parentView)374 protected void setContentBackgroundWithParent( 375 @NonNull Drawable drawable, @NonNull View parentView) { 376 mContentBackground = drawable; 377 mContentBackgroundParentView = parentView; 378 } 379 380 /** Draw scaled background during predictive back animation. */ drawScaledBackground(Canvas canvas)381 private void drawScaledBackground(Canvas canvas) { 382 if (mContentBackground == null || mContentBackgroundParentView == null) { 383 return; 384 } 385 mContentBackground.setBounds( 386 mContentBackgroundParentView.getLeft(), 387 mContentBackgroundParentView.getTop() + (int) mContent.getTranslationY(), 388 mContentBackgroundParentView.getRight(), 389 mContentBackgroundParentView.getBottom() 390 + (mIsDismissInProgress ? getBottomOffsetPx() : 0)); 391 mContentBackground.draw(canvas); 392 } 393 394 /** Return extra space revealed during predictive back animation. */ 395 @Px getBottomOffsetPx()396 protected int getBottomOffsetPx() { 397 return (int) (getMeasuredHeight() * (1 - PREDICTIVE_BACK_MIN_SCALE)); 398 } 399 400 /** 401 * Returns {@code true} if the touch event is over the visible area of the bottom sheet. 402 * 403 * By default will check if the touch event is over {@code mContent}, subclasses should override 404 * this method if the visible area of the bottom sheet is different from {@code mContent}. 405 */ isEventOverContent(MotionEvent ev)406 protected boolean isEventOverContent(MotionEvent ev) { 407 return getPopupContainer().isEventOverView(mContent, ev); 408 } 409 isOpeningAnimationRunning()410 private boolean isOpeningAnimationRunning() { 411 return mIsOpen && mOpenCloseAnimation.getAnimationPlayer().isRunning(); 412 } 413 414 /* SingleAxisSwipeDetector.Listener */ 415 416 @Override onDragStart(boolean start, float startDisplacement)417 public void onDragStart(boolean start, float startDisplacement) { 418 if (mOpenCloseAnimation.getAnimationPlayer().isRunning()) { 419 mOpenCloseAnimation.pause(); 420 mDragStartProgress = mOpenCloseAnimation.getProgressFraction(); 421 } else { 422 setUpCloseAnimation(DEFAULT_DURATION); 423 mDragStartProgress = 0; 424 } 425 } 426 427 @Override onDrag(float displacement)428 public boolean onDrag(float displacement) { 429 float progress = mDragStartProgress 430 + Math.signum(mToTranslationShift - mFromTranslationShift) 431 * (displacement / getShiftRange()); 432 mOpenCloseAnimation.setPlayFraction(Utilities.boundToRange(progress, 0, 1)); 433 return true; 434 } 435 436 @Override onDragEnd(float velocity)437 public void onDragEnd(float velocity) { 438 float successfulShiftThreshold = mActivityContext.getDeviceProfile().isTablet 439 ? TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS : SUCCESS_TRANSITION_PROGRESS; 440 if ((mSwipeDetector.isFling(velocity) && velocity > 0) 441 || mTranslationShift > successfulShiftThreshold) { 442 mScrollInterpolator = scrollInterpolatorForVelocity(velocity); 443 mScrollDuration = BaseSwipeDetector.calculateDuration( 444 velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift); 445 mScrollEndProgress = mToTranslationShift == TRANSLATION_SHIFT_OPENED ? 0 : 1; 446 close(true); 447 } else { 448 ValueAnimator animator = mOpenCloseAnimation.getAnimationPlayer(); 449 animator.setInterpolator(Interpolators.DECELERATE); 450 animator.setFloatValues( 451 mOpenCloseAnimation.getProgressFraction(), 452 mToTranslationShift == TRANSLATION_SHIFT_OPENED ? 1 : 0); 453 animator.setDuration(BaseSwipeDetector.calculateDuration(velocity, mTranslationShift)) 454 .start(); 455 } 456 } 457 458 /** Callback invoked when the view is beginning to close (e.g. close animation is started). */ setOnCloseBeginListener(@ullable OnCloseListener onCloseBeginListener)459 public void setOnCloseBeginListener(@Nullable OnCloseListener onCloseBeginListener) { 460 mOnCloseBeginListener = onCloseBeginListener; 461 } 462 463 /** Registers an {@link OnCloseListener}. */ addOnCloseListener(OnCloseListener listener)464 public void addOnCloseListener(OnCloseListener listener) { 465 mOnCloseListeners.add(listener); 466 } 467 handleClose(boolean animate, long defaultDuration)468 protected void handleClose(boolean animate, long defaultDuration) { 469 if (!mIsOpen) { 470 return; 471 } 472 Optional.ofNullable(mOnCloseBeginListener).ifPresent(OnCloseListener::onSlideInViewClosed); 473 474 if (!animate) { 475 mOpenCloseAnimation.pause(); 476 setTranslationShift(TRANSLATION_SHIFT_CLOSED); 477 onCloseComplete(); 478 return; 479 } 480 481 final ValueAnimator animator; 482 if (mSwipeDetector.isIdleState()) { 483 setUpCloseAnimation(defaultDuration); 484 animator = mOpenCloseAnimation.getAnimationPlayer(); 485 animator.setInterpolator(getIdleInterpolator()); 486 } else { 487 animator = mOpenCloseAnimation.getAnimationPlayer(); 488 animator.setInterpolator(mScrollInterpolator); 489 animator.setDuration(mScrollDuration); 490 mOpenCloseAnimation.getAnimationPlayer().setFloatValues( 491 mOpenCloseAnimation.getProgressFraction(), mScrollEndProgress); 492 } 493 494 animator.addListener(AnimatorListeners.forEndCallback(this::onCloseComplete)); 495 animator.start(); 496 } 497 getIdleInterpolator()498 protected Interpolator getIdleInterpolator() { 499 return Interpolators.ACCELERATE; 500 } 501 getScrimInterpolator()502 protected Interpolator getScrimInterpolator() { 503 return LINEAR; 504 } 505 onCloseComplete()506 protected void onCloseComplete() { 507 mIsOpen = false; 508 getPopupContainer().removeView(this); 509 if (mColorScrim != null) { 510 getPopupContainer().removeView(mColorScrim); 511 } 512 mOnCloseListeners.forEach(OnCloseListener::onSlideInViewClosed); 513 } 514 getPopupContainer()515 protected BaseDragLayer getPopupContainer() { 516 return mActivityContext.getDragLayer(); 517 } 518 createColorScrim(Context context, int bgColor)519 protected View createColorScrim(Context context, int bgColor) { 520 View view = new View(context); 521 view.forceHasOverlappingRendering(false); 522 view.setBackgroundColor(bgColor); 523 524 BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(MATCH_PARENT, MATCH_PARENT); 525 lp.ignoreInsets = true; 526 view.setLayoutParams(lp); 527 528 return view; 529 } 530 531 /** 532 * Interface to report that the {@link AbstractSlideInView} has closed. 533 */ 534 public interface OnCloseListener { 535 536 /** 537 * Called when {@link AbstractSlideInView} closes. 538 */ onSlideInViewClosed()539 void onSlideInViewClosed(); 540 } 541 } 542