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