1 /* 2 * Copyright (C) 2012 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 17 18 package com.android.systemui; 19 20 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_ROW_EXPAND; 21 22 import android.content.Context; 23 import android.util.FloatProperty; 24 import android.util.Log; 25 import android.view.Gravity; 26 import android.view.HapticFeedbackConstants; 27 import android.view.MotionEvent; 28 import android.view.ScaleGestureDetector; 29 import android.view.ScaleGestureDetector.OnScaleGestureListener; 30 import android.view.VelocityTracker; 31 import android.view.View; 32 import android.view.ViewConfiguration; 33 34 import androidx.annotation.NonNull; 35 import androidx.core.animation.Animator; 36 import androidx.core.animation.AnimatorListenerAdapter; 37 import androidx.core.animation.ObjectAnimator; 38 39 import com.android.internal.annotations.VisibleForTesting; 40 import com.android.internal.jank.InteractionJankMonitor; 41 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 42 import com.android.systemui.statusbar.notification.row.ExpandableView; 43 import com.android.systemui.statusbar.policy.ScrollAdapter; 44 import com.android.wm.shell.animation.FlingAnimationUtils; 45 46 public class ExpandHelper implements Gefingerpoken { 47 public interface Callback { getChildAtRawPosition(float x, float y)48 ExpandableView getChildAtRawPosition(float x, float y); getChildAtPosition(float x, float y)49 ExpandableView getChildAtPosition(float x, float y); canChildBeExpanded(View v)50 boolean canChildBeExpanded(View v); setUserExpandedChild(View v, boolean userExpanded)51 void setUserExpandedChild(View v, boolean userExpanded); setUserLockedChild(View v, boolean userLocked)52 void setUserLockedChild(View v, boolean userLocked); expansionStateChanged(boolean isExpanding)53 void expansionStateChanged(boolean isExpanding); getMaxExpandHeight(ExpandableView view)54 int getMaxExpandHeight(ExpandableView view); setExpansionCancelled(View view)55 void setExpansionCancelled(View view); 56 } 57 58 private static final String TAG = "ExpandHelper"; 59 protected static final boolean DEBUG = false; 60 protected static final boolean DEBUG_SCALE = false; 61 private static final float EXPAND_DURATION = 0.3f; 62 63 // Set to false to disable focus-based gestures (spread-finger vertical pull). 64 private static final boolean USE_DRAG = true; 65 // Set to false to disable scale-based gestures (both horizontal and vertical). 66 private static final boolean USE_SPAN = true; 67 // Both gestures types may be active at the same time. 68 // At least one gesture type should be active. 69 // A variant of the screwdriver gesture will emerge from either gesture type. 70 71 // amount of overstretch for maximum brightness expressed in U 72 // 2f: maximum brightness is stretching a 1U to 3U, or a 4U to 6U 73 private static final float STRETCH_INTERVAL = 2f; 74 75 private static final FloatProperty<ViewScaler> VIEW_SCALER_HEIGHT_PROPERTY = 76 new FloatProperty<ViewScaler>("ViewScalerHeight") { 77 @Override 78 public void setValue(ViewScaler object, float value) { 79 object.setHeight(value); 80 } 81 82 @Override 83 public Float get(ViewScaler object) { 84 return object.getHeight(); 85 } 86 }; 87 88 @SuppressWarnings("unused") 89 private Context mContext; 90 91 private boolean mExpanding; 92 private static final int NONE = 0; 93 private static final int BLINDS = 1<<0; 94 private static final int PULL = 1<<1; 95 private static final int STRETCH = 1<<2; 96 private int mExpansionStyle = NONE; 97 private boolean mWatchingForPull; 98 private boolean mHasPopped; 99 private View mEventSource; 100 private float mOldHeight; 101 private float mNaturalHeight; 102 private float mInitialTouchFocusY; 103 private float mInitialTouchX; 104 private float mInitialTouchY; 105 private float mInitialTouchSpan; 106 private float mLastFocusY; 107 private float mLastSpanY; 108 private final int mTouchSlop; 109 private final float mSlopMultiplier; 110 private float mLastMotionY; 111 private float mPullGestureMinXSpan; 112 private Callback mCallback; 113 private ScaleGestureDetector mSGD; 114 private ViewScaler mScaler; 115 private ObjectAnimator mScaleAnimation; 116 private boolean mEnabled = true; 117 private ExpandableView mResizedView; 118 private float mCurrentHeight; 119 120 private int mSmallSize; 121 private int mLargeSize; 122 private float mMaximumStretch; 123 private boolean mOnlyMovements; 124 125 private int mGravity; 126 127 private ScrollAdapter mScrollAdapter; 128 private FlingAnimationUtils mFlingAnimationUtils; 129 private VelocityTracker mVelocityTracker; 130 131 private OnScaleGestureListener mScaleGestureListener 132 = new ScaleGestureDetector.SimpleOnScaleGestureListener() { 133 @Override 134 public boolean onScaleBegin(ScaleGestureDetector detector) { 135 if (DEBUG_SCALE) Log.v(TAG, "onscalebegin()"); 136 137 if (!mOnlyMovements) { 138 startExpanding(mResizedView, STRETCH); 139 } 140 return mExpanding; 141 } 142 143 @Override 144 public boolean onScale(ScaleGestureDetector detector) { 145 if (DEBUG_SCALE) Log.v(TAG, "onscale() on " + mResizedView); 146 return true; 147 } 148 149 @Override 150 public void onScaleEnd(ScaleGestureDetector detector) { 151 } 152 }; 153 154 @VisibleForTesting getScaleAnimation()155 ObjectAnimator getScaleAnimation() { 156 return mScaleAnimation; 157 } 158 159 private class ViewScaler { 160 ExpandableView mView; 161 ViewScaler()162 public ViewScaler() {} setView(ExpandableView v)163 public void setView(ExpandableView v) { 164 mView = v; 165 } 166 setHeight(float h)167 public void setHeight(float h) { 168 if (DEBUG_SCALE) Log.v(TAG, "SetHeight: setting to " + h); 169 mView.setActualHeight((int) h); 170 mCurrentHeight = h; 171 } getHeight()172 public float getHeight() { 173 return mView.getActualHeight(); 174 } getNaturalHeight()175 public int getNaturalHeight() { 176 return mCallback.getMaxExpandHeight(mView); 177 } 178 } 179 180 /** 181 * Handle expansion gestures to expand and contract children of the callback. 182 * 183 * @param context application context 184 * @param callback the container that holds the items to be manipulated 185 * @param small the smallest allowable size for the manipulated items. 186 * @param large the largest allowable size for the manipulated items. 187 */ ExpandHelper(Context context, Callback callback, int small, int large)188 public ExpandHelper(Context context, Callback callback, int small, int large) { 189 mSmallSize = small; 190 mMaximumStretch = mSmallSize * STRETCH_INTERVAL; 191 mLargeSize = large; 192 mContext = context; 193 mCallback = callback; 194 mScaler = new ViewScaler(); 195 mGravity = Gravity.TOP; 196 mScaleAnimation = ObjectAnimator.ofFloat(mScaler, VIEW_SCALER_HEIGHT_PROPERTY, 0f); 197 mPullGestureMinXSpan = mContext.getResources().getDimension(R.dimen.pull_span_min); 198 199 final ViewConfiguration configuration = ViewConfiguration.get(mContext); 200 mTouchSlop = configuration.getScaledTouchSlop(); 201 mSlopMultiplier = configuration.getAmbiguousGestureMultiplier(); 202 203 mSGD = new ScaleGestureDetector(context, mScaleGestureListener); 204 mFlingAnimationUtils = new FlingAnimationUtils(mContext.getResources().getDisplayMetrics(), 205 EXPAND_DURATION); 206 } 207 208 @VisibleForTesting updateExpansion()209 void updateExpansion() { 210 if (DEBUG_SCALE) Log.v(TAG, "updateExpansion()"); 211 // are we scaling or dragging? 212 float span = mSGD.getCurrentSpan() - mInitialTouchSpan; 213 span *= USE_SPAN ? 1f : 0f; 214 float drag = mSGD.getFocusY() - mInitialTouchFocusY; 215 drag *= USE_DRAG ? 1f : 0f; 216 drag *= mGravity == Gravity.BOTTOM ? -1f : 1f; 217 float pull = Math.abs(drag) + Math.abs(span) + 1f; 218 float hand = drag * Math.abs(drag) / pull + span * Math.abs(span) / pull; 219 float target = hand + mOldHeight; 220 float newHeight = clamp(target); 221 mScaler.setHeight(newHeight); 222 mLastFocusY = mSGD.getFocusY(); 223 mLastSpanY = mSGD.getCurrentSpan(); 224 } 225 clamp(float target)226 private float clamp(float target) { 227 float out = target; 228 out = out < mSmallSize ? mSmallSize : out; 229 out = out > mNaturalHeight ? mNaturalHeight : out; 230 return out; 231 } 232 findView(float x, float y)233 private ExpandableView findView(float x, float y) { 234 ExpandableView v; 235 if (mEventSource != null) { 236 int[] location = new int[2]; 237 mEventSource.getLocationOnScreen(location); 238 x += location[0]; 239 y += location[1]; 240 v = mCallback.getChildAtRawPosition(x, y); 241 } else { 242 v = mCallback.getChildAtPosition(x, y); 243 } 244 return v; 245 } 246 isInside(View v, float x, float y)247 private boolean isInside(View v, float x, float y) { 248 if (DEBUG) Log.d(TAG, "isinside (" + x + ", " + y + ")"); 249 250 if (v == null) { 251 if (DEBUG) Log.d(TAG, "isinside null subject"); 252 return false; 253 } 254 if (mEventSource != null) { 255 int[] location = new int[2]; 256 mEventSource.getLocationOnScreen(location); 257 x += location[0]; 258 y += location[1]; 259 if (DEBUG) Log.d(TAG, " to global (" + x + ", " + y + ")"); 260 } 261 int[] location = new int[2]; 262 v.getLocationOnScreen(location); 263 x -= location[0]; 264 y -= location[1]; 265 if (DEBUG) Log.d(TAG, " to local (" + x + ", " + y + ")"); 266 if (DEBUG) Log.d(TAG, " inside (" + v.getWidth() + ", " + v.getHeight() + ")"); 267 boolean inside = (x > 0f && y > 0f && x < v.getWidth() & y < v.getHeight()); 268 return inside; 269 } 270 setEventSource(View eventSource)271 public void setEventSource(View eventSource) { 272 mEventSource = eventSource; 273 } 274 setGravity(int gravity)275 public void setGravity(int gravity) { 276 mGravity = gravity; 277 } 278 setScrollAdapter(ScrollAdapter adapter)279 public void setScrollAdapter(ScrollAdapter adapter) { 280 mScrollAdapter = adapter; 281 } 282 getTouchSlop(MotionEvent event)283 private float getTouchSlop(MotionEvent event) { 284 // Adjust the touch slop if another gesture may be being performed. 285 return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE 286 ? mTouchSlop * mSlopMultiplier 287 : mTouchSlop; 288 } 289 290 @Override onInterceptTouchEvent(MotionEvent ev)291 public boolean onInterceptTouchEvent(MotionEvent ev) { 292 if (!isEnabled()) { 293 return false; 294 } 295 trackVelocity(ev); 296 final int action = ev.getAction(); 297 if (DEBUG_SCALE) Log.d(TAG, "intercept: act=" + MotionEvent.actionToString(action) + 298 " expanding=" + mExpanding + 299 (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") + 300 (0 != (mExpansionStyle & PULL) ? " (pull)" : "") + 301 (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : "")); 302 // check for a spread-finger vertical pull gesture 303 mSGD.onTouchEvent(ev); 304 final int x = (int) mSGD.getFocusX(); 305 final int y = (int) mSGD.getFocusY(); 306 307 mInitialTouchFocusY = y; 308 mInitialTouchSpan = mSGD.getCurrentSpan(); 309 mLastFocusY = mInitialTouchFocusY; 310 mLastSpanY = mInitialTouchSpan; 311 if (DEBUG_SCALE) Log.d(TAG, "set initial span: " + mInitialTouchSpan); 312 313 if (mExpanding) { 314 mLastMotionY = ev.getRawY(); 315 maybeRecycleVelocityTracker(ev); 316 return true; 317 } else { 318 if ((action == MotionEvent.ACTION_MOVE) && 0 != (mExpansionStyle & BLINDS)) { 319 // we've begun Venetian blinds style expansion 320 return true; 321 } 322 switch (action & MotionEvent.ACTION_MASK) { 323 case MotionEvent.ACTION_MOVE: { 324 final float xspan = mSGD.getCurrentSpanX(); 325 if (xspan > mPullGestureMinXSpan && 326 xspan > mSGD.getCurrentSpanY() && !mExpanding) { 327 // detect a vertical pulling gesture with fingers somewhat separated 328 if (DEBUG_SCALE) Log.v(TAG, "got pull gesture (xspan=" + xspan + "px)"); 329 startExpanding(mResizedView, PULL); 330 mWatchingForPull = false; 331 } 332 if (mWatchingForPull) { 333 final float yDiff = ev.getRawY() - mInitialTouchY; 334 final float xDiff = ev.getRawX() - mInitialTouchX; 335 if (yDiff > getTouchSlop(ev) && yDiff > Math.abs(xDiff)) { 336 if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)"); 337 mWatchingForPull = false; 338 if (mResizedView != null && !isFullyExpanded(mResizedView)) { 339 if (startExpanding(mResizedView, BLINDS)) { 340 mLastMotionY = ev.getRawY(); 341 mInitialTouchY = ev.getRawY(); 342 mHasPopped = false; 343 } 344 } 345 } 346 } 347 break; 348 } 349 350 case MotionEvent.ACTION_DOWN: 351 mWatchingForPull = mScrollAdapter != null && 352 isInside(mScrollAdapter.getHostView(), x, y) 353 && mScrollAdapter.isScrolledToTop(); 354 mResizedView = findView(x, y); 355 if (mResizedView != null && !mCallback.canChildBeExpanded(mResizedView)) { 356 mResizedView = null; 357 mWatchingForPull = false; 358 } 359 mInitialTouchY = ev.getRawY(); 360 mInitialTouchX = ev.getRawX(); 361 break; 362 363 case MotionEvent.ACTION_CANCEL: 364 case MotionEvent.ACTION_UP: 365 if (DEBUG) Log.d(TAG, "up/cancel"); 366 finishExpanding(ev.getActionMasked() == MotionEvent.ACTION_CANCEL /* forceAbort */, 367 getCurrentVelocity()); 368 clearView(); 369 break; 370 } 371 mLastMotionY = ev.getRawY(); 372 maybeRecycleVelocityTracker(ev); 373 return mExpanding; 374 } 375 } 376 trackVelocity(MotionEvent event)377 private void trackVelocity(MotionEvent event) { 378 int action = event.getActionMasked(); 379 switch(action) { 380 case MotionEvent.ACTION_DOWN: 381 if (mVelocityTracker == null) { 382 mVelocityTracker = VelocityTracker.obtain(); 383 } else { 384 mVelocityTracker.clear(); 385 } 386 mVelocityTracker.addMovement(event); 387 break; 388 case MotionEvent.ACTION_MOVE: 389 if (mVelocityTracker == null) { 390 mVelocityTracker = VelocityTracker.obtain(); 391 } 392 mVelocityTracker.addMovement(event); 393 break; 394 default: 395 break; 396 } 397 } 398 maybeRecycleVelocityTracker(MotionEvent event)399 private void maybeRecycleVelocityTracker(MotionEvent event) { 400 if (mVelocityTracker != null && (event.getActionMasked() == MotionEvent.ACTION_CANCEL 401 || event.getActionMasked() == MotionEvent.ACTION_UP)) { 402 mVelocityTracker.recycle(); 403 mVelocityTracker = null; 404 } 405 } 406 getCurrentVelocity()407 private float getCurrentVelocity() { 408 if (mVelocityTracker != null) { 409 mVelocityTracker.computeCurrentVelocity(1000); 410 return mVelocityTracker.getYVelocity(); 411 } else { 412 return 0f; 413 } 414 } 415 setEnabled(boolean enable)416 public void setEnabled(boolean enable) { 417 mEnabled = enable; 418 } 419 isEnabled()420 private boolean isEnabled() { 421 return mEnabled; 422 } 423 isFullyExpanded(ExpandableView underFocus)424 private boolean isFullyExpanded(ExpandableView underFocus) { 425 return underFocus.getIntrinsicHeight() == underFocus.getMaxContentHeight() 426 && (!underFocus.isSummaryWithChildren() || underFocus.areChildrenExpanded()); 427 } 428 429 @Override onTouchEvent(MotionEvent ev)430 public boolean onTouchEvent(MotionEvent ev) { 431 if (!isEnabled() && !mExpanding) { 432 // In case we're expanding we still want to finish the current motion. 433 return false; 434 } 435 trackVelocity(ev); 436 final int action = ev.getActionMasked(); 437 if (DEBUG_SCALE) Log.d(TAG, "touch: act=" + MotionEvent.actionToString(action) + 438 " expanding=" + mExpanding + 439 (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") + 440 (0 != (mExpansionStyle & PULL) ? " (pull)" : "") + 441 (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : "")); 442 443 mSGD.onTouchEvent(ev); 444 final int x = (int) mSGD.getFocusX(); 445 final int y = (int) mSGD.getFocusY(); 446 447 if (mOnlyMovements) { 448 mLastMotionY = ev.getRawY(); 449 return false; 450 } 451 switch (action) { 452 case MotionEvent.ACTION_DOWN: 453 mWatchingForPull = mScrollAdapter != null && 454 isInside(mScrollAdapter.getHostView(), x, y); 455 mResizedView = findView(x, y); 456 mInitialTouchX = ev.getRawX(); 457 mInitialTouchY = ev.getRawY(); 458 break; 459 case MotionEvent.ACTION_MOVE: { 460 if (mWatchingForPull) { 461 final float yDiff = ev.getRawY() - mInitialTouchY; 462 final float xDiff = ev.getRawX() - mInitialTouchX; 463 if (yDiff > getTouchSlop(ev) && yDiff > Math.abs(xDiff)) { 464 if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)"); 465 mWatchingForPull = false; 466 if (mResizedView != null && !isFullyExpanded(mResizedView)) { 467 if (startExpanding(mResizedView, BLINDS)) { 468 mInitialTouchY = ev.getRawY(); 469 mLastMotionY = ev.getRawY(); 470 mHasPopped = false; 471 } 472 } 473 } 474 } 475 if (mExpanding && 0 != (mExpansionStyle & BLINDS)) { 476 final float rawHeight = ev.getRawY() - mLastMotionY + mCurrentHeight; 477 final float newHeight = clamp(rawHeight); 478 boolean isFinished = false; 479 boolean expanded = false; 480 if (rawHeight > mNaturalHeight) { 481 isFinished = true; 482 expanded = true; 483 } 484 if (rawHeight < mSmallSize) { 485 isFinished = true; 486 expanded = false; 487 } 488 489 if (!mHasPopped) { 490 if (mEventSource != null) { 491 mEventSource.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); 492 } 493 mHasPopped = true; 494 } 495 496 mScaler.setHeight(newHeight); 497 mLastMotionY = ev.getRawY(); 498 if (isFinished) { 499 mCallback.expansionStateChanged(false); 500 } else { 501 mCallback.expansionStateChanged(true); 502 } 503 return true; 504 } 505 506 if (mExpanding) { 507 508 // Gestural expansion is running 509 updateExpansion(); 510 mLastMotionY = ev.getRawY(); 511 return true; 512 } 513 514 break; 515 } 516 517 case MotionEvent.ACTION_POINTER_UP: 518 case MotionEvent.ACTION_POINTER_DOWN: 519 if (DEBUG) Log.d(TAG, "pointer change"); 520 mInitialTouchY += mSGD.getFocusY() - mLastFocusY; 521 mInitialTouchSpan += mSGD.getCurrentSpan() - mLastSpanY; 522 break; 523 524 case MotionEvent.ACTION_UP: 525 case MotionEvent.ACTION_CANCEL: 526 if (DEBUG) Log.d(TAG, "up/cancel"); 527 finishExpanding(!isEnabled() || ev.getActionMasked() == MotionEvent.ACTION_CANCEL, 528 getCurrentVelocity()); 529 clearView(); 530 break; 531 } 532 mLastMotionY = ev.getRawY(); 533 maybeRecycleVelocityTracker(ev); 534 return mResizedView != null; 535 } 536 537 /** 538 * @return True if the view is expandable, false otherwise. 539 */ 540 @VisibleForTesting startExpanding(ExpandableView v, int expandType)541 boolean startExpanding(ExpandableView v, int expandType) { 542 if (!(v instanceof ExpandableNotificationRow)) { 543 return false; 544 } 545 mExpansionStyle = expandType; 546 if (mExpanding && v == mResizedView) { 547 return true; 548 } 549 mExpanding = true; 550 mCallback.expansionStateChanged(true); 551 if (DEBUG) Log.d(TAG, "scale type " + expandType + " beginning on view: " + v); 552 mCallback.setUserLockedChild(v, true); 553 mScaler.setView(v); 554 mOldHeight = mScaler.getHeight(); 555 mCurrentHeight = mOldHeight; 556 boolean canBeExpanded = mCallback.canChildBeExpanded(v); 557 if (canBeExpanded) { 558 if (DEBUG) Log.d(TAG, "working on an expandable child"); 559 mNaturalHeight = mScaler.getNaturalHeight(); 560 mSmallSize = v.getCollapsedHeight(); 561 } else { 562 if (DEBUG) Log.d(TAG, "working on a non-expandable child"); 563 mNaturalHeight = mOldHeight; 564 } 565 if (DEBUG) Log.d(TAG, "got mOldHeight: " + mOldHeight + 566 " mNaturalHeight: " + mNaturalHeight); 567 InteractionJankMonitor.getInstance().begin(v, CUJ_NOTIFICATION_SHADE_ROW_EXPAND); 568 return true; 569 } 570 571 /** 572 * Finish the current expand motion 573 * @param forceAbort whether the expansion should be forcefully aborted and returned to the old 574 * state 575 * @param velocity the velocity this was expanded/ collapsed with 576 */ 577 @VisibleForTesting finishExpanding(boolean forceAbort, float velocity)578 void finishExpanding(boolean forceAbort, float velocity) { 579 finishExpanding(forceAbort, velocity, true /* allowAnimation */); 580 } 581 582 /** 583 * Finish the current expand motion 584 * @param forceAbort whether the expansion should be forcefully aborted and returned to the old 585 * state 586 * @param velocity the velocity this was expanded/ collapsed with 587 */ finishExpanding(boolean forceAbort, float velocity, boolean allowAnimation)588 private void finishExpanding(boolean forceAbort, float velocity, boolean allowAnimation) { 589 if (!mExpanding) return; 590 591 if (DEBUG) Log.d(TAG, "scale in finishing on view: " + mResizedView); 592 593 float currentHeight = mScaler.getHeight(); 594 final boolean wasClosed = (mOldHeight == mSmallSize); 595 boolean nowExpanded; 596 if (!forceAbort) { 597 if (wasClosed) { 598 nowExpanded = currentHeight > mOldHeight && velocity >= 0; 599 } else { 600 nowExpanded = currentHeight >= mOldHeight || velocity > 0; 601 } 602 nowExpanded |= mNaturalHeight == mSmallSize; 603 } else { 604 nowExpanded = !wasClosed; 605 } 606 if (mScaleAnimation.isRunning()) { 607 mScaleAnimation.cancel(); 608 } 609 mCallback.expansionStateChanged(false); 610 int naturalHeight = mScaler.getNaturalHeight(); 611 float targetHeight = nowExpanded ? naturalHeight : mSmallSize; 612 if (targetHeight != currentHeight && mEnabled && allowAnimation) { 613 mScaleAnimation.setFloatValues(targetHeight); 614 mScaleAnimation.setupStartValues(); 615 final View scaledView = mResizedView; 616 final boolean expand = nowExpanded; 617 mScaleAnimation.addListener(new AnimatorListenerAdapter() { 618 public boolean mCancelled; 619 620 @Override 621 public void onAnimationEnd(@NonNull Animator animation) { 622 if (!mCancelled) { 623 mCallback.setUserExpandedChild(scaledView, expand); 624 if (!mExpanding) { 625 mScaler.setView(null); 626 } 627 } else { 628 mCallback.setExpansionCancelled(scaledView); 629 } 630 mCallback.setUserLockedChild(scaledView, false); 631 mScaleAnimation.removeListener(this); 632 if (wasClosed) { 633 InteractionJankMonitor.getInstance().end(CUJ_NOTIFICATION_SHADE_ROW_EXPAND); 634 } 635 } 636 637 @Override 638 public void onAnimationCancel(@NonNull Animator animation) { 639 mCancelled = true; 640 } 641 }); 642 velocity = nowExpanded == velocity >= 0 ? velocity : 0; 643 mFlingAnimationUtils.apply(mScaleAnimation, currentHeight, targetHeight, velocity); 644 mScaleAnimation.start(); 645 } else { 646 if (targetHeight != currentHeight) { 647 mScaler.setHeight(targetHeight); 648 } 649 mCallback.setUserExpandedChild(mResizedView, nowExpanded); 650 mCallback.setUserLockedChild(mResizedView, false); 651 mScaler.setView(null); 652 if (wasClosed) { 653 InteractionJankMonitor.getInstance().end(CUJ_NOTIFICATION_SHADE_ROW_EXPAND); 654 } 655 } 656 657 mExpanding = false; 658 mExpansionStyle = NONE; 659 660 if (DEBUG) Log.d(TAG, "wasClosed is: " + wasClosed); 661 if (DEBUG) Log.d(TAG, "currentHeight is: " + currentHeight); 662 if (DEBUG) Log.d(TAG, "mSmallSize is: " + mSmallSize); 663 if (DEBUG) Log.d(TAG, "targetHeight is: " + targetHeight); 664 if (DEBUG) Log.d(TAG, "scale was finished on view: " + mResizedView); 665 } 666 clearView()667 private void clearView() { 668 mResizedView = null; 669 } 670 671 /** 672 * Use this to abort any pending expansions in progress and force that there will be no 673 * animations. 674 */ cancelImmediately()675 public void cancelImmediately() { 676 cancel(false /* allowAnimation */); 677 } 678 679 /** 680 * Use this to abort any pending expansions in progress. 681 */ cancel()682 public void cancel() { 683 cancel(true /* allowAnimation */); 684 } 685 cancel(boolean allowAnimation)686 private void cancel(boolean allowAnimation) { 687 finishExpanding(true /* forceAbort */, 0f /* velocity */, allowAnimation); 688 clearView(); 689 690 // reset the gesture detector 691 mSGD = new ScaleGestureDetector(mContext, mScaleGestureListener); 692 } 693 694 /** 695 * Change the expansion mode to only observe movements and don't perform any resizing. 696 * This is needed when the expanding is finished and the scroller kicks in, 697 * performing an overscroll motion. We only want to shrink it again when we are not 698 * overscrolled. 699 * 700 * @param onlyMovements Should only movements be observed? 701 */ onlyObserveMovements(boolean onlyMovements)702 public void onlyObserveMovements(boolean onlyMovements) { 703 mOnlyMovements = onlyMovements; 704 } 705 } 706 707