1 /* 2 * Copyright (C) 2015 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.stack; 18 19 import android.app.Notification; 20 import android.content.Context; 21 import android.content.res.Configuration; 22 import android.graphics.drawable.ColorDrawable; 23 import android.service.notification.StatusBarNotification; 24 import android.util.AttributeSet; 25 import android.view.LayoutInflater; 26 import android.view.NotificationHeaderView; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.widget.RemoteViews; 30 import android.widget.TextView; 31 32 import com.android.systemui.R; 33 import com.android.systemui.ViewInvertHelper; 34 import com.android.systemui.statusbar.CrossFadeHelper; 35 import com.android.systemui.statusbar.ExpandableNotificationRow; 36 import com.android.systemui.statusbar.NotificationHeaderUtil; 37 import com.android.systemui.statusbar.notification.HybridGroupManager; 38 import com.android.systemui.statusbar.notification.HybridNotificationView; 39 import com.android.systemui.statusbar.notification.NotificationUtils; 40 import com.android.systemui.statusbar.notification.NotificationViewWrapper; 41 import com.android.systemui.statusbar.phone.NotificationPanelView; 42 43 import java.util.ArrayList; 44 import java.util.List; 45 46 /** 47 * A container containing child notifications 48 */ 49 public class NotificationChildrenContainer extends ViewGroup { 50 51 private static final int NUMBER_OF_CHILDREN_WHEN_COLLAPSED = 2; 52 private static final int NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED = 5; 53 private static final int NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED = 8; 54 55 private final List<View> mDividers = new ArrayList<>(); 56 private final List<ExpandableNotificationRow> mChildren = new ArrayList<>(); 57 private final HybridGroupManager mHybridGroupManager; 58 private int mChildPadding; 59 private int mDividerHeight; 60 private int mMaxNotificationHeight; 61 private int mNotificationHeaderMargin; 62 private int mNotificatonTopPadding; 63 private float mCollapsedBottompadding; 64 private ViewInvertHelper mOverflowInvertHelper; 65 private boolean mChildrenExpanded; 66 private ExpandableNotificationRow mNotificationParent; 67 private TextView mOverflowNumber; 68 private ViewState mGroupOverFlowState; 69 private int mRealHeight; 70 private boolean mUserLocked; 71 private int mActualHeight; 72 private boolean mNeverAppliedGroupState; 73 private int mHeaderHeight; 74 75 private NotificationHeaderView mNotificationHeader; 76 private NotificationViewWrapper mNotificationHeaderWrapper; 77 private NotificationHeaderUtil mHeaderUtil; 78 private ViewState mHeaderViewState; 79 80 public NotificationChildrenContainer(Context context) { 81 this(context, null); 82 } 83 84 public NotificationChildrenContainer(Context context, AttributeSet attrs) { 85 this(context, attrs, 0); 86 } 87 88 public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr) { 89 this(context, attrs, defStyleAttr, 0); 90 } 91 92 public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr, 93 int defStyleRes) { 94 super(context, attrs, defStyleAttr, defStyleRes); 95 initDimens(); 96 mHybridGroupManager = new HybridGroupManager(getContext(), this); 97 } 98 99 private void initDimens() { 100 mChildPadding = getResources().getDimensionPixelSize( 101 R.dimen.notification_children_padding); 102 mDividerHeight = Math.max(1, getResources().getDimensionPixelSize( 103 R.dimen.notification_divider_height)); 104 mHeaderHeight = getResources().getDimensionPixelSize(R.dimen.notification_header_height); 105 mMaxNotificationHeight = getResources().getDimensionPixelSize( 106 R.dimen.notification_max_height); 107 mNotificationHeaderMargin = getResources().getDimensionPixelSize( 108 com.android.internal.R.dimen.notification_content_margin_top); 109 mNotificatonTopPadding = getResources().getDimensionPixelSize( 110 R.dimen.notification_children_container_top_padding); 111 mCollapsedBottompadding = getResources().getDimensionPixelSize( 112 com.android.internal.R.dimen.notification_content_margin_bottom); 113 } 114 115 @Override 116 protected void onLayout(boolean changed, int l, int t, int r, int b) { 117 int childCount = Math.min(mChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED); 118 for (int i = 0; i < childCount; i++) { 119 View child = mChildren.get(i); 120 // We need to layout all children even the GONE ones, such that the heights are 121 // calculated correctly as they are used to calculate how many we can fit on the screen 122 child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight()); 123 mDividers.get(i).layout(0, 0, getWidth(), mDividerHeight); 124 } 125 if (mOverflowNumber != null) { 126 mOverflowNumber.layout(getWidth() - mOverflowNumber.getMeasuredWidth(), 0, getWidth(), 127 mOverflowNumber.getMeasuredHeight()); 128 } 129 if (mNotificationHeader != null) { 130 mNotificationHeader.layout(0, 0, mNotificationHeader.getMeasuredWidth(), 131 mNotificationHeader.getMeasuredHeight()); 132 } 133 } 134 135 @Override 136 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 137 int ownMaxHeight = mMaxNotificationHeight; 138 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 139 boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY; 140 boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST; 141 int size = MeasureSpec.getSize(heightMeasureSpec); 142 if (hasFixedHeight || isHeightLimited) { 143 ownMaxHeight = Math.min(ownMaxHeight, size); 144 } 145 int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST); 146 int width = MeasureSpec.getSize(widthMeasureSpec); 147 if (mOverflowNumber != null) { 148 mOverflowNumber.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), 149 newHeightSpec); 150 } 151 int dividerHeightSpec = MeasureSpec.makeMeasureSpec(mDividerHeight, MeasureSpec.EXACTLY); 152 int height = mNotificationHeaderMargin + mNotificatonTopPadding; 153 int childCount = Math.min(mChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED); 154 int collapsedChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */); 155 int overflowIndex = childCount > collapsedChildren ? collapsedChildren - 1 : -1; 156 for (int i = 0; i < childCount; i++) { 157 ExpandableNotificationRow child = mChildren.get(i); 158 // We need to measure all children even the GONE ones, such that the heights are 159 // calculated correctly as they are used to calculate how many we can fit on the screen. 160 boolean isOverflow = i == overflowIndex; 161 child.setSingleLineWidthIndention(isOverflow && mOverflowNumber != null 162 ? mOverflowNumber.getMeasuredWidth() 163 : 0); 164 child.measure(widthMeasureSpec, newHeightSpec); 165 // layout the divider 166 View divider = mDividers.get(i); 167 divider.measure(widthMeasureSpec, dividerHeightSpec); 168 if (child.getVisibility() != GONE) { 169 height += child.getMeasuredHeight() + mDividerHeight; 170 } 171 } 172 mRealHeight = height; 173 if (heightMode != MeasureSpec.UNSPECIFIED) { 174 height = Math.min(height, size); 175 } 176 177 if (mNotificationHeader != null) { 178 int headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY); 179 mNotificationHeader.measure(widthMeasureSpec, headerHeightSpec); 180 } 181 182 setMeasuredDimension(width, height); 183 } 184 185 @Override 186 public boolean pointInView(float localX, float localY, float slop) { 187 return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) && 188 localY < (mRealHeight + slop); 189 } 190 191 /** 192 * Add a child notification to this view. 193 * 194 * @param row the row to add 195 * @param childIndex the index to add it at, if -1 it will be added at the end 196 */ 197 public void addNotification(ExpandableNotificationRow row, int childIndex) { 198 int newIndex = childIndex < 0 ? mChildren.size() : childIndex; 199 mChildren.add(newIndex, row); 200 addView(row); 201 row.setUserLocked(mUserLocked); 202 203 View divider = inflateDivider(); 204 addView(divider); 205 mDividers.add(newIndex, divider); 206 207 updateGroupOverflow(); 208 } 209 210 public void removeNotification(ExpandableNotificationRow row) { 211 int childIndex = mChildren.indexOf(row); 212 mChildren.remove(row); 213 removeView(row); 214 215 final View divider = mDividers.remove(childIndex); 216 removeView(divider); 217 getOverlay().add(divider); 218 CrossFadeHelper.fadeOut(divider, new Runnable() { 219 @Override 220 public void run() { 221 getOverlay().remove(divider); 222 } 223 }); 224 225 row.setSystemChildExpanded(false); 226 row.setUserLocked(false); 227 updateGroupOverflow(); 228 if (!row.isRemoved()) { 229 mHeaderUtil.restoreNotificationHeader(row); 230 } 231 } 232 233 /** 234 * @return The number of notification children in the container. 235 */ 236 public int getNotificationChildCount() { 237 return mChildren.size(); 238 } 239 240 public void recreateNotificationHeader(OnClickListener listener, StatusBarNotification notification) { 241 final Notification.Builder builder = Notification.Builder.recoverBuilder(getContext(), 242 mNotificationParent.getStatusBarNotification().getNotification()); 243 final RemoteViews header = builder.makeNotificationHeader(); 244 if (mNotificationHeader == null) { 245 mNotificationHeader = (NotificationHeaderView) header.apply(getContext(), this); 246 final View expandButton = mNotificationHeader.findViewById( 247 com.android.internal.R.id.expand_button); 248 expandButton.setVisibility(VISIBLE); 249 mNotificationHeader.setOnClickListener(listener); 250 mNotificationHeaderWrapper = NotificationViewWrapper.wrap(getContext(), 251 mNotificationHeader, mNotificationParent); 252 addView(mNotificationHeader, 0); 253 invalidate(); 254 } else { 255 header.reapply(getContext(), mNotificationHeader); 256 mNotificationHeaderWrapper.notifyContentUpdated(notification); 257 } 258 updateChildrenHeaderAppearance(); 259 } 260 261 public void updateChildrenHeaderAppearance() { 262 mHeaderUtil.updateChildrenHeaderAppearance(); 263 } 264 265 public void updateGroupOverflow() { 266 int childCount = mChildren.size(); 267 int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */); 268 if (childCount > maxAllowedVisibleChildren) { 269 mOverflowNumber = mHybridGroupManager.bindOverflowNumber( 270 mOverflowNumber, childCount - maxAllowedVisibleChildren); 271 if (mOverflowInvertHelper == null) { 272 mOverflowInvertHelper = new ViewInvertHelper(mOverflowNumber, 273 NotificationPanelView.DOZE_ANIMATION_DURATION); 274 } 275 if (mGroupOverFlowState == null) { 276 mGroupOverFlowState = new ViewState(); 277 mNeverAppliedGroupState = true; 278 } 279 } else if (mOverflowNumber != null) { 280 removeView(mOverflowNumber); 281 if (isShown()) { 282 final View removedOverflowNumber = mOverflowNumber; 283 addTransientView(removedOverflowNumber, getTransientViewCount()); 284 CrossFadeHelper.fadeOut(removedOverflowNumber, new Runnable() { 285 @Override 286 public void run() { 287 removeTransientView(removedOverflowNumber); 288 } 289 }); 290 } 291 mOverflowNumber = null; 292 mOverflowInvertHelper = null; 293 mGroupOverFlowState = null; 294 } 295 } 296 297 @Override 298 protected void onConfigurationChanged(Configuration newConfig) { 299 super.onConfigurationChanged(newConfig); 300 updateGroupOverflow(); 301 } 302 303 private View inflateDivider() { 304 return LayoutInflater.from(mContext).inflate( 305 R.layout.notification_children_divider, this, false); 306 } 307 308 public List<ExpandableNotificationRow> getNotificationChildren() { 309 return mChildren; 310 } 311 312 /** 313 * Apply the order given in the list to the children. 314 * 315 * @param childOrder the new list order 316 * @return whether the list order has changed 317 */ 318 public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder) { 319 if (childOrder == null) { 320 return false; 321 } 322 boolean result = false; 323 for (int i = 0; i < mChildren.size() && i < childOrder.size(); i++) { 324 ExpandableNotificationRow child = mChildren.get(i); 325 ExpandableNotificationRow desiredChild = childOrder.get(i); 326 if (child != desiredChild) { 327 mChildren.remove(desiredChild); 328 mChildren.add(i, desiredChild); 329 result = true; 330 } 331 } 332 updateExpansionStates(); 333 return result; 334 } 335 336 private void updateExpansionStates() { 337 if (mChildrenExpanded || mUserLocked) { 338 // we don't modify it the group is expanded or if we are expanding it 339 return; 340 } 341 int size = mChildren.size(); 342 for (int i = 0; i < size; i++) { 343 ExpandableNotificationRow child = mChildren.get(i); 344 child.setSystemChildExpanded(i == 0 && size == 1); 345 } 346 } 347 348 /** 349 * 350 * @return the intrinsic size of this children container, i.e the natural fully expanded state 351 */ 352 public int getIntrinsicHeight() { 353 int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(); 354 return getIntrinsicHeight(maxAllowedVisibleChildren); 355 } 356 357 /** 358 * @return the intrinsic height with a number of children given 359 * in @param maxAllowedVisibleChildren 360 */ 361 private int getIntrinsicHeight(float maxAllowedVisibleChildren) { 362 int intrinsicHeight = mNotificationHeaderMargin; 363 int visibleChildren = 0; 364 int childCount = mChildren.size(); 365 boolean firstChild = true; 366 float expandFactor = 0; 367 if (mUserLocked) { 368 expandFactor = getGroupExpandFraction(); 369 } 370 for (int i = 0; i < childCount; i++) { 371 if (visibleChildren >= maxAllowedVisibleChildren) { 372 break; 373 } 374 if (!firstChild) { 375 if (mUserLocked) { 376 intrinsicHeight += NotificationUtils.interpolate(mChildPadding, mDividerHeight, 377 expandFactor); 378 } else { 379 intrinsicHeight += mChildrenExpanded ? mDividerHeight : mChildPadding; 380 } 381 } else { 382 if (mUserLocked) { 383 intrinsicHeight += NotificationUtils.interpolate( 384 0, 385 mNotificatonTopPadding + mDividerHeight, 386 expandFactor); 387 } else { 388 intrinsicHeight += mChildrenExpanded 389 ? mNotificatonTopPadding + mDividerHeight 390 : 0; 391 } 392 firstChild = false; 393 } 394 ExpandableNotificationRow child = mChildren.get(i); 395 intrinsicHeight += child.getIntrinsicHeight(); 396 visibleChildren++; 397 } 398 if (mUserLocked) { 399 intrinsicHeight += NotificationUtils.interpolate(mCollapsedBottompadding, 0.0f, 400 expandFactor); 401 } else if (!mChildrenExpanded) { 402 intrinsicHeight += mCollapsedBottompadding; 403 } 404 return intrinsicHeight; 405 } 406 407 /** 408 * Update the state of all its children based on a linear layout algorithm. 409 * 410 * @param resultState the state to update 411 * @param parentState the state of the parent 412 */ 413 public void getState(StackScrollState resultState, StackViewState parentState) { 414 int childCount = mChildren.size(); 415 int yPosition = mNotificationHeaderMargin; 416 boolean firstChild = true; 417 int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(); 418 int lastVisibleIndex = maxAllowedVisibleChildren - 1; 419 int firstOverflowIndex = lastVisibleIndex + 1; 420 float expandFactor = 0; 421 if (mUserLocked) { 422 expandFactor = getGroupExpandFraction(); 423 firstOverflowIndex = getMaxAllowedVisibleChildren(true /* likeCollapsed */); 424 } 425 426 boolean childrenExpanded = !mNotificationParent.isGroupExpansionChanging() 427 && mChildrenExpanded; 428 int parentHeight = parentState.height; 429 for (int i = 0; i < childCount; i++) { 430 ExpandableNotificationRow child = mChildren.get(i); 431 if (!firstChild) { 432 if (mUserLocked) { 433 yPosition += NotificationUtils.interpolate(mChildPadding, mDividerHeight, 434 expandFactor); 435 } else { 436 yPosition += mChildrenExpanded ? mDividerHeight : mChildPadding; 437 } 438 } else { 439 if (mUserLocked) { 440 yPosition += NotificationUtils.interpolate( 441 0, 442 mNotificatonTopPadding + mDividerHeight, 443 expandFactor); 444 } else { 445 yPosition += mChildrenExpanded ? mNotificatonTopPadding + mDividerHeight : 0; 446 } 447 firstChild = false; 448 } 449 450 StackViewState childState = resultState.getViewStateForView(child); 451 int intrinsicHeight = child.getIntrinsicHeight(); 452 if (childrenExpanded) { 453 // When a group is expanded and moving into bottom stack, the bottom visible child 454 // adjusts its height to move into it. Children after it are hidden. 455 if (updateChildStateForExpandedGroup(child, parentHeight, childState, yPosition)) { 456 // Clipping might be deactivated if the view is transforming, however, clipping 457 // the child into the bottom stack should take precedent over this. 458 childState.isBottomClipped = true; 459 } 460 } else { 461 childState.hidden = false; 462 childState.height = intrinsicHeight; 463 childState.isBottomClipped = false; 464 } 465 childState.yTranslation = yPosition; 466 // When the group is expanded, the children cast the shadows rather than the parent 467 // so use the parent's elevation here. 468 childState.zTranslation = childrenExpanded 469 ? mNotificationParent.getTranslationZ() 470 : 0; 471 childState.dimmed = parentState.dimmed; 472 childState.dark = parentState.dark; 473 childState.hideSensitive = parentState.hideSensitive; 474 childState.belowSpeedBump = parentState.belowSpeedBump; 475 childState.clipTopAmount = 0; 476 childState.alpha = 0; 477 if (i < firstOverflowIndex) { 478 childState.alpha = 1; 479 } else if (expandFactor == 1.0f && i <= lastVisibleIndex) { 480 childState.alpha = (mActualHeight - childState.yTranslation) / childState.height; 481 childState.alpha = Math.max(0.0f, Math.min(1.0f, childState.alpha)); 482 } 483 childState.location = parentState.location; 484 yPosition += intrinsicHeight; 485 } 486 if (mOverflowNumber != null) { 487 ExpandableNotificationRow overflowView = mChildren.get(Math.min( 488 getMaxAllowedVisibleChildren(true /* likeCollpased */), childCount) - 1); 489 mGroupOverFlowState.copyFrom(resultState.getViewStateForView(overflowView)); 490 if (!mChildrenExpanded) { 491 if (mUserLocked) { 492 HybridNotificationView singleLineView = overflowView.getSingleLineView(); 493 View mirrorView = singleLineView.getTextView(); 494 if (mirrorView.getVisibility() == GONE) { 495 mirrorView = singleLineView.getTitleView(); 496 } 497 if (mirrorView.getVisibility() == GONE) { 498 mirrorView = singleLineView; 499 } 500 mGroupOverFlowState.yTranslation += NotificationUtils.getRelativeYOffset( 501 mirrorView, overflowView); 502 mGroupOverFlowState.alpha = mirrorView.getAlpha(); 503 } 504 } else { 505 mGroupOverFlowState.yTranslation += mNotificationHeaderMargin; 506 mGroupOverFlowState.alpha = 0.0f; 507 } 508 } 509 if (mNotificationHeader != null) { 510 if (mHeaderViewState == null) { 511 mHeaderViewState = new ViewState(); 512 } 513 mHeaderViewState.initFrom(mNotificationHeader); 514 mHeaderViewState.zTranslation = childrenExpanded 515 ? mNotificationParent.getTranslationZ() 516 : 0; 517 } 518 } 519 520 /** 521 * When moving into the bottom stack, the bottom visible child in an expanded group adjusts its 522 * height, children in the group after this are gone. 523 * 524 * @param child the child who's height to adjust. 525 * @param parentHeight the height of the parent. 526 * @param childState the state to update. 527 * @param yPosition the yPosition of the view. 528 * @return true if children after this one should be hidden. 529 */ 530 private boolean updateChildStateForExpandedGroup(ExpandableNotificationRow child, 531 int parentHeight, StackViewState childState, int yPosition) { 532 final int top = yPosition + child.getClipTopAmount(); 533 final int intrinsicHeight = child.getIntrinsicHeight(); 534 final int bottom = top + intrinsicHeight; 535 int newHeight = intrinsicHeight; 536 if (bottom >= parentHeight) { 537 // Child is either clipped or gone 538 newHeight = Math.max((parentHeight - top), 0); 539 } 540 childState.hidden = newHeight == 0; 541 childState.height = newHeight; 542 return childState.height != intrinsicHeight && !childState.hidden; 543 } 544 545 private int getMaxAllowedVisibleChildren() { 546 return getMaxAllowedVisibleChildren(false /* likeCollapsed */); 547 } 548 549 private int getMaxAllowedVisibleChildren(boolean likeCollapsed) { 550 if (!likeCollapsed && (mChildrenExpanded || mNotificationParent.isUserLocked())) { 551 return NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED; 552 } 553 if (!mNotificationParent.isOnKeyguard() 554 && (mNotificationParent.isExpanded() || mNotificationParent.isHeadsUp())) { 555 return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED; 556 } 557 return NUMBER_OF_CHILDREN_WHEN_COLLAPSED; 558 } 559 560 public void applyState(StackScrollState state) { 561 int childCount = mChildren.size(); 562 ViewState tmpState = new ViewState(); 563 float expandFraction = 0.0f; 564 if (mUserLocked) { 565 expandFraction = getGroupExpandFraction(); 566 } 567 final boolean dividersVisible = mUserLocked 568 || mNotificationParent.isGroupExpansionChanging(); 569 for (int i = 0; i < childCount; i++) { 570 ExpandableNotificationRow child = mChildren.get(i); 571 StackViewState viewState = state.getViewStateForView(child); 572 state.applyState(child, viewState); 573 574 // layout the divider 575 View divider = mDividers.get(i); 576 tmpState.initFrom(divider); 577 tmpState.yTranslation = viewState.yTranslation - mDividerHeight; 578 float alpha = mChildrenExpanded && viewState.alpha != 0 ? 0.5f : 0; 579 if (mUserLocked && viewState.alpha != 0) { 580 alpha = NotificationUtils.interpolate(0, 0.5f, 581 Math.min(viewState.alpha, expandFraction)); 582 } 583 tmpState.hidden = !dividersVisible; 584 tmpState.alpha = alpha; 585 state.applyViewState(divider, tmpState); 586 // There is no fake shadow to be drawn on the children 587 child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0); 588 } 589 if (mOverflowNumber != null) { 590 state.applyViewState(mOverflowNumber, mGroupOverFlowState); 591 mNeverAppliedGroupState = false; 592 } 593 if (mNotificationHeader != null) { 594 state.applyViewState(mNotificationHeader, mHeaderViewState); 595 } 596 } 597 598 /** 599 * This is called when the children expansion has changed and positions the children properly 600 * for an appear animation. 601 * 602 * @param state the new state we animate to 603 */ 604 public void prepareExpansionChanged(StackScrollState state) { 605 // TODO: do something that makes sense, like placing the invisible views correctly 606 return; 607 } 608 609 public void startAnimationToState(StackScrollState state, StackStateAnimator stateAnimator, 610 long baseDelay, long duration) { 611 int childCount = mChildren.size(); 612 ViewState tmpState = new ViewState(); 613 float expandFraction = getGroupExpandFraction(); 614 final boolean dividersVisible = mUserLocked 615 || mNotificationParent.isGroupExpansionChanging(); 616 for (int i = childCount - 1; i >= 0; i--) { 617 ExpandableNotificationRow child = mChildren.get(i); 618 StackViewState viewState = state.getViewStateForView(child); 619 stateAnimator.startStackAnimations(child, viewState, state, -1, baseDelay); 620 621 // layout the divider 622 View divider = mDividers.get(i); 623 tmpState.initFrom(divider); 624 tmpState.yTranslation = viewState.yTranslation - mDividerHeight; 625 float alpha = mChildrenExpanded && viewState.alpha != 0 ? 0.5f : 0; 626 if (mUserLocked && viewState.alpha != 0) { 627 alpha = NotificationUtils.interpolate(0, 0.5f, 628 Math.min(viewState.alpha, expandFraction)); 629 } 630 tmpState.hidden = !dividersVisible; 631 tmpState.alpha = alpha; 632 stateAnimator.startViewAnimations(divider, tmpState, baseDelay, duration); 633 // There is no fake shadow to be drawn on the children 634 child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0); 635 } 636 if (mOverflowNumber != null) { 637 if (mNeverAppliedGroupState) { 638 float alpha = mGroupOverFlowState.alpha; 639 mGroupOverFlowState.alpha = 0; 640 state.applyViewState(mOverflowNumber, mGroupOverFlowState); 641 mGroupOverFlowState.alpha = alpha; 642 mNeverAppliedGroupState = false; 643 } 644 stateAnimator.startViewAnimations(mOverflowNumber, mGroupOverFlowState, 645 baseDelay, duration); 646 } 647 if (mNotificationHeader != null) { 648 state.applyViewState(mNotificationHeader, mHeaderViewState); 649 } 650 } 651 652 public ExpandableNotificationRow getViewAtPosition(float y) { 653 // find the view under the pointer, accounting for GONE views 654 final int count = mChildren.size(); 655 for (int childIdx = 0; childIdx < count; childIdx++) { 656 ExpandableNotificationRow slidingChild = mChildren.get(childIdx); 657 float childTop = slidingChild.getTranslationY(); 658 float top = childTop + slidingChild.getClipTopAmount(); 659 float bottom = childTop + slidingChild.getActualHeight(); 660 if (y >= top && y <= bottom) { 661 return slidingChild; 662 } 663 } 664 return null; 665 } 666 667 public void setChildrenExpanded(boolean childrenExpanded) { 668 mChildrenExpanded = childrenExpanded; 669 updateExpansionStates(); 670 if (mNotificationHeader != null) { 671 mNotificationHeader.setExpanded(childrenExpanded); 672 } 673 final int count = mChildren.size(); 674 for (int childIdx = 0; childIdx < count; childIdx++) { 675 ExpandableNotificationRow child = mChildren.get(childIdx); 676 child.setChildrenExpanded(childrenExpanded, false); 677 } 678 } 679 680 public void setNotificationParent(ExpandableNotificationRow parent) { 681 mNotificationParent = parent; 682 mHeaderUtil = new NotificationHeaderUtil(mNotificationParent); 683 } 684 685 public ExpandableNotificationRow getNotificationParent() { 686 return mNotificationParent; 687 } 688 689 public NotificationHeaderView getHeaderView() { 690 return mNotificationHeader; 691 } 692 693 public void updateHeaderVisibility(int visiblity) { 694 if (mNotificationHeader != null) { 695 mNotificationHeader.setVisibility(visiblity); 696 } 697 } 698 699 /** 700 * Called when a groups expansion changes to adjust the background of the header view. 701 * 702 * @param expanded whether the group is expanded. 703 */ 704 public void updateHeaderForExpansion(boolean expanded) { 705 if (mNotificationHeader != null) { 706 if (expanded) { 707 ColorDrawable cd = new ColorDrawable(); 708 cd.setColor(mNotificationParent.calculateBgColor()); 709 mNotificationHeader.setHeaderBackgroundDrawable(cd); 710 } else { 711 mNotificationHeader.setHeaderBackgroundDrawable(null); 712 } 713 } 714 } 715 716 public int getMaxContentHeight() { 717 int maxContentHeight = mNotificationHeaderMargin + mNotificatonTopPadding; 718 int visibleChildren = 0; 719 int childCount = mChildren.size(); 720 for (int i = 0; i < childCount; i++) { 721 if (visibleChildren >= NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED) { 722 break; 723 } 724 ExpandableNotificationRow child = mChildren.get(i); 725 float childHeight = child.isExpanded(true /* allowOnKeyguard */) 726 ? child.getMaxExpandHeight() 727 : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */); 728 maxContentHeight += childHeight; 729 visibleChildren++; 730 } 731 if (visibleChildren > 0) { 732 maxContentHeight += visibleChildren * mDividerHeight; 733 } 734 return maxContentHeight; 735 } 736 737 public void setActualHeight(int actualHeight) { 738 if (!mUserLocked) { 739 return; 740 } 741 mActualHeight = actualHeight; 742 float fraction = getGroupExpandFraction(); 743 int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */); 744 int childCount = mChildren.size(); 745 for (int i = 0; i < childCount; i++) { 746 ExpandableNotificationRow child = mChildren.get(i); 747 float childHeight = child.isExpanded(true /* allowOnKeyguard */) 748 ? child.getMaxExpandHeight() 749 : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */); 750 if (i < maxAllowedVisibleChildren) { 751 float singleLineHeight = child.getShowingLayout().getMinHeight( 752 false /* likeGroupExpanded */); 753 child.setActualHeight((int) NotificationUtils.interpolate(singleLineHeight, 754 childHeight, fraction), false); 755 } else { 756 child.setActualHeight((int) childHeight, false); 757 } 758 } 759 } 760 761 public float getGroupExpandFraction() { 762 int visibleChildrenExpandedHeight = getVisibleChildrenExpandHeight(); 763 int minExpandHeight = getCollapsedHeight(); 764 float factor = (mActualHeight - minExpandHeight) 765 / (float) (visibleChildrenExpandedHeight - minExpandHeight); 766 return Math.max(0.0f, Math.min(1.0f, factor)); 767 } 768 769 private int getVisibleChildrenExpandHeight() { 770 int intrinsicHeight = mNotificationHeaderMargin + mNotificatonTopPadding + mDividerHeight; 771 int visibleChildren = 0; 772 int childCount = mChildren.size(); 773 int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */); 774 for (int i = 0; i < childCount; i++) { 775 if (visibleChildren >= maxAllowedVisibleChildren) { 776 break; 777 } 778 ExpandableNotificationRow child = mChildren.get(i); 779 float childHeight = child.isExpanded(true /* allowOnKeyguard */) 780 ? child.getMaxExpandHeight() 781 : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */); 782 intrinsicHeight += childHeight; 783 visibleChildren++; 784 } 785 return intrinsicHeight; 786 } 787 788 public int getMinHeight() { 789 return getMinHeight(NUMBER_OF_CHILDREN_WHEN_COLLAPSED); 790 } 791 792 public int getCollapsedHeight() { 793 return getMinHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */)); 794 } 795 796 private int getMinHeight(int maxAllowedVisibleChildren) { 797 int minExpandHeight = mNotificationHeaderMargin; 798 int visibleChildren = 0; 799 boolean firstChild = true; 800 int childCount = mChildren.size(); 801 for (int i = 0; i < childCount; i++) { 802 if (visibleChildren >= maxAllowedVisibleChildren) { 803 break; 804 } 805 if (!firstChild) { 806 minExpandHeight += mChildPadding; 807 } else { 808 firstChild = false; 809 } 810 ExpandableNotificationRow child = mChildren.get(i); 811 minExpandHeight += child.getSingleLineView().getHeight(); 812 visibleChildren++; 813 } 814 minExpandHeight += mCollapsedBottompadding; 815 return minExpandHeight; 816 } 817 818 public void setDark(boolean dark, boolean fade, long delay) { 819 if (mOverflowNumber != null) { 820 mOverflowInvertHelper.setInverted(dark, fade, delay); 821 } 822 mNotificationHeaderWrapper.setDark(dark, fade, delay); 823 } 824 825 public void reInflateViews(OnClickListener listener, StatusBarNotification notification) { 826 removeView(mNotificationHeader); 827 mNotificationHeader = null; 828 recreateNotificationHeader(listener, notification); 829 initDimens(); 830 for (int i = 0; i < mDividers.size(); i++) { 831 View prevDivider = mDividers.get(i); 832 int index = indexOfChild(prevDivider); 833 removeView(prevDivider); 834 View divider = inflateDivider(); 835 addView(divider, index); 836 mDividers.set(i, divider); 837 } 838 removeView(mOverflowNumber); 839 mOverflowNumber = null; 840 mOverflowInvertHelper = null; 841 mGroupOverFlowState = null; 842 updateGroupOverflow(); 843 } 844 845 public void setUserLocked(boolean userLocked) { 846 mUserLocked = userLocked; 847 int childCount = mChildren.size(); 848 for (int i = 0; i < childCount; i++) { 849 ExpandableNotificationRow child = mChildren.get(i); 850 child.setUserLocked(userLocked); 851 } 852 } 853 854 public void onNotificationUpdated() { 855 mHybridGroupManager.setOverflowNumberColor(mOverflowNumber, 856 mNotificationParent.getNotificationColor()); 857 } 858 859 public int getPositionInLinearLayout(View childInGroup) { 860 int position = mNotificationHeaderMargin + mNotificatonTopPadding; 861 862 for (int i = 0; i < mChildren.size(); i++) { 863 ExpandableNotificationRow child = mChildren.get(i); 864 boolean notGone = child.getVisibility() != View.GONE; 865 if (notGone) { 866 position += mDividerHeight; 867 } 868 if (child == childInGroup) { 869 return position; 870 } 871 if (notGone) { 872 position += child.getIntrinsicHeight(); 873 } 874 } 875 return 0; 876 } 877 } 878