1 /* 2 * Copyright (C) 2016 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.phone; 18 19 import static com.android.systemui.statusbar.notification.NotificationUtils.isHapticFeedbackDisabled; 20 21 import android.content.Context; 22 import android.content.res.Configuration; 23 import android.graphics.Canvas; 24 import android.graphics.Color; 25 import android.graphics.Paint; 26 import android.graphics.drawable.Icon; 27 import android.os.AsyncTask; 28 import android.os.VibrationEffect; 29 import android.os.Vibrator; 30 import android.support.v4.util.ArrayMap; 31 import android.support.v4.util.ArraySet; 32 import android.util.AttributeSet; 33 import android.view.View; 34 35 import com.android.internal.statusbar.StatusBarIcon; 36 import com.android.systemui.Interpolators; 37 import com.android.systemui.R; 38 import com.android.systemui.statusbar.AlphaOptimizedFrameLayout; 39 import com.android.systemui.statusbar.StatusBarIconView; 40 import com.android.systemui.statusbar.stack.AnimationFilter; 41 import com.android.systemui.statusbar.stack.AnimationProperties; 42 import com.android.systemui.statusbar.stack.ViewState; 43 44 import java.util.ArrayList; 45 import java.util.HashMap; 46 47 /** 48 * A container for notification icons. It handles overflowing icons properly and positions them 49 * correctly on the screen. 50 */ 51 public class NotificationIconContainer extends AlphaOptimizedFrameLayout { 52 /** 53 * A float value indicating how much before the overflow start the icons should transform into 54 * a dot. A value of 0 means that they are exactly at the end and a value of 1 means it starts 55 * 1 icon width early. 56 */ 57 public static final float OVERFLOW_EARLY_AMOUNT = 0.2f; 58 private static final int NO_VALUE = Integer.MIN_VALUE; 59 private static final String TAG = "NotificationIconContainer"; 60 private static final boolean DEBUG = false; 61 private static final int CANNED_ANIMATION_DURATION = 100; 62 private static final AnimationProperties DOT_ANIMATION_PROPERTIES = new AnimationProperties() { 63 private AnimationFilter mAnimationFilter = new AnimationFilter().animateX(); 64 65 @Override 66 public AnimationFilter getAnimationFilter() { 67 return mAnimationFilter; 68 } 69 }.setDuration(200); 70 71 private static final AnimationProperties ICON_ANIMATION_PROPERTIES = new AnimationProperties() { 72 private AnimationFilter mAnimationFilter = new AnimationFilter().animateY().animateAlpha() 73 .animateScale(); 74 75 @Override 76 public AnimationFilter getAnimationFilter() { 77 return mAnimationFilter; 78 } 79 80 }.setDuration(CANNED_ANIMATION_DURATION) 81 .setCustomInterpolator(View.TRANSLATION_Y, Interpolators.ICON_OVERSHOT); 82 83 /** 84 * Temporary AnimationProperties to avoid unnecessary allocations. 85 */ 86 private static final AnimationProperties sTempProperties = new AnimationProperties() { 87 private AnimationFilter mAnimationFilter = new AnimationFilter(); 88 89 @Override 90 public AnimationFilter getAnimationFilter() { 91 return mAnimationFilter; 92 } 93 }; 94 95 private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() { 96 private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha(); 97 98 @Override 99 public AnimationFilter getAnimationFilter() { 100 return mAnimationFilter; 101 } 102 }.setDuration(200).setDelay(50); 103 104 public static final int MAX_VISIBLE_ICONS_WHEN_DARK = 5; 105 106 private boolean mShowAllIcons = true; 107 private final HashMap<View, IconState> mIconStates = new HashMap<>(); 108 private int mDotPadding; 109 private int mStaticDotRadius; 110 private int mActualLayoutWidth = NO_VALUE; 111 private float mActualPaddingEnd = NO_VALUE; 112 private float mActualPaddingStart = NO_VALUE; 113 private boolean mDark; 114 private boolean mChangingViewPositions; 115 private int mAddAnimationStartIndex = -1; 116 private int mCannedAnimationStartIndex = -1; 117 private int mSpeedBumpIndex = -1; 118 private int mIconSize; 119 private float mOpenedAmount = 0.0f; 120 private float mVisualOverflowAdaption; 121 private boolean mDisallowNextAnimation; 122 private boolean mAnimationsEnabled = true; 123 private boolean mVibrateOnAnimation; 124 private Vibrator mVibrator; 125 private ArrayMap<String, ArrayList<StatusBarIcon>> mReplacingIcons; 126 private int mDarkOffsetX; 127 128 public NotificationIconContainer(Context context, AttributeSet attrs) { 129 super(context, attrs); 130 initDimens(); 131 setWillNotDraw(!DEBUG); 132 mVibrator = mContext.getSystemService(Vibrator.class); 133 } 134 135 private void initDimens() { 136 mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding); 137 mStaticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius); 138 } 139 140 @Override 141 protected void onDraw(Canvas canvas) { 142 super.onDraw(canvas); 143 Paint paint = new Paint(); 144 paint.setColor(Color.RED); 145 paint.setStyle(Paint.Style.STROKE); 146 canvas.drawRect(getActualPaddingStart(), 0, getLayoutEnd(), getHeight(), paint); 147 } 148 149 @Override 150 protected void onConfigurationChanged(Configuration newConfig) { 151 super.onConfigurationChanged(newConfig); 152 initDimens(); 153 } 154 @Override 155 protected void onLayout(boolean changed, int l, int t, int r, int b) { 156 float centerY = getHeight() / 2.0f; 157 // we layout all our children on the left at the top 158 mIconSize = 0; 159 for (int i = 0; i < getChildCount(); i++) { 160 View child = getChildAt(i); 161 // We need to layout all children even the GONE ones, such that the heights are 162 // calculated correctly as they are used to calculate how many we can fit on the screen 163 int width = child.getMeasuredWidth(); 164 int height = child.getMeasuredHeight(); 165 int top = (int) (centerY - height / 2.0f); 166 child.layout(0, top, width, top + height); 167 if (i == 0) { 168 mIconSize = child.getWidth(); 169 } 170 } 171 if (mShowAllIcons) { 172 resetViewStates(); 173 calculateIconTranslations(); 174 applyIconStates(); 175 } 176 } 177 178 public void applyIconStates() { 179 for (int i = 0; i < getChildCount(); i++) { 180 View child = getChildAt(i); 181 ViewState childState = mIconStates.get(child); 182 if (childState != null) { 183 childState.applyToView(child); 184 } 185 } 186 mAddAnimationStartIndex = -1; 187 mCannedAnimationStartIndex = -1; 188 mDisallowNextAnimation = false; 189 } 190 191 @Override 192 public void onViewAdded(View child) { 193 super.onViewAdded(child); 194 boolean isReplacingIcon = isReplacingIcon(child); 195 if (!mChangingViewPositions) { 196 IconState v = new IconState(); 197 if (isReplacingIcon) { 198 v.justAdded = false; 199 v.justReplaced = true; 200 } 201 mIconStates.put(child, v); 202 } 203 int childIndex = indexOfChild(child); 204 if (childIndex < getChildCount() - 1 && !isReplacingIcon 205 && mIconStates.get(getChildAt(childIndex + 1)).iconAppearAmount > 0.0f) { 206 if (mAddAnimationStartIndex < 0) { 207 mAddAnimationStartIndex = childIndex; 208 } else { 209 mAddAnimationStartIndex = Math.min(mAddAnimationStartIndex, childIndex); 210 } 211 } 212 if (mDark && child instanceof StatusBarIconView) { 213 ((StatusBarIconView) child).setDark(mDark, false, 0); 214 } 215 } 216 217 private boolean isReplacingIcon(View child) { 218 if (mReplacingIcons == null) { 219 return false; 220 } 221 if (!(child instanceof StatusBarIconView)) { 222 return false; 223 } 224 StatusBarIconView iconView = (StatusBarIconView) child; 225 Icon sourceIcon = iconView.getSourceIcon(); 226 String groupKey = iconView.getNotification().getGroupKey(); 227 ArrayList<StatusBarIcon> statusBarIcons = mReplacingIcons.get(groupKey); 228 if (statusBarIcons != null) { 229 StatusBarIcon replacedIcon = statusBarIcons.get(0); 230 if (sourceIcon.sameAs(replacedIcon.icon)) { 231 return true; 232 } 233 } 234 return false; 235 } 236 237 @Override 238 public void onViewRemoved(View child) { 239 super.onViewRemoved(child); 240 if (child instanceof StatusBarIconView) { 241 boolean isReplacingIcon = isReplacingIcon(child); 242 final StatusBarIconView icon = (StatusBarIconView) child; 243 if (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN 244 && child.getVisibility() == VISIBLE && isReplacingIcon) { 245 int animationStartIndex = findFirstViewIndexAfter(icon.getTranslationX()); 246 if (mAddAnimationStartIndex < 0) { 247 mAddAnimationStartIndex = animationStartIndex; 248 } else { 249 mAddAnimationStartIndex = Math.min(mAddAnimationStartIndex, animationStartIndex); 250 } 251 } 252 if (!mChangingViewPositions) { 253 mIconStates.remove(child); 254 if (!isReplacingIcon) { 255 addTransientView(icon, 0); 256 icon.setVisibleState(StatusBarIconView.STATE_HIDDEN, true /* animate */, 257 () -> removeTransientView(icon)); 258 } 259 } 260 } 261 } 262 263 /** 264 * Finds the first view with a translation bigger then a given value 265 */ 266 private int findFirstViewIndexAfter(float translationX) { 267 for (int i = 0; i < getChildCount(); i++) { 268 View view = getChildAt(i); 269 if (view.getTranslationX() > translationX) { 270 return i; 271 } 272 } 273 return getChildCount(); 274 } 275 276 public void resetViewStates() { 277 for (int i = 0; i < getChildCount(); i++) { 278 View view = getChildAt(i); 279 ViewState iconState = mIconStates.get(view); 280 iconState.initFrom(view); 281 iconState.alpha = 1.0f; 282 iconState.hidden = false; 283 } 284 } 285 286 /** 287 * Calulate the horizontal translations for each notification based on how much the icons 288 * are inserted into the notification container. 289 * If this is not a whole number, the fraction means by how much the icon is appearing. 290 */ 291 public void calculateIconTranslations() { 292 float translationX = getActualPaddingStart(); 293 int firstOverflowIndex = -1; 294 int childCount = getChildCount(); 295 int maxVisibleIcons = mDark ? MAX_VISIBLE_ICONS_WHEN_DARK : childCount; 296 float layoutEnd = getLayoutEnd(); 297 float overflowStart = layoutEnd - mIconSize * (2 + OVERFLOW_EARLY_AMOUNT); 298 boolean hasAmbient = mSpeedBumpIndex != -1 && mSpeedBumpIndex < getChildCount(); 299 float visualOverflowStart = 0; 300 for (int i = 0; i < childCount; i++) { 301 View view = getChildAt(i); 302 IconState iconState = mIconStates.get(view); 303 iconState.xTranslation = translationX; 304 boolean forceOverflow = mSpeedBumpIndex != -1 && i >= mSpeedBumpIndex 305 && iconState.iconAppearAmount > 0.0f || i >= maxVisibleIcons; 306 boolean noOverflowAfter = i == childCount - 1; 307 float drawingScale = mDark && view instanceof StatusBarIconView 308 ? ((StatusBarIconView) view).getIconScaleFullyDark() 309 : 1f; 310 if (mOpenedAmount != 0.0f) { 311 noOverflowAfter = noOverflowAfter && !hasAmbient && !forceOverflow; 312 } 313 iconState.visibleState = StatusBarIconView.STATE_ICON; 314 if (firstOverflowIndex == -1 && (forceOverflow 315 || (translationX >= (noOverflowAfter ? layoutEnd - mIconSize : overflowStart)))) { 316 firstOverflowIndex = noOverflowAfter && !forceOverflow ? i - 1 : i; 317 int totalDotLength = mStaticDotRadius * 6 + 2 * mDotPadding; 318 visualOverflowStart = overflowStart + mIconSize * (1 + OVERFLOW_EARLY_AMOUNT) 319 - totalDotLength / 2 320 - mIconSize * 0.5f + mStaticDotRadius; 321 if (forceOverflow) { 322 visualOverflowStart = Math.min(translationX, visualOverflowStart 323 + mStaticDotRadius * 2 + mDotPadding); 324 } else { 325 visualOverflowStart += (translationX - overflowStart) / mIconSize 326 * (mStaticDotRadius * 2 + mDotPadding); 327 } 328 if (mShowAllIcons) { 329 // We want to perfectly position the overflow in the static state, such that 330 // it's perfectly centered instead of measuring it from the end. 331 mVisualOverflowAdaption = 0; 332 if (firstOverflowIndex != -1) { 333 View firstOverflowView = getChildAt(i); 334 IconState overflowState = mIconStates.get(firstOverflowView); 335 float totalAmount = layoutEnd - overflowState.xTranslation; 336 float newPosition = overflowState.xTranslation + totalAmount / 2 337 - totalDotLength / 2 338 - mIconSize * 0.5f + mStaticDotRadius; 339 mVisualOverflowAdaption = newPosition - visualOverflowStart; 340 visualOverflowStart = newPosition; 341 } 342 } else { 343 visualOverflowStart += mVisualOverflowAdaption * (1f - mOpenedAmount); 344 } 345 } 346 translationX += iconState.iconAppearAmount * view.getWidth() * drawingScale; 347 } 348 if (firstOverflowIndex != -1) { 349 int numDots = 1; 350 translationX = visualOverflowStart; 351 for (int i = firstOverflowIndex; i < childCount; i++) { 352 View view = getChildAt(i); 353 IconState iconState = mIconStates.get(view); 354 int dotWidth = mStaticDotRadius * 2 + mDotPadding; 355 iconState.xTranslation = translationX; 356 if (numDots <= 3) { 357 if (numDots == 1 && iconState.iconAppearAmount < 0.8f) { 358 iconState.visibleState = StatusBarIconView.STATE_ICON; 359 numDots--; 360 } else { 361 iconState.visibleState = StatusBarIconView.STATE_DOT; 362 } 363 translationX += (numDots == 3 ? 3 * dotWidth : dotWidth) 364 * iconState.iconAppearAmount; 365 } else { 366 iconState.visibleState = StatusBarIconView.STATE_HIDDEN; 367 } 368 numDots++; 369 } 370 } 371 boolean center = mDark; 372 if (center && translationX < getLayoutEnd()) { 373 float delta = (getLayoutEnd() - translationX) / 2; 374 if (firstOverflowIndex != -1) { 375 // If we have an overflow, only count those half for centering because the dots 376 // don't have a lot of visual weight. 377 float deltaIgnoringOverflow = (getLayoutEnd() - visualOverflowStart) / 2; 378 delta = (deltaIgnoringOverflow + delta) / 2; 379 } 380 for (int i = 0; i < childCount; i++) { 381 View view = getChildAt(i); 382 IconState iconState = mIconStates.get(view); 383 iconState.xTranslation += delta; 384 } 385 } 386 387 if (isLayoutRtl()) { 388 for (int i = 0; i < childCount; i++) { 389 View view = getChildAt(i); 390 IconState iconState = mIconStates.get(view); 391 iconState.xTranslation = getWidth() - iconState.xTranslation - view.getWidth(); 392 } 393 } 394 395 if (mDark && mDarkOffsetX != 0) { 396 for (int i = 0; i < childCount; i++) { 397 View view = getChildAt(i); 398 IconState iconState = mIconStates.get(view); 399 iconState.xTranslation += mDarkOffsetX; 400 } 401 } 402 } 403 404 private float getLayoutEnd() { 405 return getActualWidth() - getActualPaddingEnd(); 406 } 407 408 private float getActualPaddingEnd() { 409 if (mActualPaddingEnd == NO_VALUE) { 410 return getPaddingEnd(); 411 } 412 return mActualPaddingEnd; 413 } 414 415 private float getActualPaddingStart() { 416 if (mActualPaddingStart == NO_VALUE) { 417 return getPaddingStart(); 418 } 419 return mActualPaddingStart; 420 } 421 422 /** 423 * Sets whether the layout should always show all icons. 424 * If this is true, the icon positions will be updated on layout. 425 * If this if false, the layout is managed from the outside and layouting won't trigger a 426 * repositioning of the icons. 427 */ 428 public void setShowAllIcons(boolean showAllIcons) { 429 mShowAllIcons = showAllIcons; 430 } 431 432 public void setActualLayoutWidth(int actualLayoutWidth) { 433 mActualLayoutWidth = actualLayoutWidth; 434 if (DEBUG) { 435 invalidate(); 436 } 437 } 438 439 public void setActualPaddingEnd(float paddingEnd) { 440 mActualPaddingEnd = paddingEnd; 441 if (DEBUG) { 442 invalidate(); 443 } 444 } 445 446 public void setActualPaddingStart(float paddingStart) { 447 mActualPaddingStart = paddingStart; 448 if (DEBUG) { 449 invalidate(); 450 } 451 } 452 453 public int getActualWidth() { 454 if (mActualLayoutWidth == NO_VALUE) { 455 return getWidth(); 456 } 457 return mActualLayoutWidth; 458 } 459 460 public void setChangingViewPositions(boolean changingViewPositions) { 461 mChangingViewPositions = changingViewPositions; 462 } 463 464 public void setDark(boolean dark, boolean fade, long delay) { 465 mDark = dark; 466 mDisallowNextAnimation |= !fade; 467 for (int i = 0; i < getChildCount(); i++) { 468 View view = getChildAt(i); 469 if (view instanceof StatusBarIconView) { 470 ((StatusBarIconView) view).setDark(dark, fade, delay); 471 } 472 } 473 } 474 475 public IconState getIconState(StatusBarIconView icon) { 476 return mIconStates.get(icon); 477 } 478 479 public void setSpeedBumpIndex(int speedBumpIndex) { 480 mSpeedBumpIndex = speedBumpIndex; 481 } 482 483 public void setOpenedAmount(float expandAmount) { 484 mOpenedAmount = expandAmount; 485 } 486 487 public float getVisualOverflowAdaption() { 488 return mVisualOverflowAdaption; 489 } 490 491 public void setVisualOverflowAdaption(float visualOverflowAdaption) { 492 mVisualOverflowAdaption = visualOverflowAdaption; 493 } 494 495 public boolean hasOverflow() { 496 float width = (getChildCount() + OVERFLOW_EARLY_AMOUNT) * mIconSize; 497 return width - (getWidth() - getActualPaddingStart() - getActualPaddingEnd()) > 0; 498 } 499 500 public void setVibrateOnAnimation(boolean vibrateOnAnimation) { 501 mVibrateOnAnimation = vibrateOnAnimation; 502 } 503 504 public int getIconSize() { 505 return mIconSize; 506 } 507 508 public void setAnimationsEnabled(boolean enabled) { 509 if (!enabled && mAnimationsEnabled) { 510 for (int i = 0; i < getChildCount(); i++) { 511 View child = getChildAt(i); 512 ViewState childState = mIconStates.get(child); 513 if (childState != null) { 514 childState.cancelAnimations(child); 515 childState.applyToView(child); 516 } 517 } 518 } 519 mAnimationsEnabled = enabled; 520 } 521 522 public void setDarkOffsetX(int offsetX) { 523 mDarkOffsetX = offsetX; 524 } 525 526 public void setReplacingIcons(ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons) { 527 mReplacingIcons = replacingIcons; 528 } 529 530 public class IconState extends ViewState { 531 public static final int NO_VALUE = NotificationIconContainer.NO_VALUE; 532 public float iconAppearAmount = 1.0f; 533 public float clampedAppearAmount = 1.0f; 534 public int visibleState; 535 public boolean justAdded = true; 536 private boolean justReplaced; 537 public boolean needsCannedAnimation; 538 public boolean useFullTransitionAmount; 539 public boolean useLinearTransitionAmount; 540 public boolean translateContent; 541 public int iconColor = StatusBarIconView.NO_COLOR; 542 public boolean noAnimations; 543 public boolean isLastExpandIcon; 544 public int customTransformHeight = NO_VALUE; 545 546 @Override 547 public void applyToView(View view) { 548 if (view instanceof StatusBarIconView) { 549 StatusBarIconView icon = (StatusBarIconView) view; 550 boolean animate = false; 551 AnimationProperties animationProperties = null; 552 boolean animationsAllowed = mAnimationsEnabled && !mDisallowNextAnimation 553 && !noAnimations; 554 if (animationsAllowed) { 555 if (justAdded || justReplaced) { 556 super.applyToView(icon); 557 if (justAdded && iconAppearAmount != 0.0f) { 558 icon.setAlpha(0.0f); 559 icon.setVisibleState(StatusBarIconView.STATE_HIDDEN, 560 false /* animate */); 561 animationProperties = ADD_ICON_PROPERTIES; 562 animate = true; 563 } 564 } else if (visibleState != icon.getVisibleState()) { 565 animationProperties = DOT_ANIMATION_PROPERTIES; 566 animate = true; 567 } 568 if (!animate && mAddAnimationStartIndex >= 0 569 && indexOfChild(view) >= mAddAnimationStartIndex 570 && (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN 571 || visibleState != StatusBarIconView.STATE_HIDDEN)) { 572 animationProperties = DOT_ANIMATION_PROPERTIES; 573 animate = true; 574 } 575 if (needsCannedAnimation) { 576 AnimationFilter animationFilter = sTempProperties.getAnimationFilter(); 577 animationFilter.reset(); 578 animationFilter.combineFilter( 579 ICON_ANIMATION_PROPERTIES.getAnimationFilter()); 580 sTempProperties.resetCustomInterpolators(); 581 sTempProperties.combineCustomInterpolators(ICON_ANIMATION_PROPERTIES); 582 if (animationProperties != null) { 583 animationFilter.combineFilter(animationProperties.getAnimationFilter()); 584 sTempProperties.combineCustomInterpolators(animationProperties); 585 } 586 animationProperties = sTempProperties; 587 animationProperties.setDuration(CANNED_ANIMATION_DURATION); 588 animate = true; 589 mCannedAnimationStartIndex = indexOfChild(view); 590 } 591 if (!animate && mCannedAnimationStartIndex >= 0 592 && indexOfChild(view) > mCannedAnimationStartIndex 593 && (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN 594 || visibleState != StatusBarIconView.STATE_HIDDEN)) { 595 AnimationFilter animationFilter = sTempProperties.getAnimationFilter(); 596 animationFilter.reset(); 597 animationFilter.animateX(); 598 sTempProperties.resetCustomInterpolators(); 599 animationProperties = sTempProperties; 600 animationProperties.setDuration(CANNED_ANIMATION_DURATION); 601 animate = true; 602 } 603 } 604 icon.setVisibleState(visibleState, animationsAllowed); 605 icon.setIconColor(iconColor, needsCannedAnimation && animationsAllowed); 606 if (animate) { 607 animateTo(icon, animationProperties); 608 } else { 609 super.applyToView(view); 610 } 611 boolean wasInShelf = icon.isInShelf(); 612 boolean inShelf = iconAppearAmount == 1.0f; 613 icon.setIsInShelf(inShelf); 614 if (shouldVibrateChange(wasInShelf != inShelf)) { 615 AsyncTask.execute( 616 () -> mVibrator.vibrate(VibrationEffect.get( 617 VibrationEffect.EFFECT_TICK))); 618 } 619 } 620 justAdded = false; 621 justReplaced = false; 622 needsCannedAnimation = false; 623 } 624 625 private boolean shouldVibrateChange(boolean inShelfChanged) { 626 if (!mVibrateOnAnimation) { 627 return false; 628 } 629 if (justAdded) { 630 return false; 631 } 632 if (!mAnimationsEnabled) { 633 return false; 634 } 635 if (!inShelfChanged) { 636 return false; 637 } 638 if (isHapticFeedbackDisabled(mContext)) { 639 return false; 640 } 641 return true; 642 } 643 644 public boolean hasCustomTransformHeight() { 645 return isLastExpandIcon && customTransformHeight != NO_VALUE; 646 } 647 648 @Override 649 public void initFrom(View view) { 650 super.initFrom(view); 651 if (view instanceof StatusBarIconView) { 652 iconColor = ((StatusBarIconView) view).getStaticDrawableColor(); 653 } 654 } 655 } 656 } 657