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 float deceleratedProgress = Interpolators.BACK_GESTURE.getInterpolation(progress); 302 mSwipeToDismissProgress.updateValue(deceleratedProgress); 303 } 304 305 /** 306 * During predictive back swipe, the default behavior is to scale {@link AbstractSlideInView} 307 * during back swipe. This method allow subclass to scale {@link #mContent}, typically to exit 308 * search mode. 309 * 310 * <p>Note that this method can be expensive, and should only be called from 311 * {@link #onBackStarted(BackEvent)}, not from {@link #onBackProgressed(BackEvent)}. 312 */ shouldAnimateContentViewInBackSwipe()313 protected boolean shouldAnimateContentViewInBackSwipe() { 314 return false; 315 } 316 onUserSwipeToDismissProgressChanged()317 protected void onUserSwipeToDismissProgressChanged() { 318 float progress = mSwipeToDismissProgress.value; 319 mIsDismissInProgress = progress > 0f; 320 321 float scale = PREDICTIVE_BACK_MIN_SCALE + (1 - PREDICTIVE_BACK_MIN_SCALE) * (1f - progress); 322 SCALE_PROPERTY.set(mViewToAnimateInSwipeToDismiss, scale); 323 setClipChildren(!mIsDismissInProgress); 324 setClipToPadding(!mIsDismissInProgress); 325 mContent.setClipChildren(!mIsDismissInProgress); 326 mContent.setClipToPadding(!mIsDismissInProgress); 327 invalidate(); 328 } 329 330 @Override onBackCancelled()331 public void onBackCancelled() { 332 super.onBackCancelled(); 333 animateSwipeToDismissProgressToStart(); 334 } 335 animateSwipeToDismissProgressToStart()336 protected void animateSwipeToDismissProgressToStart() { 337 ObjectAnimator objectAnimator = mSwipeToDismissProgress.animateToValue(0f) 338 .setDuration(REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS); 339 340 // If we are animating a different view, we should reset the animating view back to 341 // AbstractSlideInView as it is the default view to animate. 342 if (this != mViewToAnimateInSwipeToDismiss) { 343 objectAnimator.addListener(new Animator.AnimatorListener() { 344 @Override 345 public void onAnimationCancel(Animator animator) { 346 mViewToAnimateInSwipeToDismiss = AbstractSlideInView.this; 347 } 348 349 @Override 350 public void onAnimationEnd(Animator animator) { 351 mViewToAnimateInSwipeToDismiss = AbstractSlideInView.this; 352 } 353 354 @Override 355 public void onAnimationRepeat(Animator animator) {} 356 357 @Override 358 public void onAnimationStart(Animator animator) {} 359 }); 360 } 361 362 objectAnimator.start(); 363 } 364 365 @Override dispatchDraw(Canvas canvas)366 protected void dispatchDraw(Canvas canvas) { 367 drawScaledBackground(canvas); 368 super.dispatchDraw(canvas); 369 } 370 371 /** 372 * Set slide in view's background {@link Drawable} which will be draw onto a parent view in 373 * {@link #dispatchDraw(Canvas)} 374 */ setContentBackgroundWithParent( @onNull Drawable drawable, @NonNull View parentView)375 protected void setContentBackgroundWithParent( 376 @NonNull Drawable drawable, @NonNull View parentView) { 377 mContentBackground = drawable; 378 mContentBackgroundParentView = parentView; 379 } 380 381 /** Draw scaled background during predictive back animation. */ drawScaledBackground(Canvas canvas)382 private void drawScaledBackground(Canvas canvas) { 383 if (mContentBackground == null || mContentBackgroundParentView == null) { 384 return; 385 } 386 mContentBackground.setBounds( 387 mContentBackgroundParentView.getLeft(), 388 mContentBackgroundParentView.getTop() + (int) mContent.getTranslationY(), 389 mContentBackgroundParentView.getRight(), 390 mContentBackgroundParentView.getBottom() 391 + (mIsDismissInProgress ? getBottomOffsetPx() : 0)); 392 mContentBackground.draw(canvas); 393 } 394 395 /** Return extra space revealed during predictive back animation. */ 396 @Px getBottomOffsetPx()397 protected int getBottomOffsetPx() { 398 return (int) (getMeasuredHeight() * (1 - PREDICTIVE_BACK_MIN_SCALE)); 399 } 400 401 /** 402 * Returns {@code true} if the touch event is over the visible area of the bottom sheet. 403 * 404 * By default will check if the touch event is over {@code mContent}, subclasses should override 405 * this method if the visible area of the bottom sheet is different from {@code mContent}. 406 */ isEventOverContent(MotionEvent ev)407 protected boolean isEventOverContent(MotionEvent ev) { 408 return getPopupContainer().isEventOverView(mContent, ev); 409 } 410 isOpeningAnimationRunning()411 private boolean isOpeningAnimationRunning() { 412 return mIsOpen && mOpenCloseAnimation.getAnimationPlayer().isRunning(); 413 } 414 415 /* SingleAxisSwipeDetector.Listener */ 416 417 @Override onDragStart(boolean start, float startDisplacement)418 public void onDragStart(boolean start, float startDisplacement) { 419 if (mOpenCloseAnimation.getAnimationPlayer().isRunning()) { 420 mOpenCloseAnimation.pause(); 421 mDragStartProgress = mOpenCloseAnimation.getProgressFraction(); 422 } else { 423 setUpCloseAnimation(DEFAULT_DURATION); 424 mDragStartProgress = 0; 425 } 426 } 427 428 @Override onDrag(float displacement)429 public boolean onDrag(float displacement) { 430 float progress = mDragStartProgress 431 + Math.signum(mToTranslationShift - mFromTranslationShift) 432 * (displacement / getShiftRange()); 433 mOpenCloseAnimation.setPlayFraction(Utilities.boundToRange(progress, 0, 1)); 434 return true; 435 } 436 437 @Override onDragEnd(float velocity)438 public void onDragEnd(float velocity) { 439 float successfulShiftThreshold = mActivityContext.getDeviceProfile().isTablet 440 ? TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS : SUCCESS_TRANSITION_PROGRESS; 441 if ((mSwipeDetector.isFling(velocity) && velocity > 0) 442 || mTranslationShift > successfulShiftThreshold) { 443 mScrollInterpolator = scrollInterpolatorForVelocity(velocity); 444 mScrollDuration = BaseSwipeDetector.calculateDuration( 445 velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift); 446 mScrollEndProgress = mToTranslationShift == TRANSLATION_SHIFT_OPENED ? 0 : 1; 447 close(true); 448 } else { 449 ValueAnimator animator = mOpenCloseAnimation.getAnimationPlayer(); 450 animator.setInterpolator(Interpolators.DECELERATE); 451 animator.setFloatValues( 452 mOpenCloseAnimation.getProgressFraction(), 453 mToTranslationShift == TRANSLATION_SHIFT_OPENED ? 1 : 0); 454 animator.setDuration(BaseSwipeDetector.calculateDuration(velocity, mTranslationShift)) 455 .start(); 456 } 457 } 458 459 /** Callback invoked when the view is beginning to close (e.g. close animation is started). */ setOnCloseBeginListener(@ullable OnCloseListener onCloseBeginListener)460 public void setOnCloseBeginListener(@Nullable OnCloseListener onCloseBeginListener) { 461 mOnCloseBeginListener = onCloseBeginListener; 462 } 463 464 /** Registers an {@link OnCloseListener}. */ addOnCloseListener(OnCloseListener listener)465 public void addOnCloseListener(OnCloseListener listener) { 466 mOnCloseListeners.add(listener); 467 } 468 handleClose(boolean animate, long defaultDuration)469 protected void handleClose(boolean animate, long defaultDuration) { 470 if (!mIsOpen) { 471 return; 472 } 473 Optional.ofNullable(mOnCloseBeginListener).ifPresent(OnCloseListener::onSlideInViewClosed); 474 475 if (!animate) { 476 mOpenCloseAnimation.pause(); 477 setTranslationShift(TRANSLATION_SHIFT_CLOSED); 478 onCloseComplete(); 479 return; 480 } 481 482 final ValueAnimator animator; 483 if (mSwipeDetector.isIdleState()) { 484 setUpCloseAnimation(defaultDuration); 485 animator = mOpenCloseAnimation.getAnimationPlayer(); 486 animator.setInterpolator(getIdleInterpolator()); 487 } else { 488 animator = mOpenCloseAnimation.getAnimationPlayer(); 489 animator.setInterpolator(mScrollInterpolator); 490 animator.setDuration(mScrollDuration); 491 mOpenCloseAnimation.getAnimationPlayer().setFloatValues( 492 mOpenCloseAnimation.getProgressFraction(), mScrollEndProgress); 493 } 494 495 animator.addListener(AnimatorListeners.forEndCallback(this::onCloseComplete)); 496 animator.start(); 497 } 498 getIdleInterpolator()499 protected Interpolator getIdleInterpolator() { 500 return Interpolators.ACCELERATE; 501 } 502 getScrimInterpolator()503 protected Interpolator getScrimInterpolator() { 504 return LINEAR; 505 } 506 onCloseComplete()507 protected void onCloseComplete() { 508 mIsOpen = false; 509 getPopupContainer().removeView(this); 510 if (mColorScrim != null) { 511 getPopupContainer().removeView(mColorScrim); 512 } 513 mOnCloseListeners.forEach(OnCloseListener::onSlideInViewClosed); 514 } 515 getPopupContainer()516 protected BaseDragLayer getPopupContainer() { 517 return mActivityContext.getDragLayer(); 518 } 519 createColorScrim(Context context, int bgColor)520 protected View createColorScrim(Context context, int bgColor) { 521 View view = new View(context); 522 view.forceHasOverlappingRendering(false); 523 view.setBackgroundColor(bgColor); 524 525 BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(MATCH_PARENT, MATCH_PARENT); 526 lp.ignoreInsets = true; 527 view.setLayoutParams(lp); 528 529 return view; 530 } 531 532 /** 533 * Interface to report that the {@link AbstractSlideInView} has closed. 534 */ 535 public interface OnCloseListener { 536 537 /** 538 * Called when {@link AbstractSlideInView} closes. 539 */ onSlideInViewClosed()540 void onSlideInViewClosed(); 541 } 542 } 543