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