1 /* 2 * Copyright (C) 2016 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 package com.android.systemui.statusbar; 18 19 import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN_REVERSE; 20 import static com.android.systemui.statusbar.phone.NotificationIconContainer.IconState.NO_VALUE; 21 import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT; 22 23 import android.content.Context; 24 import android.content.res.Configuration; 25 import android.content.res.Resources; 26 import android.graphics.Rect; 27 import android.os.SystemProperties; 28 import android.util.AttributeSet; 29 import android.util.Log; 30 import android.util.MathUtils; 31 import android.view.DisplayCutout; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.view.ViewTreeObserver; 35 import android.view.WindowInsets; 36 import android.view.accessibility.AccessibilityNodeInfo; 37 38 import com.android.internal.annotations.VisibleForTesting; 39 import com.android.systemui.Dependency; 40 import com.android.systemui.Interpolators; 41 import com.android.systemui.R; 42 import com.android.systemui.plugins.statusbar.StatusBarStateController; 43 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; 44 import com.android.systemui.statusbar.notification.NotificationUtils; 45 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; 46 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 47 import com.android.systemui.statusbar.notification.row.ExpandableView; 48 import com.android.systemui.statusbar.notification.stack.AmbientState; 49 import com.android.systemui.statusbar.notification.stack.AnimationProperties; 50 import com.android.systemui.statusbar.notification.stack.ExpandableViewState; 51 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; 52 import com.android.systemui.statusbar.notification.stack.ViewState; 53 import com.android.systemui.statusbar.phone.KeyguardBypassController; 54 import com.android.systemui.statusbar.phone.NotificationIconContainer; 55 56 import javax.inject.Inject; 57 import javax.inject.Named; 58 59 /** 60 * A notification shelf view that is placed inside the notification scroller. It manages the 61 * overflow icons that don't fit into the regular list anymore. 62 */ 63 public class NotificationShelf extends ActivatableNotificationView implements 64 View.OnLayoutChangeListener, StateListener { 65 66 private static final boolean USE_ANIMATIONS_WHEN_OPENING = 67 SystemProperties.getBoolean("debug.icon_opening_animations", true); 68 private static final boolean ICON_ANMATIONS_WHILE_SCROLLING 69 = SystemProperties.getBoolean("debug.icon_scroll_animations", true); 70 private static final int TAG_CONTINUOUS_CLIPPING = R.id.continuous_clipping_tag; 71 private static final String TAG = "NotificationShelf"; 72 private final KeyguardBypassController mBypassController; 73 74 private NotificationIconContainer mShelfIcons; 75 private int[] mTmp = new int[2]; 76 private boolean mHideBackground; 77 private int mIconAppearTopPadding; 78 private float mHiddenShelfIconSize; 79 private int mStatusBarHeight; 80 private int mStatusBarPaddingStart; 81 private AmbientState mAmbientState; 82 private NotificationStackScrollLayout mHostLayout; 83 private int mMaxLayoutHeight; 84 private int mPaddingBetweenElements; 85 private int mNotGoneIndex; 86 private boolean mHasItemsInStableShelf; 87 private NotificationIconContainer mCollapsedIcons; 88 private int mScrollFastThreshold; 89 private int mIconSize; 90 private int mStatusBarState; 91 private float mMaxShelfEnd; 92 private int mRelativeOffset; 93 private boolean mInteractive; 94 private float mOpenedAmount; 95 private boolean mNoAnimationsInThisFrame; 96 private boolean mAnimationsEnabled = true; 97 private boolean mShowNotificationShelf; 98 private float mFirstElementRoundness; 99 private Rect mClipRect = new Rect(); 100 private int mCutoutHeight; 101 private int mGapHeight; 102 103 @Inject NotificationShelf(@amedVIEW_CONTEXT) Context context, AttributeSet attrs, KeyguardBypassController keyguardBypassController)104 public NotificationShelf(@Named(VIEW_CONTEXT) Context context, 105 AttributeSet attrs, 106 KeyguardBypassController keyguardBypassController) { 107 super(context, attrs); 108 mBypassController = keyguardBypassController; 109 } 110 111 @Override 112 @VisibleForTesting onFinishInflate()113 public void onFinishInflate() { 114 super.onFinishInflate(); 115 mShelfIcons = findViewById(R.id.content); 116 mShelfIcons.setClipChildren(false); 117 mShelfIcons.setClipToPadding(false); 118 119 setClipToActualHeight(false); 120 setClipChildren(false); 121 setClipToPadding(false); 122 mShelfIcons.setIsStaticLayout(false); 123 setBottomRoundness(1.0f, false /* animate */); 124 125 // Setting this to first in section to get the clipping to the top roundness correct. This 126 // value determines the way we are clipping to the top roundness of the overall shade 127 setFirstInSection(true); 128 initDimens(); 129 } 130 131 @Override onAttachedToWindow()132 protected void onAttachedToWindow() { 133 super.onAttachedToWindow(); 134 ((SysuiStatusBarStateController) Dependency.get(StatusBarStateController.class)) 135 .addCallback(this, SysuiStatusBarStateController.RANK_SHELF); 136 } 137 138 @Override onDetachedFromWindow()139 protected void onDetachedFromWindow() { 140 super.onDetachedFromWindow(); 141 Dependency.get(StatusBarStateController.class).removeCallback(this); 142 } 143 bind(AmbientState ambientState, NotificationStackScrollLayout hostLayout)144 public void bind(AmbientState ambientState, NotificationStackScrollLayout hostLayout) { 145 mAmbientState = ambientState; 146 mHostLayout = hostLayout; 147 } 148 initDimens()149 private void initDimens() { 150 Resources res = getResources(); 151 mIconAppearTopPadding = res.getDimensionPixelSize(R.dimen.notification_icon_appear_padding); 152 mStatusBarHeight = res.getDimensionPixelOffset(R.dimen.status_bar_height); 153 mStatusBarPaddingStart = res.getDimensionPixelOffset(R.dimen.status_bar_padding_start); 154 mPaddingBetweenElements = res.getDimensionPixelSize(R.dimen.notification_divider_height); 155 156 ViewGroup.LayoutParams layoutParams = getLayoutParams(); 157 layoutParams.height = res.getDimensionPixelOffset(R.dimen.notification_shelf_height); 158 setLayoutParams(layoutParams); 159 160 int padding = res.getDimensionPixelOffset(R.dimen.shelf_icon_container_padding); 161 mShelfIcons.setPadding(padding, 0, padding, 0); 162 mScrollFastThreshold = res.getDimensionPixelOffset(R.dimen.scroll_fast_threshold); 163 mShowNotificationShelf = res.getBoolean(R.bool.config_showNotificationShelf); 164 mIconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size); 165 mHiddenShelfIconSize = res.getDimensionPixelOffset(R.dimen.hidden_shelf_icon_size); 166 mGapHeight = res.getDimensionPixelSize(R.dimen.qs_notification_padding); 167 168 if (!mShowNotificationShelf) { 169 setVisibility(GONE); 170 } 171 } 172 173 @Override onConfigurationChanged(Configuration newConfig)174 protected void onConfigurationChanged(Configuration newConfig) { 175 super.onConfigurationChanged(newConfig); 176 initDimens(); 177 } 178 179 @Override getContentView()180 protected View getContentView() { 181 return mShelfIcons; 182 } 183 getShelfIcons()184 public NotificationIconContainer getShelfIcons() { 185 return mShelfIcons; 186 } 187 188 @Override createExpandableViewState()189 public ExpandableViewState createExpandableViewState() { 190 return new ShelfState(); 191 } 192 193 /** Update the state of the shelf. */ updateState(AmbientState ambientState)194 public void updateState(AmbientState ambientState) { 195 ExpandableView lastView = ambientState.getLastVisibleBackgroundChild(); 196 ShelfState viewState = (ShelfState) getViewState(); 197 if (mShowNotificationShelf && lastView != null) { 198 float maxShelfEnd = ambientState.getInnerHeight() + ambientState.getTopPadding() 199 + ambientState.getStackTranslation(); 200 ExpandableViewState lastViewState = lastView.getViewState(); 201 float viewEnd = lastViewState.yTranslation + lastViewState.height; 202 viewState.copyFrom(lastViewState); 203 viewState.height = getIntrinsicHeight(); 204 205 viewState.yTranslation = Math.max(Math.min(viewEnd, maxShelfEnd) - viewState.height, 206 getFullyClosedTranslation()); 207 viewState.zTranslation = ambientState.getBaseZHeight(); 208 // For the small display size, it's not enough to make the icon not covered by 209 // the top cutout so the denominator add the height of cutout. 210 // Totally, (getIntrinsicHeight() * 2 + mCutoutHeight) should be smaller then 211 // mAmbientState.getTopPadding(). 212 float openedAmount = (viewState.yTranslation - getFullyClosedTranslation()) 213 / (getIntrinsicHeight() * 2 + mCutoutHeight); 214 openedAmount = Math.min(1.0f, openedAmount); 215 viewState.openedAmount = openedAmount; 216 viewState.clipTopAmount = 0; 217 viewState.alpha = 1; 218 viewState.belowSpeedBump = mAmbientState.getSpeedBumpIndex() == 0; 219 viewState.hideSensitive = false; 220 viewState.xTranslation = getTranslationX(); 221 if (mNotGoneIndex != -1) { 222 viewState.notGoneIndex = Math.min(viewState.notGoneIndex, mNotGoneIndex); 223 } 224 viewState.hasItemsInStableShelf = lastViewState.inShelf; 225 viewState.hidden = !mAmbientState.isShadeExpanded() 226 || mAmbientState.isQsCustomizerShowing(); 227 viewState.maxShelfEnd = maxShelfEnd; 228 } else { 229 viewState.hidden = true; 230 viewState.location = ExpandableViewState.LOCATION_GONE; 231 viewState.hasItemsInStableShelf = false; 232 } 233 } 234 235 /** 236 * Update the shelf appearance based on the other notifications around it. This transforms 237 * the icons from the notification area into the shelf. 238 */ updateAppearance()239 public void updateAppearance() { 240 // If the shelf should not be shown, then there is no need to update anything. 241 if (!mShowNotificationShelf) { 242 return; 243 } 244 245 mShelfIcons.resetViewStates(); 246 float shelfStart = getTranslationY(); 247 float numViewsInShelf = 0.0f; 248 View lastChild = mAmbientState.getLastVisibleBackgroundChild(); 249 mNotGoneIndex = -1; 250 float interpolationStart = mMaxLayoutHeight - getIntrinsicHeight() * 2; 251 float expandAmount = 0.0f; 252 if (shelfStart >= interpolationStart) { 253 expandAmount = (shelfStart - interpolationStart) / getIntrinsicHeight(); 254 expandAmount = Math.min(1.0f, expandAmount); 255 } 256 // find the first view that doesn't overlap with the shelf 257 int notGoneIndex = 0; 258 int colorOfViewBeforeLast = NO_COLOR; 259 boolean backgroundForceHidden = false; 260 if (mHideBackground && !((ShelfState) getViewState()).hasItemsInStableShelf) { 261 backgroundForceHidden = true; 262 } 263 int colorTwoBefore = NO_COLOR; 264 int previousColor = NO_COLOR; 265 float transitionAmount = 0.0f; 266 float currentScrollVelocity = mAmbientState.getCurrentScrollVelocity(); 267 boolean scrollingFast = currentScrollVelocity > mScrollFastThreshold 268 || (mAmbientState.isExpansionChanging() 269 && Math.abs(mAmbientState.getExpandingVelocity()) > mScrollFastThreshold); 270 boolean scrolling = currentScrollVelocity > 0; 271 boolean expandingAnimated = mAmbientState.isExpansionChanging() 272 && !mAmbientState.isPanelTracking(); 273 int baseZHeight = mAmbientState.getBaseZHeight(); 274 int backgroundTop = 0; 275 int clipTopAmount = 0; 276 float firstElementRoundness = 0.0f; 277 ActivatableNotificationView previousAnv = null; 278 279 for (int i = 0; i < mHostLayout.getChildCount(); i++) { 280 ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i); 281 282 if (!child.needsClippingToShelf() || child.getVisibility() == GONE) { 283 continue; 284 } 285 286 float notificationClipEnd; 287 boolean aboveShelf = ViewState.getFinalTranslationZ(child) > baseZHeight 288 || child.isPinned(); 289 boolean isLastChild = child == lastChild; 290 float rowTranslationY = child.getTranslationY(); 291 if ((isLastChild && !child.isInShelf()) || aboveShelf || backgroundForceHidden) { 292 notificationClipEnd = shelfStart + getIntrinsicHeight(); 293 } else { 294 notificationClipEnd = shelfStart - mPaddingBetweenElements; 295 } 296 int clipTop = updateNotificationClipHeight(child, notificationClipEnd, notGoneIndex); 297 clipTopAmount = Math.max(clipTop, clipTopAmount); 298 299 300 float inShelfAmount = updateShelfTransformation(child, expandAmount, scrolling, 301 scrollingFast, expandingAnimated, isLastChild); 302 // If the current row is an ExpandableNotificationRow, update its color, roundedness, 303 // and icon state. 304 if (child instanceof ExpandableNotificationRow) { 305 ExpandableNotificationRow expandableRow = (ExpandableNotificationRow) child; 306 numViewsInShelf += inShelfAmount; 307 int ownColorUntinted = expandableRow.getBackgroundColorWithoutTint(); 308 if (rowTranslationY >= shelfStart && mNotGoneIndex == -1) { 309 mNotGoneIndex = notGoneIndex; 310 setTintColor(previousColor); 311 setOverrideTintColor(colorTwoBefore, transitionAmount); 312 313 } else if (mNotGoneIndex == -1) { 314 colorTwoBefore = previousColor; 315 transitionAmount = inShelfAmount; 316 } 317 // We don't want to modify the color if the notification is hun'd 318 boolean canModifyColor = mAmbientState.isShadeExpanded() 319 && !(mAmbientState.isOnKeyguard() && mBypassController.getBypassEnabled()); 320 if (isLastChild && canModifyColor) { 321 if (colorOfViewBeforeLast == NO_COLOR) { 322 colorOfViewBeforeLast = ownColorUntinted; 323 } 324 expandableRow.setOverrideTintColor(colorOfViewBeforeLast, inShelfAmount); 325 } else { 326 colorOfViewBeforeLast = ownColorUntinted; 327 expandableRow.setOverrideTintColor(NO_COLOR, 0 /* overrideAmount */); 328 } 329 if (notGoneIndex != 0 || !aboveShelf) { 330 expandableRow.setAboveShelf(false); 331 } 332 if (notGoneIndex == 0) { 333 StatusBarIconView icon = expandableRow.getEntry().getIcons().getShelfIcon(); 334 NotificationIconContainer.IconState iconState = getIconState(icon); 335 // The icon state might be null in rare cases where the notification is actually 336 // added to the layout, but not to the shelf. An example are replied messages, 337 // since they don't show up on AOD 338 if (iconState != null && iconState.clampedAppearAmount == 1.0f) { 339 // only if the first icon is fully in the shelf we want to clip to it! 340 backgroundTop = (int) (child.getTranslationY() - getTranslationY()); 341 firstElementRoundness = expandableRow.getCurrentTopRoundness(); 342 } 343 } 344 345 previousColor = ownColorUntinted; 346 notGoneIndex++; 347 } 348 349 if (child instanceof ActivatableNotificationView) { 350 ActivatableNotificationView anv = 351 (ActivatableNotificationView) child; 352 if (anv.isFirstInSection() && previousAnv != null 353 && previousAnv.isLastInSection()) { 354 // If the top of the shelf is between the view before a gap and the view after a 355 // gap then we need to adjust the shelf's top roundness. 356 float distanceToGapBottom = child.getTranslationY() - getTranslationY(); 357 float distanceToGapTop = getTranslationY() 358 - (previousAnv.getTranslationY() + previousAnv.getActualHeight()); 359 if (distanceToGapTop > 0) { 360 // We interpolate our top roundness so that it's fully rounded if we're at 361 // the bottom of the gap, and not rounded at all if we're at the top of the 362 // gap (directly up against the bottom of previousAnv) 363 // Then we apply the same roundness to the bottom of previousAnv so that the 364 // corners join together as the shelf approaches previousAnv. 365 firstElementRoundness = (float) Math.min(1.0, 366 distanceToGapTop / mGapHeight); 367 previousAnv.setBottomRoundness(firstElementRoundness, 368 false /* don't animate */); 369 backgroundTop = (int) distanceToGapBottom; 370 } 371 } 372 previousAnv = anv; 373 } 374 } 375 clipTransientViews(); 376 377 setClipTopAmount(clipTopAmount); 378 boolean isHidden = getViewState().hidden || clipTopAmount >= getIntrinsicHeight(); 379 if (mShowNotificationShelf) { 380 setVisibility(isHidden ? View.INVISIBLE : View.VISIBLE); 381 } 382 setBackgroundTop(backgroundTop); 383 setFirstElementRoundness(firstElementRoundness); 384 mShelfIcons.setSpeedBumpIndex(mAmbientState.getSpeedBumpIndex()); 385 mShelfIcons.calculateIconTranslations(); 386 mShelfIcons.applyIconStates(); 387 for (int i = 0; i < mHostLayout.getChildCount(); i++) { 388 View child = mHostLayout.getChildAt(i); 389 if (!(child instanceof ExpandableNotificationRow) 390 || child.getVisibility() == GONE) { 391 continue; 392 } 393 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 394 updateIconClipAmount(row); 395 updateContinuousClipping(row); 396 } 397 boolean hideBackground = numViewsInShelf < 1.0f; 398 setHideBackground(hideBackground || backgroundForceHidden); 399 if (mNotGoneIndex == -1) { 400 mNotGoneIndex = notGoneIndex; 401 } 402 } 403 404 /** 405 * Clips transient views to the top of the shelf - Transient views are only used for 406 * disappearing views/animations and need to be clipped correctly by the shelf to ensure they 407 * don't show underneath the notification stack when something is animating and the user 408 * swipes quickly. 409 */ 410 private void clipTransientViews() { 411 for (int i = 0; i < mHostLayout.getTransientViewCount(); i++) { 412 View transientView = mHostLayout.getTransientView(i); 413 if (transientView instanceof ExpandableView) { 414 ExpandableView transientExpandableView = (ExpandableView) transientView; 415 updateNotificationClipHeight(transientExpandableView, getTranslationY(), -1); 416 } 417 } 418 } 419 420 private void setFirstElementRoundness(float firstElementRoundness) { 421 if (mFirstElementRoundness != firstElementRoundness) { 422 mFirstElementRoundness = firstElementRoundness; 423 setTopRoundness(firstElementRoundness, false /* animate */); 424 } 425 } 426 427 private void updateIconClipAmount(ExpandableNotificationRow row) { 428 float maxTop = row.getTranslationY(); 429 if (getClipTopAmount() != 0) { 430 // if the shelf is clipped, lets make sure we also clip the icon 431 maxTop = Math.max(maxTop, getTranslationY() + getClipTopAmount()); 432 } 433 StatusBarIconView icon = row.getEntry().getIcons().getShelfIcon(); 434 float shelfIconPosition = getTranslationY() + icon.getTop() + icon.getTranslationY(); 435 if (shelfIconPosition < maxTop && !mAmbientState.isFullyHidden()) { 436 int top = (int) (maxTop - shelfIconPosition); 437 Rect clipRect = new Rect(0, top, icon.getWidth(), Math.max(top, icon.getHeight())); 438 icon.setClipBounds(clipRect); 439 } else { 440 icon.setClipBounds(null); 441 } 442 } 443 444 private void updateContinuousClipping(final ExpandableNotificationRow row) { 445 StatusBarIconView icon = row.getEntry().getIcons().getShelfIcon(); 446 boolean needsContinuousClipping = ViewState.isAnimatingY(icon) && !mAmbientState.isDozing(); 447 boolean isContinuousClipping = icon.getTag(TAG_CONTINUOUS_CLIPPING) != null; 448 if (needsContinuousClipping && !isContinuousClipping) { 449 final ViewTreeObserver observer = icon.getViewTreeObserver(); 450 ViewTreeObserver.OnPreDrawListener predrawListener = 451 new ViewTreeObserver.OnPreDrawListener() { 452 @Override 453 public boolean onPreDraw() { 454 boolean animatingY = ViewState.isAnimatingY(icon); 455 if (!animatingY) { 456 if (observer.isAlive()) { 457 observer.removeOnPreDrawListener(this); 458 } 459 icon.setTag(TAG_CONTINUOUS_CLIPPING, null); 460 return true; 461 } 462 updateIconClipAmount(row); 463 return true; 464 } 465 }; 466 observer.addOnPreDrawListener(predrawListener); 467 icon.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { 468 @Override 469 public void onViewAttachedToWindow(View v) { 470 } 471 472 @Override 473 public void onViewDetachedFromWindow(View v) { 474 if (v == icon) { 475 if (observer.isAlive()) { 476 observer.removeOnPreDrawListener(predrawListener); 477 } 478 icon.setTag(TAG_CONTINUOUS_CLIPPING, null); 479 } 480 } 481 }); 482 icon.setTag(TAG_CONTINUOUS_CLIPPING, predrawListener); 483 } 484 } 485 486 /** 487 * Update the clipping of this view. 488 * @return the amount that our own top should be clipped 489 */ 490 private int updateNotificationClipHeight(ExpandableView view, 491 float notificationClipEnd, int childIndex) { 492 float viewEnd = view.getTranslationY() + view.getActualHeight(); 493 boolean isPinned = (view.isPinned() || view.isHeadsUpAnimatingAway()) 494 && !mAmbientState.isDozingAndNotPulsing(view); 495 boolean shouldClipOwnTop; 496 if (mAmbientState.isPulseExpanding()) { 497 shouldClipOwnTop = childIndex == 0; 498 } else { 499 shouldClipOwnTop = view.showingPulsing(); 500 } 501 if (viewEnd > notificationClipEnd && !shouldClipOwnTop 502 && (mAmbientState.isShadeExpanded() || !isPinned)) { 503 int clipBottomAmount = (int) (viewEnd - notificationClipEnd); 504 if (isPinned) { 505 clipBottomAmount = Math.min(view.getIntrinsicHeight() - view.getCollapsedHeight(), 506 clipBottomAmount); 507 } 508 view.setClipBottomAmount(clipBottomAmount); 509 } else { 510 view.setClipBottomAmount(0); 511 } 512 if (shouldClipOwnTop) { 513 return (int) (viewEnd - getTranslationY()); 514 } else { 515 return 0; 516 } 517 } 518 519 @Override 520 public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd, 521 int outlineTranslation) { 522 if (!mHasItemsInStableShelf) { 523 shadowIntensity = 0.0f; 524 } 525 super.setFakeShadowIntensity(shadowIntensity, outlineAlpha, shadowYEnd, outlineTranslation); 526 } 527 528 /** 529 * @return the amount how much this notification is in the shelf 530 */ 531 private float updateShelfTransformation(ExpandableView view, float expandAmount, 532 boolean scrolling, boolean scrollingFast, boolean expandingAnimated, 533 boolean isLastChild) { 534 StatusBarIconView icon = view.getShelfIcon(); 535 NotificationIconContainer.IconState iconState = getIconState(icon); 536 537 // Let calculate how much the view is in the shelf 538 float viewStart = view.getTranslationY(); 539 int fullHeight = view.getActualHeight() + mPaddingBetweenElements; 540 float iconTransformStart = calculateIconTransformationStart(view); 541 542 float transformDistance = getIntrinsicHeight() * 1.5f; 543 transformDistance *= NotificationUtils.interpolate(1.f, 1.5f, expandAmount); 544 transformDistance = Math.min(transformDistance, fullHeight); 545 546 // Let's make sure the transform distance is 547 // at most to the icon (relevant for conversations) 548 transformDistance = Math.min(viewStart + fullHeight - iconTransformStart, 549 transformDistance); 550 551 if (isLastChild) { 552 fullHeight = Math.min(fullHeight, view.getMinHeight() - getIntrinsicHeight()); 553 transformDistance = Math.min(transformDistance, view.getMinHeight() 554 - getIntrinsicHeight()); 555 } 556 float viewEnd = viewStart + fullHeight; 557 handleCustomTransformHeight(view, expandingAnimated, iconState); 558 559 float fullTransitionAmount; 560 float transitionAmount; 561 float contentTransformationAmount; 562 float shelfStart = getTranslationY(); 563 boolean fullyInOrOut = true; 564 if (viewEnd >= shelfStart && (!mAmbientState.isUnlockHintRunning() || view.isInShelf()) 565 && (mAmbientState.isShadeExpanded() 566 || (!view.isPinned() && !view.isHeadsUpAnimatingAway()))) { 567 if (viewStart < shelfStart) { 568 if (iconState != null && iconState.hasCustomTransformHeight()) { 569 fullHeight = iconState.customTransformHeight; 570 transformDistance = iconState.customTransformHeight; 571 } 572 573 float fullAmount = (shelfStart - viewStart) / fullHeight; 574 fullAmount = Math.min(1.0f, fullAmount); 575 float interpolatedAmount = Interpolators.ACCELERATE_DECELERATE.getInterpolation( 576 fullAmount); 577 interpolatedAmount = NotificationUtils.interpolate( 578 interpolatedAmount, fullAmount, expandAmount); 579 fullTransitionAmount = 1.0f - interpolatedAmount; 580 581 if (isLastChild) { 582 // If it's the last child we should use all of the notification to transform 583 // instead of just to the icon, since that can be quite low. 584 transitionAmount = (shelfStart - viewStart) / transformDistance; 585 } else { 586 transitionAmount = (shelfStart - iconTransformStart) / transformDistance; 587 } 588 transitionAmount = MathUtils.constrain(transitionAmount, 0.0f, 1.0f); 589 transitionAmount = 1.0f - transitionAmount; 590 fullyInOrOut = false; 591 } else { 592 fullTransitionAmount = 1.0f; 593 transitionAmount = 1.0f; 594 } 595 596 // Transforming the content 597 contentTransformationAmount = (shelfStart - viewStart) / transformDistance; 598 contentTransformationAmount = Math.min(1.0f, contentTransformationAmount); 599 contentTransformationAmount = 1.0f - contentTransformationAmount; 600 } else { 601 fullTransitionAmount = 0.0f; 602 transitionAmount = 0.0f; 603 contentTransformationAmount = 0.0f; 604 } 605 if (iconState != null && fullyInOrOut && !expandingAnimated && iconState.isLastExpandIcon) { 606 iconState.isLastExpandIcon = false; 607 iconState.customTransformHeight = NO_VALUE; 608 } 609 610 // Update the content transformation amount 611 if (view.isAboveShelf() || view.showingPulsing() 612 || (!isLastChild && iconState != null && !iconState.translateContent)) { 613 contentTransformationAmount = 0.0f; 614 } 615 view.setContentTransformationAmount(contentTransformationAmount, isLastChild); 616 617 // Update the positioning of the icon 618 updateIconPositioning(view, transitionAmount, fullTransitionAmount, 619 transformDistance, scrolling, scrollingFast, expandingAnimated, isLastChild); 620 621 return fullTransitionAmount; 622 } 623 624 /** 625 * @return the location where the transformation into the shelf should start. 626 */ 627 private float calculateIconTransformationStart(ExpandableView view) { 628 View target = view.getShelfTransformationTarget(); 629 if (target == null) { 630 return view.getTranslationY(); 631 } 632 float start = view.getTranslationY() + view.getRelativeTopPadding(target); 633 634 // Let's not start the transformation right at the icon but by the padding before it. 635 start -= view.getShelfIcon().getTop(); 636 return start; 637 } 638 639 private void handleCustomTransformHeight(ExpandableView view, boolean expandingAnimated, 640 NotificationIconContainer.IconState iconState) { 641 if (iconState != null && expandingAnimated && mAmbientState.getScrollY() == 0 642 && !mAmbientState.isOnKeyguard() && !iconState.isLastExpandIcon) { 643 // We are expanding animated. Because we switch to a linear interpolation in this case, 644 // the last icon may be stuck in between the shelf position and the notification 645 // position, which looks pretty bad. We therefore optimize this case by applying a 646 // shorter transition such that the icon is either fully in the notification or we clamp 647 // it into the shelf if it's close enough. 648 // We need to persist this, since after the expansion, the behavior should still be the 649 // same. 650 float position = mAmbientState.getIntrinsicPadding() 651 + mHostLayout.getPositionInLinearLayout(view); 652 int maxShelfStart = mMaxLayoutHeight - getIntrinsicHeight(); 653 if (position < maxShelfStart && position + view.getIntrinsicHeight() >= maxShelfStart 654 && view.getTranslationY() < position) { 655 iconState.isLastExpandIcon = true; 656 iconState.customTransformHeight = NO_VALUE; 657 // Let's check if we're close enough to snap into the shelf 658 boolean forceInShelf = mMaxLayoutHeight - getIntrinsicHeight() - position 659 < getIntrinsicHeight(); 660 if (!forceInShelf) { 661 // We are overlapping the shelf but not enough, so the icon needs to be 662 // repositioned 663 iconState.customTransformHeight = (int) (mMaxLayoutHeight 664 - getIntrinsicHeight() - position); 665 } 666 } 667 } 668 } 669 670 private void updateIconPositioning(ExpandableView view, float iconTransitionAmount, 671 float fullTransitionAmount, float iconTransformDistance, boolean scrolling, 672 boolean scrollingFast, boolean expandingAnimated, boolean isLastChild) { 673 StatusBarIconView icon = view.getShelfIcon(); 674 NotificationIconContainer.IconState iconState = getIconState(icon); 675 if (iconState == null) { 676 return; 677 } 678 boolean forceInShelf = 679 iconState.isLastExpandIcon && !iconState.hasCustomTransformHeight(); 680 boolean clampInShelf = iconTransitionAmount > 0.5f || isTargetClipped(view); 681 float clampedAmount = clampInShelf ? 1.0f : 0.0f; 682 if (iconTransitionAmount == clampedAmount) { 683 iconState.noAnimations = (scrollingFast || expandingAnimated) && !forceInShelf; 684 iconState.useFullTransitionAmount = iconState.noAnimations 685 || (!ICON_ANMATIONS_WHILE_SCROLLING && iconTransitionAmount == 0.0f 686 && scrolling); 687 iconState.useLinearTransitionAmount = !ICON_ANMATIONS_WHILE_SCROLLING 688 && iconTransitionAmount == 0.0f && !mAmbientState.isExpansionChanging(); 689 iconState.translateContent = mMaxLayoutHeight - getTranslationY() 690 - getIntrinsicHeight() > 0; 691 } 692 if (!forceInShelf && (scrollingFast || (expandingAnimated 693 && iconState.useFullTransitionAmount && !ViewState.isAnimatingY(icon)))) { 694 iconState.cancelAnimations(icon); 695 iconState.useFullTransitionAmount = true; 696 iconState.noAnimations = true; 697 } 698 if (iconState.hasCustomTransformHeight()) { 699 iconState.useFullTransitionAmount = true; 700 } 701 if (iconState.isLastExpandIcon) { 702 iconState.translateContent = false; 703 } 704 float transitionAmount; 705 if (mAmbientState.isHiddenAtAll() && !view.isInShelf()) { 706 transitionAmount = mAmbientState.isFullyHidden() ? 1 : 0; 707 } else if (isLastChild || !USE_ANIMATIONS_WHEN_OPENING 708 || iconState.useFullTransitionAmount 709 || iconState.useLinearTransitionAmount) { 710 transitionAmount = iconTransitionAmount; 711 } else { 712 // We take the clamped position instead 713 transitionAmount = clampedAmount; 714 iconState.needsCannedAnimation = iconState.clampedAppearAmount != clampedAmount 715 && !mNoAnimationsInThisFrame; 716 } 717 iconState.iconAppearAmount = !USE_ANIMATIONS_WHEN_OPENING 718 || iconState.useFullTransitionAmount 719 ? fullTransitionAmount 720 : transitionAmount; 721 iconState.clampedAppearAmount = clampedAmount; 722 setIconTransformationAmount(view, transitionAmount, iconTransformDistance, 723 clampedAmount != transitionAmount, isLastChild); 724 } 725 726 private boolean isTargetClipped(ExpandableView view) { 727 View target = view.getShelfTransformationTarget(); 728 if (target == null) { 729 return false; 730 } 731 // We should never clip the target, let's instead put it into the shelf! 732 float endOfTarget = view.getTranslationY() 733 + view.getContentTranslation() 734 + view.getRelativeTopPadding(target) 735 + target.getHeight(); 736 737 return endOfTarget >= getTranslationY() - mPaddingBetweenElements; 738 } 739 740 private void setIconTransformationAmount(ExpandableView view, 741 float transitionAmount, float iconTransformDistance, boolean usingLinearInterpolation, 742 boolean isLastChild) { 743 if (!(view instanceof ExpandableNotificationRow)) { 744 return; 745 } 746 ExpandableNotificationRow row = (ExpandableNotificationRow) view; 747 748 StatusBarIconView icon = row.getShelfIcon(); 749 NotificationIconContainer.IconState iconState = getIconState(icon); 750 View rowIcon = row.getShelfTransformationTarget(); 751 752 // Let's resolve the relative positions of the icons 753 float notificationIconSize = 0.0f; 754 int iconTopPadding; 755 int iconStartPadding; 756 if (rowIcon != null) { 757 iconTopPadding = row.getRelativeTopPadding(rowIcon); 758 iconStartPadding = row.getRelativeStartPadding(rowIcon); 759 notificationIconSize = rowIcon.getHeight(); 760 } else { 761 iconTopPadding = mIconAppearTopPadding; 762 iconStartPadding = 0; 763 } 764 765 float shelfIconSize = mAmbientState.isFullyHidden() ? mHiddenShelfIconSize : mIconSize; 766 shelfIconSize = shelfIconSize * icon.getIconScale(); 767 768 // Get the icon correctly positioned in Y 769 float notificationIconPositionY = row.getTranslationY() + row.getContentTranslation(); 770 float targetYPosition = 0; 771 boolean stayingInShelf = row.isInShelf() && !row.isTransformingIntoShelf(); 772 if (usingLinearInterpolation && !stayingInShelf) { 773 // If we interpolate from the notification position, this might lead to a slightly 774 // odd interpolation, since the notification position changes as well. 775 // Let's instead interpolate directly to the top left of the notification 776 targetYPosition = NotificationUtils.interpolate( 777 Math.min(notificationIconPositionY + mIconAppearTopPadding 778 - getTranslationY(), 0), 779 0, 780 transitionAmount); 781 } 782 notificationIconPositionY += iconTopPadding; 783 float shelfIconPositionY = getTranslationY() + icon.getTop(); 784 shelfIconPositionY += (icon.getHeight() - shelfIconSize) / 2.0f; 785 float iconYTranslation = NotificationUtils.interpolate( 786 notificationIconPositionY - shelfIconPositionY, 787 targetYPosition, 788 transitionAmount); 789 790 // Get the icon correctly positioned in X 791 // Even in RTL it's the left, since we're inverting the location in post 792 float shelfIconPositionX = icon.getLeft(); 793 shelfIconPositionX += (1.0f - icon.getIconScale()) * icon.getWidth() / 2.0f; 794 float iconXTranslation = NotificationUtils.interpolate( 795 iconStartPadding - shelfIconPositionX, 796 mShelfIcons.getActualPaddingStart(), 797 transitionAmount); 798 799 // Let's handle the case that there's no Icon 800 float alpha = 1.0f; 801 boolean noIcon = !row.isShowingIcon(); 802 if (noIcon) { 803 // The view currently doesn't have an icon, lets transform it in! 804 alpha = transitionAmount; 805 notificationIconSize = shelfIconSize / 2.0f; 806 iconXTranslation = mShelfIcons.getActualPaddingStart(); 807 } 808 // The notification size is different from the size in the shelf / statusbar 809 float newSize = NotificationUtils.interpolate(notificationIconSize, shelfIconSize, 810 transitionAmount); 811 if (iconState != null) { 812 iconState.scaleX = newSize / shelfIconSize; 813 iconState.scaleY = iconState.scaleX; 814 iconState.hidden = transitionAmount == 0.0f && !iconState.isAnimating(icon); 815 boolean isAppearing = row.isDrawingAppearAnimation() && !row.isInShelf(); 816 if (isAppearing) { 817 iconState.hidden = true; 818 iconState.iconAppearAmount = 0.0f; 819 } 820 iconState.alpha = alpha; 821 iconState.yTranslation = iconYTranslation; 822 iconState.xTranslation = iconXTranslation; 823 if (stayingInShelf) { 824 iconState.iconAppearAmount = 1.0f; 825 iconState.alpha = 1.0f; 826 iconState.scaleX = 1.0f; 827 iconState.scaleY = 1.0f; 828 iconState.hidden = false; 829 } 830 if (row.isAboveShelf() 831 || row.showingPulsing() 832 || (!row.isInShelf() && (isLastChild && row.areGutsExposed() 833 || row.getTranslationZ() > mAmbientState.getBaseZHeight()))) { 834 iconState.hidden = true; 835 } 836 int backgroundColor = getBackgroundColorWithoutTint(); 837 int shelfColor = icon.getContrastedStaticDrawableColor(backgroundColor); 838 if (!noIcon && shelfColor != StatusBarIconView.NO_COLOR) { 839 int iconColor = row.getOriginalIconColor(); 840 shelfColor = NotificationUtils.interpolateColors(iconColor, shelfColor, 841 iconState.iconAppearAmount); 842 } 843 iconState.iconColor = shelfColor; 844 } 845 } 846 getIconState(StatusBarIconView icon)847 private NotificationIconContainer.IconState getIconState(StatusBarIconView icon) { 848 return mShelfIcons.getIconState(icon); 849 } 850 getFullyClosedTranslation()851 private float getFullyClosedTranslation() { 852 return - (getIntrinsicHeight() - mStatusBarHeight) / 2; 853 } 854 getNotificationMergeSize()855 public int getNotificationMergeSize() { 856 return getIntrinsicHeight(); 857 } 858 859 @Override hasNoContentHeight()860 public boolean hasNoContentHeight() { 861 return true; 862 } 863 setHideBackground(boolean hideBackground)864 private void setHideBackground(boolean hideBackground) { 865 if (mHideBackground != hideBackground) { 866 mHideBackground = hideBackground; 867 updateBackground(); 868 updateOutline(); 869 } 870 } 871 872 @Override needsOutline()873 protected boolean needsOutline() { 874 return !mHideBackground && super.needsOutline(); 875 } 876 877 @Override shouldHideBackground()878 protected boolean shouldHideBackground() { 879 return super.shouldHideBackground() || mHideBackground; 880 } 881 882 @Override onLayout(boolean changed, int left, int top, int right, int bottom)883 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 884 super.onLayout(changed, left, top, right, bottom); 885 updateRelativeOffset(); 886 887 // we always want to clip to our sides, such that nothing can draw outside of these bounds 888 int height = getResources().getDisplayMetrics().heightPixels; 889 mClipRect.set(0, -height, getWidth(), height); 890 mShelfIcons.setClipBounds(mClipRect); 891 } 892 updateRelativeOffset()893 private void updateRelativeOffset() { 894 mCollapsedIcons.getLocationOnScreen(mTmp); 895 mRelativeOffset = mTmp[0]; 896 getLocationOnScreen(mTmp); 897 mRelativeOffset -= mTmp[0]; 898 } 899 900 @Override onApplyWindowInsets(WindowInsets insets)901 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 902 WindowInsets ret = super.onApplyWindowInsets(insets); 903 904 // NotificationShelf drag from the status bar and the status bar dock on the top 905 // of the display for current design so just focus on the top of ScreenDecorations. 906 // In landscape or multiple window split mode, the NotificationShelf still drag from 907 // the top and the physical notch/cutout goes to the right, left, or both side of the 908 // display so it doesn't matter for the NotificationSelf in landscape. 909 DisplayCutout displayCutout = insets.getDisplayCutout(); 910 mCutoutHeight = displayCutout == null || displayCutout.getSafeInsetTop() < 0 911 ? 0 : displayCutout.getSafeInsetTop(); 912 913 return ret; 914 } 915 916 private void setOpenedAmount(float openedAmount) { 917 mNoAnimationsInThisFrame = openedAmount == 1.0f && mOpenedAmount == 0.0f; 918 mOpenedAmount = openedAmount; 919 if (!mAmbientState.isPanelFullWidth() || mAmbientState.isDozing()) { 920 // We don't do a transformation at all, lets just assume we are fully opened 921 openedAmount = 1.0f; 922 } 923 int start = mRelativeOffset; 924 if (isLayoutRtl()) { 925 start = getWidth() - start - mCollapsedIcons.getWidth(); 926 } 927 int width = (int) NotificationUtils.interpolate( 928 start + mCollapsedIcons.getFinalTranslationX(), 929 mShelfIcons.getWidth(), 930 FAST_OUT_SLOW_IN_REVERSE.getInterpolation(openedAmount)); 931 mShelfIcons.setActualLayoutWidth(width); 932 boolean hasOverflow = mCollapsedIcons.hasOverflow(); 933 int collapsedPadding = mCollapsedIcons.getPaddingEnd(); 934 if (!hasOverflow) { 935 // we have to ensure that adding the low priority notification won't lead to an 936 // overflow 937 collapsedPadding -= mCollapsedIcons.getNoOverflowExtraPadding(); 938 } else { 939 // Partial overflow padding will fill enough space to add extra dots 940 collapsedPadding -= mCollapsedIcons.getPartialOverflowExtraPadding(); 941 } 942 float padding = NotificationUtils.interpolate(collapsedPadding, 943 mShelfIcons.getPaddingEnd(), 944 openedAmount); 945 mShelfIcons.setActualPaddingEnd(padding); 946 float paddingStart = NotificationUtils.interpolate(start, 947 mShelfIcons.getPaddingStart(), openedAmount); 948 mShelfIcons.setActualPaddingStart(paddingStart); 949 mShelfIcons.setOpenedAmount(openedAmount); 950 } 951 952 public void setMaxLayoutHeight(int maxLayoutHeight) { 953 mMaxLayoutHeight = maxLayoutHeight; 954 } 955 956 /** 957 * @return the index of the notification at which the shelf visually resides 958 */ 959 public int getNotGoneIndex() { 960 return mNotGoneIndex; 961 } 962 963 private void setHasItemsInStableShelf(boolean hasItemsInStableShelf) { 964 if (mHasItemsInStableShelf != hasItemsInStableShelf) { 965 mHasItemsInStableShelf = hasItemsInStableShelf; 966 updateInteractiveness(); 967 } 968 } 969 970 /** 971 * @return whether the shelf has any icons in it when a potential animation has finished, i.e 972 * if the current state would be applied right now 973 */ 974 public boolean hasItemsInStableShelf() { 975 return mHasItemsInStableShelf; 976 } 977 978 public void setCollapsedIcons(NotificationIconContainer collapsedIcons) { 979 mCollapsedIcons = collapsedIcons; 980 mCollapsedIcons.addOnLayoutChangeListener(this); 981 } 982 983 @Override 984 public void onStateChanged(int newState) { 985 mStatusBarState = newState; 986 updateInteractiveness(); 987 } 988 989 private void updateInteractiveness() { 990 mInteractive = mStatusBarState == StatusBarState.KEYGUARD && mHasItemsInStableShelf; 991 setClickable(mInteractive); 992 setFocusable(mInteractive); 993 setImportantForAccessibility(mInteractive ? View.IMPORTANT_FOR_ACCESSIBILITY_YES 994 : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); 995 } 996 997 @Override 998 protected boolean isInteractive() { 999 return mInteractive; 1000 } 1001 1002 public void setMaxShelfEnd(float maxShelfEnd) { 1003 mMaxShelfEnd = maxShelfEnd; 1004 } 1005 1006 public void setAnimationsEnabled(boolean enabled) { 1007 mAnimationsEnabled = enabled; 1008 if (!enabled) { 1009 // we need to wait with enabling the animations until the first frame has passed 1010 mShelfIcons.setAnimationsEnabled(false); 1011 } 1012 } 1013 1014 @Override 1015 public boolean hasOverlappingRendering() { 1016 return false; // Shelf only uses alpha for transitions where the difference can't be seen. 1017 } 1018 1019 @Override 1020 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1021 super.onInitializeAccessibilityNodeInfo(info); 1022 if (mInteractive) { 1023 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND); 1024 AccessibilityNodeInfo.AccessibilityAction unlock 1025 = new AccessibilityNodeInfo.AccessibilityAction( 1026 AccessibilityNodeInfo.ACTION_CLICK, 1027 getContext().getString(R.string.accessibility_overflow_action)); 1028 info.addAction(unlock); 1029 } 1030 } 1031 1032 @Override 1033 public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, 1034 int oldTop, int oldRight, int oldBottom) { 1035 updateRelativeOffset(); 1036 } 1037 1038 @Override 1039 public boolean needsClippingToShelf() { 1040 return false; 1041 } 1042 1043 public void onUiModeChanged() { 1044 updateBackgroundColors(); 1045 } 1046 1047 private class ShelfState extends ExpandableViewState { 1048 private float openedAmount; 1049 private boolean hasItemsInStableShelf; 1050 private float maxShelfEnd; 1051 1052 @Override 1053 public void applyToView(View view) { 1054 if (!mShowNotificationShelf) { 1055 return; 1056 } 1057 1058 super.applyToView(view); 1059 setMaxShelfEnd(maxShelfEnd); 1060 setOpenedAmount(openedAmount); 1061 updateAppearance(); 1062 setHasItemsInStableShelf(hasItemsInStableShelf); 1063 mShelfIcons.setAnimationsEnabled(mAnimationsEnabled); 1064 } 1065 1066 @Override 1067 public void animateTo(View child, AnimationProperties properties) { 1068 if (!mShowNotificationShelf) { 1069 return; 1070 } 1071 1072 super.animateTo(child, properties); 1073 setMaxShelfEnd(maxShelfEnd); 1074 setOpenedAmount(openedAmount); 1075 updateAppearance(); 1076 setHasItemsInStableShelf(hasItemsInStableShelf); 1077 mShelfIcons.setAnimationsEnabled(mAnimationsEnabled); 1078 } 1079 } 1080 } 1081