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 android.content.Context; 20 import android.content.res.Configuration; 21 import android.content.res.Resources; 22 import android.graphics.Rect; 23 import android.util.AttributeSet; 24 import android.util.MathUtils; 25 import android.view.View; 26 import android.view.ViewGroup; 27 import android.view.ViewTreeObserver; 28 import android.view.accessibility.AccessibilityNodeInfo; 29 import android.view.animation.Interpolator; 30 import android.view.animation.PathInterpolator; 31 32 import com.android.internal.annotations.VisibleForTesting; 33 import com.android.internal.policy.SystemBarUtils; 34 import com.android.systemui.R; 35 import com.android.systemui.animation.ShadeInterpolation; 36 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; 37 import com.android.systemui.statusbar.notification.NotificationUtils; 38 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; 39 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 40 import com.android.systemui.statusbar.notification.row.ExpandableView; 41 import com.android.systemui.statusbar.notification.stack.AmbientState; 42 import com.android.systemui.statusbar.notification.stack.AnimationProperties; 43 import com.android.systemui.statusbar.notification.stack.ExpandableViewState; 44 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; 45 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm; 46 import com.android.systemui.statusbar.notification.stack.ViewState; 47 import com.android.systemui.statusbar.phone.NotificationIconContainer; 48 49 /** 50 * A notification shelf view that is placed inside the notification scroller. It manages the 51 * overflow icons that don't fit into the regular list anymore. 52 */ 53 public class NotificationShelf extends ActivatableNotificationView implements 54 View.OnLayoutChangeListener, StateListener { 55 56 private static final int TAG_CONTINUOUS_CLIPPING = R.id.continuous_clipping_tag; 57 private static final String TAG = "NotificationShelf"; 58 59 // More extreme version of SLOW_OUT_LINEAR_IN which keeps the icon nearly invisible until after 60 // the next icon has translated out of the way, to avoid overlapping. 61 private static final Interpolator ICON_ALPHA_INTERPOLATOR = 62 new PathInterpolator(0.6f, 0f, 0.6f, 0f); 63 64 private NotificationIconContainer mShelfIcons; 65 private int[] mTmp = new int[2]; 66 private boolean mHideBackground; 67 private int mStatusBarHeight; 68 private AmbientState mAmbientState; 69 private NotificationStackScrollLayoutController mHostLayoutController; 70 private int mPaddingBetweenElements; 71 private int mNotGoneIndex; 72 private boolean mHasItemsInStableShelf; 73 private NotificationIconContainer mCollapsedIcons; 74 private int mScrollFastThreshold; 75 private int mStatusBarState; 76 private boolean mInteractive; 77 private boolean mAnimationsEnabled = true; 78 private boolean mShowNotificationShelf; 79 private float mFirstElementRoundness; 80 private Rect mClipRect = new Rect(); 81 private int mIndexOfFirstViewInShelf = -1; 82 private float mCornerAnimationDistance; 83 private NotificationShelfController mController; 84 NotificationShelf(Context context, AttributeSet attrs)85 public NotificationShelf(Context context, AttributeSet attrs) { 86 super(context, attrs); 87 } 88 89 @Override 90 @VisibleForTesting onFinishInflate()91 public void onFinishInflate() { 92 super.onFinishInflate(); 93 mShelfIcons = findViewById(R.id.content); 94 mShelfIcons.setClipChildren(false); 95 mShelfIcons.setClipToPadding(false); 96 97 setClipToActualHeight(false); 98 setClipChildren(false); 99 setClipToPadding(false); 100 mShelfIcons.setIsStaticLayout(false); 101 setBottomRoundness(1.0f, false /* animate */); 102 setTopRoundness(1f, false /* animate */); 103 104 // Setting this to first in section to get the clipping to the top roundness correct. This 105 // value determines the way we are clipping to the top roundness of the overall shade 106 setFirstInSection(true); 107 initDimens(); 108 } 109 bind(AmbientState ambientState, NotificationStackScrollLayoutController hostLayoutController)110 public void bind(AmbientState ambientState, 111 NotificationStackScrollLayoutController hostLayoutController) { 112 mAmbientState = ambientState; 113 mHostLayoutController = hostLayoutController; 114 } 115 initDimens()116 private void initDimens() { 117 Resources res = getResources(); 118 mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext); 119 mPaddingBetweenElements = res.getDimensionPixelSize(R.dimen.notification_divider_height); 120 121 ViewGroup.LayoutParams layoutParams = getLayoutParams(); 122 layoutParams.height = res.getDimensionPixelOffset(R.dimen.notification_shelf_height); 123 setLayoutParams(layoutParams); 124 125 int padding = res.getDimensionPixelOffset(R.dimen.shelf_icon_container_padding); 126 mShelfIcons.setPadding(padding, 0, padding, 0); 127 mScrollFastThreshold = res.getDimensionPixelOffset(R.dimen.scroll_fast_threshold); 128 mShowNotificationShelf = res.getBoolean(R.bool.config_showNotificationShelf); 129 mCornerAnimationDistance = res.getDimensionPixelSize( 130 R.dimen.notification_corner_animation_distance); 131 132 mShelfIcons.setInNotificationIconShelf(true); 133 if (!mShowNotificationShelf) { 134 setVisibility(GONE); 135 } 136 } 137 138 @Override onConfigurationChanged(Configuration newConfig)139 protected void onConfigurationChanged(Configuration newConfig) { 140 super.onConfigurationChanged(newConfig); 141 initDimens(); 142 } 143 144 @Override getContentView()145 protected View getContentView() { 146 return mShelfIcons; 147 } 148 getShelfIcons()149 public NotificationIconContainer getShelfIcons() { 150 return mShelfIcons; 151 } 152 153 @Override createExpandableViewState()154 public ExpandableViewState createExpandableViewState() { 155 return new ShelfState(); 156 } 157 158 /** Update the state of the shelf. */ updateState(StackScrollAlgorithm.StackScrollAlgorithmState algorithmState, AmbientState ambientState)159 public void updateState(StackScrollAlgorithm.StackScrollAlgorithmState algorithmState, 160 AmbientState ambientState) { 161 ExpandableView lastView = ambientState.getLastVisibleBackgroundChild(); 162 ShelfState viewState = (ShelfState) getViewState(); 163 if (mShowNotificationShelf && lastView != null) { 164 ExpandableViewState lastViewState = lastView.getViewState(); 165 viewState.copyFrom(lastViewState); 166 167 viewState.height = getIntrinsicHeight(); 168 viewState.zTranslation = ambientState.getBaseZHeight(); 169 viewState.clipTopAmount = 0; 170 171 if (ambientState.isExpansionChanging() && !ambientState.isOnKeyguard()) { 172 float expansion = ambientState.getExpansionFraction(); 173 viewState.alpha = ShadeInterpolation.getContentAlpha(expansion); 174 } else { 175 viewState.alpha = 1f - ambientState.getHideAmount(); 176 } 177 viewState.belowSpeedBump = mHostLayoutController.getSpeedBumpIndex() == 0; 178 viewState.hideSensitive = false; 179 viewState.xTranslation = getTranslationX(); 180 viewState.hasItemsInStableShelf = lastViewState.inShelf; 181 viewState.firstViewInShelf = algorithmState.firstViewInShelf; 182 if (mNotGoneIndex != -1) { 183 viewState.notGoneIndex = Math.min(viewState.notGoneIndex, mNotGoneIndex); 184 } 185 186 viewState.hidden = !mAmbientState.isShadeExpanded() 187 || algorithmState.firstViewInShelf == null; 188 189 final int indexOfFirstViewInShelf = algorithmState.visibleChildren.indexOf( 190 algorithmState.firstViewInShelf); 191 192 if (mAmbientState.isExpansionChanging() 193 && algorithmState.firstViewInShelf != null 194 && indexOfFirstViewInShelf > 0) { 195 196 // Show shelf if section before it is showing. 197 final ExpandableView viewBeforeShelf = algorithmState.visibleChildren.get( 198 indexOfFirstViewInShelf - 1); 199 if (viewBeforeShelf.getViewState().hidden) { 200 viewState.hidden = true; 201 } 202 } 203 204 final float stackEnd = ambientState.getStackY() + ambientState.getStackHeight(); 205 viewState.yTranslation = stackEnd - viewState.height; 206 } else { 207 viewState.hidden = true; 208 viewState.location = ExpandableViewState.LOCATION_GONE; 209 viewState.hasItemsInStableShelf = false; 210 } 211 } 212 213 /** 214 * Update the shelf appearance based on the other notifications around it. This transforms 215 * the icons from the notification area into the shelf. 216 */ updateAppearance()217 public void updateAppearance() { 218 // If the shelf should not be shown, then there is no need to update anything. 219 if (!mShowNotificationShelf) { 220 return; 221 } 222 mShelfIcons.resetViewStates(); 223 float shelfStart = getTranslationY(); 224 float numViewsInShelf = 0.0f; 225 View lastChild = mAmbientState.getLastVisibleBackgroundChild(); 226 mNotGoneIndex = -1; 227 // find the first view that doesn't overlap with the shelf 228 int notGoneIndex = 0; 229 int colorOfViewBeforeLast = NO_COLOR; 230 boolean backgroundForceHidden = false; 231 if (mHideBackground && !((ShelfState) getViewState()).hasItemsInStableShelf) { 232 backgroundForceHidden = true; 233 } 234 int colorTwoBefore = NO_COLOR; 235 int previousColor = NO_COLOR; 236 float transitionAmount = 0.0f; 237 float currentScrollVelocity = mAmbientState.getCurrentScrollVelocity(); 238 boolean scrollingFast = currentScrollVelocity > mScrollFastThreshold 239 || (mAmbientState.isExpansionChanging() 240 && Math.abs(mAmbientState.getExpandingVelocity()) > mScrollFastThreshold); 241 boolean expandingAnimated = mAmbientState.isExpansionChanging() 242 && !mAmbientState.isPanelTracking(); 243 int baseZHeight = mAmbientState.getBaseZHeight(); 244 int backgroundTop = 0; 245 int clipTopAmount = 0; 246 float firstElementRoundness = 0.0f; 247 248 for (int i = 0; i < mHostLayoutController.getChildCount(); i++) { 249 ExpandableView child = mHostLayoutController.getChildAt(i); 250 if (!child.needsClippingToShelf() || child.getVisibility() == GONE) { 251 continue; 252 } 253 float notificationClipEnd; 254 boolean aboveShelf = ViewState.getFinalTranslationZ(child) > baseZHeight 255 || child.isPinned(); 256 boolean isLastChild = child == lastChild; 257 final float viewStart = child.getTranslationY(); 258 259 final float inShelfAmount = updateShelfTransformation(i, child, scrollingFast, 260 expandingAnimated, isLastChild); 261 262 // TODO(b/172289889) scale mPaddingBetweenElements with expansion amount 263 if ((isLastChild && !child.isInShelf()) || aboveShelf || backgroundForceHidden) { 264 notificationClipEnd = shelfStart + getIntrinsicHeight(); 265 } else { 266 notificationClipEnd = shelfStart - mPaddingBetweenElements; 267 } 268 int clipTop = updateNotificationClipHeight(child, notificationClipEnd, notGoneIndex); 269 clipTopAmount = Math.max(clipTop, clipTopAmount); 270 271 // If the current row is an ExpandableNotificationRow, update its color, roundedness, 272 // and icon state. 273 if (child instanceof ExpandableNotificationRow) { 274 ExpandableNotificationRow expandableRow = (ExpandableNotificationRow) child; 275 numViewsInShelf += inShelfAmount; 276 int ownColorUntinted = expandableRow.getBackgroundColorWithoutTint(); 277 if (viewStart >= shelfStart && mNotGoneIndex == -1) { 278 mNotGoneIndex = notGoneIndex; 279 setTintColor(previousColor); 280 setOverrideTintColor(colorTwoBefore, transitionAmount); 281 282 } else if (mNotGoneIndex == -1) { 283 colorTwoBefore = previousColor; 284 transitionAmount = inShelfAmount; 285 } 286 // We don't want to modify the color if the notification is hun'd 287 if (isLastChild && mController.canModifyColorOfNotifications()) { 288 if (colorOfViewBeforeLast == NO_COLOR) { 289 colorOfViewBeforeLast = ownColorUntinted; 290 } 291 expandableRow.setOverrideTintColor(colorOfViewBeforeLast, inShelfAmount); 292 } else { 293 colorOfViewBeforeLast = ownColorUntinted; 294 expandableRow.setOverrideTintColor(NO_COLOR, 0 /* overrideAmount */); 295 } 296 if (notGoneIndex != 0 || !aboveShelf) { 297 expandableRow.setAboveShelf(false); 298 } 299 if (notGoneIndex == 0) { 300 StatusBarIconView icon = expandableRow.getEntry().getIcons().getShelfIcon(); 301 NotificationIconContainer.IconState iconState = getIconState(icon); 302 // The icon state might be null in rare cases where the notification is actually 303 // added to the layout, but not to the shelf. An example are replied messages, 304 // since they don't show up on AOD 305 if (iconState != null && iconState.clampedAppearAmount == 1.0f) { 306 // only if the first icon is fully in the shelf we want to clip to it! 307 backgroundTop = (int) (child.getTranslationY() - getTranslationY()); 308 firstElementRoundness = expandableRow.getCurrentTopRoundness(); 309 } 310 } 311 312 previousColor = ownColorUntinted; 313 notGoneIndex++; 314 } 315 316 if (child instanceof ActivatableNotificationView) { 317 ActivatableNotificationView anv = 318 (ActivatableNotificationView) child; 319 updateCornerRoundnessOnScroll(anv, viewStart, shelfStart); 320 } 321 } 322 323 clipTransientViews(); 324 325 setClipTopAmount(clipTopAmount); 326 327 boolean isHidden = getViewState().hidden 328 || clipTopAmount >= getIntrinsicHeight() 329 || !mShowNotificationShelf 330 || numViewsInShelf < 1f; 331 332 // TODO(b/172289889) transition last icon in shelf to notification icon and vice versa. 333 setVisibility(isHidden ? View.INVISIBLE : View.VISIBLE); 334 setBackgroundTop(backgroundTop); 335 setFirstElementRoundness(firstElementRoundness); 336 mShelfIcons.setSpeedBumpIndex(mHostLayoutController.getSpeedBumpIndex()); 337 mShelfIcons.calculateIconTranslations(); 338 mShelfIcons.applyIconStates(); 339 for (int i = 0; i < mHostLayoutController.getChildCount(); i++) { 340 View child = mHostLayoutController.getChildAt(i); 341 if (!(child instanceof ExpandableNotificationRow) 342 || child.getVisibility() == GONE) { 343 continue; 344 } 345 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 346 updateContinuousClipping(row); 347 } 348 boolean hideBackground = isHidden; 349 setHideBackground(hideBackground); 350 if (mNotGoneIndex == -1) { 351 mNotGoneIndex = notGoneIndex; 352 } 353 } 354 355 private void updateCornerRoundnessOnScroll(ActivatableNotificationView anv, float viewStart, 356 float shelfStart) { 357 358 final boolean isUnlockedHeadsUp = !mAmbientState.isOnKeyguard() 359 && !mAmbientState.isShadeExpanded() 360 && anv instanceof ExpandableNotificationRow 361 && ((ExpandableNotificationRow) anv).isHeadsUp(); 362 363 final boolean isHunGoingToShade = mAmbientState.isShadeExpanded() 364 && anv == mAmbientState.getTrackedHeadsUpRow(); 365 366 final boolean shouldUpdateCornerRoundness = viewStart < shelfStart 367 && !mHostLayoutController.isViewAffectedBySwipe(anv) 368 && !isUnlockedHeadsUp 369 && !isHunGoingToShade 370 && !anv.isAboveShelf() 371 && !mAmbientState.isPulsing() 372 && !mAmbientState.isDozing(); 373 374 if (!shouldUpdateCornerRoundness) { 375 return; 376 } 377 378 final float smallCornerRadius = 379 getResources().getDimension(R.dimen.notification_corner_radius_small) 380 / getResources().getDimension(R.dimen.notification_corner_radius); 381 final float viewEnd = viewStart + anv.getActualHeight(); 382 final float cornerAnimationDistance = mCornerAnimationDistance 383 * mAmbientState.getExpansionFraction(); 384 final float cornerAnimationTop = shelfStart - cornerAnimationDistance; 385 386 if (viewEnd >= cornerAnimationTop) { 387 // Round bottom corners within animation bounds 388 final float changeFraction = MathUtils.saturate( 389 (viewEnd - cornerAnimationTop) / cornerAnimationDistance); 390 anv.setBottomRoundness(anv.isLastInSection() ? 1f : changeFraction, 391 false /* animate */); 392 393 } else if (viewEnd < cornerAnimationTop) { 394 // Fast scroll skips frames and leaves corners with unfinished rounding. 395 // Reset top and bottom corners outside of animation bounds. 396 anv.setBottomRoundness(anv.isLastInSection() ? 1f : smallCornerRadius, 397 false /* animate */); 398 } 399 400 if (viewStart >= cornerAnimationTop) { 401 // Round top corners within animation bounds 402 final float changeFraction = MathUtils.saturate( 403 (viewStart - cornerAnimationTop) / cornerAnimationDistance); 404 anv.setTopRoundness(anv.isFirstInSection() ? 1f : changeFraction, 405 false /* animate */); 406 407 } else if (viewStart < cornerAnimationTop) { 408 // Fast scroll skips frames and leaves corners with unfinished rounding. 409 // Reset top and bottom corners outside of animation bounds. 410 anv.setTopRoundness(anv.isFirstInSection() ? 1f : smallCornerRadius, 411 false /* animate */); 412 } 413 } 414 415 /** 416 * Clips transient views to the top of the shelf - Transient views are only used for 417 * disappearing views/animations and need to be clipped correctly by the shelf to ensure they 418 * don't show underneath the notification stack when something is animating and the user 419 * swipes quickly. 420 */ 421 private void clipTransientViews() { 422 for (int i = 0; i < mHostLayoutController.getTransientViewCount(); i++) { 423 View transientView = mHostLayoutController.getTransientView(i); 424 if (transientView instanceof ExpandableView) { 425 ExpandableView transientExpandableView = (ExpandableView) transientView; 426 updateNotificationClipHeight(transientExpandableView, getTranslationY(), -1); 427 } 428 } 429 } 430 431 private void setFirstElementRoundness(float firstElementRoundness) { 432 if (mFirstElementRoundness != firstElementRoundness) { 433 mFirstElementRoundness = firstElementRoundness; 434 } 435 } 436 437 private void updateIconClipAmount(ExpandableNotificationRow row) { 438 float maxTop = row.getTranslationY(); 439 if (getClipTopAmount() != 0) { 440 // if the shelf is clipped, lets make sure we also clip the icon 441 maxTop = Math.max(maxTop, getTranslationY() + getClipTopAmount()); 442 } 443 StatusBarIconView icon = row.getEntry().getIcons().getShelfIcon(); 444 float shelfIconPosition = getTranslationY() + icon.getTop() + icon.getTranslationY(); 445 if (shelfIconPosition < maxTop && !mAmbientState.isFullyHidden()) { 446 int top = (int) (maxTop - shelfIconPosition); 447 Rect clipRect = new Rect(0, top, icon.getWidth(), Math.max(top, icon.getHeight())); 448 icon.setClipBounds(clipRect); 449 } else { 450 icon.setClipBounds(null); 451 } 452 } 453 454 private void updateContinuousClipping(final ExpandableNotificationRow row) { 455 StatusBarIconView icon = row.getEntry().getIcons().getShelfIcon(); 456 boolean needsContinuousClipping = ViewState.isAnimatingY(icon) && !mAmbientState.isDozing(); 457 boolean isContinuousClipping = icon.getTag(TAG_CONTINUOUS_CLIPPING) != null; 458 if (needsContinuousClipping && !isContinuousClipping) { 459 final ViewTreeObserver observer = icon.getViewTreeObserver(); 460 ViewTreeObserver.OnPreDrawListener predrawListener = 461 new ViewTreeObserver.OnPreDrawListener() { 462 @Override 463 public boolean onPreDraw() { 464 boolean animatingY = ViewState.isAnimatingY(icon); 465 if (!animatingY) { 466 if (observer.isAlive()) { 467 observer.removeOnPreDrawListener(this); 468 } 469 icon.setTag(TAG_CONTINUOUS_CLIPPING, null); 470 return true; 471 } 472 updateIconClipAmount(row); 473 return true; 474 } 475 }; 476 observer.addOnPreDrawListener(predrawListener); 477 icon.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { 478 @Override 479 public void onViewAttachedToWindow(View v) { 480 } 481 482 @Override 483 public void onViewDetachedFromWindow(View v) { 484 if (v == icon) { 485 if (observer.isAlive()) { 486 observer.removeOnPreDrawListener(predrawListener); 487 } 488 icon.setTag(TAG_CONTINUOUS_CLIPPING, null); 489 } 490 } 491 }); 492 icon.setTag(TAG_CONTINUOUS_CLIPPING, predrawListener); 493 } 494 } 495 496 /** 497 * Update the clipping of this view. 498 * @return the amount that our own top should be clipped 499 */ 500 private int updateNotificationClipHeight(ExpandableView view, 501 float notificationClipEnd, int childIndex) { 502 float viewEnd = view.getTranslationY() + view.getActualHeight(); 503 boolean isPinned = (view.isPinned() || view.isHeadsUpAnimatingAway()) 504 && !mAmbientState.isDozingAndNotPulsing(view); 505 boolean shouldClipOwnTop; 506 if (mAmbientState.isPulseExpanding()) { 507 shouldClipOwnTop = childIndex == 0; 508 } else { 509 shouldClipOwnTop = view.showingPulsing(); 510 } 511 if (viewEnd > notificationClipEnd && !shouldClipOwnTop 512 && (mAmbientState.isShadeExpanded() || !isPinned)) { 513 int clipBottomAmount = (int) (viewEnd - notificationClipEnd); 514 if (isPinned) { 515 clipBottomAmount = Math.min(view.getIntrinsicHeight() - view.getCollapsedHeight(), 516 clipBottomAmount); 517 } 518 view.setClipBottomAmount(clipBottomAmount); 519 } else { 520 view.setClipBottomAmount(0); 521 } 522 if (shouldClipOwnTop) { 523 return (int) (viewEnd - getTranslationY()); 524 } else { 525 return 0; 526 } 527 } 528 529 @Override 530 public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd, 531 int outlineTranslation) { 532 if (!mHasItemsInStableShelf) { 533 shadowIntensity = 0.0f; 534 } 535 super.setFakeShadowIntensity(shadowIntensity, outlineAlpha, shadowYEnd, outlineTranslation); 536 } 537 538 /** 539 * @return the amount how much this notification is in the shelf 540 */ 541 private float updateShelfTransformation(int i, ExpandableView view, boolean scrollingFast, 542 boolean expandingAnimated, boolean isLastChild) { 543 544 // Let's calculate how much the view is in the shelf 545 float viewStart = view.getTranslationY(); 546 int fullHeight = view.getActualHeight() + mPaddingBetweenElements; 547 float iconTransformStart = calculateIconTransformationStart(view); 548 549 // Let's make sure the transform distance is 550 // at most to the icon (relevant for conversations) 551 float transformDistance = Math.min( 552 viewStart + fullHeight - iconTransformStart, 553 getIntrinsicHeight()); 554 555 if (isLastChild) { 556 fullHeight = Math.min(fullHeight, view.getMinHeight() - getIntrinsicHeight()); 557 transformDistance = Math.min( 558 transformDistance, 559 view.getMinHeight() - getIntrinsicHeight()); 560 } 561 562 float viewEnd = viewStart + fullHeight; 563 float fullTransitionAmount = 0.0f; 564 float iconTransitionAmount = 0.0f; 565 float shelfStart = getTranslationY(); 566 if (mAmbientState.isExpansionChanging() && !mAmbientState.isOnKeyguard()) { 567 // TODO(b/172289889) handle icon placement for notification that is clipped by the shelf 568 if (mIndexOfFirstViewInShelf != -1 && i >= mIndexOfFirstViewInShelf) { 569 fullTransitionAmount = 1f; 570 iconTransitionAmount = 1f; 571 } 572 } else if (viewEnd >= shelfStart 573 && (!mAmbientState.isUnlockHintRunning() || view.isInShelf()) 574 && (mAmbientState.isShadeExpanded() 575 || (!view.isPinned() && !view.isHeadsUpAnimatingAway()))) { 576 577 if (viewStart < shelfStart) { 578 float fullAmount = (shelfStart - viewStart) / fullHeight; 579 fullAmount = Math.min(1.0f, fullAmount); 580 fullTransitionAmount = 1.0f - fullAmount; 581 if (isLastChild) { 582 // Reduce icon transform distance to completely fade in shelf icon 583 // by the time the notification icon fades out, and vice versa 584 iconTransitionAmount = (shelfStart - viewStart) 585 / (iconTransformStart - viewStart); 586 } else { 587 iconTransitionAmount = (shelfStart - iconTransformStart) / transformDistance; 588 } 589 iconTransitionAmount = MathUtils.constrain(iconTransitionAmount, 0.0f, 1.0f); 590 iconTransitionAmount = 1.0f - iconTransitionAmount; 591 } else { 592 // Fully in shelf. 593 fullTransitionAmount = 1.0f; 594 iconTransitionAmount = 1.0f; 595 } 596 } 597 updateIconPositioning(view, iconTransitionAmount, 598 scrollingFast, expandingAnimated, isLastChild); 599 return fullTransitionAmount; 600 } 601 602 /** 603 * @return the location where the transformation into the shelf should start. 604 */ 605 private float calculateIconTransformationStart(ExpandableView view) { 606 View target = view.getShelfTransformationTarget(); 607 if (target == null) { 608 return view.getTranslationY(); 609 } 610 float start = view.getTranslationY() + view.getRelativeTopPadding(target); 611 612 // Let's not start the transformation right at the icon but by the padding before it. 613 start -= view.getShelfIcon().getTop(); 614 return start; 615 } 616 617 private void updateIconPositioning(ExpandableView view, float iconTransitionAmount, 618 boolean scrollingFast, boolean expandingAnimated, boolean isLastChild) { 619 StatusBarIconView icon = view.getShelfIcon(); 620 NotificationIconContainer.IconState iconState = getIconState(icon); 621 if (iconState == null) { 622 return; 623 } 624 boolean clampInShelf = iconTransitionAmount > 0.5f || isTargetClipped(view); 625 float clampedAmount = clampInShelf ? 1.0f : 0.0f; 626 if (iconTransitionAmount == clampedAmount) { 627 iconState.noAnimations = (scrollingFast || expandingAnimated) && !isLastChild; 628 } 629 if (!isLastChild 630 && (scrollingFast || (expandingAnimated && !ViewState.isAnimatingY(icon)))) { 631 iconState.cancelAnimations(icon); 632 iconState.noAnimations = true; 633 } 634 float transitionAmount; 635 if (mAmbientState.isHiddenAtAll() && !view.isInShelf()) { 636 transitionAmount = mAmbientState.isFullyHidden() ? 1 : 0; 637 } else { 638 transitionAmount = iconTransitionAmount; 639 iconState.needsCannedAnimation = iconState.clampedAppearAmount != clampedAmount; 640 } 641 iconState.clampedAppearAmount = clampedAmount; 642 setIconTransformationAmount(view, transitionAmount); 643 } 644 645 private boolean isTargetClipped(ExpandableView view) { 646 View target = view.getShelfTransformationTarget(); 647 if (target == null) { 648 return false; 649 } 650 // We should never clip the target, let's instead put it into the shelf! 651 float endOfTarget = view.getTranslationY() 652 + view.getContentTranslation() 653 + view.getRelativeTopPadding(target) 654 + target.getHeight(); 655 return endOfTarget >= getTranslationY() - mPaddingBetweenElements; 656 } 657 658 private void setIconTransformationAmount(ExpandableView view, float transitionAmount) { 659 if (!(view instanceof ExpandableNotificationRow)) { 660 return; 661 } 662 ExpandableNotificationRow row = (ExpandableNotificationRow) view; 663 StatusBarIconView icon = row.getShelfIcon(); 664 NotificationIconContainer.IconState iconState = getIconState(icon); 665 if (iconState == null) { 666 return; 667 } 668 iconState.alpha = ICON_ALPHA_INTERPOLATOR.getInterpolation(transitionAmount); 669 boolean isAppearing = row.isDrawingAppearAnimation() && !row.isInShelf(); 670 iconState.hidden = isAppearing 671 || (view instanceof ExpandableNotificationRow 672 && ((ExpandableNotificationRow) view).isLowPriority() 673 && mShelfIcons.hasMaxNumDot()) 674 || (transitionAmount == 0.0f && !iconState.isAnimating(icon)) 675 || row.isAboveShelf() 676 || row.showingPulsing() 677 || row.getTranslationZ() > mAmbientState.getBaseZHeight(); 678 679 iconState.iconAppearAmount = iconState.hidden? 0f : transitionAmount; 680 681 // Fade in icons at shelf start 682 // This is important for conversation icons, which are badged and need x reset 683 iconState.xTranslation = mShelfIcons.getActualPaddingStart(); 684 685 final boolean stayingInShelf = row.isInShelf() && !row.isTransformingIntoShelf(); 686 if (stayingInShelf) { 687 iconState.iconAppearAmount = 1.0f; 688 iconState.alpha = 1.0f; 689 iconState.hidden = false; 690 } 691 int backgroundColor = getBackgroundColorWithoutTint(); 692 int shelfColor = icon.getContrastedStaticDrawableColor(backgroundColor); 693 if (row.isShowingIcon() && shelfColor != StatusBarIconView.NO_COLOR) { 694 int iconColor = row.getOriginalIconColor(); 695 shelfColor = NotificationUtils.interpolateColors(iconColor, shelfColor, 696 iconState.iconAppearAmount); 697 } 698 iconState.iconColor = shelfColor; 699 } 700 getIconState(StatusBarIconView icon)701 private NotificationIconContainer.IconState getIconState(StatusBarIconView icon) { 702 return mShelfIcons.getIconState(icon); 703 } 704 getFullyClosedTranslation()705 private float getFullyClosedTranslation() { 706 return - (getIntrinsicHeight() - mStatusBarHeight) / 2; 707 } 708 709 @Override hasNoContentHeight()710 public boolean hasNoContentHeight() { 711 return true; 712 } 713 setHideBackground(boolean hideBackground)714 private void setHideBackground(boolean hideBackground) { 715 if (mHideBackground != hideBackground) { 716 mHideBackground = hideBackground; 717 updateOutline(); 718 } 719 } 720 721 @Override needsOutline()722 protected boolean needsOutline() { 723 return !mHideBackground && super.needsOutline(); 724 } 725 726 727 @Override onLayout(boolean changed, int left, int top, int right, int bottom)728 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 729 super.onLayout(changed, left, top, right, bottom); 730 updateRelativeOffset(); 731 732 // we always want to clip to our sides, such that nothing can draw outside of these bounds 733 int height = getResources().getDisplayMetrics().heightPixels; 734 mClipRect.set(0, -height, getWidth(), height); 735 mShelfIcons.setClipBounds(mClipRect); 736 } 737 updateRelativeOffset()738 private void updateRelativeOffset() { 739 mCollapsedIcons.getLocationOnScreen(mTmp); 740 getLocationOnScreen(mTmp); 741 } 742 743 /** 744 * @return the index of the notification at which the shelf visually resides 745 */ getNotGoneIndex()746 public int getNotGoneIndex() { 747 return mNotGoneIndex; 748 } 749 setHasItemsInStableShelf(boolean hasItemsInStableShelf)750 private void setHasItemsInStableShelf(boolean hasItemsInStableShelf) { 751 if (mHasItemsInStableShelf != hasItemsInStableShelf) { 752 mHasItemsInStableShelf = hasItemsInStableShelf; 753 updateInteractiveness(); 754 } 755 } 756 757 /** 758 * @return whether the shelf has any icons in it when a potential animation has finished, i.e 759 * if the current state would be applied right now 760 */ hasItemsInStableShelf()761 public boolean hasItemsInStableShelf() { 762 return mHasItemsInStableShelf; 763 } 764 setCollapsedIcons(NotificationIconContainer collapsedIcons)765 public void setCollapsedIcons(NotificationIconContainer collapsedIcons) { 766 mCollapsedIcons = collapsedIcons; 767 mCollapsedIcons.addOnLayoutChangeListener(this); 768 } 769 770 @Override onStateChanged(int newState)771 public void onStateChanged(int newState) { 772 mStatusBarState = newState; 773 updateInteractiveness(); 774 } 775 updateInteractiveness()776 private void updateInteractiveness() { 777 mInteractive = mStatusBarState == StatusBarState.KEYGUARD && mHasItemsInStableShelf; 778 setClickable(mInteractive); 779 setFocusable(mInteractive); 780 setImportantForAccessibility(mInteractive ? View.IMPORTANT_FOR_ACCESSIBILITY_YES 781 : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); 782 } 783 784 @Override isInteractive()785 protected boolean isInteractive() { 786 return mInteractive; 787 } 788 setAnimationsEnabled(boolean enabled)789 public void setAnimationsEnabled(boolean enabled) { 790 mAnimationsEnabled = enabled; 791 if (!enabled) { 792 // we need to wait with enabling the animations until the first frame has passed 793 mShelfIcons.setAnimationsEnabled(false); 794 } 795 } 796 797 @Override hasOverlappingRendering()798 public boolean hasOverlappingRendering() { 799 return false; // Shelf only uses alpha for transitions where the difference can't be seen. 800 } 801 802 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)803 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 804 super.onInitializeAccessibilityNodeInfo(info); 805 if (mInteractive) { 806 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND); 807 AccessibilityNodeInfo.AccessibilityAction unlock 808 = new AccessibilityNodeInfo.AccessibilityAction( 809 AccessibilityNodeInfo.ACTION_CLICK, 810 getContext().getString(R.string.accessibility_overflow_action)); 811 info.addAction(unlock); 812 } 813 } 814 815 @Override onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)816 public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, 817 int oldTop, int oldRight, int oldBottom) { 818 updateRelativeOffset(); 819 } 820 821 @Override needsClippingToShelf()822 public boolean needsClippingToShelf() { 823 return false; 824 } 825 setController(NotificationShelfController notificationShelfController)826 public void setController(NotificationShelfController notificationShelfController) { 827 mController = notificationShelfController; 828 } 829 setIndexOfFirstViewInShelf(ExpandableView firstViewInShelf)830 public void setIndexOfFirstViewInShelf(ExpandableView firstViewInShelf) { 831 mIndexOfFirstViewInShelf = mHostLayoutController.indexOfChild(firstViewInShelf); 832 } 833 834 private class ShelfState extends ExpandableViewState { 835 private boolean hasItemsInStableShelf; 836 private ExpandableView firstViewInShelf; 837 838 @Override applyToView(View view)839 public void applyToView(View view) { 840 if (!mShowNotificationShelf) { 841 return; 842 } 843 844 super.applyToView(view); 845 setIndexOfFirstViewInShelf(firstViewInShelf); 846 updateAppearance(); 847 setHasItemsInStableShelf(hasItemsInStableShelf); 848 mShelfIcons.setAnimationsEnabled(mAnimationsEnabled); 849 } 850 851 @Override animateTo(View child, AnimationProperties properties)852 public void animateTo(View child, AnimationProperties properties) { 853 if (!mShowNotificationShelf) { 854 return; 855 } 856 857 super.animateTo(child, properties); 858 setIndexOfFirstViewInShelf(firstViewInShelf); 859 updateAppearance(); 860 setHasItemsInStableShelf(hasItemsInStableShelf); 861 mShelfIcons.setAnimationsEnabled(mAnimationsEnabled); 862 } 863 } 864 } 865