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