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.launcher3.LauncherAnimUtils.SCALE_PROPERTY; 21 import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS; 22 import static com.android.launcher3.LauncherAnimUtils.TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS; 23 import static com.android.launcher3.allapps.AllAppsTransitionController.REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS; 24 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; 25 import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE; 26 27 import android.animation.Animator; 28 import android.animation.AnimatorListenerAdapter; 29 import android.animation.ObjectAnimator; 30 import android.animation.PropertyValuesHolder; 31 import android.content.Context; 32 import android.graphics.Canvas; 33 import android.graphics.drawable.Drawable; 34 import android.util.AttributeSet; 35 import android.util.Property; 36 import android.view.MotionEvent; 37 import android.view.View; 38 import android.view.ViewGroup; 39 import android.view.animation.Interpolator; 40 41 import androidx.annotation.FloatRange; 42 import androidx.annotation.Nullable; 43 import androidx.annotation.Px; 44 45 import com.android.launcher3.AbstractFloatingView; 46 import com.android.launcher3.Utilities; 47 import com.android.launcher3.anim.AnimatedFloat; 48 import com.android.launcher3.anim.Interpolators; 49 import com.android.launcher3.touch.BaseSwipeDetector; 50 import com.android.launcher3.touch.SingleAxisSwipeDetector; 51 52 import java.util.ArrayList; 53 import java.util.List; 54 import java.util.Optional; 55 56 /** 57 * Extension of {@link AbstractFloatingView} with common methods for sliding in from bottom. 58 * 59 * @param <T> Type of ActivityContext inflating this view. 60 */ 61 public abstract class AbstractSlideInView<T extends Context & ActivityContext> 62 extends AbstractFloatingView implements SingleAxisSwipeDetector.Listener { 63 64 protected static final Property<AbstractSlideInView, Float> TRANSLATION_SHIFT = 65 new Property<AbstractSlideInView, Float>(Float.class, "translationShift") { 66 67 @Override 68 public Float get(AbstractSlideInView view) { 69 return view.mTranslationShift; 70 } 71 72 @Override 73 public void set(AbstractSlideInView view, Float value) { 74 view.setTranslationShift(value); 75 } 76 }; 77 protected static final float TRANSLATION_SHIFT_CLOSED = 1f; 78 protected static final float TRANSLATION_SHIFT_OPENED = 0f; 79 private static final float VIEW_NO_SCALE = 1f; 80 81 protected final T mActivityContext; 82 83 protected final SingleAxisSwipeDetector mSwipeDetector; 84 protected final ObjectAnimator mOpenCloseAnimator; 85 86 protected ViewGroup mContent; 87 protected final View mColorScrim; 88 protected Interpolator mScrollInterpolator; 89 90 // range [0, 1], 0=> completely open, 1=> completely closed 91 protected float mTranslationShift = TRANSLATION_SHIFT_CLOSED; 92 93 protected boolean mNoIntercept; 94 protected @Nullable OnCloseListener mOnCloseBeginListener; 95 protected List<OnCloseListener> mOnCloseListeners = new ArrayList<>(); 96 97 private final AnimatedFloat mSlideInViewScale = 98 new AnimatedFloat(this::onScaleProgressChanged, VIEW_NO_SCALE); 99 private boolean mIsBackProgressing; 100 @Nullable private Drawable mContentBackground; 101 AbstractSlideInView(Context context, AttributeSet attrs, int defStyleAttr)102 public AbstractSlideInView(Context context, AttributeSet attrs, int defStyleAttr) { 103 super(context, attrs, defStyleAttr); 104 mActivityContext = ActivityContext.lookupContext(context); 105 106 mScrollInterpolator = Interpolators.SCROLL_CUBIC; 107 mSwipeDetector = new SingleAxisSwipeDetector(context, this, 108 SingleAxisSwipeDetector.VERTICAL); 109 110 mOpenCloseAnimator = ObjectAnimator.ofPropertyValuesHolder(this); 111 mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() { 112 @Override 113 public void onAnimationEnd(Animator animation) { 114 mSwipeDetector.finishedScrolling(); 115 announceAccessibilityChanges(); 116 } 117 }); 118 int scrimColor = getScrimColor(context); 119 mColorScrim = scrimColor != -1 ? createColorScrim(context, scrimColor) : null; 120 } 121 setContentBackground(Drawable drawable)122 protected void setContentBackground(Drawable drawable) { 123 mContentBackground = drawable; 124 } 125 attachToContainer()126 protected void attachToContainer() { 127 if (mColorScrim != null) { 128 getPopupContainer().addView(mColorScrim); 129 } 130 getPopupContainer().addView(this); 131 } 132 133 /** 134 * Returns a scrim color for a sliding view. if returned value is -1, no scrim is added. 135 */ getScrimColor(Context context)136 protected int getScrimColor(Context context) { 137 return -1; 138 } 139 140 /** 141 * Returns the range in height that the slide in view can be dragged. 142 */ getShiftRange()143 protected float getShiftRange() { 144 return mContent.getHeight(); 145 } 146 setTranslationShift(float translationShift)147 protected void setTranslationShift(float translationShift) { 148 mTranslationShift = translationShift; 149 mContent.setTranslationY(mTranslationShift * getShiftRange()); 150 if (mColorScrim != null) { 151 mColorScrim.setAlpha(1 - mTranslationShift); 152 } 153 invalidate(); 154 } 155 156 @Override onControllerInterceptTouchEvent(MotionEvent ev)157 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 158 if (mNoIntercept) { 159 return false; 160 } 161 162 int directionsToDetectScroll = mSwipeDetector.isIdleState() 163 ? SingleAxisSwipeDetector.DIRECTION_NEGATIVE : 0; 164 mSwipeDetector.setDetectableScrollConditions( 165 directionsToDetectScroll, false); 166 mSwipeDetector.onTouchEvent(ev); 167 return mSwipeDetector.isDraggingOrSettling() || !isEventOverContent(ev); 168 } 169 170 @Override onControllerTouchEvent(MotionEvent ev)171 public boolean onControllerTouchEvent(MotionEvent ev) { 172 mSwipeDetector.onTouchEvent(ev); 173 if (ev.getAction() == MotionEvent.ACTION_UP && mSwipeDetector.isIdleState() 174 && !isOpeningAnimationRunning()) { 175 // If we got ACTION_UP without ever starting swipe, close the panel. 176 if (!isEventOverContent(ev)) { 177 close(true); 178 } 179 } 180 return true; 181 } 182 183 @Override onBackProgressed(@loatRangefrom = 0.0, to = 1.0) float progress)184 public void onBackProgressed(@FloatRange(from = 0.0, to = 1.0) float progress) { 185 super.onBackProgressed(progress); 186 float deceleratedProgress = 187 Interpolators.PREDICTIVE_BACK_DECELERATED_EASE.getInterpolation(progress); 188 mIsBackProgressing = progress > 0f; 189 mSlideInViewScale.updateValue(PREDICTIVE_BACK_MIN_SCALE 190 + (1 - PREDICTIVE_BACK_MIN_SCALE) * (1 - deceleratedProgress)); 191 } 192 onScaleProgressChanged()193 private void onScaleProgressChanged() { 194 float scaleProgress = mSlideInViewScale.value; 195 SCALE_PROPERTY.set(this, scaleProgress); 196 setClipChildren(!mIsBackProgressing); 197 mContent.setClipChildren(!mIsBackProgressing); 198 invalidate(); 199 } 200 201 @Override onBackInvoked()202 public void onBackInvoked() { 203 super.onBackInvoked(); 204 animateSlideInViewToNoScale(); 205 } 206 207 @Override onBackCancelled()208 public void onBackCancelled() { 209 super.onBackCancelled(); 210 animateSlideInViewToNoScale(); 211 } 212 animateSlideInViewToNoScale()213 protected void animateSlideInViewToNoScale() { 214 mSlideInViewScale.animateToValue(1f) 215 .setDuration(REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS) 216 .start(); 217 } 218 219 @Override dispatchDraw(Canvas canvas)220 protected void dispatchDraw(Canvas canvas) { 221 drawScaledBackground(canvas); 222 super.dispatchDraw(canvas); 223 } 224 225 /** Draw scaled background during predictive back animation. */ drawScaledBackground(Canvas canvas)226 protected void drawScaledBackground(Canvas canvas) { 227 if (mContentBackground == null) { 228 return; 229 } 230 mContentBackground.setBounds( 231 mContent.getLeft(), 232 mContent.getTop() + (int) mContent.getTranslationY(), 233 mContent.getRight(), 234 mContent.getBottom() + (mIsBackProgressing ? getBottomOffsetPx() : 0)); 235 mContentBackground.draw(canvas); 236 } 237 238 /** Return extra space revealed during predictive back animation. */ 239 @Px getBottomOffsetPx()240 protected int getBottomOffsetPx() { 241 final int height = getMeasuredHeight(); 242 return (int) ((height / PREDICTIVE_BACK_MIN_SCALE - height) / 2); 243 } 244 245 /** 246 * Returns {@code true} if the touch event is over the visible area of the bottom sheet. 247 * 248 * By default will check if the touch event is over {@code mContent}, subclasses should override 249 * this method if the visible area of the bottom sheet is different from {@code mContent}. 250 */ isEventOverContent(MotionEvent ev)251 protected boolean isEventOverContent(MotionEvent ev) { 252 return getPopupContainer().isEventOverView(mContent, ev); 253 } 254 isOpeningAnimationRunning()255 private boolean isOpeningAnimationRunning() { 256 return mIsOpen && mOpenCloseAnimator.isRunning(); 257 } 258 259 /* SingleAxisSwipeDetector.Listener */ 260 261 @Override onDragStart(boolean start, float startDisplacement)262 public void onDragStart(boolean start, float startDisplacement) { } 263 264 @Override onDrag(float displacement)265 public boolean onDrag(float displacement) { 266 float range = getShiftRange(); 267 displacement = Utilities.boundToRange(displacement, 0, range); 268 setTranslationShift(displacement / range); 269 return true; 270 } 271 272 @Override onDragEnd(float velocity)273 public void onDragEnd(float velocity) { 274 float successfulShiftThreshold = mActivityContext.getDeviceProfile().isTablet 275 ? TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS : SUCCESS_TRANSITION_PROGRESS; 276 if ((mSwipeDetector.isFling(velocity) && velocity > 0) 277 || mTranslationShift > successfulShiftThreshold) { 278 mScrollInterpolator = scrollInterpolatorForVelocity(velocity); 279 mOpenCloseAnimator.setDuration(BaseSwipeDetector.calculateDuration( 280 velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift)); 281 close(true); 282 } else { 283 mOpenCloseAnimator.setValues(PropertyValuesHolder.ofFloat( 284 TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED)); 285 mOpenCloseAnimator.setDuration( 286 BaseSwipeDetector.calculateDuration(velocity, mTranslationShift)) 287 .setInterpolator(Interpolators.DEACCEL); 288 mOpenCloseAnimator.start(); 289 } 290 } 291 292 /** Callback invoked when the view is beginning to close (e.g. close animation is started). */ setOnCloseBeginListener(@ullable OnCloseListener onCloseBeginListener)293 public void setOnCloseBeginListener(@Nullable OnCloseListener onCloseBeginListener) { 294 mOnCloseBeginListener = onCloseBeginListener; 295 } 296 297 /** Registers an {@link OnCloseListener}. */ addOnCloseListener(OnCloseListener listener)298 public void addOnCloseListener(OnCloseListener listener) { 299 mOnCloseListeners.add(listener); 300 } 301 handleClose(boolean animate, long defaultDuration)302 protected void handleClose(boolean animate, long defaultDuration) { 303 if (!mIsOpen) { 304 return; 305 } 306 Optional.ofNullable(mOnCloseBeginListener).ifPresent(OnCloseListener::onSlideInViewClosed); 307 308 if (!animate) { 309 mOpenCloseAnimator.cancel(); 310 setTranslationShift(TRANSLATION_SHIFT_CLOSED); 311 onCloseComplete(); 312 return; 313 } 314 mOpenCloseAnimator.setValues( 315 PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_CLOSED)); 316 mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() { 317 @Override 318 public void onAnimationEnd(Animator animation) { 319 mOpenCloseAnimator.removeListener(this); 320 onCloseComplete(); 321 } 322 }); 323 if (mSwipeDetector.isIdleState()) { 324 mOpenCloseAnimator 325 .setDuration(defaultDuration) 326 .setInterpolator(getIdleInterpolator()); 327 } else { 328 mOpenCloseAnimator.setInterpolator(mScrollInterpolator); 329 } 330 mOpenCloseAnimator.start(); 331 } 332 getIdleInterpolator()333 protected Interpolator getIdleInterpolator() { 334 return Interpolators.ACCEL; 335 } 336 onCloseComplete()337 protected void onCloseComplete() { 338 mIsOpen = false; 339 getPopupContainer().removeView(this); 340 if (mColorScrim != null) { 341 getPopupContainer().removeView(mColorScrim); 342 } 343 mOnCloseListeners.forEach(OnCloseListener::onSlideInViewClosed); 344 } 345 getPopupContainer()346 protected BaseDragLayer getPopupContainer() { 347 return mActivityContext.getDragLayer(); 348 } 349 createColorScrim(Context context, int bgColor)350 protected View createColorScrim(Context context, int bgColor) { 351 View view = new View(context); 352 view.forceHasOverlappingRendering(false); 353 view.setBackgroundColor(bgColor); 354 355 BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(MATCH_PARENT, MATCH_PARENT); 356 lp.ignoreInsets = true; 357 view.setLayoutParams(lp); 358 359 return view; 360 } 361 362 /** 363 * Interface to report that the {@link AbstractSlideInView} has closed. 364 */ 365 public interface OnCloseListener { 366 367 /** 368 * Called when {@link AbstractSlideInView} closes. 369 */ onSlideInViewClosed()370 void onSlideInViewClosed(); 371 } 372 } 373