Skip to content

Commit 894edb6

Browse files
hunterstichdrchen
authored andcommittedApr 14, 2022
[Motion] Open MotionUtils and update resolveThemeInterpolator to load both new and legacy easing attributes.
PiperOrigin-RevId: 441806915
1 parent f43995f commit 894edb6

File tree

8 files changed

+352
-37
lines changed

8 files changed

+352
-37
lines changed
 

‎lib/java/com/google/android/material/motion/MotionUtils.java

+65-37
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,17 @@
1515
*/
1616
package com.google.android.material.motion;
1717

18-
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
19-
2018
import android.animation.TimeInterpolator;
2119
import android.content.Context;
2220
import android.util.TypedValue;
21+
import android.view.animation.AnimationUtils;
2322
import androidx.annotation.AttrRes;
2423
import androidx.annotation.NonNull;
25-
import androidx.annotation.RestrictTo;
2624
import androidx.core.graphics.PathParser;
2725
import androidx.core.view.animation.PathInterpolatorCompat;
2826
import com.google.android.material.resources.MaterialAttributes;
2927

30-
/**
31-
* A utility class for motion system functions.
32-
*
33-
* @hide
34-
*/
35-
@RestrictTo(LIBRARY_GROUP)
28+
/** A utility class for motion system functions. */
3629
public class MotionUtils {
3730

3831
// Constants corresponding to motionEasing* theme attr values.
@@ -43,61 +36,96 @@ public class MotionUtils {
4336

4437
private MotionUtils() {}
4538

39+
/**
40+
* Resolve a duration from a material duration theme attribute.
41+
*
42+
* @param context the context from where the theme attribute will be resolved.
43+
* @param attrResId the {@code motionDuration*} theme attribute to resolve
44+
* @param defaultDuration the duration to be returned if unable to resolve {@code attrResId}
45+
* @return the resolved {@code int} duration which {@code attrResId} points to or the {@code
46+
* defaultDuration} if resolution was unsuccessful.
47+
*/
4648
public static int resolveThemeDuration(
4749
@NonNull Context context, @AttrRes int attrResId, int defaultDuration) {
4850
return MaterialAttributes.resolveInteger(context, attrResId, defaultDuration);
4951
}
5052

53+
/**
54+
* Load an interpolator from a material easing theme attribute.
55+
*
56+
* @param context context from where the theme attribute will be resolved
57+
* @param attrResId the {@code motionEasing*} theme attribute to resolve
58+
* @param defaultInterpolator the interpolator to be returned if unable to resolve {@code
59+
* attrResId}.
60+
* @return the resolved {@link TimeInterpolator} which {@code attrResId} points to or the {@code
61+
* defaultInterpolator} if resolution was unsuccessful.
62+
*/
5163
@NonNull
5264
public static TimeInterpolator resolveThemeInterpolator(
5365
@NonNull Context context,
5466
@AttrRes int attrResId,
5567
@NonNull TimeInterpolator defaultInterpolator) {
5668
TypedValue easingValue = new TypedValue();
57-
if (context.getTheme().resolveAttribute(attrResId, easingValue, true)) {
58-
if (easingValue.type != TypedValue.TYPE_STRING) {
59-
throw new IllegalArgumentException("Motion easing theme attribute must be a string");
60-
}
69+
if (!context.getTheme().resolveAttribute(attrResId, easingValue, true)) {
70+
return defaultInterpolator;
71+
}
6172

62-
String easingString = String.valueOf(easingValue.string);
73+
if (easingValue.type != TypedValue.TYPE_STRING) {
74+
throw new IllegalArgumentException(
75+
"Motion easing theme attribute must be an @interpolator resource for"
76+
+ " ?attr/motionEasing*Interpolator attributes or a string for"
77+
+ " ?attr/motionEasing* attributes.");
78+
}
6379

64-
if (isEasingType(easingString, EASING_TYPE_CUBIC_BEZIER)) {
65-
String controlPointsString = getEasingContent(easingString, EASING_TYPE_CUBIC_BEZIER);
66-
String[] controlPoints = controlPointsString.split(",");
67-
if (controlPoints.length != 4) {
68-
throw new IllegalArgumentException(
69-
"Motion easing theme attribute must have 4 control points if using bezier curve"
70-
+ " format; instead got: "
71-
+ controlPoints.length);
72-
}
80+
String easingString = String.valueOf(easingValue.string);
81+
if (isLegacyEasingAttribute(easingString)) {
82+
return getLegacyThemeInterpolator(easingString);
83+
}
84+
85+
return AnimationUtils.loadInterpolator(context, easingValue.resourceId);
86+
}
7387

74-
float controlX1 = getControlPoint(controlPoints, 0);
75-
float controlY1 = getControlPoint(controlPoints, 1);
76-
float controlX2 = getControlPoint(controlPoints, 2);
77-
float controlY2 = getControlPoint(controlPoints, 3);
78-
return PathInterpolatorCompat.create(controlX1, controlY1, controlX2, controlY2);
79-
} else if (isEasingType(easingString, EASING_TYPE_PATH)) {
80-
String path = getEasingContent(easingString, EASING_TYPE_PATH);
81-
return PathInterpolatorCompat.create(PathParser.createPathFromPathData(path));
82-
} else {
83-
throw new IllegalArgumentException("Invalid motion easing type: " + easingString);
88+
private static TimeInterpolator getLegacyThemeInterpolator(String easingString) {
89+
if (isLegacyEasingType(easingString, EASING_TYPE_CUBIC_BEZIER)) {
90+
String controlPointsString = getLegacyEasingContent(easingString, EASING_TYPE_CUBIC_BEZIER);
91+
String[] controlPoints = controlPointsString.split(",");
92+
if (controlPoints.length != 4) {
93+
throw new IllegalArgumentException(
94+
"Motion easing theme attribute must have 4 control points if using bezier curve"
95+
+ " format; instead got: "
96+
+ controlPoints.length);
8497
}
98+
99+
float controlX1 = getLegacyControlPoint(controlPoints, 0);
100+
float controlY1 = getLegacyControlPoint(controlPoints, 1);
101+
float controlX2 = getLegacyControlPoint(controlPoints, 2);
102+
float controlY2 = getLegacyControlPoint(controlPoints, 3);
103+
return PathInterpolatorCompat.create(controlX1, controlY1, controlX2, controlY2);
104+
} else if (isLegacyEasingType(easingString, EASING_TYPE_PATH)) {
105+
String path = getLegacyEasingContent(easingString, EASING_TYPE_PATH);
106+
return PathInterpolatorCompat.create(PathParser.createPathFromPathData(path));
107+
} else {
108+
throw new IllegalArgumentException("Invalid motion easing type: " + easingString);
85109
}
86-
return defaultInterpolator;
87110
}
88111

89-
private static boolean isEasingType(String easingString, String easingType) {
112+
private static boolean isLegacyEasingAttribute(String easingString) {
113+
return isLegacyEasingType(easingString, EASING_TYPE_CUBIC_BEZIER)
114+
|| isLegacyEasingType(easingString, EASING_TYPE_PATH);
115+
}
116+
117+
private static boolean isLegacyEasingType(String easingString, String easingType) {
90118
return easingString.startsWith(easingType + EASING_TYPE_FORMAT_START)
91119
&& easingString.endsWith(EASING_TYPE_FORMAT_END);
92120
}
93121

94-
private static String getEasingContent(String easingString, String easingType) {
122+
private static String getLegacyEasingContent(String easingString, String easingType) {
95123
return easingString.substring(
96124
easingType.length() + EASING_TYPE_FORMAT_START.length(),
97125
easingString.length() - EASING_TYPE_FORMAT_END.length());
98126
}
99127

100-
private static float getControlPoint(String[] controlPoints, int index) {
128+
private static float getLegacyControlPoint(String[] controlPoints, int index) {
101129
float controlPoint = Float.parseFloat(controlPoints[index]);
102130
if (controlPoint < 0 || controlPoint > 1) {
103131
throw new IllegalArgumentException(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Copyright 2022 The Android Open Source Project
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
18+
xmlns:tools="http://schemas.android.com/tools"
19+
package="com.google.android.material.motion">
20+
21+
<uses-sdk
22+
tools:overrideLibrary="androidx.test.core"/>
23+
24+
<application android:theme="@style/Theme.Material3.DayNight.NoActionBar"/>
25+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright (C) 2022 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.android.material.motion;
18+
19+
import com.google.android.material.R;
20+
21+
import static com.google.common.truth.Truth.assertThat;
22+
23+
import android.animation.TimeInterpolator;
24+
import android.os.Build.VERSION_CODES;
25+
import androidx.appcompat.app.AppCompatActivity;
26+
import android.view.animation.Interpolator;
27+
import android.view.animation.LinearInterpolator;
28+
import android.view.animation.PathInterpolator;
29+
import androidx.annotation.RequiresApi;
30+
import androidx.test.core.app.ApplicationProvider;
31+
import org.junit.Rule;
32+
import org.junit.Test;
33+
import org.junit.rules.ExpectedException;
34+
import org.junit.runner.RunWith;
35+
import org.robolectric.Robolectric;
36+
import org.robolectric.RobolectricTestRunner;
37+
import org.robolectric.android.controller.ActivityController;
38+
import org.robolectric.annotation.Config;
39+
import org.robolectric.annotation.internal.DoNotInstrument;
40+
41+
@RunWith(RobolectricTestRunner.class)
42+
@Config(sdk = VERSION_CODES.LOLLIPOP)
43+
@DoNotInstrument
44+
@RequiresApi(api = VERSION_CODES.LOLLIPOP)
45+
public class MotionUtilsTest {
46+
47+
private ActivityController<AppCompatActivity> activityController;
48+
49+
@Rule public final ExpectedException thrown = ExpectedException.none();
50+
51+
@Test
52+
public void testResolvesThemeInterpolator() {
53+
assertThemeInterpolatorIsInstanceOf(
54+
R.style.Theme_Material3_DayNight,
55+
R.attr.motionEasingStandardInterpolator,
56+
LinearInterpolator.class);
57+
}
58+
59+
@Test
60+
public void testCustomInterpolator_resolvesThemeInterpolator() {
61+
assertThemeInterpolatorIsInstanceOf(
62+
R.style.Theme_Material3_DayNight_CustomInterpolator,
63+
R.attr.motionEasingStandardInterpolator,
64+
LinearInterpolator.class);
65+
}
66+
67+
@Test
68+
public void testCustomAnimInterpolator_resolvesThemeInterpolator() {
69+
assertThemeInterpolatorIsInstanceOf(
70+
R.style.Theme_Material3_DayNight_CustomAnimInterpolator,
71+
R.attr.motionEasingStandardInterpolator,
72+
LinearInterpolator.class);
73+
}
74+
75+
@Test
76+
public void testResolvesLegacyInterpolator() {
77+
assertThemeInterpolatorIsInstanceOf(
78+
R.style.Theme_Material3_DayNight, R.attr.motionEasingStandard, PathInterpolator.class);
79+
}
80+
81+
@Test
82+
public void testMaterialComponentsTheme_resolveUnavailableInterpolatorReturnsDefault() {
83+
createActivityAndSetTheme(R.style.Theme_MaterialComponents_DayNight);
84+
85+
DefaultDummyInterpolator defaultInterpolator = new DefaultDummyInterpolator();
86+
TimeInterpolator standardInterpolator =
87+
MotionUtils.resolveThemeInterpolator(
88+
activityController.get().getApplicationContext(),
89+
R.attr.motionEasingStandardInterpolator,
90+
defaultInterpolator);
91+
assertThat(standardInterpolator).isEqualTo(defaultInterpolator);
92+
}
93+
94+
@Test
95+
public void testMaterialComponentsTheme_resolvesLegacyInterpolator() {
96+
assertThemeInterpolatorIsInstanceOf(
97+
R.style.Theme_MaterialComponents_DayNight,
98+
R.attr.motionEasingStandard,
99+
PathInterpolator.class);
100+
}
101+
102+
@Test
103+
public void testMaterialComponentsThemeIncorrectLegacyAttrType_shouldThrowException() {
104+
createActivityAndSetTheme(
105+
R.style.Theme_MaterialComponents_DayNight_IncorrectLegacyEasingAttrType);
106+
107+
// ThrowingRunnable used by assertThrows is not available until gradle 4.13
108+
thrown.expect(IllegalArgumentException.class);
109+
MotionUtils.resolveThemeInterpolator(
110+
activityController.get().getApplicationContext(),
111+
R.attr.motionEasingStandard,
112+
new DefaultDummyInterpolator());
113+
}
114+
115+
@Test
116+
public void testMaterialComponentsThemeIncorrectLegacyFormatting_shouldThrowException() {
117+
createActivityAndSetTheme(
118+
R.style.Theme_MaterialComponents_DayNight_IncorrectLegacyEasingFormat);
119+
120+
// ThrowingRunnable used by assertThrows is not available until gradle 4.13
121+
thrown.expect(IllegalArgumentException.class);
122+
MotionUtils.resolveThemeInterpolator(
123+
activityController.get().getApplicationContext(),
124+
R.attr.motionEasingStandard,
125+
new DefaultDummyInterpolator());
126+
}
127+
128+
private void createActivityAndSetTheme(int themeId) {
129+
ApplicationProvider.getApplicationContext().setTheme(themeId);
130+
activityController = Robolectric.buildActivity(AppCompatActivity.class).create();
131+
}
132+
133+
private void assertThemeInterpolatorIsInstanceOf(int theme, int attrId, Class<?> expectedClass) {
134+
createActivityAndSetTheme(theme);
135+
DefaultDummyInterpolator defaultInterpolator = new DefaultDummyInterpolator();
136+
TimeInterpolator themeInterpolator =
137+
MotionUtils.resolveThemeInterpolator(
138+
activityController.get().getApplicationContext(), attrId, defaultInterpolator);
139+
assertThat(themeInterpolator).isNotEqualTo(defaultInterpolator);
140+
// Robolectric's shadow of Android's AnimationUtils always returns a LinearInterpolator.
141+
assertThat(themeInterpolator).isInstanceOf(expectedClass);
142+
}
143+
144+
private static class DefaultDummyInterpolator implements Interpolator {
145+
@Override
146+
public float getInterpolation(float input) {
147+
return 0;
148+
}
149+
}
150+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="utf-8"?><!--
2+
Copyright 2022 The Android Open Source Project
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
-->
16+
17+
<accelerateInterpolator
18+
xmlns:android="http://schemas.android.com/apk/res/android"
19+
android:factor="2" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="utf-8"?><!--
2+
Copyright 2022 The Android Open Source Project
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
-->
16+
17+
<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
18+
android:controlX1="0.0"
19+
android:controlX2="0.7"
20+
android:controlY1="0.5"
21+
android:controlY2="1.0" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Copyright 2022 The Android Open Source Project
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
<resources>
18+
<color name="color_primary">#FFFFFF</color>
19+
</resources>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Copyright 2022 The Android Open Source Project
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
<resources>
18+
<string name="motion_easing_standard_missing_control_point">cubic-bezier(0.2, 0.0, 0.0)</string>
19+
</resources>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Copyright 2022 The Android Open Source Project
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
<resources>
18+
19+
<style name="Theme.MaterialComponents.DayNight.IncorrectLegacyEasingAttrType" parent="Theme.MaterialComponents.DayNight">
20+
<item name="motionEasingStandard">@color/color_primary</item>
21+
</style>
22+
<style name="Theme.MaterialComponents.DayNight.IncorrectLegacyEasingFormat" parent="Theme.MaterialComponents.DayNight">
23+
<item name="motionEasingStandard">@string/motion_easing_standard_missing_control_point</item>
24+
</style>
25+
<style name="Theme.Material3.DayNight.IncorrectEasing" parent="Theme.Material3.DayNight">
26+
<item name="motionEasingStandardInterpolator">@color/color_primary</item>
27+
</style>
28+
<style name="Theme.Material3.DayNight.CustomInterpolator" parent="Theme.Material3.DayNight">
29+
<item name="motionEasingStandardInterpolator">@interpolator/custom_interpolator</item>
30+
</style>
31+
<style name="Theme.Material3.DayNight.CustomAnimInterpolator" parent="Theme.Material3.DayNight">
32+
<item name="motionEasingStandardInterpolator">@anim/custom_anim_interpolator</item>
33+
</style>
34+
</resources>

0 commit comments

Comments
 (0)
Please sign in to comment.