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