1 package com.android.keyguard; 2 3 import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT; 4 5 import android.animation.Animator; 6 import android.animation.ValueAnimator; 7 import android.app.WallpaperManager; 8 import android.content.Context; 9 import android.graphics.Paint; 10 import android.graphics.Paint.Style; 11 import android.os.Build; 12 import android.transition.Fade; 13 import android.transition.Transition; 14 import android.transition.TransitionListenerAdapter; 15 import android.transition.TransitionManager; 16 import android.transition.TransitionSet; 17 import android.transition.TransitionValues; 18 import android.util.AttributeSet; 19 import android.util.Log; 20 import android.util.MathUtils; 21 import android.view.View; 22 import android.view.ViewGroup; 23 import android.widget.FrameLayout; 24 import android.widget.RelativeLayout; 25 import android.widget.TextClock; 26 27 import androidx.annotation.VisibleForTesting; 28 29 import com.android.internal.colorextraction.ColorExtractor; 30 import com.android.internal.colorextraction.ColorExtractor.OnColorsChangedListener; 31 import com.android.keyguard.clock.ClockManager; 32 import com.android.systemui.Interpolators; 33 import com.android.systemui.R; 34 import com.android.systemui.colorextraction.SysuiColorExtractor; 35 import com.android.systemui.plugins.ClockPlugin; 36 import com.android.systemui.plugins.statusbar.StatusBarStateController; 37 import com.android.systemui.statusbar.StatusBarState; 38 import com.android.systemui.util.wakelock.KeepAwakeAnimationListener; 39 40 import java.io.FileDescriptor; 41 import java.io.PrintWriter; 42 import java.util.Arrays; 43 import java.util.TimeZone; 44 45 import javax.inject.Inject; 46 import javax.inject.Named; 47 48 /** 49 * Switch to show plugin clock when plugin is connected, otherwise it will show default clock. 50 */ 51 public class KeyguardClockSwitch extends RelativeLayout { 52 53 private static final String TAG = "KeyguardClockSwitch"; 54 private static final boolean CUSTOM_CLOCKS_ENABLED = false; 55 56 /** 57 * Animation fraction when text is transitioned to/from bold. 58 */ 59 private static final float TO_BOLD_TRANSITION_FRACTION = 0.7f; 60 61 /** 62 * Controller used to track StatusBar state to know when to show the big_clock_container. 63 */ 64 private final StatusBarStateController mStatusBarStateController; 65 66 /** 67 * Color extractor used to apply colors from wallpaper to custom clock faces. 68 */ 69 private final SysuiColorExtractor mSysuiColorExtractor; 70 71 /** 72 * Manager used to know when to show a custom clock face. 73 */ 74 private final ClockManager mClockManager; 75 76 /** 77 * Layout transition that scales the default clock face. 78 */ 79 private final Transition mTransition; 80 81 private final ClockVisibilityTransition mClockTransition; 82 private final ClockVisibilityTransition mBoldClockTransition; 83 84 /** 85 * Optional/alternative clock injected via plugin. 86 */ 87 private ClockPlugin mClockPlugin; 88 89 /** 90 * Default clock. 91 */ 92 private TextClock mClockView; 93 94 /** 95 * Default clock, bold version. 96 * Used to transition to bold when shrinking the default clock. 97 */ 98 private TextClock mClockViewBold; 99 100 /** 101 * Frame for default and custom clock. 102 */ 103 private FrameLayout mSmallClockFrame; 104 105 /** 106 * Container for big custom clock. 107 */ 108 private ViewGroup mBigClockContainer; 109 110 /** 111 * Status area (date and other stuff) shown below the clock. Plugin can decide whether or not to 112 * show it below the alternate clock. 113 */ 114 private View mKeyguardStatusArea; 115 116 /** 117 * Maintain state so that a newly connected plugin can be initialized. 118 */ 119 private float mDarkAmount; 120 121 /** 122 * Boolean value indicating if notifications are visible on lock screen. 123 */ 124 private boolean mHasVisibleNotifications; 125 126 /** 127 * If the Keyguard Slice has a header (big center-aligned text.) 128 */ 129 private boolean mShowingHeader; 130 private boolean mSupportsDarkText; 131 private int[] mColorPalette; 132 133 /** 134 * Track the state of the status bar to know when to hide the big_clock_container. 135 */ 136 private int mStatusBarState; 137 138 private final StatusBarStateController.StateListener mStateListener = 139 new StatusBarStateController.StateListener() { 140 @Override 141 public void onStateChanged(int newState) { 142 mStatusBarState = newState; 143 updateBigClockVisibility(); 144 } 145 }; 146 147 private ClockManager.ClockChangedListener mClockChangedListener = this::setClockPlugin; 148 149 /** 150 * Listener for changes to the color palette. 151 * 152 * The color palette changes when the wallpaper is changed. 153 */ 154 private final OnColorsChangedListener mColorsListener = (extractor, which) -> { 155 if ((which & WallpaperManager.FLAG_LOCK) != 0) { 156 updateColors(); 157 } 158 }; 159 160 @Inject KeyguardClockSwitch(@amedVIEW_CONTEXT) Context context, AttributeSet attrs, StatusBarStateController statusBarStateController, SysuiColorExtractor colorExtractor, ClockManager clockManager)161 public KeyguardClockSwitch(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs, 162 StatusBarStateController statusBarStateController, SysuiColorExtractor colorExtractor, 163 ClockManager clockManager) { 164 super(context, attrs); 165 mStatusBarStateController = statusBarStateController; 166 mStatusBarState = mStatusBarStateController.getState(); 167 mSysuiColorExtractor = colorExtractor; 168 mClockManager = clockManager; 169 170 mClockTransition = new ClockVisibilityTransition().setCutoff( 171 1 - TO_BOLD_TRANSITION_FRACTION); 172 mClockTransition.addTarget(R.id.default_clock_view); 173 mBoldClockTransition = new ClockVisibilityTransition().setCutoff( 174 TO_BOLD_TRANSITION_FRACTION); 175 mBoldClockTransition.addTarget(R.id.default_clock_view_bold); 176 mTransition = new TransitionSet() 177 .setOrdering(TransitionSet.ORDERING_TOGETHER) 178 .addTransition(mClockTransition) 179 .addTransition(mBoldClockTransition) 180 .setDuration(KeyguardSliceView.DEFAULT_ANIM_DURATION / 2) 181 .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); 182 } 183 184 /** 185 * Returns if this view is presenting a custom clock, or the default implementation. 186 */ hasCustomClock()187 public boolean hasCustomClock() { 188 return mClockPlugin != null; 189 } 190 191 @Override onFinishInflate()192 protected void onFinishInflate() { 193 super.onFinishInflate(); 194 mClockView = findViewById(R.id.default_clock_view); 195 mClockViewBold = findViewById(R.id.default_clock_view_bold); 196 mSmallClockFrame = findViewById(R.id.clock_view); 197 mKeyguardStatusArea = findViewById(R.id.keyguard_status_area); 198 } 199 200 @Override onAttachedToWindow()201 protected void onAttachedToWindow() { 202 super.onAttachedToWindow(); 203 if (CUSTOM_CLOCKS_ENABLED) { 204 mClockManager.addOnClockChangedListener(mClockChangedListener); 205 } 206 mStatusBarStateController.addCallback(mStateListener); 207 mSysuiColorExtractor.addOnColorsChangedListener(mColorsListener); 208 updateColors(); 209 } 210 211 @Override onDetachedFromWindow()212 protected void onDetachedFromWindow() { 213 super.onDetachedFromWindow(); 214 if (CUSTOM_CLOCKS_ENABLED) { 215 mClockManager.removeOnClockChangedListener(mClockChangedListener); 216 } 217 mStatusBarStateController.removeCallback(mStateListener); 218 mSysuiColorExtractor.removeOnColorsChangedListener(mColorsListener); 219 setClockPlugin(null); 220 } 221 setClockPlugin(ClockPlugin plugin)222 private void setClockPlugin(ClockPlugin plugin) { 223 // Disconnect from existing plugin. 224 if (mClockPlugin != null) { 225 View smallClockView = mClockPlugin.getView(); 226 if (smallClockView != null && smallClockView.getParent() == mSmallClockFrame) { 227 mSmallClockFrame.removeView(smallClockView); 228 } 229 if (mBigClockContainer != null) { 230 mBigClockContainer.removeAllViews(); 231 updateBigClockVisibility(); 232 } 233 mClockPlugin.onDestroyView(); 234 mClockPlugin = null; 235 } 236 if (plugin == null) { 237 if (mShowingHeader) { 238 mClockView.setVisibility(View.GONE); 239 mClockViewBold.setVisibility(View.VISIBLE); 240 } else { 241 mClockView.setVisibility(View.VISIBLE); 242 mClockViewBold.setVisibility(View.INVISIBLE); 243 } 244 mKeyguardStatusArea.setVisibility(View.VISIBLE); 245 return; 246 } 247 // Attach small and big clock views to hierarchy. 248 View smallClockView = plugin.getView(); 249 if (smallClockView != null) { 250 mSmallClockFrame.addView(smallClockView, -1, 251 new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 252 ViewGroup.LayoutParams.WRAP_CONTENT)); 253 mClockView.setVisibility(View.GONE); 254 mClockViewBold.setVisibility(View.GONE); 255 } 256 View bigClockView = plugin.getBigClockView(); 257 if (bigClockView != null && mBigClockContainer != null) { 258 mBigClockContainer.addView(bigClockView); 259 updateBigClockVisibility(); 260 } 261 // Hide default clock. 262 if (!plugin.shouldShowStatusArea()) { 263 mKeyguardStatusArea.setVisibility(View.GONE); 264 } 265 // Initialize plugin parameters. 266 mClockPlugin = plugin; 267 mClockPlugin.setStyle(getPaint().getStyle()); 268 mClockPlugin.setTextColor(getCurrentTextColor()); 269 mClockPlugin.setDarkAmount(mDarkAmount); 270 if (mColorPalette != null) { 271 mClockPlugin.setColorPalette(mSupportsDarkText, mColorPalette); 272 } 273 } 274 275 /** 276 * Set container for big clock face appearing behind NSSL and KeyguardStatusView. 277 */ setBigClockContainer(ViewGroup container)278 public void setBigClockContainer(ViewGroup container) { 279 if (mClockPlugin != null && container != null) { 280 View bigClockView = mClockPlugin.getBigClockView(); 281 if (bigClockView != null) { 282 container.addView(bigClockView); 283 } 284 } 285 mBigClockContainer = container; 286 updateBigClockVisibility(); 287 } 288 289 /** 290 * It will also update plugin setStyle if plugin is connected. 291 */ setStyle(Style style)292 public void setStyle(Style style) { 293 mClockView.getPaint().setStyle(style); 294 mClockViewBold.getPaint().setStyle(style); 295 if (mClockPlugin != null) { 296 mClockPlugin.setStyle(style); 297 } 298 } 299 300 /** 301 * It will also update plugin setTextColor if plugin is connected. 302 */ setTextColor(int color)303 public void setTextColor(int color) { 304 mClockView.setTextColor(color); 305 mClockViewBold.setTextColor(color); 306 if (mClockPlugin != null) { 307 mClockPlugin.setTextColor(color); 308 } 309 } 310 setShowCurrentUserTime(boolean showCurrentUserTime)311 public void setShowCurrentUserTime(boolean showCurrentUserTime) { 312 mClockView.setShowCurrentUserTime(showCurrentUserTime); 313 mClockViewBold.setShowCurrentUserTime(showCurrentUserTime); 314 } 315 setTextSize(int unit, float size)316 public void setTextSize(int unit, float size) { 317 mClockView.setTextSize(unit, size); 318 } 319 setFormat12Hour(CharSequence format)320 public void setFormat12Hour(CharSequence format) { 321 mClockView.setFormat12Hour(format); 322 mClockViewBold.setFormat12Hour(format); 323 } 324 setFormat24Hour(CharSequence format)325 public void setFormat24Hour(CharSequence format) { 326 mClockView.setFormat24Hour(format); 327 mClockViewBold.setFormat24Hour(format); 328 } 329 330 /** 331 * Set the amount (ratio) that the device has transitioned to doze. 332 * 333 * @param darkAmount Amount of transition to doze: 1f for doze and 0f for awake. 334 */ setDarkAmount(float darkAmount)335 public void setDarkAmount(float darkAmount) { 336 mDarkAmount = darkAmount; 337 if (mClockPlugin != null) { 338 mClockPlugin.setDarkAmount(darkAmount); 339 } 340 updateBigClockAlpha(); 341 } 342 343 /** 344 * Set whether or not the lock screen is showing notifications. 345 */ setHasVisibleNotifications(boolean hasVisibleNotifications)346 void setHasVisibleNotifications(boolean hasVisibleNotifications) { 347 if (hasVisibleNotifications == mHasVisibleNotifications) { 348 return; 349 } 350 mHasVisibleNotifications = hasVisibleNotifications; 351 if (mDarkAmount == 0f && mBigClockContainer != null) { 352 // Starting a fade transition since the visibility of the big clock will change. 353 TransitionManager.beginDelayedTransition(mBigClockContainer, 354 new Fade().setDuration(KeyguardSliceView.DEFAULT_ANIM_DURATION / 2).addTarget( 355 mBigClockContainer)); 356 } 357 updateBigClockAlpha(); 358 } 359 getPaint()360 public Paint getPaint() { 361 return mClockView.getPaint(); 362 } 363 getCurrentTextColor()364 public int getCurrentTextColor() { 365 return mClockView.getCurrentTextColor(); 366 } 367 getTextSize()368 public float getTextSize() { 369 return mClockView.getTextSize(); 370 } 371 372 /** 373 * Returns the preferred Y position of the clock. 374 * 375 * @param totalHeight Height of the parent container. 376 * @return preferred Y position. 377 */ getPreferredY(int totalHeight)378 int getPreferredY(int totalHeight) { 379 if (mClockPlugin != null) { 380 return mClockPlugin.getPreferredY(totalHeight); 381 } else { 382 return totalHeight / 2; 383 } 384 } 385 386 /** 387 * Refresh the time of the clock, due to either time tick broadcast or doze time tick alarm. 388 */ refresh()389 public void refresh() { 390 mClockView.refresh(); 391 mClockViewBold.refresh(); 392 if (mClockPlugin != null) { 393 mClockPlugin.onTimeTick(); 394 } 395 if (Build.IS_DEBUGGABLE) { 396 // Log for debugging b/130888082 (sysui waking up, but clock not updating) 397 Log.d(TAG, "Updating clock: " + mClockView.getText()); 398 } 399 } 400 401 /** 402 * Notifies that the time zone has changed. 403 */ onTimeZoneChanged(TimeZone timeZone)404 public void onTimeZoneChanged(TimeZone timeZone) { 405 if (mClockPlugin != null) { 406 mClockPlugin.onTimeZoneChanged(timeZone); 407 } 408 } 409 updateColors()410 private void updateColors() { 411 ColorExtractor.GradientColors colors = mSysuiColorExtractor.getColors( 412 WallpaperManager.FLAG_LOCK); 413 mSupportsDarkText = colors.supportsDarkText(); 414 mColorPalette = colors.getColorPalette(); 415 if (mClockPlugin != null) { 416 mClockPlugin.setColorPalette(mSupportsDarkText, mColorPalette); 417 } 418 } 419 updateBigClockVisibility()420 private void updateBigClockVisibility() { 421 if (mBigClockContainer == null) { 422 return; 423 } 424 final boolean inDisplayState = mStatusBarState == StatusBarState.KEYGUARD 425 || mStatusBarState == StatusBarState.SHADE_LOCKED; 426 final int visibility = 427 inDisplayState && mBigClockContainer.getChildCount() != 0 ? View.VISIBLE 428 : View.GONE; 429 if (mBigClockContainer.getVisibility() != visibility) { 430 mBigClockContainer.setVisibility(visibility); 431 } 432 } 433 updateBigClockAlpha()434 private void updateBigClockAlpha() { 435 if (mBigClockContainer != null) { 436 final float alpha = mHasVisibleNotifications ? mDarkAmount : 1f; 437 mBigClockContainer.setAlpha(alpha); 438 if (alpha == 0f) { 439 mBigClockContainer.setVisibility(INVISIBLE); 440 } else if (mBigClockContainer.getVisibility() == INVISIBLE) { 441 mBigClockContainer.setVisibility(VISIBLE); 442 } 443 } 444 } 445 446 /** 447 * Sets if the keyguard slice is showing a center-aligned header. We need a smaller clock in 448 * these cases. 449 */ setKeyguardShowingHeader(boolean hasHeader)450 void setKeyguardShowingHeader(boolean hasHeader) { 451 if (mShowingHeader == hasHeader) { 452 return; 453 } 454 mShowingHeader = hasHeader; 455 if (hasCustomClock()) { 456 return; 457 } 458 459 float smallFontSize = mContext.getResources().getDimensionPixelSize( 460 R.dimen.widget_small_font_size); 461 float bigFontSize = mContext.getResources().getDimensionPixelSize( 462 R.dimen.widget_big_font_size); 463 mClockTransition.setScale(smallFontSize / bigFontSize); 464 mBoldClockTransition.setScale(bigFontSize / smallFontSize); 465 466 // End any current transitions before starting a new transition so that the new transition 467 // starts from a good state instead of a potentially bad intermediate state arrived at 468 // during a transition animation. 469 TransitionManager.endTransitions((ViewGroup) mClockView.getParent()); 470 471 if (hasHeader) { 472 // After the transition, make the default clock GONE so that it doesn't make the 473 // KeyguardStatusView appear taller in KeyguardClockPositionAlgorithm and elsewhere. 474 mTransition.addListener(new TransitionListenerAdapter() { 475 @Override 476 public void onTransitionEnd(Transition transition) { 477 super.onTransitionEnd(transition); 478 // Check that header is actually showing. I saw issues where this event was 479 // fired after the big clock transitioned back to visible, which causes the time 480 // to completely disappear. 481 if (mShowingHeader) { 482 mClockView.setVisibility(View.GONE); 483 } 484 transition.removeListener(this); 485 } 486 }); 487 } 488 489 TransitionManager.beginDelayedTransition((ViewGroup) mClockView.getParent(), mTransition); 490 mClockView.setVisibility(hasHeader ? View.INVISIBLE : View.VISIBLE); 491 mClockViewBold.setVisibility(hasHeader ? View.VISIBLE : View.INVISIBLE); 492 int paddingBottom = mContext.getResources().getDimensionPixelSize(hasHeader 493 ? R.dimen.widget_vertical_padding_clock : R.dimen.title_clock_padding); 494 mClockView.setPadding(mClockView.getPaddingLeft(), mClockView.getPaddingTop(), 495 mClockView.getPaddingRight(), paddingBottom); 496 mClockViewBold.setPadding(mClockViewBold.getPaddingLeft(), mClockViewBold.getPaddingTop(), 497 mClockViewBold.getPaddingRight(), paddingBottom); 498 } 499 500 @VisibleForTesting(otherwise = VisibleForTesting.NONE) getClockChangedListener()501 ClockManager.ClockChangedListener getClockChangedListener() { 502 return mClockChangedListener; 503 } 504 505 @VisibleForTesting(otherwise = VisibleForTesting.NONE) getStateListener()506 StatusBarStateController.StateListener getStateListener() { 507 return mStateListener; 508 } 509 dump(FileDescriptor fd, PrintWriter pw, String[] args)510 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 511 pw.println("KeyguardClockSwitch:"); 512 pw.println(" mClockPlugin: " + mClockPlugin); 513 pw.println(" mClockView: " + mClockView); 514 pw.println(" mClockViewBold: " + mClockViewBold); 515 pw.println(" mSmallClockFrame: " + mSmallClockFrame); 516 pw.println(" mBigClockContainer: " + mBigClockContainer); 517 pw.println(" mKeyguardStatusArea: " + mKeyguardStatusArea); 518 pw.println(" mDarkAmount: " + mDarkAmount); 519 pw.println(" mShowingHeader: " + mShowingHeader); 520 pw.println(" mSupportsDarkText: " + mSupportsDarkText); 521 pw.println(" mColorPalette: " + Arrays.toString(mColorPalette)); 522 } 523 524 /** 525 * {@link Visibility} transformation that scales the view while it is disappearing/appearing and 526 * transitions suddenly at a cutoff fraction during the animation. 527 */ 528 private class ClockVisibilityTransition extends android.transition.Visibility { 529 530 private static final String PROPNAME_VISIBILITY = "systemui:keyguard:visibility"; 531 532 private float mCutoff; 533 private float mScale; 534 535 /** 536 * Constructs a transition that switches between visible/invisible at a cutoff and scales in 537 * size while appearing/disappearing. 538 */ ClockVisibilityTransition()539 ClockVisibilityTransition() { 540 setCutoff(1f); 541 setScale(1f); 542 } 543 544 /** 545 * Sets the transition point between visible/invisible. 546 * 547 * @param cutoff The fraction in [0, 1] when the view switches between visible/invisible. 548 * @return This transition object 549 */ setCutoff(float cutoff)550 public ClockVisibilityTransition setCutoff(float cutoff) { 551 mCutoff = cutoff; 552 return this; 553 } 554 555 /** 556 * Sets the scale factor applied while appearing/disappearing. 557 * 558 * @param scale Scale factor applied while appearing/disappearing. When factor is less than 559 * one, the view will shrink while disappearing. When it is greater than one, 560 * the view will expand while disappearing. 561 * @return This transition object 562 */ setScale(float scale)563 public ClockVisibilityTransition setScale(float scale) { 564 mScale = scale; 565 return this; 566 } 567 568 @Override captureStartValues(TransitionValues transitionValues)569 public void captureStartValues(TransitionValues transitionValues) { 570 super.captureStartValues(transitionValues); 571 captureVisibility(transitionValues); 572 } 573 574 @Override captureEndValues(TransitionValues transitionValues)575 public void captureEndValues(TransitionValues transitionValues) { 576 super.captureStartValues(transitionValues); 577 captureVisibility(transitionValues); 578 } 579 captureVisibility(TransitionValues transitionValues)580 private void captureVisibility(TransitionValues transitionValues) { 581 transitionValues.values.put(PROPNAME_VISIBILITY, 582 transitionValues.view.getVisibility()); 583 } 584 585 @Override onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)586 public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, 587 TransitionValues endValues) { 588 if (!sceneRoot.isShown()) { 589 return null; 590 } 591 final float cutoff = mCutoff; 592 final int startVisibility = View.INVISIBLE; 593 final int endVisibility = (int) endValues.values.get(PROPNAME_VISIBILITY); 594 final float startScale = mScale; 595 final float endScale = 1f; 596 return createAnimator(view, cutoff, startVisibility, endVisibility, startScale, 597 endScale); 598 } 599 600 @Override onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)601 public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, 602 TransitionValues endValues) { 603 if (!sceneRoot.isShown()) { 604 return null; 605 } 606 final float cutoff = 1f - mCutoff; 607 final int startVisibility = View.VISIBLE; 608 final int endVisibility = (int) endValues.values.get(PROPNAME_VISIBILITY); 609 final float startScale = 1f; 610 final float endScale = mScale; 611 return createAnimator(view, cutoff, startVisibility, endVisibility, startScale, 612 endScale); 613 } 614 createAnimator(View view, float cutoff, int startVisibility, int endVisibility, float startScale, float endScale)615 private Animator createAnimator(View view, float cutoff, int startVisibility, 616 int endVisibility, float startScale, float endScale) { 617 view.setPivotY(view.getHeight() - view.getPaddingBottom()); 618 ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); 619 animator.addUpdateListener(animation -> { 620 final float fraction = animation.getAnimatedFraction(); 621 if (fraction > cutoff) { 622 view.setVisibility(endVisibility); 623 } 624 final float scale = MathUtils.lerp(startScale, endScale, fraction); 625 view.setScaleX(scale); 626 view.setScaleY(scale); 627 }); 628 animator.addListener(new KeepAwakeAnimationListener(getContext()) { 629 @Override 630 public void onAnimationStart(Animator animation) { 631 super.onAnimationStart(animation); 632 view.setVisibility(startVisibility); 633 } 634 635 @Override 636 public void onAnimationEnd(Animator animation) { 637 super.onAnimationEnd(animation); 638 animation.removeListener(this); 639 } 640 }); 641 addListener(new TransitionListenerAdapter() { 642 @Override 643 public void onTransitionEnd(Transition transition) { 644 view.setVisibility(endVisibility); 645 view.setScaleX(1f); 646 view.setScaleY(1f); 647 transition.removeListener(this); 648 } 649 }); 650 return animator; 651 } 652 } 653 } 654