NotificationContentView.java revision 816c8e4735f975f4d8bffa1a5a37be6557424ea3
1/* 2 * Copyright (C) 2014 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 17package com.android.systemui.statusbar; 18 19import android.content.Context; 20import android.graphics.Outline; 21import android.graphics.Paint; 22import android.graphics.PorterDuff; 23import android.graphics.PorterDuffXfermode; 24import android.graphics.Rect; 25import android.service.notification.StatusBarNotification; 26import android.util.AttributeSet; 27import android.view.View; 28import android.view.ViewGroup; 29import android.view.ViewOutlineProvider; 30import android.view.ViewTreeObserver; 31import android.view.animation.Interpolator; 32import android.view.animation.LinearInterpolator; 33import android.widget.FrameLayout; 34 35import com.android.systemui.R; 36import com.android.systemui.statusbar.notification.HybridNotificationView; 37import com.android.systemui.statusbar.notification.HybridNotificationViewManager; 38import com.android.systemui.statusbar.phone.NotificationGroupManager; 39 40/** 41 * A frame layout containing the actual payload of the notification, including the contracted, 42 * expanded and heads up layout. This class is responsible for clipping the content and and 43 * switching between the expanded, contracted and the heads up view depending on its clipped size. 44 */ 45public class NotificationContentView extends FrameLayout { 46 47 private static final long ANIMATION_DURATION_LENGTH = 170; 48 private static final int VISIBLE_TYPE_CONTRACTED = 0; 49 private static final int VISIBLE_TYPE_EXPANDED = 1; 50 private static final int VISIBLE_TYPE_HEADSUP = 2; 51 private static final int VISIBLE_TYPE_SINGLELINE = 3; 52 53 private final Rect mClipBounds = new Rect(); 54 private final int mSingleLineHeight; 55 private final int mHeadsUpHeight; 56 private final int mRoundRectRadius; 57 private final Interpolator mLinearInterpolator = new LinearInterpolator(); 58 private final boolean mRoundRectClippingEnabled; 59 60 private View mContractedChild; 61 private View mExpandedChild; 62 private View mHeadsUpChild; 63 private HybridNotificationView mSingleLineView; 64 65 private NotificationViewWrapper mContractedWrapper; 66 private NotificationViewWrapper mExpandedWrapper; 67 private NotificationViewWrapper mHeadsUpWrapper; 68 private HybridNotificationViewManager mHybridViewManager; 69 private int mClipTopAmount; 70 private int mContentHeight; 71 private int mUnrestrictedContentHeight; 72 private int mVisibleType = VISIBLE_TYPE_CONTRACTED; 73 private boolean mDark; 74 private final Paint mFadePaint = new Paint(); 75 private boolean mAnimate; 76 private boolean mIsHeadsUp; 77 private boolean mShowingLegacyBackground; 78 private boolean mIsChildInGroup; 79 private int mSmallHeight; 80 private ExpandableNotificationRow mContainingNotification; 81 private StatusBarNotification mStatusBarNotification; 82 private NotificationGroupManager mGroupManager; 83 84 private final ViewTreeObserver.OnPreDrawListener mEnableAnimationPredrawListener 85 = new ViewTreeObserver.OnPreDrawListener() { 86 @Override 87 public boolean onPreDraw() { 88 mAnimate = true; 89 getViewTreeObserver().removeOnPreDrawListener(this); 90 return true; 91 } 92 }; 93 94 private final ViewOutlineProvider mOutlineProvider = new ViewOutlineProvider() { 95 @Override 96 public void getOutline(View view, Outline outline) { 97 outline.setRoundRect(0, 0, view.getWidth(), mUnrestrictedContentHeight, 98 mRoundRectRadius); 99 } 100 }; 101 private OnClickListener mExpandClickListener = new OnClickListener() { 102 @Override 103 public void onClick(View v) { 104 if (mGroupManager.isSummaryOfGroup(mStatusBarNotification)) { 105 mGroupManager.toggleGroupExpansion(mStatusBarNotification); 106 } else { 107 mContainingNotification.setUserExpanded(!mContainingNotification.isExpanded()); 108 mContainingNotification.notifyHeightChanged(true); 109 } 110 } 111 }; 112 113 public NotificationContentView(Context context, AttributeSet attrs) { 114 super(context, attrs); 115 mHybridViewManager = new HybridNotificationViewManager(getContext(), this); 116 mFadePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD)); 117 mSingleLineHeight = getResources().getDimensionPixelSize( 118 R.dimen.notification_single_line_height); 119 mHeadsUpHeight = getResources().getDimensionPixelSize(R.dimen.notification_mid_height); 120 mRoundRectRadius = getResources().getDimensionPixelSize( 121 R.dimen.notification_material_rounded_rect_radius); 122 mRoundRectClippingEnabled = getResources().getBoolean( 123 R.bool.config_notifications_round_rect_clipping); 124 reset(true); 125 setOutlineProvider(mOutlineProvider); 126 } 127 128 public void setSmallHeight(int smallHeight) { 129 mSmallHeight = smallHeight; 130 } 131 132 @Override 133 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 134 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 135 boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY; 136 boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST; 137 int maxSize = Integer.MAX_VALUE; 138 if (hasFixedHeight || isHeightLimited) { 139 maxSize = MeasureSpec.getSize(heightMeasureSpec); 140 } 141 int maxChildHeight = 0; 142 if (mContractedChild != null) { 143 int size = Math.min(maxSize, mSmallHeight); 144 mContractedChild.measure(widthMeasureSpec, 145 MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY)); 146 maxChildHeight = Math.max(maxChildHeight, mContractedChild.getMeasuredHeight()); 147 } 148 if (mExpandedChild != null) { 149 int size = maxSize; 150 ViewGroup.LayoutParams layoutParams = mExpandedChild.getLayoutParams(); 151 if (layoutParams.height >= 0) { 152 // An actual height is set 153 size = Math.min(maxSize, layoutParams.height); 154 } 155 int spec = size == Integer.MAX_VALUE 156 ? MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) 157 : MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST); 158 mExpandedChild.measure(widthMeasureSpec, spec); 159 maxChildHeight = Math.max(maxChildHeight, mExpandedChild.getMeasuredHeight()); 160 } 161 if (mHeadsUpChild != null) { 162 int size = Math.min(maxSize, mHeadsUpHeight); 163 ViewGroup.LayoutParams layoutParams = mHeadsUpChild.getLayoutParams(); 164 if (layoutParams.height >= 0) { 165 // An actual height is set 166 size = Math.min(maxSize, layoutParams.height); 167 } 168 mHeadsUpChild.measure(widthMeasureSpec, 169 MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST)); 170 maxChildHeight = Math.max(maxChildHeight, mHeadsUpChild.getMeasuredHeight()); 171 } 172 if (mSingleLineView != null) { 173 int size = Math.min(maxSize, mSingleLineHeight); 174 mSingleLineView.measure(widthMeasureSpec, 175 MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY)); 176 maxChildHeight = Math.max(maxChildHeight, mSingleLineView.getMeasuredHeight()); 177 } 178 int ownHeight = Math.min(maxChildHeight, maxSize); 179 int width = MeasureSpec.getSize(widthMeasureSpec); 180 setMeasuredDimension(width, ownHeight); 181 } 182 183 @Override 184 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 185 super.onLayout(changed, left, top, right, bottom); 186 updateClipping(); 187 invalidateOutline(); 188 } 189 190 @Override 191 protected void onAttachedToWindow() { 192 super.onAttachedToWindow(); 193 updateVisibility(); 194 } 195 196 public void reset(boolean resetActualHeight) { 197 if (mContractedChild != null) { 198 mContractedChild.animate().cancel(); 199 } 200 if (mExpandedChild != null) { 201 mExpandedChild.animate().cancel(); 202 } 203 if (mHeadsUpChild != null) { 204 mHeadsUpChild.animate().cancel(); 205 } 206 removeAllViews(); 207 mContractedChild = null; 208 mExpandedChild = null; 209 mHeadsUpChild = null; 210 mVisibleType = VISIBLE_TYPE_CONTRACTED; 211 if (resetActualHeight) { 212 mContentHeight = mSmallHeight; 213 } 214 } 215 216 public View getContractedChild() { 217 return mContractedChild; 218 } 219 220 public View getExpandedChild() { 221 return mExpandedChild; 222 } 223 224 public View getHeadsUpChild() { 225 return mHeadsUpChild; 226 } 227 228 public void setContractedChild(View child) { 229 if (mContractedChild != null) { 230 mContractedChild.animate().cancel(); 231 removeView(mContractedChild); 232 } 233 addView(child); 234 mContractedChild = child; 235 mContractedWrapper = NotificationViewWrapper.wrap(getContext(), child); 236 selectLayout(false /* animate */, true /* force */); 237 mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */); 238 updateRoundRectClipping(); 239 } 240 241 public void setExpandedChild(View child) { 242 if (mExpandedChild != null) { 243 mExpandedChild.animate().cancel(); 244 removeView(mExpandedChild); 245 } 246 addView(child); 247 mExpandedChild = child; 248 mExpandedWrapper = NotificationViewWrapper.wrap(getContext(), child); 249 selectLayout(false /* animate */, true /* force */); 250 updateRoundRectClipping(); 251 } 252 253 public void setHeadsUpChild(View child) { 254 if (mHeadsUpChild != null) { 255 mHeadsUpChild.animate().cancel(); 256 removeView(mHeadsUpChild); 257 } 258 addView(child); 259 mHeadsUpChild = child; 260 mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child); 261 selectLayout(false /* animate */, true /* force */); 262 updateRoundRectClipping(); 263 } 264 265 @Override 266 protected void onVisibilityChanged(View changedView, int visibility) { 267 super.onVisibilityChanged(changedView, visibility); 268 updateVisibility(); 269 } 270 271 private void updateVisibility() { 272 setVisible(isShown()); 273 } 274 275 private void setVisible(final boolean isVisible) { 276 if (isVisible) { 277 278 // We only animate if we are drawn at least once, otherwise the view might animate when 279 // it's shown the first time 280 getViewTreeObserver().addOnPreDrawListener(mEnableAnimationPredrawListener); 281 } else { 282 getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener); 283 mAnimate = false; 284 } 285 } 286 287 public void setContentHeight(int contentHeight) { 288 mContentHeight = Math.max(Math.min(contentHeight, getHeight()), getMinHeight());; 289 mUnrestrictedContentHeight = Math.max(contentHeight, getMinHeight()); 290 selectLayout(mAnimate /* animate */, false /* force */); 291 updateClipping(); 292 invalidateOutline(); 293 } 294 295 public int getContentHeight() { 296 return mContentHeight; 297 } 298 299 public int getMaxHeight() { 300 if (mIsHeadsUp && mHeadsUpChild != null) { 301 return mHeadsUpChild.getHeight(); 302 } else if (mExpandedChild != null) { 303 return mExpandedChild.getHeight(); 304 } 305 return mSmallHeight; 306 } 307 308 public int getMinHeight() { 309 if (mIsChildInGroup && !isGroupExpanded()) { 310 return mSingleLineHeight; 311 } else { 312 return mSmallHeight; 313 } 314 } 315 316 private boolean isGroupExpanded() { 317 return mGroupManager.isGroupExpanded(mStatusBarNotification); 318 } 319 320 public void setClipTopAmount(int clipTopAmount) { 321 mClipTopAmount = clipTopAmount; 322 updateClipping(); 323 } 324 325 private void updateRoundRectClipping() { 326 boolean enabled = needsRoundRectClipping(); 327 setClipToOutline(enabled); 328 } 329 330 private boolean needsRoundRectClipping() { 331 if (!mRoundRectClippingEnabled) { 332 return false; 333 } 334 boolean needsForContracted = mContractedChild != null 335 && mContractedChild.getVisibility() == View.VISIBLE 336 && mContractedWrapper.needsRoundRectClipping(); 337 boolean needsForExpanded = mExpandedChild != null 338 && mExpandedChild.getVisibility() == View.VISIBLE 339 && mExpandedWrapper.needsRoundRectClipping(); 340 boolean needsForHeadsUp = mExpandedChild != null 341 && mExpandedChild.getVisibility() == View.VISIBLE 342 && mExpandedWrapper.needsRoundRectClipping(); 343 return needsForContracted || needsForExpanded || needsForHeadsUp; 344 } 345 346 private void updateClipping() { 347 mClipBounds.set(0, mClipTopAmount, getWidth(), mContentHeight); 348 setClipBounds(mClipBounds); 349 } 350 351 private void selectLayout(boolean animate, boolean force) { 352 if (mContractedChild == null) { 353 return; 354 } 355 int visibleType = calculateVisibleType(); 356 if (visibleType != mVisibleType || force) { 357 if (animate && ((visibleType == VISIBLE_TYPE_EXPANDED && mExpandedChild != null) 358 || (visibleType == VISIBLE_TYPE_HEADSUP && mHeadsUpChild != null) 359 || (visibleType == VISIBLE_TYPE_SINGLELINE && mSingleLineView != null) 360 || visibleType == VISIBLE_TYPE_CONTRACTED)) { 361 runSwitchAnimation(visibleType); 362 } else { 363 updateViewVisibilities(visibleType); 364 } 365 mVisibleType = visibleType; 366 } 367 } 368 369 private void updateViewVisibilities(int visibleType) { 370 boolean contractedVisible = visibleType == VISIBLE_TYPE_CONTRACTED; 371 mContractedChild.setVisibility(contractedVisible ? View.VISIBLE : View.INVISIBLE); 372 mContractedChild.setAlpha(contractedVisible ? 1f : 0f); 373 mContractedChild.setLayerType(LAYER_TYPE_NONE, null); 374 if (mExpandedChild != null) { 375 boolean expandedVisible = visibleType == VISIBLE_TYPE_EXPANDED; 376 mExpandedChild.setVisibility(expandedVisible ? View.VISIBLE : View.INVISIBLE); 377 mExpandedChild.setAlpha(expandedVisible ? 1f : 0f); 378 mExpandedChild.setLayerType(LAYER_TYPE_NONE, null); 379 } 380 if (mHeadsUpChild != null) { 381 boolean headsUpVisible = visibleType == VISIBLE_TYPE_HEADSUP; 382 mHeadsUpChild.setVisibility(headsUpVisible ? View.VISIBLE : View.INVISIBLE); 383 mHeadsUpChild.setAlpha(headsUpVisible ? 1f : 0f); 384 mHeadsUpChild.setLayerType(LAYER_TYPE_NONE, null); 385 } 386 if (mSingleLineView != null) { 387 boolean singleLineVisible = visibleType == VISIBLE_TYPE_SINGLELINE; 388 mSingleLineView.setVisibility(singleLineVisible ? View.VISIBLE : View.INVISIBLE); 389 mSingleLineView.setAlpha(singleLineVisible ? 1f : 0f); 390 mSingleLineView.setLayerType(LAYER_TYPE_NONE, null); 391 } 392 setLayerType(LAYER_TYPE_NONE, null); 393 updateRoundRectClipping(); 394 } 395 396 private void runSwitchAnimation(int visibleType) { 397 View shownView = getViewForVisibleType(visibleType); 398 View hiddenView = getViewForVisibleType(mVisibleType); 399 shownView.setVisibility(View.VISIBLE); 400 hiddenView.setVisibility(View.VISIBLE); 401 shownView.setLayerType(LAYER_TYPE_HARDWARE, mFadePaint); 402 hiddenView.setLayerType(LAYER_TYPE_HARDWARE, mFadePaint); 403 setLayerType(LAYER_TYPE_HARDWARE, null); 404 hiddenView.animate() 405 .alpha(0f) 406 .setDuration(ANIMATION_DURATION_LENGTH) 407 .setInterpolator(mLinearInterpolator) 408 .withEndAction(null); // In case we have multiple changes in one frame. 409 shownView.animate() 410 .alpha(1f) 411 .setDuration(ANIMATION_DURATION_LENGTH) 412 .setInterpolator(mLinearInterpolator) 413 .withEndAction(new Runnable() { 414 @Override 415 public void run() { 416 updateViewVisibilities(mVisibleType); 417 } 418 }); 419 updateRoundRectClipping(); 420 } 421 422 /** 423 * @param visibleType one of the static enum types in this view 424 * @return the corresponding view according to the given visible type 425 */ 426 private View getViewForVisibleType(int visibleType) { 427 switch (visibleType) { 428 case VISIBLE_TYPE_EXPANDED: 429 return mExpandedChild; 430 case VISIBLE_TYPE_HEADSUP: 431 return mHeadsUpChild; 432 case VISIBLE_TYPE_SINGLELINE: 433 return mSingleLineView; 434 default: 435 return mContractedChild; 436 } 437 } 438 439 /** 440 * @return one of the static enum types in this view, calculated form the current state 441 */ 442 private int calculateVisibleType() { 443 boolean noExpandedChild = mExpandedChild == null; 444 if (mIsHeadsUp && mHeadsUpChild != null) { 445 if (mContentHeight <= mHeadsUpChild.getHeight() || noExpandedChild) { 446 return VISIBLE_TYPE_HEADSUP; 447 } else { 448 return VISIBLE_TYPE_EXPANDED; 449 } 450 } else { 451 if (mIsChildInGroup && !isGroupExpanded()) { 452 return VISIBLE_TYPE_SINGLELINE; 453 } else if (mContentHeight <= mSmallHeight || noExpandedChild) { 454 return VISIBLE_TYPE_CONTRACTED; 455 } else { 456 return VISIBLE_TYPE_EXPANDED; 457 } 458 } 459 } 460 461 public void notifyContentUpdated() { 462 updateSingleLineView(); 463 selectLayout(false /* animate */, true /* force */); 464 if (mContractedChild != null) { 465 mContractedWrapper.notifyContentUpdated(); 466 mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */); 467 } 468 if (mExpandedChild != null) { 469 mExpandedWrapper.notifyContentUpdated(); 470 } 471 updateRoundRectClipping(); 472 } 473 474 public boolean isContentExpandable() { 475 return mExpandedChild != null; 476 } 477 478 public void setDark(boolean dark, boolean fade, long delay) { 479 if (mDark == dark || mContractedChild == null) return; 480 mDark = dark; 481 mContractedWrapper.setDark(dark && !mShowingLegacyBackground, fade, delay); 482 } 483 484 public void setHeadsUp(boolean headsUp) { 485 mIsHeadsUp = headsUp; 486 selectLayout(false /* animate */, true /* force */); 487 } 488 489 @Override 490 public boolean hasOverlappingRendering() { 491 492 // This is not really true, but good enough when fading from the contracted to the expanded 493 // layout, and saves us some layers. 494 return false; 495 } 496 497 public void setShowingLegacyBackground(boolean showing) { 498 mShowingLegacyBackground = showing; 499 } 500 501 public void setIsChildInGroup(boolean isChildInGroup) { 502 mIsChildInGroup = isChildInGroup; 503 updateSingleLineView(); 504 } 505 506 public void setContainingNotification(ExpandableNotificationRow notification) { 507 mContainingNotification = notification; 508 } 509 510 public void setStatusBarNotification(StatusBarNotification statusBarNotification) { 511 mStatusBarNotification = statusBarNotification; 512 updateSingleLineView(); 513 } 514 515 private void updateSingleLineView() { 516 if (mIsChildInGroup) { 517 mSingleLineView = mHybridViewManager.bindFromNotification( 518 mSingleLineView, mStatusBarNotification.getNotification()); 519 } 520 } 521 522 public void setSubTextVisible(boolean visible) { 523 if (mExpandedChild != null) { 524 mExpandedWrapper.setSubTextVisible(visible); 525 } 526 if (mContractedChild != null) { 527 mContractedWrapper.setSubTextVisible(visible); 528 } 529 if (mHeadsUpChild != null) { 530 mHeadsUpWrapper.setSubTextVisible(visible); 531 } 532 } 533 534 public void setGroupManager(NotificationGroupManager groupManager) { 535 mGroupManager = groupManager; 536 } 537 538 public void updateExpandButtons() { 539 if (mExpandedChild != null) { 540 mExpandedWrapper.updateExpandability(mContainingNotification.isExpandable(), 541 mExpandClickListener); 542 } 543 if (mContractedChild != null) { 544 mContractedWrapper.updateExpandability(mContainingNotification.isExpandable(), 545 mExpandClickListener); 546 } 547 if (mHeadsUpChild != null) { 548 mHeadsUpWrapper.updateExpandability(mContainingNotification.isExpandable(), 549 mExpandClickListener); 550 } 551 } 552} 553