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