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.content.Context; 20 import android.util.AttributeSet; 21 import android.view.LayoutInflater; 22 import android.view.View; 23 import android.view.ViewGroup; 24 import android.widget.Button; 25 26 import com.android.systemui.R; 27 import com.android.systemui.statusbar.ExpandableNotificationRow; 28 import com.android.systemui.statusbar.ExpandableView; 29 30 import java.util.ArrayList; 31 import java.util.List; 32 33 /** 34 * A container containing child notifications 35 */ 36 public class NotificationChildrenContainer extends ViewGroup { 37 38 private final int mChildPadding; 39 private final int mDividerHeight; 40 private final int mMaxNotificationHeight; 41 private final List<View> mDividers = new ArrayList<>(); 42 private final List<ExpandableNotificationRow> mChildren = new ArrayList<>(); 43 private final View mCollapseButton; 44 private final View mCollapseDivider; 45 private final int mCollapseButtonHeight; 46 private final int mNotificationAppearDistance; 47 NotificationChildrenContainer(Context context)48 public NotificationChildrenContainer(Context context) { 49 this(context, null); 50 } 51 NotificationChildrenContainer(Context context, AttributeSet attrs)52 public NotificationChildrenContainer(Context context, AttributeSet attrs) { 53 this(context, attrs, 0); 54 } 55 NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr)56 public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr) { 57 this(context, attrs, defStyleAttr, 0); 58 } 59 NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)60 public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr, 61 int defStyleRes) { 62 super(context, attrs, defStyleAttr, defStyleRes); 63 mChildPadding = getResources().getDimensionPixelSize( 64 R.dimen.notification_children_padding); 65 mDividerHeight = getResources().getDimensionPixelSize( 66 R.dimen.notification_children_divider_height); 67 mMaxNotificationHeight = getResources().getDimensionPixelSize( 68 R.dimen.notification_max_height); 69 mNotificationAppearDistance = getResources().getDimensionPixelSize( 70 R.dimen.notification_appear_distance); 71 LayoutInflater inflater = mContext.getSystemService(LayoutInflater.class); 72 mCollapseButton = inflater.inflate(R.layout.notification_collapse_button, this, 73 false); 74 mCollapseButtonHeight = getResources().getDimensionPixelSize( 75 R.dimen.notification_bottom_decor_height); 76 addView(mCollapseButton); 77 mCollapseDivider = inflateDivider(); 78 addView(mCollapseDivider); 79 } 80 81 @Override onLayout(boolean changed, int l, int t, int r, int b)82 protected void onLayout(boolean changed, int l, int t, int r, int b) { 83 int childCount = mChildren.size(); 84 boolean firstChild = true; 85 for (int i = 0; i < childCount; i++) { 86 View child = mChildren.get(i); 87 boolean viewGone = child.getVisibility() == View.GONE; 88 if (i != 0) { 89 View divider = mDividers.get(i - 1); 90 int dividerVisibility = divider.getVisibility(); 91 int newVisibility = viewGone ? INVISIBLE : VISIBLE; 92 if (dividerVisibility != newVisibility) { 93 divider.setVisibility(newVisibility); 94 } 95 } 96 if (viewGone) { 97 continue; 98 } 99 child.layout(0, 0, getWidth(), child.getMeasuredHeight()); 100 if (!firstChild) { 101 mDividers.get(i - 1).layout(0, 0, getWidth(), mDividerHeight); 102 } else { 103 firstChild = false; 104 } 105 } 106 mCollapseButton.layout(0, 0, getWidth(), mCollapseButtonHeight); 107 mCollapseDivider.layout(0, mCollapseButtonHeight - mDividerHeight, getWidth(), 108 mCollapseButtonHeight); 109 } 110 111 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)112 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 113 int ownMaxHeight = mMaxNotificationHeight; 114 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 115 boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY; 116 boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST; 117 if (hasFixedHeight || isHeightLimited) { 118 int size = MeasureSpec.getSize(heightMeasureSpec); 119 ownMaxHeight = Math.min(ownMaxHeight, size); 120 } 121 int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST); 122 int dividerHeightSpec = MeasureSpec.makeMeasureSpec(mDividerHeight, MeasureSpec.EXACTLY); 123 int collapseButtonHeightSpec = MeasureSpec.makeMeasureSpec(mCollapseButtonHeight, 124 MeasureSpec.EXACTLY); 125 mCollapseButton.measure(widthMeasureSpec, collapseButtonHeightSpec); 126 mCollapseDivider.measure(widthMeasureSpec, dividerHeightSpec); 127 int height = mCollapseButtonHeight; 128 int childCount = mChildren.size(); 129 boolean firstChild = true; 130 for (int i = 0; i < childCount; i++) { 131 View child = mChildren.get(i); 132 if (child.getVisibility() == View.GONE) { 133 continue; 134 } 135 child.measure(widthMeasureSpec, newHeightSpec); 136 height += child.getMeasuredHeight(); 137 if (!firstChild) { 138 // layout the divider 139 View divider = mDividers.get(i - 1); 140 divider.measure(widthMeasureSpec, dividerHeightSpec); 141 height += mChildPadding; 142 } else { 143 firstChild = false; 144 } 145 } 146 int width = MeasureSpec.getSize(widthMeasureSpec); 147 height = hasFixedHeight ? ownMaxHeight 148 : isHeightLimited ? Math.min(ownMaxHeight, height) 149 : height; 150 setMeasuredDimension(width, height); 151 } 152 153 /** 154 * Add a child notification to this view. 155 * 156 * @param row the row to add 157 * @param childIndex the index to add it at, if -1 it will be added at the end 158 */ addNotification(ExpandableNotificationRow row, int childIndex)159 public void addNotification(ExpandableNotificationRow row, int childIndex) { 160 int newIndex = childIndex < 0 ? mChildren.size() : childIndex; 161 mChildren.add(newIndex, row); 162 addView(row); 163 if (mChildren.size() != 1) { 164 View divider = inflateDivider(); 165 addView(divider); 166 mDividers.add(Math.max(newIndex - 1, 0), divider); 167 } 168 // TODO: adapt background corners 169 // TODO: fix overdraw 170 } 171 172 public void removeNotification(ExpandableNotificationRow row) { 173 int childIndex = mChildren.indexOf(row); 174 mChildren.remove(row); 175 removeView(row); 176 if (!mDividers.isEmpty()) { 177 View divider = mDividers.remove(Math.max(childIndex - 1, 0)); 178 removeView(divider); 179 } 180 row.setSystemChildExpanded(false); 181 // TODO: adapt background corners 182 } 183 184 private View inflateDivider() { 185 return LayoutInflater.from(mContext).inflate( 186 R.layout.notification_children_divider, this, false); 187 } 188 189 public List<ExpandableNotificationRow> getNotificationChildren() { 190 return mChildren; 191 } 192 193 /** 194 * Apply the order given in the list to the children. 195 * 196 * @param childOrder the new list order 197 * @return whether the list order has changed 198 */ 199 public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder) { 200 if (childOrder == null) { 201 return false; 202 } 203 boolean result = false; 204 for (int i = 0; i < mChildren.size() && i < childOrder.size(); i++) { 205 ExpandableNotificationRow child = mChildren.get(i); 206 ExpandableNotificationRow desiredChild = childOrder.get(i); 207 if (child != desiredChild) { 208 mChildren.remove(desiredChild); 209 mChildren.add(i, desiredChild); 210 result = true; 211 } 212 } 213 214 // Let's make the first child expanded! 215 boolean first = true; 216 for (int i = 0; i < childOrder.size(); i++) { 217 ExpandableNotificationRow child = childOrder.get(i); 218 child.setSystemChildExpanded(first); 219 first = false; 220 } 221 return result; 222 } 223 224 public int getIntrinsicHeight() { 225 int childCount = mChildren.size(); 226 int intrinsicHeight = 0; 227 int visibleChildren = 0; 228 for (int i = 0; i < childCount; i++) { 229 ExpandableNotificationRow child = mChildren.get(i); 230 if (child.getVisibility() == View.GONE) { 231 continue; 232 } 233 intrinsicHeight += child.getIntrinsicHeight(); 234 visibleChildren++; 235 } 236 if (visibleChildren > 0) { 237 intrinsicHeight += (visibleChildren - 1) * mDividerHeight; 238 } 239 return intrinsicHeight; 240 } 241 242 /** 243 * Update the state of all its children based on a linear layout algorithm. 244 * 245 * @param resultState the state to update 246 * @param parentState the state of the parent 247 */ 248 public void getState(StackScrollState resultState, StackViewState parentState) { 249 int childCount = mChildren.size(); 250 int yPosition = mCollapseButtonHeight; 251 boolean firstChild = true; 252 for (int i = 0; i < childCount; i++) { 253 ExpandableNotificationRow child = mChildren.get(i); 254 if (child.getVisibility() == View.GONE) { 255 continue; 256 } 257 if (!firstChild) { 258 // There's a divider 259 yPosition += mChildPadding; 260 } else { 261 firstChild = false; 262 } 263 StackViewState childState = resultState.getViewStateForView(child); 264 int intrinsicHeight = child.getIntrinsicHeight(); 265 childState.yTranslation = yPosition; 266 childState.zTranslation = 0; 267 childState.height = intrinsicHeight; 268 childState.dimmed = parentState.dimmed; 269 childState.dark = parentState.dark; 270 childState.hideSensitive = parentState.hideSensitive; 271 childState.belowSpeedBump = parentState.belowSpeedBump; 272 childState.scale = parentState.scale; 273 childState.clipTopAmount = 0; 274 childState.topOverLap = 0; 275 childState.location = parentState.location; 276 yPosition += intrinsicHeight; 277 } 278 } 279 280 public void applyState(StackScrollState state) { 281 int childCount = mChildren.size(); 282 boolean firstChild = true; 283 ViewState dividerState = new ViewState(); 284 for (int i = 0; i < childCount; i++) { 285 ExpandableNotificationRow child = mChildren.get(i); 286 StackViewState viewState = state.getViewStateForView(child); 287 if (child.getVisibility() == View.GONE) { 288 continue; 289 } 290 if (!firstChild) { 291 // layout the divider 292 View divider = mDividers.get(i - 1); 293 dividerState.initFrom(divider); 294 dividerState.yTranslation = (int) (viewState.yTranslation 295 - (mChildPadding + mDividerHeight) / 2.0f); 296 dividerState.alpha = 1; 297 state.applyViewState(divider, dividerState); 298 } else { 299 firstChild = false; 300 } 301 state.applyState(child, viewState); 302 } 303 } 304 305 public void setCollapseClickListener(OnClickListener collapseClickListener) { 306 mCollapseButton.setOnClickListener(collapseClickListener); 307 } 308 309 /** 310 * This is called when the children expansion has changed and positions the children properly 311 * for an appear animation. 312 * 313 * @param state the new state we animate to 314 */ 315 public void prepareExpansionChanged(StackScrollState state) { 316 int childCount = mChildren.size(); 317 boolean firstChild = true; 318 StackViewState sourceState = new StackViewState(); 319 ViewState dividerState = new ViewState(); 320 for (int i = 0; i < childCount; i++) { 321 ExpandableNotificationRow child = mChildren.get(i); 322 StackViewState viewState = state.getViewStateForView(child); 323 if (child.getVisibility() == View.GONE) { 324 continue; 325 } 326 if (!firstChild) { 327 // layout the divider 328 View divider = mDividers.get(i - 1); 329 dividerState.initFrom(divider); 330 dividerState.yTranslation = viewState.yTranslation 331 - (mChildPadding + mDividerHeight) / 2.0f + mNotificationAppearDistance; 332 dividerState.alpha = 0; 333 state.applyViewState(divider, dividerState); 334 } else { 335 firstChild = false; 336 } 337 sourceState.copyFrom(viewState); 338 sourceState.alpha = 0; 339 sourceState.yTranslation += mNotificationAppearDistance; 340 state.applyState(child, sourceState); 341 } 342 mCollapseButton.setAlpha(0); 343 mCollapseDivider.setAlpha(0); 344 mCollapseDivider.setTranslationY(mNotificationAppearDistance / 4); 345 } 346 347 public void startAnimationToState(StackScrollState state, StackStateAnimator stateAnimator, 348 boolean withDelays, long baseDelay, long duration) { 349 int childCount = mChildren.size(); 350 boolean firstChild = true; 351 ViewState dividerState = new ViewState(); 352 int notGoneIndex = 0; 353 for (int i = 0; i < childCount; i++) { 354 ExpandableNotificationRow child = mChildren.get(i); 355 StackViewState viewState = state.getViewStateForView(child); 356 if (child.getVisibility() == View.GONE) { 357 continue; 358 } 359 int difference = Math.min(StackStateAnimator.DELAY_EFFECT_MAX_INDEX_DIFFERENCE_CHILDREN, 360 notGoneIndex + 1); 361 long delay = withDelays 362 ? difference * StackStateAnimator.ANIMATION_DELAY_PER_ELEMENT_EXPAND_CHILDREN 363 : 0; 364 delay += baseDelay; 365 if (!firstChild) { 366 // layout the divider 367 View divider = mDividers.get(i - 1); 368 dividerState.initFrom(divider); 369 dividerState.yTranslation = viewState.yTranslation 370 - (mChildPadding + mDividerHeight) / 2.0f; 371 dividerState.alpha = 1; 372 stateAnimator.startViewAnimations(divider, dividerState, delay, duration); 373 } else { 374 firstChild = false; 375 } 376 stateAnimator.startStackAnimations(child, viewState, state, -1, delay); 377 notGoneIndex++; 378 } 379 dividerState.initFrom(mCollapseButton); 380 dividerState.alpha = 1.0f; 381 stateAnimator.startViewAnimations(mCollapseButton, dividerState, baseDelay, duration); 382 dividerState.initFrom(mCollapseDivider); 383 dividerState.alpha = 1.0f; 384 dividerState.yTranslation = 0.0f; 385 stateAnimator.startViewAnimations(mCollapseDivider, dividerState, baseDelay, duration); 386 } 387 388 public ExpandableNotificationRow getViewAtPosition(float y) { 389 // find the view under the pointer, accounting for GONE views 390 final int count = mChildren.size(); 391 for (int childIdx = 0; childIdx < count; childIdx++) { 392 ExpandableNotificationRow slidingChild = mChildren.get(childIdx); 393 float childTop = slidingChild.getTranslationY(); 394 float top = childTop + slidingChild.getClipTopAmount(); 395 float bottom = childTop + slidingChild.getActualHeight(); 396 if (y >= top && y <= bottom) { 397 return slidingChild; 398 } 399 } 400 return null; 401 } 402 403 public void setTintColor(int color) { 404 ExpandableNotificationRow.applyTint(mCollapseDivider, color); 405 } 406 } 407