/
MaterialContainerTransform.java
1576 lines (1405 loc) · 61.5 KB
/
MaterialContainerTransform.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.material.transition;
import com.google.android.material.R;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static androidx.core.util.Preconditions.checkNotNull;
import static com.google.android.material.transition.TransitionUtils.calculateArea;
import static com.google.android.material.transition.TransitionUtils.convertToRelativeCornerSizes;
import static com.google.android.material.transition.TransitionUtils.createColorShader;
import static com.google.android.material.transition.TransitionUtils.defaultIfNull;
import static com.google.android.material.transition.TransitionUtils.findAncestorById;
import static com.google.android.material.transition.TransitionUtils.findDescendantOrAncestorById;
import static com.google.android.material.transition.TransitionUtils.getLocationOnScreen;
import static com.google.android.material.transition.TransitionUtils.getRelativeBounds;
import static com.google.android.material.transition.TransitionUtils.lerp;
import static com.google.android.material.transition.TransitionUtils.transform;
import android.animation.Animator;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.PixelFormat;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Region.Op;
import android.graphics.drawable.Drawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import androidx.annotation.ColorInt;
import androidx.annotation.FloatRange;
import androidx.annotation.IdRes;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.StyleRes;
import androidx.core.view.ViewCompat;
import androidx.transition.ArcMotion;
import androidx.transition.PathMotion;
import androidx.transition.Transition;
import androidx.transition.TransitionValues;
import com.google.android.material.animation.AnimationUtils;
import com.google.android.material.internal.ViewUtils;
import com.google.android.material.shape.MaterialShapeDrawable;
import com.google.android.material.shape.ShapeAppearanceModel;
import com.google.android.material.shape.Shapeable;
import com.google.android.material.transition.TransitionUtils.CanvasOperation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* A shared element {@link Transition} that transforms one container to another.
*
* <p>MaterialContainerTransform can be used to morph between two Activities, Fragments, Views or a
* View to a Fragment.
*
* <p>This transition captures a start and end View which are used to create a {@link Drawable}
* which will be added to the view hierarchy. The drawable will be added to the view hierarchy as an
* overlay and handles drawing a mask that morphs between the shape of the start View to the shape
* of the end View. During the animation, the start and end View's are drawn inside the masking
* container and faded in and/or out over a duration of the transition. Additionally, the masking
* container will be translated and scaled from the position and size of the start View to the
* position and size of the end View.
*
* <p>MaterialContainerTransform supports theme-based easing, duration, and path values. In order to
* have the transition load these values upfront, use the {@link
* #MaterialContainerTransform(Context, boolean)} constructor. Otherwise, use the default
* constructor and the transition will load theme values from the View context before it runs, and
* only use them if the corresponding properties weren't already set on the transition instance.
*
* <p>The composition of MaterialContainerTransform's animation can be customized in a number of
* ways. The two most prominent customizations are the way in which content inside the container is
* swapped via {@link #setFadeMode(int)} and path the container follows from its starting position
* to its ending position via {@link #setPathMotion(PathMotion)}. For other ways to customize the
* container transform, see:
*
* @see #setInterpolator(TimeInterpolator)
* @see #setDuration(long)
* @see #setStartShapeAppearanceModel(ShapeAppearanceModel)
* @see #setEndShapeAppearanceModel(ShapeAppearanceModel)
* @see #setDrawingViewId(int)
* @see #setScrimColor(int)
* @see #setFadeMode(int)
* @see #setFitMode(int)
* @see #setPathMotion(PathMotion)
* @see #setFadeProgressThresholds(ProgressThresholds)
* @see #setScaleProgressThresholds(ProgressThresholds)
* @see #setScaleMaskProgressThresholds(ProgressThresholds)
* @see #setShapeMaskProgressThresholds(ProgressThresholds)
*/
public final class MaterialContainerTransform extends Transition {
/**
* Indicates that this transition should use automatic detection to determine whether it is an
* Enter or a Return. If the end container has a larger area than the start container then it is
* considered an Enter transition, otherwise it is a Return transition.
*/
public static final int TRANSITION_DIRECTION_AUTO = 0;
/** Indicates that this is an Enter transition, i.e., when elements are entering the scene. */
public static final int TRANSITION_DIRECTION_ENTER = 1;
/** Indicates that this is a Return transition, i.e., when elements are exiting the scene. */
public static final int TRANSITION_DIRECTION_RETURN = 2;
/** @hide */
@RestrictTo(LIBRARY_GROUP)
@IntDef({TRANSITION_DIRECTION_AUTO, TRANSITION_DIRECTION_ENTER, TRANSITION_DIRECTION_RETURN})
@Retention(RetentionPolicy.SOURCE)
public @interface TransitionDirection {}
/**
* Indicates that this transition should only fade in the incoming content, without changing the
* opacity of the outgoing content.
*/
public static final int FADE_MODE_IN = 0;
/**
* Indicates that this transition should only fade out the outgoing content, without changing the
* opacity of the incoming content.
*/
public static final int FADE_MODE_OUT = 1;
/** Indicates that this transition should cross fade the outgoing and incoming content. */
public static final int FADE_MODE_CROSS = 2;
/**
* Indicates that this transition should sequentially fade out the outgoing content and fade in
* the incoming content.
*/
public static final int FADE_MODE_THROUGH = 3;
/** @hide */
@RestrictTo(LIBRARY_GROUP)
@IntDef({FADE_MODE_IN, FADE_MODE_OUT, FADE_MODE_CROSS, FADE_MODE_THROUGH})
@Retention(RetentionPolicy.SOURCE)
public @interface FadeMode {}
/**
* Indicates that this transition should automatically choose whether to use {@link
* #FIT_MODE_WIDTH} or {@link #FIT_MODE_HEIGHT}.
*/
public static final int FIT_MODE_AUTO = 0;
/**
* Indicates that this transition should fit the incoming content to the width of the outgoing
* content during the scale animation.
*/
public static final int FIT_MODE_WIDTH = 1;
/**
* Indicates that this transition should fit the incoming content to the height of the outgoing
* content during the scale animation.
*/
public static final int FIT_MODE_HEIGHT = 2;
/** @hide */
@RestrictTo(LIBRARY_GROUP)
@IntDef({FIT_MODE_AUTO, FIT_MODE_WIDTH, FIT_MODE_HEIGHT})
@Retention(RetentionPolicy.SOURCE)
public @interface FitMode {}
private static final String TAG = MaterialContainerTransform.class.getSimpleName();
private static final String PROP_BOUNDS = "materialContainerTransition:bounds";
private static final String PROP_SHAPE_APPEARANCE = "materialContainerTransition:shapeAppearance";
private static final String[] TRANSITION_PROPS =
new String[] {PROP_BOUNDS, PROP_SHAPE_APPEARANCE};
// Default animation thresholds. Will be used by default when the default linear PathMotion is
// being used or when no other progress thresholds are appropriate (e.g., the arc thresholds for
// an arc path).
private static final ProgressThresholdsGroup DEFAULT_ENTER_THRESHOLDS =
new ProgressThresholdsGroup(
/* fade= */ new ProgressThresholds(0f, 0.25f),
/* scale= */ new ProgressThresholds(0f, 1f),
/* scaleMask= */ new ProgressThresholds(0f, 1f),
/* shapeMask= */ new ProgressThresholds(0f, 0.75f));
private static final ProgressThresholdsGroup DEFAULT_RETURN_THRESHOLDS =
new ProgressThresholdsGroup(
/* fade= */ new ProgressThresholds(0.60f, 0.90f),
/* scale= */ new ProgressThresholds(0f, 1f),
/* scaleMask= */ new ProgressThresholds(0f, 0.90f),
/* shapeMask= */ new ProgressThresholds(0.30f, 0.90f));
// Default animation thresholds for an arc path. Will be used by default when the PathMotion is
// set to ArcMotion or MaterialArcMotion.
private static final ProgressThresholdsGroup DEFAULT_ENTER_THRESHOLDS_ARC =
new ProgressThresholdsGroup(
/* fade= */ new ProgressThresholds(0.10f, 0.40f),
/* scale= */ new ProgressThresholds(0.10f, 1f),
/* scaleMask= */ new ProgressThresholds(0.10f, 1f),
/* shapeMask= */ new ProgressThresholds(0.10f, 0.90f));
private static final ProgressThresholdsGroup DEFAULT_RETURN_THRESHOLDS_ARC =
new ProgressThresholdsGroup(
/* fade= */ new ProgressThresholds(0.60f, 0.90f),
/* scale= */ new ProgressThresholds(0f, 0.90f),
/* scaleMask= */ new ProgressThresholds(0f, 0.90f),
/* shapeMask= */ new ProgressThresholds(0.20f, 0.90f));
private static final float ELEVATION_NOT_SET = -1f;
private boolean drawDebugEnabled = false;
private boolean holdAtEndEnabled = false;
private boolean pathMotionCustom = false;
private boolean appliedThemeValues = false;
@IdRes private int drawingViewId = android.R.id.content;
@IdRes private int startViewId = View.NO_ID;
@IdRes private int endViewId = View.NO_ID;
@ColorInt private int containerColor = Color.TRANSPARENT;
@ColorInt private int startContainerColor = Color.TRANSPARENT;
@ColorInt private int endContainerColor = Color.TRANSPARENT;
@ColorInt private int scrimColor = 0x52000000;
@TransitionDirection private int transitionDirection = TRANSITION_DIRECTION_AUTO;
@FadeMode private int fadeMode = FADE_MODE_IN;
@FitMode private int fitMode = FIT_MODE_AUTO;
@Nullable private View startView;
@Nullable private View endView;
@Nullable private ShapeAppearanceModel startShapeAppearanceModel;
@Nullable private ShapeAppearanceModel endShapeAppearanceModel;
@Nullable private ProgressThresholds fadeProgressThresholds;
@Nullable private ProgressThresholds scaleProgressThresholds;
@Nullable private ProgressThresholds scaleMaskProgressThresholds;
@Nullable private ProgressThresholds shapeMaskProgressThresholds;
private boolean elevationShadowEnabled = VERSION.SDK_INT >= VERSION_CODES.P;
private float startElevation = ELEVATION_NOT_SET;
private float endElevation = ELEVATION_NOT_SET;
public MaterialContainerTransform() {
// Default constructor
}
public MaterialContainerTransform(@NonNull Context context, boolean entering) {
maybeApplyThemeValues(context, entering);
appliedThemeValues = true;
}
/** Get the id of the View which will be used as the start shared element container. */
@IdRes
public int getStartViewId() {
return startViewId;
}
/**
* Set the id of the View to be used as the start shared element container. The matching View will
* be searched for in the hierarchy when starting this transition.
*
* <p>Setting a start View can be helpful when transitioning from a View to another View or if
* transitioning from a View to a Fragment.
*
* <p>Manually setting the start View id will override any View explicitly set via {@link
* #setStartView(View)} or any View picked up by the Transition system marked with a
* transitionName.
*
* <p>If the start view cannot be found during the initialization of the {@code
* MaterialContainerTransform}, then an {@link IllegalArgumentException} will be thrown.
*/
public void setStartViewId(@IdRes int startViewId) {
this.startViewId = startViewId;
}
/**
* Get the id of the View which will be used as the end shared element container.
*
* <p>Setting an end View id can be used to manually configure MaterialContainerTransform when
* transitioning between two Views in a single layout when the Transition system will not
* automatically capture shared element start or end Views for you.
*
* <p>If the end view cannot be found during the initialization of the {@code
* MaterialContainerTransform}, then an {@link IllegalArgumentException} will be thrown.
*/
@IdRes
public int getEndViewId() {
return endViewId;
}
/**
* Set the id of the View to be used as the end shared element container. The matching View will
* be searched for in the hierarchy when starting this transition.
*
* <p>Manually setting the end View id will override any View explicitly set via {@link
* #setEndView(View)} or any View picked up by the Transition system marked with a transitionName.
*/
public void setEndViewId(@IdRes int endViewId) {
this.endViewId = endViewId;
}
/** Get the View which will be used as the start shared element container. */
@Nullable
public View getStartView() {
return startView;
}
/**
* Set the View to be used as the start shared element container.
*
* @see #setStartViewId(int)
*/
public void setStartView(@Nullable View startView) {
this.startView = startView;
}
/** Get the View which will be used as the end shared element container. */
@Nullable
public View getEndView() {
return endView;
}
/**
* Set the View to be used as the end shared element container.
*
* @see #setEndViewId(int)
*/
public void setEndView(@Nullable View endView) {
this.endView = endView;
}
/**
* Get the {@link ShapeAppearanceModel} which will be used to determine the shape from which the
* container will be transformed.
*/
@Nullable
public ShapeAppearanceModel getStartShapeAppearanceModel() {
return startShapeAppearanceModel;
}
/**
* Set the {@link ShapeAppearanceModel} which will be used to determine the shape from which the
* container will be transformed.
*
* <p>Manually setting a shape appearance will override both your theme's
* transitionShapeAppearance attribute (if set) and the shape appearance of the start View (or end
* View via {@link #setEndShapeAppearanceModel(ShapeAppearanceModel)} if the View implements the
* {@link Shapeable} interface. Setting this property can be useful if your start or end View does
* not implement {@link Shapeable} but does have a shape (eg. a rounded rect background drawable)
* and you would like MaterialContainerTransform to morph from or to your View's shape.
*/
public void setStartShapeAppearanceModel(
@Nullable ShapeAppearanceModel startShapeAppearanceModel) {
this.startShapeAppearanceModel = startShapeAppearanceModel;
}
/**
* Get the {@link ShapeAppearanceModel} which will be used to determine the shape into which the
* container will be transformed.
*/
@Nullable
public ShapeAppearanceModel getEndShapeAppearanceModel() {
return endShapeAppearanceModel;
}
/**
* Set the {@link ShapeAppearanceModel} which will be used to determine the shape into which the
* container will be transformed.
*
* @see #setStartShapeAppearanceModel(ShapeAppearanceModel)
*/
public void setEndShapeAppearanceModel(@Nullable ShapeAppearanceModel endShapeAppearanceModel) {
this.endShapeAppearanceModel = endShapeAppearanceModel;
}
/**
* Get whether shadows should be drawn around the container to approximate native elevation
* shadows on the start and end views.
*/
public boolean isElevationShadowEnabled() {
return elevationShadowEnabled;
}
/**
* Set whether shadows should be drawn around the container to approximate native elevation
* shadows on the start and end views.
*
* <p>By default, the elevation shadows are only enabled for API level 28 and above, because
* {@link Paint} shadows are not supported with hardware acceleration below API level 28. If
* enabled for below API level 28, then the shadows will be drawn using {@link
* MaterialShapeDrawable}, however this may cause performance issues.
*
* <p>Additionally, the rendering of elevation shadows may cause performance issues if the
* container's shape is not a round rect or a regular rect, e.g., a rect with cut corners.
*/
public void setElevationShadowEnabled(boolean elevationShadowEnabled) {
this.elevationShadowEnabled = elevationShadowEnabled;
}
/**
* Get the elevation that will be used to render a shadow around the container at the start of the
* transition.
*
* <p>Default is -1, which means the elevation of the start view will be used.
*/
public float getStartElevation() {
return startElevation;
}
/**
* Set the elevation that will be used to render a shadow around the container at the start of the
* transition.
*
* <p>By default the elevation of the start view will be used.
*/
public void setStartElevation(float startElevation) {
this.startElevation = startElevation;
}
/**
* Get the elevation that will be used to render a shadow around the container at the end of the
* transition.
*
* <p>Default is -1, which means the elevation of the end view will be used.
*/
public float getEndElevation() {
return endElevation;
}
/**
* Set the elevation that will be used to render a shadow around the container at the end of the
* transition.
*
* <p>By default the elevation of the end view will be used.
*/
public void setEndElevation(float endElevation) {
this.endElevation = endElevation;
}
/** Get the id of the View whose overlay this transitions will be added to. */
@IdRes
public int getDrawingViewId() {
return drawingViewId;
}
/**
* Set the id of the View whose overlay this transition will be added to.
*
* <p>This can be used to limit the bounds of the animation (including the background scrim) to
* the bounds of the provided drawing view, and also have the animation drawn at the relative
* z-order of the drawing view.
*
* <p>By default, the {@code drawingViewId} will be {@code android.R.id.content}. Additionally, if
* {@code drawingViewId} is the same as the end View's id, {@code MaterialContainerTransform} will
* add the transition's drawable to the {@code drawingViewId}'s parent instead.
*
* <p>If the drawing view cannot be found during the initialization of the {@code
* MaterialContainerTransform}, then an {@link IllegalArgumentException} will be thrown.
*/
public void setDrawingViewId(@IdRes int drawingViewId) {
this.drawingViewId = drawingViewId;
}
/**
* Get the color to be drawn beneath both the start view and end view.
*
* @see #setContainerColor(int)
*/
@ColorInt
public int getContainerColor() {
return containerColor;
}
/**
* Set a color to be drawn beneath both the start and end view.
*
* <p>This color is the background color of the transforming container inside of which the start
* and end views are drawn. Unlike the start view, start container color, end view and end
* container color, this color will always be drawn as fully opaque, beneath all other content in
* the container. By default, this color is set to transparent (0), meaning a container color will
* not be drawn.
*
* <p>If a default container transform results in the start view being visible beneath the end
* view, or vica versa, this is due to one or both views not having a background. The most common
* way to solve this issue is by sequentially fading the contents with {@link #FADE_MODE_THROUGH}
* and setting this color to the start and end view's desired background color.
*
* <p>If the start and end views have different background colors, or you would like to use a fade
* mode other than {@link #FADE_MODE_THROUGH}, handle this by using {@link
* #setStartContainerColor(int)} and {@link #setEndContainerColor(int)}.
*/
public void setContainerColor(@ColorInt int containerColor) {
this.containerColor = containerColor;
}
/**
* Get the color to be drawn beneath the start view.
*
* @see #setStartContainerColor(int)
*/
@ColorInt
public int getStartContainerColor() {
return startContainerColor;
}
/**
* Set a color to be drawn beneath the start view.
*
* <p>This color will be drawn directly beneath the start view, will fill the entire transforming
* container, and will animate its opacity to match the start view's. By default, this color is
* set to transparent (0), meaning no color will be drawn.
*
* <p>This method can be useful when the color of the start and end view differ and the start view
* does not handle drawing its own background. This can also be used if an expanding container is
* larger than the start view. Setting this color to match that of the start view's background
* will cause the start view to look like its background is expanding to fill the transforming
* container.
*/
public void setStartContainerColor(@ColorInt int containerColor) {
this.startContainerColor = containerColor;
}
/**
* Get the color to be drawn beneath the end view.
*
* @see #setEndContainerColor(int)
*/
@ColorInt
public int getEndContainerColor() {
return endContainerColor;
}
/**
* Set a color to be drawn beneath the end view.
*
* <p>This color will be drawn directly beneath the end view, will fill the entire transforming
* container, and the will animate its opacity to match the end view's. By default, this color is
* set to transparent (0), meaning no color will be drawn.
*
* <p>This method can be useful when the color of the start and end view differ and the end view
* does not handle drawing its own background. Setting this color will prevent the start view from
* being visible beneath the end view while transforming.
*/
public void setEndContainerColor(@ColorInt int containerColor) {
this.endContainerColor = containerColor;
}
/**
* Set the container color, the start container color and the end container color.
*
* <p>This is a helper for the common case of transitioning between a start and end view when
* neither draws its own background but a common color is shared. This prevents the start or end
* view from being visible below one another.
*
* @see #setContainerColor(int)
* @see #setStartContainerColor(int)
* @see #setEndContainerColor(int)
*/
public void setAllContainerColors(@ColorInt int containerColor) {
this.containerColor = containerColor;
this.startContainerColor = containerColor;
this.endContainerColor = containerColor;
}
/**
* Get the color to be drawn under the morphing container but within the bounds of the {@link
* #getDrawingViewId()}.
*/
@ColorInt
public int getScrimColor() {
return scrimColor;
}
/**
* Set the color to be drawn under the morphing container but within the bounds of the {@link
* #getDrawingViewId()}.
*
* <p>By default this is set to black with 32% opacity. Drawing a scrim is primarily useful for
* transforming from a partial-screen View (eg. Card in a grid) to a full screen. The scrim will
* gradually fade in and cover the content being transformed over by the morphing container.
*
* <p>Changing the default scrim color can be useful when transitioning between two Views in a
* layout, where the ending View does not cover any outgoing content (eg. a FAB to a bottom
* toolbar). For scenarios such as these, set the scrim color to transparent.
*/
public void setScrimColor(@ColorInt int scrimColor) {
this.scrimColor = scrimColor;
}
/**
* The direction to be used by this transform.
*
* @see #TRANSITION_DIRECTION_AUTO
* @see #TRANSITION_DIRECTION_ENTER
* @see #TRANSITION_DIRECTION_RETURN
*/
@TransitionDirection
public int getTransitionDirection() {
return transitionDirection;
}
/**
* Set the transition direction to be used by this transform.
*
* <p>By default, the transition direction is determined by the change in size between the start
* and end Views.
*
* @see #TRANSITION_DIRECTION_AUTO
*/
public void setTransitionDirection(@TransitionDirection int transitionDirection) {
this.transitionDirection = transitionDirection;
}
/** The fade mode to be used to swap the content of the start View with that of the end View. */
@FadeMode
public int getFadeMode() {
return fadeMode;
}
/**
* Set the fade mode to be used to swap the content of the start View with that of the end View.
*
* <p>By default, the fade mode is set to {@link #FADE_MODE_IN}.
*
* @see #FADE_MODE_IN
* @see #FADE_MODE_OUT
* @see #FADE_MODE_CROSS
* @see #FADE_MODE_THROUGH
*/
public void setFadeMode(@FadeMode int fadeMode) {
this.fadeMode = fadeMode;
}
/** The fit mode to be used when scaling the incoming content of the end View. */
@FitMode
public int getFitMode() {
return fitMode;
}
/**
* Set the fit mode to be used when scaling the incoming content of the end View.
*
* <p>By default, the fit mode is set to {@link #FIT_MODE_AUTO}.
*/
public void setFitMode(@FitMode int fitMode) {
this.fitMode = fitMode;
}
/**
* Get the {@link ProgressThresholds} which define the sub-range (any range inside the full
* progress range of 0.0 - 1.0) between which the fade animation, determined by {@link
* #getFadeMode()} will complete.
*/
@Nullable
public ProgressThresholds getFadeProgressThresholds() {
return fadeProgressThresholds;
}
/**
* Set the {@link ProgressThresholds} which define the sub-range (any range inside the full
* progress range of 0.0 - 1.0) between which the fade animation, determined by {@link
* #getFadeMode()} will complete.
*
* <p>See {@link ProgressThresholds} for an example of how the threshold ranges work.
*/
public void setFadeProgressThresholds(@Nullable ProgressThresholds fadeProgressThresholds) {
this.fadeProgressThresholds = fadeProgressThresholds;
}
/**
* Get the {@link ProgressThresholds} which define the sub-range (any range inside the full
* progress range of 0.0 - 1.0) between which the outgoing and incoming content will scale to the
* full dimensions of the end container.
*/
@Nullable
public ProgressThresholds getScaleProgressThresholds() {
return scaleProgressThresholds;
}
/**
* Set the {@link ProgressThresholds} which define the sub-range (any range inside the full
* progress range of 0.0 - 1.0) between which the outgoing and incoming content will scale to the
* full dimensions of the end container.
*
* <p>See {@link ProgressThresholds} for an example of how the threshold ranges work.
*/
public void setScaleProgressThresholds(@Nullable ProgressThresholds scaleProgressThresholds) {
this.scaleProgressThresholds = scaleProgressThresholds;
}
/**
* Get the {@link ProgressThresholds} which define the sub-range (any range inside the full
* progress range of 0.0 and 1.0) between which the container will morph between the start and end
* View's dimensions.
*/
@Nullable
public ProgressThresholds getScaleMaskProgressThresholds() {
return scaleMaskProgressThresholds;
}
/**
* Set the {@link ProgressThresholds} which define the sub-range (any range inside the full
* progress range of 0.0 and 1.0) between which the container will morph between the start and end
* View's dimensions.
*
* <p>See {@link ProgressThresholds} for an example of how the threshold ranges work.
*/
public void setScaleMaskProgressThresholds(
@Nullable ProgressThresholds scaleMaskProgressThresholds) {
this.scaleMaskProgressThresholds = scaleMaskProgressThresholds;
}
/**
* Get the {@link ProgressThresholds} which define the sub-range (any range inside the full
* progress range of 0.0 and 1.0) between which the container will morph between the starting
* {@link ShapeAppearanceModel} and ending {@link ShapeAppearanceModel}.
*/
@Nullable
public ProgressThresholds getShapeMaskProgressThresholds() {
return shapeMaskProgressThresholds;
}
/**
* Set the {@link ProgressThresholds} which define the sub-range (any range inside the full
* progress range of 0.0 and 1.0) between which the container will morph between the starting
* {@link ShapeAppearanceModel} and ending {@link ShapeAppearanceModel}.
*
* <p>See {@link ProgressThresholds} for an example of how the threshold ranges work.
*/
public void setShapeMaskProgressThresholds(
@Nullable ProgressThresholds shapeMaskProgressThresholds) {
this.shapeMaskProgressThresholds = shapeMaskProgressThresholds;
}
/**
* Whether to hold the last frame at the end of the animation.
*
* @see #setHoldAtEndEnabled(boolean)
*/
public boolean isHoldAtEndEnabled() {
return holdAtEndEnabled;
}
/**
* If true, the last frame of the animation will be held in place, and the original outgoing and
* incoming views will not be re-shown.
*
* <p>Useful for Activity return transitions to make sure the screen doesn't flash at the end.
*/
public void setHoldAtEndEnabled(boolean holdAtEndEnabled) {
this.holdAtEndEnabled = holdAtEndEnabled;
}
/**
* Whether debug drawing is enabled.
*
* @see #setDrawDebugEnabled(boolean)
*/
public boolean isDrawDebugEnabled() {
return drawDebugEnabled;
}
/**
* Set whether or not to draw paths which follow the shape and path of animating containers.
*
* @param drawDebugEnabled true if debugging lines and borders should be drawn during animation.
*/
public void setDrawDebugEnabled(boolean drawDebugEnabled) {
this.drawDebugEnabled = drawDebugEnabled;
}
@Override
public void setPathMotion(@Nullable PathMotion pathMotion) {
super.setPathMotion(pathMotion);
pathMotionCustom = true;
}
@Nullable
@Override
public String[] getTransitionProperties() {
return TRANSITION_PROPS;
}
@Override
public void captureStartValues(@NonNull TransitionValues transitionValues) {
captureValues(transitionValues, startView, startViewId, startShapeAppearanceModel);
}
@Override
public void captureEndValues(@NonNull TransitionValues transitionValues) {
captureValues(transitionValues, endView, endViewId, endShapeAppearanceModel);
}
private static void captureValues(
@NonNull TransitionValues transitionValues,
@Nullable View viewOverride,
@IdRes int viewIdOverride,
@Nullable ShapeAppearanceModel shapeAppearanceModelOverride) {
if (viewIdOverride != View.NO_ID) {
transitionValues.view = findDescendantOrAncestorById(transitionValues.view, viewIdOverride);
} else if (viewOverride != null) {
transitionValues.view = viewOverride;
} else if (transitionValues.view.getTag(R.id.mtrl_motion_snapshot_view) instanceof View) {
View snapshotView = (View) transitionValues.view.getTag(R.id.mtrl_motion_snapshot_view);
// Clear snapshot so that we don't accidentally use it for another transform transition.
transitionValues.view.setTag(R.id.mtrl_motion_snapshot_view, null);
// Use snapshot if entering and capturing start values or returning and capturing end values.
transitionValues.view = snapshotView;
}
View view = transitionValues.view;
if (ViewCompat.isLaidOut(view) || view.getWidth() != 0 || view.getHeight() != 0) {
// Capture location in screen co-ordinates
RectF bounds = view.getParent() == null ? getRelativeBounds(view) : getLocationOnScreen(view);
transitionValues.values.put(PROP_BOUNDS, bounds);
transitionValues.values.put(
PROP_SHAPE_APPEARANCE,
captureShapeAppearance(view, bounds, shapeAppearanceModelOverride));
}
}
// Get the shape appearance and convert it to relative corner sizes to simplify the interpolation.
private static ShapeAppearanceModel captureShapeAppearance(
@NonNull View view,
@NonNull RectF bounds,
@Nullable ShapeAppearanceModel shapeAppearanceModelOverride) {
ShapeAppearanceModel shapeAppearanceModel =
getShapeAppearance(view, shapeAppearanceModelOverride);
return convertToRelativeCornerSizes(shapeAppearanceModel, bounds);
}
// Use the shape appearance from the override if it's present, the transitionShapeAppearance attr
// if it's set, the view if it's [Shapeable], or else an empty model.
private static ShapeAppearanceModel getShapeAppearance(
@NonNull View view, @Nullable ShapeAppearanceModel shapeAppearanceModelOverride) {
if (shapeAppearanceModelOverride != null) {
return shapeAppearanceModelOverride;
}
if (view.getTag(R.id.mtrl_motion_snapshot_view) instanceof ShapeAppearanceModel) {
return (ShapeAppearanceModel) view.getTag(R.id.mtrl_motion_snapshot_view);
}
Context context = view.getContext();
int transitionShapeAppearanceResId = getTransitionShapeAppearanceResId(context);
if (transitionShapeAppearanceResId != -1) {
return ShapeAppearanceModel.builder(context, transitionShapeAppearanceResId, 0).build();
}
if (view instanceof Shapeable) {
return ((Shapeable) view).getShapeAppearanceModel();
}
return ShapeAppearanceModel.builder().build();
}
@StyleRes
private static int getTransitionShapeAppearanceResId(Context context) {
TypedArray a = context.obtainStyledAttributes(new int[] {R.attr.transitionShapeAppearance});
int transitionShapeAppearanceResId = a.getResourceId(0, -1);
a.recycle();
return transitionShapeAppearanceResId;
}
@Nullable
@Override
public Animator createAnimator(
@NonNull ViewGroup sceneRoot,
@Nullable TransitionValues startValues,
@Nullable TransitionValues endValues) {
if (startValues == null || endValues == null) {
return null;
}
RectF startBounds = (RectF) startValues.values.get(PROP_BOUNDS);
ShapeAppearanceModel startShapeAppearanceModel =
(ShapeAppearanceModel) startValues.values.get(PROP_SHAPE_APPEARANCE);
if (startBounds == null || startShapeAppearanceModel == null) {
Log.w(TAG, "Skipping due to null start bounds. Ensure start view is laid out and measured.");
return null;
}
RectF endBounds = (RectF) endValues.values.get(PROP_BOUNDS);
ShapeAppearanceModel endShapeAppearanceModel =
(ShapeAppearanceModel) endValues.values.get(PROP_SHAPE_APPEARANCE);
if (endBounds == null || endShapeAppearanceModel == null) {
Log.w(TAG, "Skipping due to null end bounds. Ensure end view is laid out and measured.");
return null;
}
final View startView = startValues.view;
final View endView = endValues.view;
final View drawingView;
View boundingView;
View drawingBaseView = endView.getParent() != null ? endView : startView;
if (drawingViewId == drawingBaseView.getId()) {
drawingView = (View) drawingBaseView.getParent();
boundingView = drawingBaseView;
} else {
drawingView = findAncestorById(drawingBaseView, drawingViewId);
boundingView = null;
}
// Calculate drawable bounds and offset start/end bounds as needed
RectF drawingViewBounds = getLocationOnScreen(drawingView);
float offsetX = -drawingViewBounds.left;
float offsetY = -drawingViewBounds.top;
RectF drawableBounds = calculateDrawableBounds(drawingView, boundingView, offsetX, offsetY);
startBounds.offset(offsetX, offsetY);
endBounds.offset(offsetX, offsetY);
boolean entering = isEntering(startBounds, endBounds);
if (!appliedThemeValues) {
// Apply theme values if we didn't already apply them up front in the constructor and if they
// haven't already been set by the user.
maybeApplyThemeValues(drawingBaseView.getContext(), entering);
}
final TransitionDrawable transitionDrawable =
new TransitionDrawable(
getPathMotion(),
startView,
startBounds,
startShapeAppearanceModel,
getElevationOrDefault(startElevation, startView),
endView,
endBounds,
endShapeAppearanceModel,
getElevationOrDefault(endElevation, endView),
containerColor,
startContainerColor,
endContainerColor,
scrimColor,
entering,
elevationShadowEnabled,
FadeModeEvaluators.get(fadeMode, entering),
FitModeEvaluators.get(fitMode, entering, startBounds, endBounds),
buildThresholdsGroup(entering),
drawDebugEnabled);
// Set the bounds of the transition drawable to not exceed the bounds of the drawingView.
transitionDrawable.setBounds(
Math.round(drawableBounds.left),
Math.round(drawableBounds.top),
Math.round(drawableBounds.right),
Math.round(drawableBounds.bottom));
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
animator.addUpdateListener(
new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
transitionDrawable.setProgress(animation.getAnimatedFraction());
}
});
addListener(
new TransitionListenerAdapter() {
@Override
public void onTransitionStart(@NonNull Transition transition) {
// Add the transition drawable to the root ViewOverlay
ViewUtils.getOverlay(drawingView).add(transitionDrawable);
// Hide the actual views at the beginning of the transition
startView.setAlpha(0);
endView.setAlpha(0);
}
@Override
public void onTransitionEnd(@NonNull Transition transition) {
removeListener(this);
if (holdAtEndEnabled) {
// Keep drawable showing and views hidden (useful for Activity return transitions)
return;
}
// Show the actual views at the end of the transition
startView.setAlpha(1);
endView.setAlpha(1);
// Remove the transition drawable from the root ViewOverlay
ViewUtils.getOverlay(drawingView).remove(transitionDrawable);
}