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 NotificationShelf(Context context, AttributeSet attrs)90 public NotificationShelf(Context context, AttributeSet attrs) { 91 super(context, attrs); 92 } 93 94 @Override onFinishInflate()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 bind(AmbientState ambientState, NotificationStackScrollLayout hostLayout)110 public void bind(AmbientState ambientState, NotificationStackScrollLayout hostLayout) { 111 mAmbientState = ambientState; 112 mHostLayout = hostLayout; 113 } 114 initDimens()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 onConfigurationChanged(Configuration newConfig)139 protected void onConfigurationChanged(Configuration newConfig) { 140 super.onConfigurationChanged(newConfig); 141 initDimens(); 142 } 143 144 @Override setDark(boolean dark, boolean fade, long delay)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 fadeInTranslating()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 getContentView()170 protected View getContentView() { 171 return mShelfIcons; 172 } 173 getShelfIcons()174 public NotificationIconContainer getShelfIcons() { 175 return mShelfIcons; 176 } 177 178 @Override createNewViewState(StackScrollState stackScrollState)179 public ExpandableViewState createNewViewState(StackScrollState stackScrollState) { 180 return mShelfState; 181 } 182 updateState(StackScrollState resultState, AmbientState ambientState)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 */ updateAppearance()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 getIconState(StatusBarIconView icon)689 private NotificationIconContainer.IconState getIconState(StatusBarIconView icon) { 690 return mShelfIcons.getIconState(icon); 691 } 692 getFullyClosedTranslation()693 private float getFullyClosedTranslation() { 694 return - (getIntrinsicHeight() - mStatusBarHeight) / 2; 695 } 696 getNotificationMergeSize()697 public int getNotificationMergeSize() { 698 return getIntrinsicHeight(); 699 } 700 701 @Override hasNoContentHeight()702 public boolean hasNoContentHeight() { 703 return true; 704 } 705 setHideBackground(boolean hideBackground)706 private void setHideBackground(boolean hideBackground) { 707 if (mHideBackground != hideBackground) { 708 mHideBackground = hideBackground; 709 updateBackground(); 710 updateOutline(); 711 } 712 } 713 hidesBackground()714 public boolean hidesBackground() { 715 return mHideBackground; 716 } 717 718 @Override needsOutline()719 protected boolean needsOutline() { 720 return !mHideBackground && super.needsOutline(); 721 } 722 723 @Override shouldHideBackground()724 protected boolean shouldHideBackground() { 725 return super.shouldHideBackground() || mHideBackground; 726 } 727 728 @Override onLayout(boolean changed, int left, int top, int right, int bottom)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 updateRelativeOffset()739 private void updateRelativeOffset() { 740 mCollapsedIcons.getLocationOnScreen(mTmp); 741 mRelativeOffset = mTmp[0]; 742 getLocationOnScreen(mTmp); 743 mRelativeOffset -= mTmp[0]; 744 } 745 setOpenedAmount(float openedAmount)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 setMaxLayoutHeight(int maxLayoutHeight)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 */ getNotGoneIndex()789 public int getNotGoneIndex() { 790 return mNotGoneIndex; 791 } 792 setHasItemsInStableShelf(boolean hasItemsInStableShelf)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 */ hasItemsInStableShelf()804 public boolean hasItemsInStableShelf() { 805 return mHasItemsInStableShelf; 806 } 807 setCollapsedIcons(NotificationIconContainer collapsedIcons)808 public void setCollapsedIcons(NotificationIconContainer collapsedIcons) { 809 mCollapsedIcons = collapsedIcons; 810 mCollapsedIcons.addOnLayoutChangeListener(this); 811 } 812 setStatusBarState(int statusBarState)813 public void setStatusBarState(int statusBarState) { 814 if (mStatusBarState != statusBarState) { 815 mStatusBarState = statusBarState; 816 updateInteractiveness(); 817 } 818 } 819 updateInteractiveness()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 isInteractive()830 protected boolean isInteractive() { 831 return mInteractive; 832 } 833 setMaxShelfEnd(float maxShelfEnd)834 public void setMaxShelfEnd(float maxShelfEnd) { 835 mMaxShelfEnd = maxShelfEnd; 836 } 837 setAnimationsEnabled(boolean enabled)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 hasOverlappingRendering()848 public boolean hasOverlappingRendering() { 849 return false; // Shelf only uses alpha for transitions where the difference can't be seen. 850 } 851 852 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)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 onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)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 applyToView(View view)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 animateTo(View child, AnimationProperties properties)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