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.notification.stack; 18 19 import android.app.Notification; 20 import android.content.Context; 21 import android.content.res.Configuration; 22 import android.content.res.Resources; 23 import android.content.res.TypedArray; 24 import android.graphics.Canvas; 25 import android.graphics.Path; 26 import android.graphics.Path.Direction; 27 import android.graphics.drawable.ColorDrawable; 28 import android.os.Trace; 29 import android.service.notification.StatusBarNotification; 30 import android.util.AttributeSet; 31 import android.util.Log; 32 import android.view.ContextThemeWrapper; 33 import android.view.LayoutInflater; 34 import android.view.NotificationHeaderView; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.widget.RemoteViews; 38 import android.widget.TextView; 39 40 import androidx.annotation.NonNull; 41 import androidx.annotation.Nullable; 42 43 import com.android.internal.annotations.VisibleForTesting; 44 import com.android.internal.widget.NotificationExpandButton; 45 import com.android.systemui.R; 46 import com.android.systemui.statusbar.CrossFadeHelper; 47 import com.android.systemui.statusbar.NotificationGroupingUtil; 48 import com.android.systemui.statusbar.notification.FeedbackIcon; 49 import com.android.systemui.statusbar.notification.NotificationFadeAware; 50 import com.android.systemui.statusbar.notification.NotificationUtils; 51 import com.android.systemui.statusbar.notification.Roundable; 52 import com.android.systemui.statusbar.notification.RoundableState; 53 import com.android.systemui.statusbar.notification.SourceType; 54 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 55 import com.android.systemui.statusbar.notification.row.ExpandableView; 56 import com.android.systemui.statusbar.notification.row.HybridGroupManager; 57 import com.android.systemui.statusbar.notification.row.HybridNotificationView; 58 import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper; 59 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; 60 61 import java.util.ArrayList; 62 import java.util.List; 63 64 /** 65 * A container containing child notifications 66 */ 67 public class NotificationChildrenContainer extends ViewGroup 68 implements NotificationFadeAware, Roundable { 69 70 private static final String TAG = "NotificationChildrenContainer"; 71 72 @VisibleForTesting 73 static final int NUMBER_OF_CHILDREN_WHEN_COLLAPSED = 2; 74 @VisibleForTesting 75 static final int NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED = 5; 76 public static final int NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED = 8; 77 private static final AnimationProperties ALPHA_FADE_IN = new AnimationProperties() { 78 private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha(); 79 80 @Override 81 public AnimationFilter getAnimationFilter() { 82 return mAnimationFilter; 83 } 84 }.setDuration(200); 85 private static final SourceType FROM_PARENT = SourceType.from("FromParent(NCC)"); 86 87 private final List<View> mDividers = new ArrayList<>(); 88 private final List<ExpandableNotificationRow> mAttachedChildren = new ArrayList<>(); 89 private final HybridGroupManager mHybridGroupManager; 90 private int mChildPadding; 91 private int mDividerHeight; 92 private float mDividerAlpha; 93 private int mNotificationHeaderMargin; 94 95 private int mNotificationTopPadding; 96 private float mCollapsedBottomPadding; 97 private boolean mChildrenExpanded; 98 private ExpandableNotificationRow mContainingNotification; 99 private TextView mOverflowNumber; 100 private ViewState mGroupOverFlowState; 101 private int mRealHeight; 102 private boolean mUserLocked; 103 private int mActualHeight; 104 private boolean mNeverAppliedGroupState; 105 private int mHeaderHeight; 106 107 /** 108 * Whether or not individual notifications that are part of this container will have shadows. 109 */ 110 private boolean mEnableShadowOnChildNotifications; 111 112 private NotificationHeaderView mNotificationHeader; 113 private NotificationHeaderViewWrapper mNotificationHeaderWrapper; 114 private NotificationHeaderView mNotificationHeaderLowPriority; 115 private NotificationHeaderViewWrapper mNotificationHeaderWrapperLowPriority; 116 private NotificationGroupingUtil mGroupingUtil; 117 private ViewState mHeaderViewState; 118 private int mClipBottomAmount; 119 private boolean mIsLowPriority; 120 private OnClickListener mHeaderClickListener; 121 private ViewGroup mCurrentHeader; 122 private boolean mIsConversation; 123 private Path mChildClipPath = null; 124 private final Path mHeaderPath = new Path(); 125 private boolean mShowGroupCountInExpander; 126 private boolean mShowDividersWhenExpanded; 127 private boolean mHideDividersDuringExpand; 128 private int mTranslationForHeader; 129 private int mCurrentHeaderTranslation = 0; 130 private float mHeaderVisibleAmount = 1.0f; 131 private int mUntruncatedChildCount; 132 private boolean mContainingNotificationIsFaded = false; 133 private RoundableState mRoundableState; 134 135 private NotificationChildrenContainerLogger mLogger; 136 NotificationChildrenContainer(Context context)137 public NotificationChildrenContainer(Context context) { 138 this(context, null); 139 } 140 NotificationChildrenContainer(Context context, AttributeSet attrs)141 public NotificationChildrenContainer(Context context, AttributeSet attrs) { 142 this(context, attrs, 0); 143 } 144 NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr)145 public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr) { 146 this(context, attrs, defStyleAttr, 0); 147 } 148 NotificationChildrenContainer( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)149 public NotificationChildrenContainer( 150 Context context, 151 AttributeSet attrs, 152 int defStyleAttr, 153 int defStyleRes) { 154 super(context, attrs, defStyleAttr, defStyleRes); 155 mHybridGroupManager = new HybridGroupManager(getContext()); 156 mRoundableState = new RoundableState(this, this, 0f); 157 initDimens(); 158 setClipChildren(false); 159 } 160 initDimens()161 private void initDimens() { 162 Resources res = getResources(); 163 mChildPadding = res.getDimensionPixelOffset(R.dimen.notification_children_padding); 164 mDividerHeight = res.getDimensionPixelOffset( 165 R.dimen.notification_children_container_divider_height); 166 mDividerAlpha = res.getFloat(R.dimen.notification_divider_alpha); 167 mNotificationHeaderMargin = res.getDimensionPixelOffset( 168 R.dimen.notification_children_container_margin_top); 169 mNotificationTopPadding = res.getDimensionPixelOffset( 170 R.dimen.notification_children_container_top_padding); 171 mHeaderHeight = mNotificationHeaderMargin + mNotificationTopPadding; 172 mCollapsedBottomPadding = res.getDimensionPixelOffset( 173 R.dimen.notification_children_collapsed_bottom_padding); 174 mEnableShadowOnChildNotifications = 175 res.getBoolean(R.bool.config_enableShadowOnChildNotifications); 176 mShowGroupCountInExpander = 177 res.getBoolean(R.bool.config_showNotificationGroupCountInExpander); 178 mShowDividersWhenExpanded = 179 res.getBoolean(R.bool.config_showDividersWhenGroupNotificationExpanded); 180 mHideDividersDuringExpand = 181 res.getBoolean(R.bool.config_hideDividersDuringExpand); 182 mTranslationForHeader = res.getDimensionPixelOffset( 183 com.android.internal.R.dimen.notification_content_margin) 184 - mNotificationHeaderMargin; 185 mHybridGroupManager.initDimens(); 186 } 187 188 @NonNull 189 @Override getRoundableState()190 public RoundableState getRoundableState() { 191 return mRoundableState; 192 } 193 194 @Override getClipHeight()195 public int getClipHeight() { 196 return Math.max(mActualHeight - mClipBottomAmount, 0); 197 } 198 199 @Override onLayout(boolean changed, int l, int t, int r, int b)200 protected void onLayout(boolean changed, int l, int t, int r, int b) { 201 int childCount = 202 Math.min(mAttachedChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED); 203 for (int i = 0; i < childCount; i++) { 204 View child = mAttachedChildren.get(i); 205 // We need to layout all children even the GONE ones, such that the heights are 206 // calculated correctly as they are used to calculate how many we can fit on the screen 207 child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight()); 208 mDividers.get(i).layout(0, 0, getWidth(), mDividerHeight); 209 } 210 if (mOverflowNumber != null) { 211 boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; 212 int left = (isRtl ? 0 : getWidth() - mOverflowNumber.getMeasuredWidth()); 213 int right = left + mOverflowNumber.getMeasuredWidth(); 214 mOverflowNumber.layout(left, 0, right, mOverflowNumber.getMeasuredHeight()); 215 } 216 if (mNotificationHeader != null) { 217 mNotificationHeader.layout(0, 0, mNotificationHeader.getMeasuredWidth(), 218 mNotificationHeader.getMeasuredHeight()); 219 } 220 if (mNotificationHeaderLowPriority != null) { 221 mNotificationHeaderLowPriority.layout(0, 0, 222 mNotificationHeaderLowPriority.getMeasuredWidth(), 223 mNotificationHeaderLowPriority.getMeasuredHeight()); 224 } 225 } 226 227 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)228 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 229 Trace.beginSection("NotificationChildrenContainer#onMeasure"); 230 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 231 boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY; 232 boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST; 233 int size = MeasureSpec.getSize(heightMeasureSpec); 234 int newHeightSpec = heightMeasureSpec; 235 if (hasFixedHeight || isHeightLimited) { 236 newHeightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST); 237 } 238 int width = MeasureSpec.getSize(widthMeasureSpec); 239 if (mOverflowNumber != null) { 240 mOverflowNumber.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), 241 newHeightSpec); 242 } 243 int dividerHeightSpec = MeasureSpec.makeMeasureSpec(mDividerHeight, MeasureSpec.EXACTLY); 244 int height = mNotificationHeaderMargin + mNotificationTopPadding; 245 int childCount = 246 Math.min(mAttachedChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED); 247 int collapsedChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */); 248 int overflowIndex = childCount > collapsedChildren ? collapsedChildren - 1 : -1; 249 for (int i = 0; i < childCount; i++) { 250 ExpandableNotificationRow child = mAttachedChildren.get(i); 251 // We need to measure all children even the GONE ones, such that the heights are 252 // calculated correctly as they are used to calculate how many we can fit on the screen. 253 boolean isOverflow = i == overflowIndex; 254 child.setSingleLineWidthIndention(isOverflow && mOverflowNumber != null 255 ? mOverflowNumber.getMeasuredWidth() : 0); 256 child.measure(widthMeasureSpec, newHeightSpec); 257 // layout the divider 258 View divider = mDividers.get(i); 259 divider.measure(widthMeasureSpec, dividerHeightSpec); 260 if (child.getVisibility() != GONE) { 261 height += child.getMeasuredHeight() + mDividerHeight; 262 } 263 } 264 mRealHeight = height; 265 if (heightMode != MeasureSpec.UNSPECIFIED) { 266 height = Math.min(height, size); 267 } 268 269 int headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY); 270 if (mNotificationHeader != null) { 271 mNotificationHeader.measure(widthMeasureSpec, headerHeightSpec); 272 } 273 if (mNotificationHeaderLowPriority != null) { 274 mNotificationHeaderLowPriority.measure(widthMeasureSpec, headerHeightSpec); 275 } 276 277 setMeasuredDimension(width, height); 278 Trace.endSection(); 279 } 280 281 @Override hasOverlappingRendering()282 public boolean hasOverlappingRendering() { 283 return false; 284 } 285 286 @Override pointInView(float localX, float localY, float slop)287 public boolean pointInView(float localX, float localY, float slop) { 288 return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) && 289 localY < (mRealHeight + slop); 290 } 291 292 /** 293 * Set the untruncated number of children in the group so that the view can update the UI 294 * appropriately. Note that this may differ from the number of views attached as truncated 295 * children will not have views. 296 */ setUntruncatedChildCount(int childCount)297 public void setUntruncatedChildCount(int childCount) { 298 mUntruncatedChildCount = childCount; 299 updateGroupOverflow(); 300 } 301 302 /** 303 * Set the notification time in the group so that the view can show the latest event in the UI 304 * appropriately. 305 */ setNotificationGroupWhen(long whenMillis)306 public void setNotificationGroupWhen(long whenMillis) { 307 if (mNotificationHeaderWrapper != null) { 308 mNotificationHeaderWrapper.setNotificationWhen(whenMillis); 309 } 310 if (mNotificationHeaderWrapperLowPriority != null) { 311 mNotificationHeaderWrapperLowPriority.setNotificationWhen(whenMillis); 312 } 313 } 314 315 /** 316 * Add a child notification to this view. 317 * 318 * @param row the row to add 319 * @param childIndex the index to add it at, if -1 it will be added at the end 320 */ addNotification(ExpandableNotificationRow row, int childIndex)321 public void addNotification(ExpandableNotificationRow row, int childIndex) { 322 ensureRemovedFromTransientContainer(row); 323 int newIndex = childIndex < 0 ? mAttachedChildren.size() : childIndex; 324 mAttachedChildren.add(newIndex, row); 325 addView(row); 326 row.setUserLocked(mUserLocked); 327 328 View divider = inflateDivider(); 329 addView(divider); 330 mDividers.add(newIndex, divider); 331 332 row.setContentTransformationAmount(0, false /* isLastChild */); 333 row.setNotificationFaded(mContainingNotificationIsFaded); 334 335 // It doesn't make sense to keep old animations around, lets cancel them! 336 ExpandableViewState viewState = row.getViewState(); 337 if (viewState != null) { 338 viewState.cancelAnimations(row); 339 row.cancelAppearDrawing(); 340 } 341 342 applyRoundnessAndInvalidate(); 343 } 344 345 private void ensureRemovedFromTransientContainer(View v) { 346 if (v.getParent() != null && v instanceof ExpandableView) { 347 // If the child is animating away, it will still have a parent, so detach it first 348 // TODO: We should really cancel the active animations here. This will 349 // happen automatically when the view's intro animation starts, but 350 // it's a fragile link. 351 ((ExpandableView) v).removeFromTransientContainerForAdditionTo(this); 352 } 353 } 354 355 public void removeNotification(ExpandableNotificationRow row) { 356 int childIndex = mAttachedChildren.indexOf(row); 357 mAttachedChildren.remove(row); 358 removeView(row); 359 360 final View divider = mDividers.remove(childIndex); 361 removeView(divider); 362 getOverlay().add(divider); 363 CrossFadeHelper.fadeOut(divider, new Runnable() { 364 @Override 365 public void run() { 366 getOverlay().remove(divider); 367 } 368 }); 369 370 row.setSystemChildExpanded(false); 371 row.setNotificationFaded(false); 372 row.setUserLocked(false); 373 if (!row.isRemoved()) { 374 mGroupingUtil.restoreChildNotification(row); 375 } 376 377 row.requestRoundnessReset(FROM_PARENT, /* animate = */ false); 378 applyRoundnessAndInvalidate(); 379 } 380 381 /** 382 * @return The number of notification children in the container. 383 */ 384 public int getNotificationChildCount() { 385 return mAttachedChildren.size(); 386 } 387 388 public void recreateNotificationHeader(OnClickListener listener, boolean isConversation) { 389 Trace.beginSection("NotifChildCont#recreateHeader"); 390 mHeaderClickListener = listener; 391 mIsConversation = isConversation; 392 StatusBarNotification notification = mContainingNotification.getEntry().getSbn(); 393 final Notification.Builder builder = Notification.Builder.recoverBuilder(getContext(), 394 notification.getNotification()); 395 RemoteViews header = builder.makeNotificationGroupHeader(); 396 if (mNotificationHeader == null) { 397 mNotificationHeader = (NotificationHeaderView) header.apply(getContext(), this); 398 mNotificationHeader.findViewById(com.android.internal.R.id.expand_button) 399 .setVisibility(VISIBLE); 400 mNotificationHeader.setOnClickListener(mHeaderClickListener); 401 mNotificationHeaderWrapper = 402 (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap( 403 getContext(), 404 mNotificationHeader, 405 mContainingNotification); 406 mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); 407 addView(mNotificationHeader, 0); 408 invalidate(); 409 } else { 410 header.reapply(getContext(), mNotificationHeader); 411 } 412 mNotificationHeaderWrapper.setExpanded(mChildrenExpanded); 413 mNotificationHeaderWrapper.onContentUpdated(mContainingNotification); 414 recreateLowPriorityHeader(builder, isConversation); 415 updateHeaderVisibility(false /* animate */); 416 updateChildrenAppearance(); 417 Trace.endSection(); 418 } 419 420 /** 421 * Recreate the low-priority header. 422 * 423 * @param builder a builder to reuse. Otherwise the builder will be recovered. 424 */ 425 private void recreateLowPriorityHeader(Notification.Builder builder, boolean isConversation) { 426 RemoteViews header; 427 StatusBarNotification notification = mContainingNotification.getEntry().getSbn(); 428 if (mIsLowPriority) { 429 if (builder == null) { 430 builder = Notification.Builder.recoverBuilder(getContext(), 431 notification.getNotification()); 432 } 433 header = builder.makeLowPriorityContentView(true /* useRegularSubtext */); 434 if (mNotificationHeaderLowPriority == null) { 435 mNotificationHeaderLowPriority = (NotificationHeaderView) header.apply(getContext(), 436 this); 437 mNotificationHeaderLowPriority.findViewById(com.android.internal.R.id.expand_button) 438 .setVisibility(VISIBLE); 439 mNotificationHeaderLowPriority.setOnClickListener(mHeaderClickListener); 440 mNotificationHeaderWrapperLowPriority = 441 (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap( 442 getContext(), 443 mNotificationHeaderLowPriority, 444 mContainingNotification); 445 mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); 446 addView(mNotificationHeaderLowPriority, 0); 447 invalidate(); 448 } else { 449 header.reapply(getContext(), mNotificationHeaderLowPriority); 450 } 451 mNotificationHeaderWrapperLowPriority.onContentUpdated(mContainingNotification); 452 resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, calculateDesiredHeader()); 453 } else { 454 removeView(mNotificationHeaderLowPriority); 455 mNotificationHeaderLowPriority = null; 456 mNotificationHeaderWrapperLowPriority = null; 457 } 458 } 459 460 /** 461 * Update the appearance of the children to reduce redundancies. 462 */ 463 public void updateChildrenAppearance() { 464 mGroupingUtil.updateChildrenAppearance(); 465 } 466 467 private void setExpandButtonNumber(NotificationViewWrapper wrapper) { 468 View expandButton = wrapper == null 469 ? null : wrapper.getExpandButton(); 470 if (expandButton instanceof NotificationExpandButton) { 471 ((NotificationExpandButton) expandButton).setNumber(mUntruncatedChildCount); 472 } 473 } 474 475 public void updateGroupOverflow() { 476 if (mShowGroupCountInExpander) { 477 setExpandButtonNumber(mNotificationHeaderWrapper); 478 setExpandButtonNumber(mNotificationHeaderWrapperLowPriority); 479 return; 480 } 481 int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */); 482 if (mUntruncatedChildCount > maxAllowedVisibleChildren) { 483 int number = mUntruncatedChildCount - maxAllowedVisibleChildren; 484 mOverflowNumber = mHybridGroupManager.bindOverflowNumber(mOverflowNumber, number, this); 485 if (mGroupOverFlowState == null) { 486 mGroupOverFlowState = new ViewState(); 487 mNeverAppliedGroupState = true; 488 } 489 } else if (mOverflowNumber != null) { 490 removeView(mOverflowNumber); 491 if (isShown() && isAttachedToWindow()) { 492 final View removedOverflowNumber = mOverflowNumber; 493 addTransientView(removedOverflowNumber, getTransientViewCount()); 494 CrossFadeHelper.fadeOut(removedOverflowNumber, new Runnable() { 495 @Override 496 public void run() { 497 removeTransientView(removedOverflowNumber); 498 } 499 }); 500 } 501 mOverflowNumber = null; 502 mGroupOverFlowState = null; 503 } 504 } 505 506 @Override 507 protected void onConfigurationChanged(Configuration newConfig) { 508 super.onConfigurationChanged(newConfig); 509 updateGroupOverflow(); 510 } 511 512 private View inflateDivider() { 513 return LayoutInflater.from(mContext).inflate( 514 R.layout.notification_children_divider, this, false); 515 } 516 517 /** 518 * Get notification children that are attached currently. 519 */ 520 public List<ExpandableNotificationRow> getAttachedChildren() { 521 return mAttachedChildren; 522 } 523 524 /** 525 * Sets the alpha on the content, while leaving the background of the container itself as is. 526 * 527 * @param alpha alpha value to apply to the content 528 */ 529 public void setContentAlpha(float alpha) { 530 for (int i = 0; i < mNotificationHeader.getChildCount(); i++) { 531 mNotificationHeader.getChildAt(i).setAlpha(alpha); 532 } 533 for (ExpandableNotificationRow child : getAttachedChildren()) { 534 child.setContentAlpha(alpha); 535 } 536 } 537 538 /** 539 * To be called any time the rows have been updated 540 */ 541 public void updateExpansionStates() { 542 if (mChildrenExpanded || mUserLocked) { 543 // we don't modify it the group is expanded or if we are expanding it 544 return; 545 } 546 int size = mAttachedChildren.size(); 547 for (int i = 0; i < size; i++) { 548 ExpandableNotificationRow child = mAttachedChildren.get(i); 549 child.setSystemChildExpanded(i == 0 && size == 1); 550 } 551 } 552 553 /** 554 * @return the intrinsic size of this children container, i.e the natural fully expanded state 555 */ 556 public int getIntrinsicHeight() { 557 int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(); 558 return getIntrinsicHeight(maxAllowedVisibleChildren); 559 } 560 561 /** 562 * @return the intrinsic height with a number of children given 563 * in @param maxAllowedVisibleChildren 564 */ 565 private int getIntrinsicHeight(float maxAllowedVisibleChildren) { 566 if (showingAsLowPriority()) { 567 return mNotificationHeaderLowPriority.getHeight(); 568 } 569 int intrinsicHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation; 570 int visibleChildren = 0; 571 int childCount = mAttachedChildren.size(); 572 boolean firstChild = true; 573 float expandFactor = 0; 574 if (mUserLocked) { 575 expandFactor = getGroupExpandFraction(); 576 } 577 boolean childrenExpanded = mChildrenExpanded; 578 for (int i = 0; i < childCount; i++) { 579 if (visibleChildren >= maxAllowedVisibleChildren) { 580 break; 581 } 582 if (!firstChild) { 583 if (mUserLocked) { 584 intrinsicHeight += NotificationUtils.interpolate(mChildPadding, mDividerHeight, 585 expandFactor); 586 } else { 587 intrinsicHeight += childrenExpanded ? mDividerHeight : mChildPadding; 588 } 589 } else { 590 if (mUserLocked) { 591 intrinsicHeight += NotificationUtils.interpolate( 592 0, 593 mNotificationTopPadding + mDividerHeight, 594 expandFactor); 595 } else { 596 intrinsicHeight += childrenExpanded 597 ? mNotificationTopPadding + mDividerHeight 598 : 0; 599 } 600 firstChild = false; 601 } 602 ExpandableNotificationRow child = mAttachedChildren.get(i); 603 intrinsicHeight += child.getIntrinsicHeight(); 604 visibleChildren++; 605 } 606 if (mUserLocked) { 607 intrinsicHeight += NotificationUtils.interpolate(mCollapsedBottomPadding, 0.0f, 608 expandFactor); 609 } else if (!childrenExpanded) { 610 intrinsicHeight += mCollapsedBottomPadding; 611 } 612 return intrinsicHeight; 613 } 614 615 /** 616 * Update the state of all its children based on a linear layout algorithm. 617 * 618 * @param parentState the state of the parent 619 */ 620 public void updateState(ExpandableViewState parentState) { 621 int childCount = mAttachedChildren.size(); 622 int yPosition = mNotificationHeaderMargin + mCurrentHeaderTranslation; 623 boolean firstChild = true; 624 int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(); 625 int lastVisibleIndex = maxAllowedVisibleChildren - 1; 626 int firstOverflowIndex = lastVisibleIndex + 1; 627 float expandFactor = 0; 628 boolean expandingToExpandedGroup = mUserLocked && !showingAsLowPriority(); 629 if (mUserLocked) { 630 expandFactor = getGroupExpandFraction(); 631 firstOverflowIndex = getMaxAllowedVisibleChildren(true /* likeCollapsed */); 632 } 633 634 boolean childrenExpandedAndNotAnimating = mChildrenExpanded 635 && !mContainingNotification.isGroupExpansionChanging(); 636 int launchTransitionCompensation = 0; 637 for (int i = 0; i < childCount; i++) { 638 ExpandableNotificationRow child = mAttachedChildren.get(i); 639 if (!firstChild) { 640 if (expandingToExpandedGroup) { 641 yPosition += NotificationUtils.interpolate(mChildPadding, mDividerHeight, 642 expandFactor); 643 } else { 644 yPosition += mChildrenExpanded ? mDividerHeight : mChildPadding; 645 } 646 } else { 647 if (expandingToExpandedGroup) { 648 yPosition += NotificationUtils.interpolate( 649 0, 650 mNotificationTopPadding + mDividerHeight, 651 expandFactor); 652 } else { 653 yPosition += mChildrenExpanded ? mNotificationTopPadding + mDividerHeight : 0; 654 } 655 firstChild = false; 656 } 657 658 ExpandableViewState childState = child.getViewState(); 659 int intrinsicHeight = child.getIntrinsicHeight(); 660 childState.height = intrinsicHeight; 661 childState.setYTranslation(yPosition + launchTransitionCompensation); 662 childState.hidden = false; 663 if (child.isExpandAnimationRunning() || mContainingNotification.hasExpandingChild()) { 664 // Not modifying translationZ during launch animation. The translationZ of the 665 // expanding child is handled inside ExpandableNotificationRow and the translationZ 666 // of the other children inside the group should remain unchanged. In particular, 667 // they should not take over the translationZ of the parent, since the parent has 668 // a positive translationZ set only for the expanding child to be drawn above other 669 // notifications. 670 childState.setZTranslation(child.getTranslationZ()); 671 } else if (childrenExpandedAndNotAnimating && mEnableShadowOnChildNotifications) { 672 // When the group is expanded, the children cast the shadows rather than the parent 673 // so use the parent's elevation here. 674 childState.setZTranslation(parentState.getZTranslation()); 675 } else { 676 childState.setZTranslation(0); 677 } 678 childState.dimmed = parentState.dimmed; 679 childState.hideSensitive = parentState.hideSensitive; 680 childState.belowSpeedBump = parentState.belowSpeedBump; 681 childState.clipTopAmount = 0; 682 childState.setAlpha(0); 683 if (i < firstOverflowIndex) { 684 childState.setAlpha(showingAsLowPriority() ? expandFactor : 1.0f); 685 } else if (expandFactor == 1.0f && i <= lastVisibleIndex) { 686 childState.setAlpha( 687 (mActualHeight - childState.getYTranslation()) / childState.height); 688 childState.setAlpha(Math.max(0.0f, Math.min(1.0f, childState.getAlpha()))); 689 } 690 childState.location = parentState.location; 691 childState.inShelf = parentState.inShelf; 692 yPosition += intrinsicHeight; 693 } 694 if (mOverflowNumber != null) { 695 ExpandableNotificationRow overflowView = mAttachedChildren.get(Math.min( 696 getMaxAllowedVisibleChildren(true /* likeCollapsed */), childCount) - 1); 697 mGroupOverFlowState.copyFrom(overflowView.getViewState()); 698 699 if (!mChildrenExpanded) { 700 HybridNotificationView alignView = overflowView.getSingleLineView(); 701 if (alignView != null) { 702 View mirrorView = alignView.getTextView(); 703 if (mirrorView.getVisibility() == GONE) { 704 mirrorView = alignView.getTitleView(); 705 } 706 if (mirrorView.getVisibility() == GONE) { 707 mirrorView = alignView; 708 } 709 mGroupOverFlowState.setAlpha(mirrorView.getAlpha()); 710 float yTranslation = mGroupOverFlowState.getYTranslation() 711 + NotificationUtils.getRelativeYOffset( 712 mirrorView, overflowView); 713 mGroupOverFlowState.setYTranslation(yTranslation); 714 } 715 } else { 716 mGroupOverFlowState.setYTranslation( 717 mGroupOverFlowState.getYTranslation() + mNotificationHeaderMargin); 718 mGroupOverFlowState.setAlpha(0.0f); 719 } 720 } 721 if (mNotificationHeader != null) { 722 if (mHeaderViewState == null) { 723 mHeaderViewState = new ViewState(); 724 } 725 mHeaderViewState.initFrom(mNotificationHeader); 726 727 if (mContainingNotification.hasExpandingChild()) { 728 // Not modifying translationZ during expand animation. 729 mHeaderViewState.setZTranslation(mNotificationHeader.getTranslationZ()); 730 } else if (childrenExpandedAndNotAnimating) { 731 mHeaderViewState.setZTranslation(parentState.getZTranslation()); 732 } else { 733 mHeaderViewState.setZTranslation(0); 734 } 735 mHeaderViewState.setYTranslation(mCurrentHeaderTranslation); 736 mHeaderViewState.setAlpha(mHeaderVisibleAmount); 737 // The hiding is done automatically by the alpha, otherwise we'll pick it up again 738 // in the next frame with the initFrom call above and have an invisible header 739 mHeaderViewState.hidden = false; 740 } 741 } 742 743 /** 744 * When moving into the bottom stack, the bottom visible child in an expanded group adjusts its 745 * height, children in the group after this are gone. 746 * 747 * @param child the child who's height to adjust. 748 * @param parentHeight the height of the parent. 749 * @param childState the state to update. 750 * @param yPosition the yPosition of the view. 751 * @return true if children after this one should be hidden. 752 */ 753 private boolean updateChildStateForExpandedGroup( 754 ExpandableNotificationRow child, 755 int parentHeight, 756 ExpandableViewState childState, 757 int yPosition) { 758 final int top = yPosition + child.getClipTopAmount(); 759 final int intrinsicHeight = child.getIntrinsicHeight(); 760 final int bottom = top + intrinsicHeight; 761 int newHeight = intrinsicHeight; 762 if (bottom >= parentHeight) { 763 // Child is either clipped or gone 764 newHeight = Math.max((parentHeight - top), 0); 765 } 766 childState.hidden = newHeight == 0; 767 childState.height = newHeight; 768 return childState.height != intrinsicHeight && !childState.hidden; 769 } 770 771 @VisibleForTesting 772 int getMaxAllowedVisibleChildren() { 773 return getMaxAllowedVisibleChildren(false /* likeCollapsed */); 774 } 775 776 @VisibleForTesting 777 int getMaxAllowedVisibleChildren(boolean likeCollapsed) { 778 if (!likeCollapsed && (mChildrenExpanded || mContainingNotification.isUserLocked()) 779 && !showingAsLowPriority()) { 780 return NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED; 781 } 782 if (mIsLowPriority 783 || (!mContainingNotification.isOnKeyguard() && mContainingNotification.isExpanded()) 784 || (mContainingNotification.isHeadsUpState() 785 && mContainingNotification.canShowHeadsUp())) { 786 return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED; 787 } 788 return NUMBER_OF_CHILDREN_WHEN_COLLAPSED; 789 } 790 791 /** 792 * Applies state to children. 793 */ 794 public void applyState() { 795 int childCount = mAttachedChildren.size(); 796 ViewState tmpState = new ViewState(); 797 float expandFraction = 0.0f; 798 if (mUserLocked) { 799 expandFraction = getGroupExpandFraction(); 800 } 801 final boolean isExpanding = !showingAsLowPriority() 802 && (mUserLocked || mContainingNotification.isGroupExpansionChanging()); 803 final boolean dividersVisible = (mChildrenExpanded && mShowDividersWhenExpanded) 804 || (isExpanding && !mHideDividersDuringExpand); 805 for (int i = 0; i < childCount; i++) { 806 ExpandableNotificationRow child = mAttachedChildren.get(i); 807 ExpandableViewState viewState = child.getViewState(); 808 viewState.applyToView(child); 809 810 // layout the divider 811 View divider = mDividers.get(i); 812 tmpState.initFrom(divider); 813 tmpState.setYTranslation(viewState.getYTranslation() - mDividerHeight); 814 float alpha = mChildrenExpanded && viewState.getAlpha() != 0 ? mDividerAlpha : 0; 815 if (mUserLocked && !showingAsLowPriority() && viewState.getAlpha() != 0) { 816 alpha = NotificationUtils.interpolate(0, mDividerAlpha, 817 Math.min(viewState.getAlpha(), expandFraction)); 818 } 819 tmpState.hidden = !dividersVisible; 820 tmpState.setAlpha(alpha); 821 tmpState.applyToView(divider); 822 // There is no fake shadow to be drawn on the children 823 child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0); 824 } 825 if (mGroupOverFlowState != null) { 826 mGroupOverFlowState.applyToView(mOverflowNumber); 827 mNeverAppliedGroupState = false; 828 } 829 if (mHeaderViewState != null) { 830 mHeaderViewState.applyToView(mNotificationHeader); 831 } 832 updateChildrenClipping(); 833 } 834 835 private void updateChildrenClipping() { 836 if (mContainingNotification.hasExpandingChild()) { 837 return; 838 } 839 int childCount = mAttachedChildren.size(); 840 int layoutEnd = mContainingNotification.getActualHeight() - mClipBottomAmount; 841 for (int i = 0; i < childCount; i++) { 842 ExpandableNotificationRow child = mAttachedChildren.get(i); 843 if (child.getVisibility() == GONE) { 844 continue; 845 } 846 float childTop = child.getTranslationY(); 847 float childBottom = childTop + child.getActualHeight(); 848 boolean visible = true; 849 int clipBottomAmount = 0; 850 if (childTop > layoutEnd) { 851 visible = false; 852 } else if (childBottom > layoutEnd) { 853 clipBottomAmount = (int) (childBottom - layoutEnd); 854 } 855 856 boolean isVisible = child.getVisibility() == VISIBLE; 857 if (visible != isVisible) { 858 child.setVisibility(visible ? VISIBLE : INVISIBLE); 859 } 860 861 child.setClipBottomAmount(clipBottomAmount); 862 } 863 } 864 865 @Override 866 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 867 boolean isCanvasChanged = false; 868 869 Path clipPath = mChildClipPath; 870 if (clipPath != null) { 871 final float translation; 872 if (child instanceof ExpandableNotificationRow) { 873 ExpandableNotificationRow notificationRow = (ExpandableNotificationRow) child; 874 translation = notificationRow.getTranslation(); 875 } else { 876 translation = child.getTranslationX(); 877 } 878 879 isCanvasChanged = true; 880 canvas.save(); 881 if (translation != 0f) { 882 clipPath.offset(translation, 0f); 883 canvas.clipPath(clipPath); 884 clipPath.offset(-translation, 0f); 885 } else { 886 canvas.clipPath(clipPath); 887 } 888 } 889 890 if (child instanceof NotificationHeaderView 891 && mNotificationHeaderWrapper.hasRoundedCorner()) { 892 float[] radii = mNotificationHeaderWrapper.getUpdatedRadii(); 893 mHeaderPath.reset(); 894 mHeaderPath.addRoundRect( 895 child.getLeft(), 896 child.getTop(), 897 child.getRight(), 898 child.getBottom(), 899 radii, 900 Direction.CW 901 ); 902 if (!isCanvasChanged) { 903 isCanvasChanged = true; 904 canvas.save(); 905 } 906 canvas.clipPath(mHeaderPath); 907 } 908 909 if (isCanvasChanged) { 910 boolean result = super.drawChild(canvas, child, drawingTime); 911 canvas.restore(); 912 return result; 913 } else { 914 // If there have been no changes to the canvas we can proceed as usual 915 return super.drawChild(canvas, child, drawingTime); 916 } 917 } 918 919 920 /** 921 * This is called when the children expansion has changed and positions the children properly 922 * for an appear animation. 923 */ 924 public void prepareExpansionChanged() { 925 // TODO: do something that makes sense, like placing the invisible views correctly 926 return; 927 } 928 929 /** 930 * Animate to a given state. 931 */ 932 public void startAnimationToState(AnimationProperties properties) { 933 int childCount = mAttachedChildren.size(); 934 ViewState tmpState = new ViewState(); 935 float expandFraction = getGroupExpandFraction(); 936 final boolean isExpanding = !showingAsLowPriority() 937 && (mUserLocked || mContainingNotification.isGroupExpansionChanging()); 938 final boolean dividersVisible = (mChildrenExpanded && mShowDividersWhenExpanded) 939 || (isExpanding && !mHideDividersDuringExpand); 940 for (int i = childCount - 1; i >= 0; i--) { 941 ExpandableNotificationRow child = mAttachedChildren.get(i); 942 ExpandableViewState viewState = child.getViewState(); 943 viewState.animateTo(child, properties); 944 945 // layout the divider 946 View divider = mDividers.get(i); 947 tmpState.initFrom(divider); 948 tmpState.setYTranslation(viewState.getYTranslation() - mDividerHeight); 949 float alpha = mChildrenExpanded && viewState.getAlpha() != 0 ? mDividerAlpha : 0; 950 if (mUserLocked && !showingAsLowPriority() && viewState.getAlpha() != 0) { 951 alpha = NotificationUtils.interpolate(0, mDividerAlpha, 952 Math.min(viewState.getAlpha(), expandFraction)); 953 } 954 tmpState.hidden = !dividersVisible; 955 tmpState.setAlpha(alpha); 956 tmpState.animateTo(divider, properties); 957 // There is no fake shadow to be drawn on the children 958 child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0); 959 } 960 if (mOverflowNumber != null) { 961 if (mNeverAppliedGroupState) { 962 float alpha = mGroupOverFlowState.getAlpha(); 963 mGroupOverFlowState.setAlpha(0); 964 mGroupOverFlowState.applyToView(mOverflowNumber); 965 mGroupOverFlowState.setAlpha(alpha); 966 mNeverAppliedGroupState = false; 967 } 968 mGroupOverFlowState.animateTo(mOverflowNumber, properties); 969 } 970 if (mNotificationHeader != null) { 971 mHeaderViewState.applyToView(mNotificationHeader); 972 } 973 updateChildrenClipping(); 974 } 975 976 public ExpandableNotificationRow getViewAtPosition(float y) { 977 // find the view under the pointer, accounting for GONE views 978 final int count = mAttachedChildren.size(); 979 for (int childIdx = 0; childIdx < count; childIdx++) { 980 ExpandableNotificationRow slidingChild = mAttachedChildren.get(childIdx); 981 float childTop = slidingChild.getTranslationY(); 982 float top = childTop + Math.max(0, slidingChild.getClipTopAmount()); 983 float bottom = childTop + slidingChild.getActualHeight(); 984 if (y >= top && y <= bottom) { 985 return slidingChild; 986 } 987 } 988 return null; 989 } 990 991 public void setChildrenExpanded(boolean childrenExpanded) { 992 mChildrenExpanded = childrenExpanded; 993 updateExpansionStates(); 994 if (mNotificationHeaderWrapper != null) { 995 mNotificationHeaderWrapper.setExpanded(childrenExpanded); 996 } 997 final int count = mAttachedChildren.size(); 998 for (int childIdx = 0; childIdx < count; childIdx++) { 999 ExpandableNotificationRow child = mAttachedChildren.get(childIdx); 1000 child.setChildrenExpanded(childrenExpanded, false); 1001 } 1002 updateHeaderTouchability(); 1003 } 1004 1005 public void setContainingNotification(ExpandableNotificationRow parent) { 1006 mContainingNotification = parent; 1007 mGroupingUtil = new NotificationGroupingUtil(mContainingNotification); 1008 } 1009 1010 public ExpandableNotificationRow getContainingNotification() { 1011 return mContainingNotification; 1012 } 1013 1014 public NotificationViewWrapper getNotificationViewWrapper() { 1015 return mNotificationHeaderWrapper; 1016 } 1017 1018 public NotificationViewWrapper getLowPriorityViewWrapper() { 1019 return mNotificationHeaderWrapperLowPriority; 1020 } 1021 1022 @VisibleForTesting 1023 public ViewGroup getCurrentHeaderView() { 1024 return mCurrentHeader; 1025 } 1026 1027 private void updateHeaderVisibility(boolean animate) { 1028 ViewGroup desiredHeader; 1029 ViewGroup currentHeader = mCurrentHeader; 1030 desiredHeader = calculateDesiredHeader(); 1031 1032 if (currentHeader == desiredHeader) { 1033 return; 1034 } 1035 1036 if (animate) { 1037 if (desiredHeader != null && currentHeader != null) { 1038 currentHeader.setVisibility(VISIBLE); 1039 desiredHeader.setVisibility(VISIBLE); 1040 NotificationViewWrapper visibleWrapper = getWrapperForView(desiredHeader); 1041 NotificationViewWrapper hiddenWrapper = getWrapperForView(currentHeader); 1042 visibleWrapper.transformFrom(hiddenWrapper); 1043 hiddenWrapper.transformTo(visibleWrapper, () -> updateHeaderVisibility(false)); 1044 startChildAlphaAnimations(desiredHeader == mNotificationHeader); 1045 } else { 1046 animate = false; 1047 } 1048 } 1049 if (!animate) { 1050 if (desiredHeader != null) { 1051 getWrapperForView(desiredHeader).setVisible(true); 1052 desiredHeader.setVisibility(VISIBLE); 1053 } 1054 if (currentHeader != null) { 1055 // Wrapper can be null if we were a low priority notification 1056 // and just destroyed it by calling setIsLowPriority(false) 1057 NotificationViewWrapper wrapper = getWrapperForView(currentHeader); 1058 if (wrapper != null) { 1059 wrapper.setVisible(false); 1060 } 1061 currentHeader.setVisibility(INVISIBLE); 1062 } 1063 } 1064 1065 resetHeaderVisibilityIfNeeded(mNotificationHeader, desiredHeader); 1066 resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, desiredHeader); 1067 1068 mCurrentHeader = desiredHeader; 1069 } 1070 1071 private void resetHeaderVisibilityIfNeeded(View header, View desiredHeader) { 1072 if (header == null) { 1073 return; 1074 } 1075 if (header != mCurrentHeader && header != desiredHeader) { 1076 getWrapperForView(header).setVisible(false); 1077 header.setVisibility(INVISIBLE); 1078 } 1079 if (header == desiredHeader && header.getVisibility() != VISIBLE) { 1080 getWrapperForView(header).setVisible(true); 1081 header.setVisibility(VISIBLE); 1082 } 1083 } 1084 1085 private ViewGroup calculateDesiredHeader() { 1086 ViewGroup desiredHeader; 1087 if (showingAsLowPriority()) { 1088 desiredHeader = mNotificationHeaderLowPriority; 1089 } else { 1090 desiredHeader = mNotificationHeader; 1091 } 1092 return desiredHeader; 1093 } 1094 1095 private void startChildAlphaAnimations(boolean toVisible) { 1096 float target = toVisible ? 1.0f : 0.0f; 1097 float start = 1.0f - target; 1098 int childCount = mAttachedChildren.size(); 1099 for (int i = 0; i < childCount; i++) { 1100 if (i >= NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED) { 1101 break; 1102 } 1103 ExpandableNotificationRow child = mAttachedChildren.get(i); 1104 child.setAlpha(start); 1105 ViewState viewState = new ViewState(); 1106 viewState.initFrom(child); 1107 viewState.setAlpha(target); 1108 ALPHA_FADE_IN.setDelay(i * 50); 1109 viewState.animateTo(child, ALPHA_FADE_IN); 1110 } 1111 } 1112 1113 1114 private void updateHeaderTransformation() { 1115 if (mUserLocked && showingAsLowPriority()) { 1116 float fraction = getGroupExpandFraction(); 1117 mNotificationHeaderWrapper.transformFrom(mNotificationHeaderWrapperLowPriority, 1118 fraction); 1119 mNotificationHeader.setVisibility(VISIBLE); 1120 mNotificationHeaderWrapperLowPriority.transformTo(mNotificationHeaderWrapper, 1121 fraction); 1122 } 1123 1124 } 1125 1126 private NotificationViewWrapper getWrapperForView(View visibleHeader) { 1127 if (visibleHeader == mNotificationHeader) { 1128 return mNotificationHeaderWrapper; 1129 } 1130 return mNotificationHeaderWrapperLowPriority; 1131 } 1132 1133 /** 1134 * Called when a groups expansion changes to adjust the background of the header view. 1135 * 1136 * @param expanded whether the group is expanded. 1137 */ 1138 public void updateHeaderForExpansion(boolean expanded) { 1139 if (mNotificationHeader != null) { 1140 if (expanded) { 1141 ColorDrawable cd = new ColorDrawable(); 1142 cd.setColor(mContainingNotification.calculateBgColor()); 1143 mNotificationHeader.setHeaderBackgroundDrawable(cd); 1144 } else { 1145 mNotificationHeader.setHeaderBackgroundDrawable(null); 1146 } 1147 } 1148 } 1149 1150 public int getMaxContentHeight() { 1151 if (showingAsLowPriority()) { 1152 return getMinHeight(NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED, true 1153 /* likeHighPriority */); 1154 } 1155 int maxContentHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation 1156 + mNotificationTopPadding; 1157 int visibleChildren = 0; 1158 int childCount = mAttachedChildren.size(); 1159 for (int i = 0; i < childCount; i++) { 1160 if (visibleChildren >= NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED) { 1161 break; 1162 } 1163 ExpandableNotificationRow child = mAttachedChildren.get(i); 1164 float childHeight = child.isExpanded(true /* allowOnKeyguard */) 1165 ? child.getMaxExpandHeight() 1166 : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */); 1167 maxContentHeight += childHeight; 1168 visibleChildren++; 1169 } 1170 if (visibleChildren > 0) { 1171 maxContentHeight += visibleChildren * mDividerHeight; 1172 } 1173 return maxContentHeight; 1174 } 1175 1176 public void setActualHeight(int actualHeight) { 1177 if (!mUserLocked) { 1178 return; 1179 } 1180 mActualHeight = actualHeight; 1181 float fraction = getGroupExpandFraction(); 1182 boolean showingLowPriority = showingAsLowPriority(); 1183 updateHeaderTransformation(); 1184 int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */); 1185 int childCount = mAttachedChildren.size(); 1186 for (int i = 0; i < childCount; i++) { 1187 ExpandableNotificationRow child = mAttachedChildren.get(i); 1188 float childHeight; 1189 if (showingLowPriority) { 1190 childHeight = child.getShowingLayout().getMinHeight(false /* likeGroupExpanded */); 1191 } else if (child.isExpanded(true /* allowOnKeyguard */)) { 1192 childHeight = child.getMaxExpandHeight(); 1193 } else { 1194 childHeight = child.getShowingLayout().getMinHeight( 1195 true /* likeGroupExpanded */); 1196 } 1197 if (i < maxAllowedVisibleChildren) { 1198 float singleLineHeight = child.getShowingLayout().getMinHeight( 1199 false /* likeGroupExpanded */); 1200 child.setActualHeight((int) NotificationUtils.interpolate(singleLineHeight, 1201 childHeight, fraction), false); 1202 } else { 1203 child.setActualHeight((int) childHeight, false); 1204 } 1205 } 1206 } 1207 1208 public float getGroupExpandFraction() { 1209 int visibleChildrenExpandedHeight = showingAsLowPriority() ? getMaxContentHeight() 1210 : getVisibleChildrenExpandHeight(); 1211 int minExpandHeight = getCollapsedHeight(); 1212 float factor = (mActualHeight - minExpandHeight) 1213 / (float) (visibleChildrenExpandedHeight - minExpandHeight); 1214 return Math.max(0.0f, Math.min(1.0f, factor)); 1215 } 1216 1217 private int getVisibleChildrenExpandHeight() { 1218 int intrinsicHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation 1219 + mNotificationTopPadding + mDividerHeight; 1220 int visibleChildren = 0; 1221 int childCount = mAttachedChildren.size(); 1222 int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */); 1223 for (int i = 0; i < childCount; i++) { 1224 if (visibleChildren >= maxAllowedVisibleChildren) { 1225 break; 1226 } 1227 ExpandableNotificationRow child = mAttachedChildren.get(i); 1228 float childHeight = child.isExpanded(true /* allowOnKeyguard */) 1229 ? child.getMaxExpandHeight() 1230 : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */); 1231 intrinsicHeight += childHeight; 1232 visibleChildren++; 1233 } 1234 return intrinsicHeight; 1235 } 1236 1237 public int getMinHeight() { 1238 return getMinHeight(NUMBER_OF_CHILDREN_WHEN_COLLAPSED, false /* likeHighPriority */); 1239 } 1240 1241 public int getCollapsedHeight() { 1242 return getMinHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */), 1243 false /* likeHighPriority */); 1244 } 1245 1246 public int getCollapsedHeightWithoutHeader() { 1247 return getMinHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */), 1248 false /* likeHighPriority */, 0); 1249 } 1250 1251 /** 1252 * Get the minimum Height for this group. 1253 * 1254 * @param maxAllowedVisibleChildren the number of children that should be visible 1255 * @param likeHighPriority if the height should be calculated as if it were not low 1256 * priority 1257 */ 1258 private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority) { 1259 return getMinHeight(maxAllowedVisibleChildren, likeHighPriority, mCurrentHeaderTranslation); 1260 } 1261 1262 /** 1263 * Get the minimum Height for this group. 1264 * 1265 * @param maxAllowedVisibleChildren the number of children that should be visible 1266 * @param likeHighPriority if the height should be calculated as if it were not low 1267 * priority 1268 * @param headerTranslation the translation amount of the header 1269 */ 1270 private int getMinHeight( 1271 int maxAllowedVisibleChildren, 1272 boolean likeHighPriority, 1273 int headerTranslation) { 1274 if (!likeHighPriority && showingAsLowPriority()) { 1275 if (mNotificationHeaderLowPriority == null) { 1276 Log.e(TAG, "getMinHeight: low priority header is null", new Exception()); 1277 return 0; 1278 } 1279 return mNotificationHeaderLowPriority.getHeight(); 1280 } 1281 int minExpandHeight = mNotificationHeaderMargin + headerTranslation; 1282 int visibleChildren = 0; 1283 boolean firstChild = true; 1284 int childCount = mAttachedChildren.size(); 1285 for (int i = 0; i < childCount; i++) { 1286 if (visibleChildren >= maxAllowedVisibleChildren) { 1287 break; 1288 } 1289 if (!firstChild) { 1290 minExpandHeight += mChildPadding; 1291 } else { 1292 firstChild = false; 1293 } 1294 ExpandableNotificationRow child = mAttachedChildren.get(i); 1295 View singleLineView = child.getSingleLineView(); 1296 if (singleLineView != null) { 1297 minExpandHeight += singleLineView.getHeight(); 1298 } else { 1299 Log.e(TAG, "getMinHeight: child " + child + " single line view is null", 1300 new Exception()); 1301 } 1302 visibleChildren++; 1303 } 1304 minExpandHeight += mCollapsedBottomPadding; 1305 return minExpandHeight; 1306 } 1307 1308 public boolean showingAsLowPriority() { 1309 return mIsLowPriority && !mContainingNotification.isExpanded(); 1310 } 1311 1312 public void reInflateViews(OnClickListener listener, StatusBarNotification notification) { 1313 if (mNotificationHeader != null) { 1314 removeView(mNotificationHeader); 1315 mNotificationHeader = null; 1316 } 1317 if (mNotificationHeaderLowPriority != null) { 1318 removeView(mNotificationHeaderLowPriority); 1319 mNotificationHeaderLowPriority = null; 1320 } 1321 recreateNotificationHeader(listener, mIsConversation); 1322 initDimens(); 1323 for (int i = 0; i < mDividers.size(); i++) { 1324 View prevDivider = mDividers.get(i); 1325 int index = indexOfChild(prevDivider); 1326 removeView(prevDivider); 1327 View divider = inflateDivider(); 1328 addView(divider, index); 1329 mDividers.set(i, divider); 1330 } 1331 removeView(mOverflowNumber); 1332 mOverflowNumber = null; 1333 mGroupOverFlowState = null; 1334 updateGroupOverflow(); 1335 } 1336 1337 public void setUserLocked(boolean userLocked) { 1338 mUserLocked = userLocked; 1339 if (!mUserLocked) { 1340 updateHeaderVisibility(false /* animate */); 1341 } 1342 int childCount = mAttachedChildren.size(); 1343 for (int i = 0; i < childCount; i++) { 1344 ExpandableNotificationRow child = mAttachedChildren.get(i); 1345 child.setUserLocked(userLocked && !showingAsLowPriority()); 1346 } 1347 updateHeaderTouchability(); 1348 } 1349 1350 private void updateHeaderTouchability() { 1351 if (mNotificationHeader != null) { 1352 mNotificationHeader.setAcceptAllTouches(mChildrenExpanded || mUserLocked); 1353 } 1354 } 1355 1356 public void onNotificationUpdated() { 1357 if (mShowGroupCountInExpander) { 1358 // The overflow number is not used, so its color is irrelevant; skip this 1359 return; 1360 } 1361 int color = mContainingNotification.getNotificationColor(); 1362 Resources.Theme theme = new ContextThemeWrapper(mContext, 1363 com.android.internal.R.style.Theme_DeviceDefault_DayNight).getTheme(); 1364 try (TypedArray ta = theme.obtainStyledAttributes( 1365 new int[]{com.android.internal.R.attr.colorAccent})) { 1366 color = ta.getColor(0, color); 1367 } 1368 mHybridGroupManager.setOverflowNumberColor(mOverflowNumber, color); 1369 } 1370 1371 public int getPositionInLinearLayout(View childInGroup) { 1372 int position = mNotificationHeaderMargin + mCurrentHeaderTranslation 1373 + mNotificationTopPadding; 1374 1375 for (int i = 0; i < mAttachedChildren.size(); i++) { 1376 ExpandableNotificationRow child = mAttachedChildren.get(i); 1377 boolean notGone = child.getVisibility() != View.GONE; 1378 if (notGone) { 1379 position += mDividerHeight; 1380 } 1381 if (child == childInGroup) { 1382 return position; 1383 } 1384 if (notGone) { 1385 position += child.getIntrinsicHeight(); 1386 } 1387 } 1388 return 0; 1389 } 1390 1391 public void setClipBottomAmount(int clipBottomAmount) { 1392 mClipBottomAmount = clipBottomAmount; 1393 updateChildrenClipping(); 1394 } 1395 1396 public void setIsLowPriority(boolean isLowPriority) { 1397 mIsLowPriority = isLowPriority; 1398 if (mContainingNotification != null) { /* we're not yet set up yet otherwise */ 1399 recreateLowPriorityHeader(null /* existingBuilder */, mIsConversation); 1400 updateHeaderVisibility(false /* animate */); 1401 } 1402 if (mUserLocked) { 1403 setUserLocked(mUserLocked); 1404 } 1405 } 1406 1407 /** 1408 * @return the view wrapper for the currently showing priority. 1409 */ 1410 public NotificationViewWrapper getVisibleWrapper() { 1411 if (showingAsLowPriority()) { 1412 return mNotificationHeaderWrapperLowPriority; 1413 } 1414 return mNotificationHeaderWrapper; 1415 } 1416 1417 public void onExpansionChanged() { 1418 if (mIsLowPriority) { 1419 if (mUserLocked) { 1420 setUserLocked(mUserLocked); 1421 } 1422 updateHeaderVisibility(true /* animate */); 1423 } 1424 } 1425 1426 @VisibleForTesting 1427 public boolean isUserLocked() { 1428 return mUserLocked; 1429 } 1430 1431 @Override 1432 public void applyRoundnessAndInvalidate() { 1433 boolean last = true; 1434 if (mNotificationHeaderWrapper != null) { 1435 mNotificationHeaderWrapper.requestTopRoundness( 1436 /* value = */ getTopRoundness(), 1437 /* sourceType = */ FROM_PARENT, 1438 /* animate = */ false 1439 ); 1440 } 1441 if (mNotificationHeaderWrapperLowPriority != null) { 1442 mNotificationHeaderWrapperLowPriority.requestTopRoundness( 1443 /* value = */ getTopRoundness(), 1444 /* sourceType = */ FROM_PARENT, 1445 /* animate = */ false 1446 ); 1447 } 1448 for (int i = mAttachedChildren.size() - 1; i >= 0; i--) { 1449 ExpandableNotificationRow child = mAttachedChildren.get(i); 1450 if (child.getVisibility() == View.GONE) { 1451 continue; 1452 } 1453 child.requestRoundness( 1454 /* top = */ 0f, 1455 /* bottom = */ last ? getBottomRoundness() : 0f, 1456 /* sourceType = */ FROM_PARENT, 1457 /* animate = */ false); 1458 last = false; 1459 } 1460 Roundable.super.applyRoundnessAndInvalidate(); 1461 } 1462 1463 public void setHeaderVisibleAmount(float headerVisibleAmount) { 1464 mHeaderVisibleAmount = headerVisibleAmount; 1465 mCurrentHeaderTranslation = (int) ((1.0f - headerVisibleAmount) * mTranslationForHeader); 1466 } 1467 1468 /** 1469 * Shows the given feedback icon, or hides the icon if null. 1470 */ 1471 public void setFeedbackIcon(@Nullable FeedbackIcon icon) { 1472 if (mNotificationHeaderWrapper != null) { 1473 mNotificationHeaderWrapper.setFeedbackIcon(icon); 1474 } 1475 if (mNotificationHeaderWrapperLowPriority != null) { 1476 mNotificationHeaderWrapperLowPriority.setFeedbackIcon(icon); 1477 } 1478 } 1479 1480 public void setRecentlyAudiblyAlerted(boolean audiblyAlertedRecently) { 1481 if (mNotificationHeaderWrapper != null) { 1482 mNotificationHeaderWrapper.setRecentlyAudiblyAlerted(audiblyAlertedRecently); 1483 } 1484 if (mNotificationHeaderWrapperLowPriority != null) { 1485 mNotificationHeaderWrapperLowPriority.setRecentlyAudiblyAlerted(audiblyAlertedRecently); 1486 } 1487 } 1488 1489 @Override 1490 public void setNotificationFaded(boolean faded) { 1491 mContainingNotificationIsFaded = faded; 1492 if (mNotificationHeaderWrapper != null) { 1493 mNotificationHeaderWrapper.setNotificationFaded(faded); 1494 } 1495 if (mNotificationHeaderWrapperLowPriority != null) { 1496 mNotificationHeaderWrapperLowPriority.setNotificationFaded(faded); 1497 } 1498 for (ExpandableNotificationRow child : mAttachedChildren) { 1499 child.setNotificationFaded(faded); 1500 } 1501 } 1502 1503 /** 1504 * Allow to define a path the clip the children in #drawChild() 1505 * 1506 * @param childClipPath path used to clip the children 1507 */ 1508 public void setChildClipPath(@Nullable Path childClipPath) { 1509 mChildClipPath = childClipPath; 1510 invalidate(); 1511 } 1512 1513 public NotificationHeaderViewWrapper getNotificationHeaderWrapper() { 1514 return mNotificationHeaderWrapper; 1515 } 1516 1517 public void setLogger(NotificationChildrenContainerLogger logger) { 1518 mLogger = logger; 1519 } 1520 1521 @Override 1522 public void addTransientView(View view, int index) { 1523 if (mLogger != null && view instanceof ExpandableNotificationRow) { 1524 mLogger.addTransientRow( 1525 ((ExpandableNotificationRow) view).getEntry(), 1526 getContainingNotification().getEntry(), 1527 index 1528 ); 1529 } 1530 super.addTransientView(view, index); 1531 } 1532 1533 @Override 1534 public void removeTransientView(View view) { 1535 if (mLogger != null && view instanceof ExpandableNotificationRow) { 1536 mLogger.removeTransientRow( 1537 ((ExpandableNotificationRow) view).getEntry(), 1538 getContainingNotification().getEntry() 1539 ); 1540 } 1541 super.removeTransientView(view); 1542 } 1543 1544 public String debugString() { 1545 return TAG + " { " 1546 + "visibility: " + getVisibility() 1547 + ", alpha: " + getAlpha() 1548 + ", translationY: " + getTranslationY() 1549 + ", roundableState: " + getRoundableState().debugString() + "}"; 1550 } 1551 } 1552