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