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