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