1 /* 2 * Copyright (C) 2023 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 package com.android.launcher3.taskbar.bubbles; 17 18 import static android.view.View.INVISIBLE; 19 import static android.view.View.VISIBLE; 20 21 import android.content.res.Resources; 22 import android.graphics.Point; 23 import android.graphics.PointF; 24 import android.graphics.Rect; 25 import android.util.DisplayMetrics; 26 import android.util.Log; 27 import android.util.TypedValue; 28 import android.view.Gravity; 29 import android.view.MotionEvent; 30 import android.view.View; 31 import android.widget.FrameLayout; 32 33 import androidx.annotation.NonNull; 34 import androidx.annotation.Nullable; 35 36 import com.android.launcher3.R; 37 import com.android.launcher3.anim.AnimatedFloat; 38 import com.android.launcher3.taskbar.TaskbarActivityContext; 39 import com.android.launcher3.taskbar.TaskbarControllers; 40 import com.android.launcher3.taskbar.TaskbarInsetsController; 41 import com.android.launcher3.taskbar.TaskbarStashController; 42 import com.android.launcher3.taskbar.bubbles.animation.BubbleBarViewAnimator; 43 import com.android.launcher3.util.MultiPropertyFactory; 44 import com.android.launcher3.util.MultiValueAlpha; 45 import com.android.quickstep.SystemUiProxy; 46 import com.android.wm.shell.common.bubbles.BubbleBarLocation; 47 48 import java.util.List; 49 import java.util.Objects; 50 import java.util.function.Consumer; 51 52 /** 53 * Controller for {@link BubbleBarView}. Manages the visibility of the bubble bar as well as 54 * responding to changes in bubble state provided by BubbleBarController. 55 */ 56 public class BubbleBarViewController { 57 58 private static final String TAG = "BubbleBarViewController"; 59 private static final float APP_ICON_SMALL_DP = 44f; 60 private static final float APP_ICON_MEDIUM_DP = 48f; 61 private static final float APP_ICON_LARGE_DP = 52f; 62 private final SystemUiProxy mSystemUiProxy; 63 private final TaskbarActivityContext mActivity; 64 private final BubbleBarView mBarView; 65 private int mIconSize; 66 67 // Initialized in init. 68 private BubbleStashController mBubbleStashController; 69 private BubbleBarController mBubbleBarController; 70 private BubbleDragController mBubbleDragController; 71 private TaskbarStashController mTaskbarStashController; 72 private TaskbarInsetsController mTaskbarInsetsController; 73 private View.OnClickListener mBubbleClickListener; 74 private View.OnClickListener mBubbleBarClickListener; 75 76 // These are exposed to {@link BubbleStashController} to animate for stashing/un-stashing 77 private final MultiValueAlpha mBubbleBarAlpha; 78 private final AnimatedFloat mBubbleBarScale = new AnimatedFloat(this::updateScale); 79 private final AnimatedFloat mBubbleBarTranslationY = new AnimatedFloat( 80 this::updateTranslationY); 81 82 // Modified when swipe up is happening on the bubble bar or task bar. 83 private float mBubbleBarSwipeUpTranslationY; 84 85 // Whether the bar is hidden for a sysui state. 86 private boolean mHiddenForSysui; 87 // Whether the bar is hidden because there are no bubbles. 88 private boolean mHiddenForNoBubbles = true; 89 private boolean mShouldShowEducation; 90 91 private BubbleBarViewAnimator mBubbleBarViewAnimator; 92 93 @Nullable 94 private BubbleBarBoundsChangeListener mBoundsChangeListener; 95 BubbleBarViewController(TaskbarActivityContext activity, BubbleBarView barView)96 public BubbleBarViewController(TaskbarActivityContext activity, BubbleBarView barView) { 97 mActivity = activity; 98 mBarView = barView; 99 mSystemUiProxy = SystemUiProxy.INSTANCE.get(mActivity); 100 mBubbleBarAlpha = new MultiValueAlpha(mBarView, 1 /* num alpha channels */); 101 mIconSize = activity.getResources().getDimensionPixelSize( 102 R.dimen.bubblebar_icon_size); 103 } 104 init(TaskbarControllers controllers, BubbleControllers bubbleControllers)105 public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) { 106 mBubbleStashController = bubbleControllers.bubbleStashController; 107 mBubbleBarController = bubbleControllers.bubbleBarController; 108 mBubbleDragController = bubbleControllers.bubbleDragController; 109 mTaskbarStashController = controllers.taskbarStashController; 110 mTaskbarInsetsController = controllers.taskbarInsetsController; 111 mBubbleBarViewAnimator = new BubbleBarViewAnimator(mBarView, mBubbleStashController); 112 113 mActivity.addOnDeviceProfileChangeListener( 114 dp -> updateBubbleBarIconSize(dp.taskbarIconSize, /* animate= */ true)); 115 updateBubbleBarIconSize(mActivity.getDeviceProfile().taskbarIconSize, /* animate= */ false); 116 mBubbleBarScale.updateValue(1f); 117 mBubbleClickListener = v -> onBubbleClicked(v); 118 mBubbleBarClickListener = v -> onBubbleBarClicked(); 119 mBubbleDragController.setupBubbleBarView(mBarView); 120 mBarView.setOnClickListener(mBubbleBarClickListener); 121 mBarView.addOnLayoutChangeListener( 122 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { 123 mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged(); 124 if (mBoundsChangeListener != null) { 125 mBoundsChangeListener.onBoundsChanged(); 126 } 127 }); 128 mBarView.setController(new BubbleBarView.Controller() { 129 @Override 130 public float getBubbleBarTranslationY() { 131 return mBubbleStashController.getBubbleBarTranslationY(); 132 } 133 134 @Override 135 public void onBubbleBarTouchedWhileAnimating() { 136 BubbleBarViewController.this.onBubbleBarTouchedWhileAnimating(); 137 } 138 }); 139 } 140 onBubbleClicked(View v)141 private void onBubbleClicked(View v) { 142 BubbleBarItem bubble = ((BubbleView) v).getBubble(); 143 if (bubble == null) { 144 Log.e(TAG, "bubble click listener, bubble was null"); 145 } 146 147 final String currentlySelected = mBubbleBarController.getSelectedBubbleKey(); 148 if (mBarView.isExpanded() && Objects.equals(bubble.getKey(), currentlySelected)) { 149 // Tapping the currently selected bubble while expanded collapses the view. 150 setExpanded(false); 151 mBubbleStashController.stashBubbleBar(); 152 } else { 153 mBubbleBarController.showAndSelectBubble(bubble); 154 } 155 } 156 onBubbleBarTouchedWhileAnimating()157 private void onBubbleBarTouchedWhileAnimating() { 158 mBubbleBarViewAnimator.onBubbleBarTouchedWhileAnimating(); 159 mBubbleStashController.onNewBubbleAnimationInterrupted(false, mBarView.getTranslationY()); 160 } 161 onBubbleBarClicked()162 private void onBubbleBarClicked() { 163 if (mShouldShowEducation) { 164 mShouldShowEducation = false; 165 // Get the bubble bar bounds on screen 166 Rect bounds = new Rect(); 167 mBarView.getBoundsOnScreen(bounds); 168 // Calculate user education reference position in Screen coordinates 169 Point position = new Point(bounds.centerX(), bounds.top); 170 // Show user education relative to the reference point 171 mSystemUiProxy.showUserEducation(position); 172 } else { 173 // ensure that the bubble bar has the correct translation. we may have just interrupted 174 // the animation by touching the bubble bar. 175 mBubbleBarTranslationY.animateToValue(mBubbleStashController.getBubbleBarTranslationY()) 176 .start(); 177 setExpanded(true); 178 } 179 } 180 181 /** Notifies that the stash state is changing. */ onStashStateChanging()182 public void onStashStateChanging() { 183 if (isAnimatingNewBubble()) { 184 mBubbleBarViewAnimator.onStashStateChangingWhileAnimating(); 185 } 186 } 187 188 // 189 // The below animators are exposed to BubbleStashController so it can manage the stashing 190 // animation. 191 // 192 getBubbleBarAlpha()193 public MultiPropertyFactory<View> getBubbleBarAlpha() { 194 return mBubbleBarAlpha; 195 } 196 getBubbleBarScale()197 public AnimatedFloat getBubbleBarScale() { 198 return mBubbleBarScale; 199 } 200 getBubbleBarTranslationY()201 public AnimatedFloat getBubbleBarTranslationY() { 202 return mBubbleBarTranslationY; 203 } 204 getBubbleBarCollapsedHeight()205 float getBubbleBarCollapsedHeight() { 206 return mBarView.getBubbleBarCollapsedHeight(); 207 } 208 209 /** 210 * Whether the bubble bar is visible or not. 211 */ isBubbleBarVisible()212 public boolean isBubbleBarVisible() { 213 return mBarView.getVisibility() == VISIBLE; 214 } 215 216 /** Whether the bubble bar has bubbles. */ hasBubbles()217 public boolean hasBubbles() { 218 return mBubbleBarController.getSelectedBubbleKey() != null; 219 } 220 221 /** 222 * @return current {@link BubbleBarLocation} 223 */ getBubbleBarLocation()224 public BubbleBarLocation getBubbleBarLocation() { 225 return mBarView.getBubbleBarLocation(); 226 } 227 228 /** 229 * Update bar {@link BubbleBarLocation} 230 */ setBubbleBarLocation(BubbleBarLocation bubbleBarLocation)231 public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { 232 mBarView.setBubbleBarLocation(bubbleBarLocation); 233 } 234 235 /** 236 * Animate bubble bar to the given location. The location change is transient. It does not 237 * update the state of the bubble bar. 238 * To update bubble bar pinned location, use {@link #setBubbleBarLocation(BubbleBarLocation)}. 239 */ animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation)240 public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { 241 mBarView.animateToBubbleBarLocation(bubbleBarLocation); 242 } 243 244 /** 245 * The bounds of the bubble bar. 246 */ getBubbleBarBounds()247 public Rect getBubbleBarBounds() { 248 return mBarView.getBubbleBarBounds(); 249 } 250 251 /** Whether a new bubble is animating. */ isAnimatingNewBubble()252 public boolean isAnimatingNewBubble() { 253 return mBarView.isAnimatingNewBubble(); 254 } 255 256 /** The horizontal margin of the bubble bar from the edge of the screen. */ getHorizontalMargin()257 public int getHorizontalMargin() { 258 return mBarView.getHorizontalMargin(); 259 } 260 261 /** 262 * When the bubble bar is not stashed, it can be collapsed (the icons are in a stack) or 263 * expanded (the icons are in a row). This indicates whether the bubble bar is expanded. 264 */ isExpanded()265 public boolean isExpanded() { 266 return mBarView.isExpanded(); 267 } 268 269 /** 270 * Whether the motion event is within the bounds of the bubble bar. 271 */ isEventOverAnyItem(MotionEvent ev)272 public boolean isEventOverAnyItem(MotionEvent ev) { 273 return mBarView.isEventOverAnyItem(ev); 274 } 275 276 // 277 // Visibility of the bubble bar 278 // 279 280 /** 281 * Returns whether the bubble bar is hidden because there are no bubbles. 282 */ isHiddenForNoBubbles()283 public boolean isHiddenForNoBubbles() { 284 return mHiddenForNoBubbles; 285 } 286 287 /** 288 * Sets whether the bubble bar should be hidden because there are no bubbles. 289 */ setHiddenForBubbles(boolean hidden)290 public void setHiddenForBubbles(boolean hidden) { 291 if (mHiddenForNoBubbles != hidden) { 292 mHiddenForNoBubbles = hidden; 293 updateVisibilityForStateChange(); 294 if (hidden) { 295 mBarView.setAlpha(0); 296 mBarView.setExpanded(false); 297 } 298 mActivity.bubbleBarVisibilityChanged(!hidden); 299 } 300 } 301 302 /** Sets a callback that updates the selected bubble after the bubble bar collapses. */ setUpdateSelectedBubbleAfterCollapse( Consumer<String> updateSelectedBubbleAfterCollapse)303 public void setUpdateSelectedBubbleAfterCollapse( 304 Consumer<String> updateSelectedBubbleAfterCollapse) { 305 mBarView.setUpdateSelectedBubbleAfterCollapse(updateSelectedBubbleAfterCollapse); 306 } 307 308 /** Returns whether the bubble bar should be hidden because of the current sysui state. */ isHiddenForSysui()309 boolean isHiddenForSysui() { 310 return mHiddenForSysui; 311 } 312 313 /** 314 * Sets whether the bubble bar should be hidden due to SysUI state (e.g. on lockscreen). 315 */ setHiddenForSysui(boolean hidden)316 public void setHiddenForSysui(boolean hidden) { 317 if (mHiddenForSysui != hidden) { 318 mHiddenForSysui = hidden; 319 updateVisibilityForStateChange(); 320 } 321 } 322 323 // TODO: (b/273592694) animate it updateVisibilityForStateChange()324 private void updateVisibilityForStateChange() { 325 if (!mHiddenForSysui && !mHiddenForNoBubbles) { 326 mBarView.setVisibility(VISIBLE); 327 } else { 328 mBarView.setVisibility(INVISIBLE); 329 } 330 } 331 332 // 333 // Modifying view related properties. 334 // 335 updateBubbleBarIconSize(int newIconSize, boolean animate)336 private void updateBubbleBarIconSize(int newIconSize, boolean animate) { 337 Resources res = mActivity.getResources(); 338 DisplayMetrics dm = res.getDisplayMetrics(); 339 float smallIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 340 APP_ICON_SMALL_DP, dm); 341 float mediumIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 342 APP_ICON_MEDIUM_DP, dm); 343 float largeIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 344 APP_ICON_LARGE_DP, dm); 345 float smallMediumThreshold = (smallIconSize + mediumIconSize) / 2f; 346 float mediumLargeThreshold = (mediumIconSize + largeIconSize) / 2f; 347 mIconSize = newIconSize <= smallMediumThreshold 348 ? res.getDimensionPixelSize(R.dimen.bubblebar_icon_size_small) : 349 res.getDimensionPixelSize(R.dimen.bubblebar_icon_size); 350 float bubbleBarPadding = newIconSize >= mediumLargeThreshold 351 ? res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing_large) : 352 res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing); 353 if (animate) { 354 mBarView.animateBubbleBarIconSize(mIconSize, bubbleBarPadding); 355 } else { 356 mBarView.setIconSizeAndPadding(mIconSize, bubbleBarPadding); 357 } 358 } 359 360 /** 361 * Sets the translation of the bubble bar during the swipe up gesture. 362 */ setTranslationYForSwipe(float transY)363 public void setTranslationYForSwipe(float transY) { 364 mBubbleBarSwipeUpTranslationY = transY; 365 updateTranslationY(); 366 } 367 updateTranslationY()368 private void updateTranslationY() { 369 mBarView.setTranslationY(mBubbleBarTranslationY.value 370 + mBubbleBarSwipeUpTranslationY); 371 } 372 373 /** 374 * Applies scale properties for the entire bubble bar. 375 */ updateScale()376 private void updateScale() { 377 float scale = mBubbleBarScale.value; 378 mBarView.setScaleX(scale); 379 mBarView.setScaleY(scale); 380 } 381 382 // 383 // Manipulating the specific bubble views in the bar 384 // 385 386 /** 387 * Removes the provided bubble from the bubble bar. 388 */ removeBubble(BubbleBarItem b)389 public void removeBubble(BubbleBarItem b) { 390 if (b != null) { 391 mBarView.removeView(b.getView()); 392 } else { 393 Log.w(TAG, "removeBubble, bubble was null!"); 394 } 395 } 396 397 /** 398 * Adds the provided bubble to the bubble bar. 399 */ addBubble(BubbleBarItem b, boolean isExpanding, boolean suppressAnimation)400 public void addBubble(BubbleBarItem b, boolean isExpanding, boolean suppressAnimation) { 401 if (b != null) { 402 mBarView.addBubble( 403 b.getView(), new FrameLayout.LayoutParams(mIconSize, mIconSize, Gravity.LEFT)); 404 b.getView().setOnClickListener(mBubbleClickListener); 405 mBubbleDragController.setupBubbleView(b.getView()); 406 407 if (b instanceof BubbleBarOverflow) { 408 return; 409 } 410 411 if (suppressAnimation || !(b instanceof BubbleBarBubble bubble)) { 412 // the bubble bar and handle are initialized as part of the first bubble animation. 413 // if the animation is suppressed, immediately stash or show the bubble bar to 414 // ensure they've been initialized. 415 if (mTaskbarStashController.isInApp()) { 416 mBubbleStashController.stashBubbleBarImmediate(); 417 } else { 418 mBubbleStashController.showBubbleBarImmediate(); 419 } 420 return; 421 } 422 animateBubbleNotification(bubble, isExpanding); 423 } else { 424 Log.w(TAG, "addBubble, bubble was null!"); 425 } 426 } 427 428 /** Animates the bubble bar to notify the user about a bubble change. */ animateBubbleNotification(BubbleBarBubble bubble, boolean isExpanding)429 public void animateBubbleNotification(BubbleBarBubble bubble, boolean isExpanding) { 430 boolean isInApp = mTaskbarStashController.isInApp(); 431 // if this is the first bubble, animate to the initial state. one bubble is the overflow 432 // so check for at most 2 children. 433 if (mBarView.getChildCount() <= 2) { 434 mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding); 435 return; 436 } 437 438 // only animate the new bubble if we're in an app and not auto expanding 439 if (isInApp && !isExpanding && !isExpanded()) { 440 mBubbleBarViewAnimator.animateBubbleInForStashed(bubble); 441 } 442 } 443 444 /** 445 * Reorders the bubbles based on the provided list. 446 */ reorderBubbles(List<BubbleBarBubble> newOrder)447 public void reorderBubbles(List<BubbleBarBubble> newOrder) { 448 List<BubbleView> viewList = newOrder.stream().filter(Objects::nonNull) 449 .map(BubbleBarBubble::getView).toList(); 450 mBarView.reorder(viewList); 451 } 452 453 /** 454 * Updates the selected bubble. 455 */ updateSelectedBubble(BubbleBarItem newlySelected)456 public void updateSelectedBubble(BubbleBarItem newlySelected) { 457 mBarView.setSelectedBubble(newlySelected.getView()); 458 } 459 460 /** 461 * Sets whether the bubble bar should be expanded (not unstashed, but have the contents 462 * within it expanded). This method notifies SystemUI that the bubble bar is expanded and 463 * showing a selected bubble. This method should ONLY be called from UI events originating 464 * from Launcher. 465 */ setExpanded(boolean isExpanded)466 public void setExpanded(boolean isExpanded) { 467 if (isExpanded != mBarView.isExpanded()) { 468 mBarView.setExpanded(isExpanded); 469 if (!isExpanded) { 470 mSystemUiProxy.collapseBubbles(); 471 } else { 472 mBubbleBarController.showSelectedBubble(); 473 mTaskbarStashController.updateAndAnimateTransientTaskbar(true /* stash */, 474 false /* shouldBubblesFollow */); 475 } 476 } 477 } 478 479 /** 480 * Sets whether the bubble bar should be expanded. This method is used in response to UI events 481 * from SystemUI. 482 */ setExpandedFromSysui(boolean isExpanded)483 public void setExpandedFromSysui(boolean isExpanded) { 484 if (!isExpanded) { 485 mBubbleStashController.stashBubbleBar(); 486 } else { 487 mBubbleStashController.showBubbleBar(true /* expand the bubbles */); 488 } 489 } 490 491 /** Marks as should show education and shows the bubble bar in a collapsed state */ prepareToShowEducation()492 public void prepareToShowEducation() { 493 mShouldShowEducation = true; 494 mBubbleStashController.showBubbleBar(false /* expand the bubbles */); 495 } 496 497 /** 498 * Updates the dragged bubble view in the bubble bar view, and notifies SystemUI 499 * that a bubble is being dragged to dismiss. 500 * @param bubbleView dragged bubble view 501 */ onBubbleDragStart(@onNull BubbleView bubbleView)502 public void onBubbleDragStart(@NonNull BubbleView bubbleView) { 503 if (bubbleView.getBubble() == null) return; 504 505 mSystemUiProxy.startBubbleDrag(bubbleView.getBubble().getKey()); 506 mBarView.setDraggedBubble(bubbleView); 507 } 508 509 /** 510 * Notifies SystemUI to expand the selected bubble when the bubble is released. 511 */ onBubbleDragRelease(BubbleBarLocation location)512 public void onBubbleDragRelease(BubbleBarLocation location) { 513 mSystemUiProxy.stopBubbleDrag(location, mBarView.getRestingTopPositionOnScreen()); 514 } 515 516 /** 517 * Notifies {@link BubbleBarView} that drag and all animations are finished. 518 */ onBubbleDragEnd()519 public void onBubbleDragEnd() { 520 mBarView.setDraggedBubble(null); 521 } 522 523 /** Notifies that dragging the bubble bar ended. */ onBubbleBarDragEnd()524 public void onBubbleBarDragEnd() { 525 // we may have changed the bubble bar translation Y value from the value it had at the 526 // beginning of the drag, so update the translation Y animator state 527 mBubbleBarTranslationY.updateValue(mBarView.getTranslationY()); 528 } 529 530 /** 531 * Get translation for bubble bar when drag is released. 532 * 533 * @see BubbleBarView#getBubbleBarDragReleaseTranslation(PointF, BubbleBarLocation) 534 */ getBubbleBarDragReleaseTranslation(PointF initialTranslation, BubbleBarLocation location)535 public PointF getBubbleBarDragReleaseTranslation(PointF initialTranslation, 536 BubbleBarLocation location) { 537 return mBarView.getBubbleBarDragReleaseTranslation(initialTranslation, location); 538 } 539 540 /** 541 * Get translation for bubble view when drag is released. 542 * 543 * @see BubbleBarView#getDraggedBubbleReleaseTranslation(PointF, BubbleBarLocation) 544 */ getDraggedBubbleReleaseTranslation(PointF initialTranslation, BubbleBarLocation location)545 public PointF getDraggedBubbleReleaseTranslation(PointF initialTranslation, 546 BubbleBarLocation location) { 547 if (location == mBarView.getBubbleBarLocation()) { 548 return initialTranslation; 549 } 550 return mBarView.getDraggedBubbleReleaseTranslation(initialTranslation, location); 551 } 552 553 /** 554 * Called when bubble was dragged into the dismiss target. Notifies System 555 * @param bubble dismissed bubble item 556 */ onDismissBubbleWhileDragging(@onNull BubbleBarItem bubble)557 public void onDismissBubbleWhileDragging(@NonNull BubbleBarItem bubble) { 558 mSystemUiProxy.dragBubbleToDismiss(bubble.getKey()); 559 } 560 561 /** 562 * Called when bubble stack was dragged into the dismiss target 563 */ onDismissAllBubblesWhileDragging()564 public void onDismissAllBubblesWhileDragging() { 565 mSystemUiProxy.removeAllBubbles(); 566 } 567 568 /** 569 * Set listener to be notified when bubble bar bounds have changed 570 */ setBoundsChangeListener(@ullable BubbleBarBoundsChangeListener listener)571 public void setBoundsChangeListener(@Nullable BubbleBarBoundsChangeListener listener) { 572 mBoundsChangeListener = listener; 573 } 574 575 /** 576 * Listener to receive updates about bubble bar bounds changing 577 */ 578 public interface BubbleBarBoundsChangeListener { 579 /** Called when bounds have changed */ onBoundsChanged()580 void onBoundsChanged(); 581 } 582 } 583