1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package android.animation.cts;
17 
18 import static com.android.compatibility.common.util.CtsMockitoUtils.within;
19 
20 import static org.junit.Assert.assertEquals;
21 import static org.junit.Assert.assertNotNull;
22 import static org.junit.Assert.assertTrue;
23 import static org.mockito.Mockito.mock;
24 import static org.mockito.Mockito.verify;
25 
26 import android.animation.Animator;
27 import android.animation.AnimatorListenerAdapter;
28 import android.animation.ArgbEvaluator;
29 import android.animation.Keyframe;
30 import android.animation.ObjectAnimator;
31 import android.animation.PropertyValuesHolder;
32 import android.animation.TypeConverter;
33 import android.animation.ValueAnimator;
34 import android.app.Instrumentation;
35 import android.graphics.Color;
36 import android.graphics.Path;
37 import android.graphics.PointF;
38 import android.graphics.drawable.ShapeDrawable;
39 import android.os.SystemClock;
40 import android.util.FloatProperty;
41 import android.util.Property;
42 import android.view.View;
43 import android.view.animation.AccelerateInterpolator;
44 
45 import androidx.test.InstrumentationRegistry;
46 import androidx.test.filters.FlakyTest;
47 import androidx.test.filters.LargeTest;
48 import androidx.test.rule.ActivityTestRule;
49 import androidx.test.runner.AndroidJUnit4;
50 
51 import com.android.compatibility.common.util.AdoptShellPermissionsRule;
52 
53 import org.junit.Before;
54 import org.junit.Rule;
55 import org.junit.Test;
56 import org.junit.runner.RunWith;
57 
58 import java.util.concurrent.CountDownLatch;
59 import java.util.concurrent.TimeUnit;
60 
61 @LargeTest
62 @RunWith(AndroidJUnit4.class)
63 public class PropertyValuesHolderTest {
64     private static final float LINE1_START = -32f;
65     private static final float LINE1_END = -2f;
66     private static final float LINE2_START = 2f;
67     private static final float LINE2_END = 12f;
68     private static final float QUADRATIC_CTRL_PT1_X = 0f;
69     private static final float QUADRATIC_CTRL_PT1_Y = 0f;
70     private static final float QUADRATIC_CTRL_PT2_X = 50f;
71     private static final float QUADRATIC_CTRL_PT2_Y = 20f;
72     private static final float QUADRATIC_CTRL_PT3_X = 100f;
73     private static final float QUADRATIC_CTRL_PT3_Y = 0f;
74     private static final float EPSILON = .001f;
75 
76     private Instrumentation mInstrumentation;
77     private AnimationActivity mActivity;
78     private long mDuration = 1000;
79     private float mStartY;
80     private float mEndY;
81     private Object mObject;
82     private String mProperty;
83 
84     @Rule(order = 0)
85     public AdoptShellPermissionsRule mAdoptShellPermissionsRule =
86             new AdoptShellPermissionsRule(
87                     androidx.test.platform.app.InstrumentationRegistry
88                             .getInstrumentation().getUiAutomation(),
89                     android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX);
90 
91     @Rule(order = 1)
92     public ActivityTestRule<AnimationActivity> mActivityRule =
93             new ActivityTestRule<>(AnimationActivity.class);
94 
95     @Before
setup()96     public void setup() {
97         mInstrumentation = InstrumentationRegistry.getInstrumentation();
98         mInstrumentation.setInTouchMode(false);
99         mActivity = mActivityRule.getActivity();
100         mProperty = "y";
101         mStartY = mActivity.mStartY;
102         mEndY = mActivity.mStartY + mActivity.mDeltaY;
103         mObject = mActivity.view.newBall;
104     }
105 
106     @Test
testGetPropertyName()107     public void testGetPropertyName() {
108         float[] values = {mStartY, mEndY};
109         PropertyValuesHolder pVHolder = PropertyValuesHolder.ofFloat(mProperty, values);
110         assertEquals(mProperty, pVHolder.getPropertyName());
111     }
112 
113     @Test
testSetPropertyName()114     public void testSetPropertyName() {
115         float[] values = {mStartY, mEndY};
116         PropertyValuesHolder pVHolder = PropertyValuesHolder.ofFloat("", values);
117         pVHolder.setPropertyName(mProperty);
118         assertEquals(mProperty, pVHolder.getPropertyName());
119     }
120 
121     @Test
testClone()122     public void testClone() {
123         float[] values = {mStartY, mEndY};
124         PropertyValuesHolder pVHolder = PropertyValuesHolder.ofFloat(mProperty, values);
125         PropertyValuesHolder cloneHolder = pVHolder.clone();
126         assertEquals(pVHolder.getPropertyName(), cloneHolder.getPropertyName());
127     }
128 
129     @FlakyTest
130     @Test
testSetValues()131     public void testSetValues() throws Throwable {
132         float[] dummyValues = {100, 150};
133         float[] values = {mStartY, mEndY};
134         PropertyValuesHolder pVHolder = PropertyValuesHolder.ofFloat(mProperty, dummyValues);
135         pVHolder.setFloatValues(values);
136 
137         ObjectAnimator objAnimator = ObjectAnimator.ofPropertyValuesHolder(mObject,pVHolder);
138         assertTrue(objAnimator != null);
139         setAnimatorProperties(objAnimator);
140 
141         startAnimation(objAnimator);
142         assertTrue(objAnimator != null);
143         float[] yArray = getYPosition();
144         assertResults(yArray, mStartY, mEndY);
145     }
146 
createAnimator(Keyframe... keyframes)147     private ObjectAnimator createAnimator(Keyframe... keyframes) {
148         PropertyValuesHolder pVHolder = PropertyValuesHolder.ofKeyframe(mProperty, keyframes);
149         ObjectAnimator objAnimator = ObjectAnimator.ofPropertyValuesHolder(mObject,pVHolder);
150         objAnimator.setDuration(mDuration);
151         objAnimator.setInterpolator(new AccelerateInterpolator());
152         return objAnimator;
153     }
154 
waitUntilFinished(ObjectAnimator objectAnimator, long timeoutMilliseconds)155     private void waitUntilFinished(ObjectAnimator objectAnimator, long timeoutMilliseconds)
156             throws InterruptedException {
157         final Animator.AnimatorListener listener = mock(Animator.AnimatorListener.class);
158         objectAnimator.addListener(listener);
159         verify(listener, within(timeoutMilliseconds)).onAnimationEnd(objectAnimator, false);
160         mInstrumentation.waitForIdleSync();
161     }
162 
setTarget(final Animator animator, final Object target)163     private void setTarget(final Animator animator, final Object target) throws Throwable {
164         mActivityRule.runOnUiThread(() -> animator.setTarget(target));
165     }
166 
startSingleAnimation(final Animator animator)167     private void startSingleAnimation(final Animator animator) throws Throwable {
168         mActivityRule.runOnUiThread(() -> mActivity.startSingleAnimation(animator));
169     }
170 
171     @FlakyTest
172     @Test
testResetValues()173     public void testResetValues() throws Throwable {
174         final float initialY = mActivity.view.newBall.getY();
175         Keyframe emptyKeyframe1 = Keyframe.ofFloat(.0f);
176         ObjectAnimator objAnimator1 = createAnimator(emptyKeyframe1, Keyframe.ofFloat(1f, 100f));
177         startSingleAnimation(objAnimator1);
178         assertTrue("Keyframe should be assigned a value", emptyKeyframe1.hasValue());
179         assertEquals("Keyframe should get the value from the target",
180                 (float) emptyKeyframe1.getValue(), initialY, 0.0f);
181         waitUntilFinished(objAnimator1, mDuration * 2);
182         assertEquals(100f, mActivity.view.newBall.getY(), 0.0f);
183         startSingleAnimation(objAnimator1);
184         waitUntilFinished(objAnimator1, mDuration * 2);
185 
186         // run another ObjectAnimator that will move the Y value to something else
187         Keyframe emptyKeyframe2 = Keyframe.ofFloat(.0f);
188         ObjectAnimator objAnimator2 = createAnimator(emptyKeyframe2, Keyframe.ofFloat(1f, 200f));
189         startSingleAnimation(objAnimator2);
190         assertTrue("Keyframe should be assigned a value", emptyKeyframe2.hasValue());
191         assertEquals("Keyframe should get the value from the target",
192                 (float) emptyKeyframe2.getValue(), 100f, 0.0f);
193         waitUntilFinished(objAnimator2, mDuration * 2);
194         assertEquals(200f, mActivity.view.newBall.getY(), 0.0f);
195 
196         // re-run first object animator. since its target did not change, it should have the same
197         // start value for kf1
198         startSingleAnimation(objAnimator1);
199         assertEquals((float) emptyKeyframe1.getValue(), initialY, 0.0f);
200         waitUntilFinished(objAnimator1, mDuration * 2);
201 
202         Keyframe fullKeyframe = Keyframe.ofFloat(.0f, 333f);
203         ObjectAnimator objAnimator3 = createAnimator(fullKeyframe, Keyframe.ofFloat(1f, 500f));
204         startSingleAnimation(objAnimator3);
205         assertEquals("When keyframe has value, should not be assigned from the target object",
206                 (float) fullKeyframe.getValue(), 333f, 0.0f);
207         waitUntilFinished(objAnimator3, mDuration * 2);
208 
209         // now, null out the target of the first animator
210         float updatedY = mActivity.view.newBall.getY();
211         setTarget(objAnimator1, null);
212         startSingleAnimation(objAnimator1);
213         assertTrue("Keyframe should get a value", emptyKeyframe1.hasValue());
214         assertEquals("Keyframe should get the updated Y value",
215                 (float) emptyKeyframe1.getValue(), updatedY, 0.0f);
216         waitUntilFinished(objAnimator1, mDuration * 2);
217         assertEquals("Animation should run as expected", 100f, mActivity.view.newBall.getY(), 0.0f);
218 
219         // now, reset the target of the fully defined animation.
220         setTarget(objAnimator3, null);
221         startSingleAnimation(objAnimator3);
222         assertEquals("When keyframe is fully defined, its value should not change when target is"
223                 + " reset", (float) fullKeyframe.getValue(), 333f, 0.0f);
224         waitUntilFinished(objAnimator3, mDuration * 2);
225 
226         // run the other one to change Y value
227         startSingleAnimation(objAnimator2);
228         waitUntilFinished(objAnimator2, mDuration * 2);
229         // now, set another target w/ the same View type. it should still reset
230         ShapeHolder view = new ShapeHolder(new ShapeDrawable());
231         updatedY = mActivity.view.newBall.getY();
232         setTarget(objAnimator1, view);
233         startSingleAnimation(objAnimator1);
234         assertTrue("Keyframe should get a value when target is set to another view of the same"
235                 + " class", emptyKeyframe1.hasValue());
236         assertEquals("Keyframe should get the updated Y value when target is set to another view"
237                 + " of the same class", (float) emptyKeyframe1.getValue(), updatedY, 0.0f);
238         waitUntilFinished(objAnimator1, mDuration * 2);
239         assertEquals("Animation should run as expected", 100f, mActivity.view.newBall.getY(), 0.0f);
240     }
241 
242     @FlakyTest
243     @Test
testOfFloat()244     public void testOfFloat() throws Throwable {
245         float[] values = {mStartY, mEndY};
246         PropertyValuesHolder pVHolder = PropertyValuesHolder.ofFloat(mProperty, values);
247         assertNotNull(pVHolder);
248         ObjectAnimator objAnimator = ObjectAnimator.ofPropertyValuesHolder(mObject,pVHolder);
249         assertTrue(objAnimator != null);
250 
251         setAnimatorProperties(objAnimator);
252         startAnimation(objAnimator);
253         assertTrue(objAnimator != null);
254         float[] yArray = getYPosition();
255         assertResults(yArray, mStartY, mEndY);
256     }
257 
258     @FlakyTest
259     @Test
testOfFloat_Property()260     public void testOfFloat_Property() throws Throwable {
261         float[] values = {mStartY, mEndY};
262         ShapeHolderYProperty property=new ShapeHolderYProperty(ShapeHolder.class,"y");
263         property.setObject(mObject);
264         PropertyValuesHolder pVHolder = PropertyValuesHolder.ofFloat(property, values);
265         assertNotNull(pVHolder);
266         ObjectAnimator objAnimator = ObjectAnimator.ofPropertyValuesHolder(mObject,pVHolder);
267         assertTrue(objAnimator != null);
268 
269         setAnimatorProperties(objAnimator);
270         startAnimation(objAnimator);
271         assertTrue(objAnimator != null);
272         float[] yArray = getYPosition();
273         assertResults(yArray, mStartY, mEndY);
274     }
275 
276     @FlakyTest
277     @Test
testOfInt()278     public void testOfInt() throws Throwable {
279         int start = 0;
280         int end = 10;
281         int[] values = {start, end};
282         PropertyValuesHolder pVHolder = PropertyValuesHolder.ofInt(mProperty, values);
283         assertNotNull(pVHolder);
284         final ObjectAnimator objAnimator = ObjectAnimator.ofPropertyValuesHolder(mObject,pVHolder);
285         assertTrue(objAnimator != null);
286         setAnimatorProperties(objAnimator);
287         mActivityRule.runOnUiThread(objAnimator::start);
288         SystemClock.sleep(2000);
289         Integer animatedValue = (Integer) objAnimator.getAnimatedValue();
290         assertTrue(animatedValue >= start);
291         assertTrue(animatedValue <= end);
292     }
293 
294     @Test
testOfInt_Property()295     public void testOfInt_Property() throws Throwable{
296         Object object = mActivity.view;
297         String property = "backgroundColor";
298         int startColor = mActivity.view.RED;
299         int endColor = mActivity.view.BLUE;
300         int values[] = {startColor, endColor};
301 
302         ViewColorProperty colorProperty=new ViewColorProperty(Integer.class,property);
303         colorProperty.setObject(object);
304         PropertyValuesHolder pVHolder = PropertyValuesHolder.ofInt(colorProperty, values);
305         assertNotNull(pVHolder);
306 
307         ObjectAnimator colorAnimator = ObjectAnimator.ofPropertyValuesHolder(object,pVHolder);
308         colorAnimator.setDuration(1000);
309         colorAnimator.setEvaluator(new ArgbEvaluator());
310         colorAnimator.setRepeatCount(ValueAnimator.INFINITE);
311         colorAnimator.setRepeatMode(ValueAnimator.REVERSE);
312 
313         ObjectAnimator objectAnimator = (ObjectAnimator) mActivity.createAnimatorWithDuration(
314             mDuration);
315         startAnimation(objectAnimator, colorAnimator);
316         SystemClock.sleep(1000);
317         Integer animatedValue = (Integer) colorAnimator.getAnimatedValue();
318         int redMin = Math.min(Color.red(startColor), Color.red(endColor));
319         int redMax = Math.max(Color.red(startColor), Color.red(endColor));
320         int blueMin = Math.min(Color.blue(startColor), Color.blue(endColor));
321         int blueMax = Math.max(Color.blue(startColor), Color.blue(endColor));
322         assertTrue(Color.red(animatedValue) >= redMin);
323         assertTrue(Color.red(animatedValue) <= redMax);
324         assertTrue(Color.blue(animatedValue) >= blueMin);
325         assertTrue(Color.blue(animatedValue) <= blueMax);
326     }
327 
328     @Test
testOfMultiFloat_Path()329     public void testOfMultiFloat_Path() throws Throwable {
330         // Test for PropertyValuesHolder.ofMultiFloat(String, Path);
331         // Create a quadratic bezier curve that are symmetric about the vertical line (x = 50).
332         // Expect when fraction < 0.5, x < 50, otherwise, x >= 50.
333         Path path = new Path();
334         path.moveTo(QUADRATIC_CTRL_PT1_X, QUADRATIC_CTRL_PT1_Y);
335         path.quadTo(QUADRATIC_CTRL_PT2_X, QUADRATIC_CTRL_PT2_Y,
336                 QUADRATIC_CTRL_PT3_X, QUADRATIC_CTRL_PT3_Y);
337 
338         PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiFloat("position", path);
339         final ValueAnimator anim = ValueAnimator.ofPropertyValuesHolder(pvh);
340 
341         // Linear interpolator
342         anim.setInterpolator(null);
343         anim.setDuration(200);
344 
345         anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
346             float lastFraction = 0;
347             float lastX = 0;
348             float lastY = 0;
349             @Override
350             public void onAnimationUpdate(ValueAnimator animation) {
351                 float[] values = (float[]) animation.getAnimatedValue();
352                 assertEquals(2, values.length);
353                 float x = values[0];
354                 float y = values[1];
355                 float fraction = animation.getAnimatedFraction();
356                 // Given that the curve is symmetric about the line (x = 50), x should be less than
357                 // 50 for half of the animation duration.
358                 if (fraction < 0.5) {
359                     assertTrue(x < QUADRATIC_CTRL_PT2_X);
360                 } else {
361                     assertTrue(x >= QUADRATIC_CTRL_PT2_X);
362                 }
363 
364                 if (lastFraction > 0.5) {
365                     // x should be increasing, y should be decreasing
366                     assertTrue(x >= lastX);
367                     assertTrue(y <= lastY);
368                 } else if (fraction <= 0.5) {
369                     // when fraction <= 0.5, both x, y should be increasing
370                     assertTrue(x >= lastX);
371                     assertTrue(y >= lastY);
372                 }
373                 lastX = x;
374                 lastY = y;
375                 lastFraction = fraction;
376             }
377         });
378         final Animator.AnimatorListener listener = mock(Animator.AnimatorListener.class);
379         anim.addListener(listener);
380         mActivityRule.runOnUiThread(anim::start);
381         verify(listener, within(400)).onAnimationEnd(anim, false);
382     }
383 
384     @Test
testOfMultiFloat_Array()385     public void testOfMultiFloat_Array() throws Throwable {
386         // Test for PropertyValuesHolder.ofMultiFloat(String, float[][]);
387         final float[][] data = new float[10][];
388         for (int i = 0; i < data.length; i++) {
389             data[i] = new float[3];
390             data[i][0] = i;
391             data[i][1] = i * 2;
392             data[i][2] = 0f;
393         }
394         final CountDownLatch endLatch = new CountDownLatch(1);
395         final PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiFloat("position", data);
396 
397         final ValueAnimator anim = ValueAnimator.ofPropertyValuesHolder(pvh);
398         anim.setInterpolator(null);
399         anim.setDuration(60);
400         anim.addListener(new AnimatorListenerAdapter() {
401             @Override
402             public void onAnimationEnd(Animator animation) {
403                 endLatch.countDown();
404             }
405         });
406 
407         anim.addUpdateListener((ValueAnimator animation) -> {
408             float fraction = animation.getAnimatedFraction();
409             float[] values = (float[]) animation.getAnimatedValue();
410             assertEquals(3, values.length);
411 
412             float expectedX = fraction * (data.length - 1);
413 
414             assertEquals(expectedX, values[0], EPSILON);
415             assertEquals(expectedX * 2, values[1], EPSILON);
416             assertEquals(0.0f, values[2], 0.0f);
417         });
418 
419         mActivityRule.runOnUiThread(anim::start);
420         assertTrue(endLatch.await(200, TimeUnit.MILLISECONDS));
421     }
422 
423     @Test
testOfMultiInt_Path()424     public void testOfMultiInt_Path() throws Throwable {
425         // Test for PropertyValuesHolder.ofMultiInt(String, Path);
426         // Create a quadratic bezier curve that are symmetric about the vertical line (x = 50).
427         // Expect when fraction < 0.5, x < 50, otherwise, x >= 50.
428         Path path = new Path();
429         path.moveTo(QUADRATIC_CTRL_PT1_X, QUADRATIC_CTRL_PT1_Y);
430         path.quadTo(QUADRATIC_CTRL_PT2_X, QUADRATIC_CTRL_PT2_Y,
431                 QUADRATIC_CTRL_PT3_X, QUADRATIC_CTRL_PT3_Y);
432 
433         final PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiInt("position", path);
434         final ValueAnimator anim = ValueAnimator.ofPropertyValuesHolder(pvh);
435         // Linear interpolator
436         anim.setInterpolator(null);
437         anim.setDuration(200);
438 
439         anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
440             float lastFraction = 0;
441             int lastX = 0;
442             int lastY = 0;
443             @Override
444             public void onAnimationUpdate(ValueAnimator animation) {
445                 int[] values = (int[]) animation.getAnimatedValue();
446                 assertEquals(2, values.length);
447                 int x = values[0];
448                 int y = values[1];
449                 float fraction = animation.getAnimatedFraction();
450                 // Given that the curve is symmetric about the line (x = 50), x should be less than
451                 // 50 for half of the animation duration.
452                 if (fraction < 0.5) {
453                     assertTrue(x < QUADRATIC_CTRL_PT2_X);
454                 } else {
455                     assertTrue(x >= QUADRATIC_CTRL_PT2_X);
456                 }
457 
458                 if (lastFraction > 0.5) {
459                     // x should be increasing, y should be decreasing
460                     assertTrue(x >= lastX);
461                     assertTrue(y <= lastY);
462                 } else if (fraction <= 0.5) {
463                     // when fraction <= 0.5, both x, y should be increasing
464                     assertTrue(x >= lastX);
465                     assertTrue(y >= lastY);
466                 }
467                 lastX = x;
468                 lastY = y;
469                 lastFraction = fraction;
470             }
471         });
472         final Animator.AnimatorListener listener = mock(Animator.AnimatorListener.class);
473         anim.addListener(listener);
474         mActivityRule.runOnUiThread(anim::start);
475         verify(listener, within(400)).onAnimationEnd(anim, false);
476     }
477 
478     @Test
testOfMultiInt_Array()479     public void testOfMultiInt_Array() throws Throwable {
480         // Test for PropertyValuesHolder.ofMultiFloat(String, int[][]);
481         final int[][] data = new int[10][];
482         for (int i = 0; i < data.length; i++) {
483             data[i] = new int[3];
484             data[i][0] = i;
485             data[i][1] = i * 2;
486             data[i][2] = 0;
487         }
488 
489         final CountDownLatch endLatch = new CountDownLatch(1);
490         final PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiInt("position", data);
491         final ValueAnimator anim = ValueAnimator.ofPropertyValuesHolder(pvh);
492         anim.setInterpolator(null);
493         anim.setDuration(60);
494         anim.addListener(new AnimatorListenerAdapter() {
495             @Override
496             public void onAnimationEnd(Animator animation) {
497                 endLatch.countDown();
498             }
499         });
500 
501         anim.addUpdateListener((ValueAnimator animation) -> {
502             float fraction = animation.getAnimatedFraction();
503             int[] values = (int[]) animation.getAnimatedValue();
504             assertEquals(3, values.length);
505 
506             int expectedX = Math.round(fraction * (data.length - 1));
507             int expectedY = Math.round(fraction * (data.length - 1) * 2);
508 
509             // Allow a delta of 1 for rounding errors.
510             assertEquals(expectedX, values[0], 1);
511             assertEquals(expectedY, values[1], 1);
512             assertEquals(0, values[2]);
513         });
514 
515         mActivityRule.runOnUiThread(anim::start);
516         assertTrue(endLatch.await(200, TimeUnit.MILLISECONDS));
517     }
518 
519     @Test
testOfObject_Converter()520     public void testOfObject_Converter() throws Throwable {
521         // Test for PropertyValuesHolder.ofObject(String, TypeConverter<T, V>, Path)
522         // and for PropertyValuesHolder.ofObject(Property, TypeConverter<T, V>, Path)
523         // Create a path that contains two disconnected line segments. Check that the animated
524         // property x and property y always stay on the line segments.
525         Path path = new Path();
526         path.moveTo(LINE1_START, -LINE1_START);
527         path.lineTo(LINE1_END, -LINE1_END);
528         path.moveTo(LINE2_START, LINE2_START);
529         path.lineTo(LINE2_END, LINE2_END);
530         TypeConverter<PointF, Float> converter = new TypeConverter<PointF, Float>(
531                 PointF.class, Float.class) {
532             @Override
533             public Float convert(PointF value) {
534                 return (float) Math.sqrt(value.x * value.x + value.y * value.y);
535             }
536         };
537         final CountDownLatch endLatch = new CountDownLatch(3);
538 
539         // Create three animators. The first one use a converter that converts the point to distance
540         // to  origin. The second one does not have a type converter. The third animator uses a
541         // converter to changes sign of the x, y value of the input pointF.
542         FloatProperty property = new FloatProperty("distance") {
543             @Override
544             public void setValue(Object object, float value) {
545             }
546 
547             @Override
548             public Object get(Object object) {
549                 return null;
550             }
551         };
552         final PropertyValuesHolder pvh1 =
553                 PropertyValuesHolder.ofObject(property, converter, path);
554         final ValueAnimator anim1 = ValueAnimator.ofPropertyValuesHolder(pvh1);
555         anim1.setDuration(100);
556         anim1.setInterpolator(null);
557         anim1.addListener(new AnimatorListenerAdapter() {
558             @Override
559             public void onAnimationEnd(Animator animation) {
560                 endLatch.countDown();
561             }
562         });
563 
564         final PropertyValuesHolder pvh2 =
565                 PropertyValuesHolder.ofObject("position", null, path);
566         final ValueAnimator anim2 = ValueAnimator.ofPropertyValuesHolder(pvh2);
567         anim2.setDuration(100);
568         anim2.setInterpolator(null);
569         anim2.addListener(new AnimatorListenerAdapter() {
570             @Override
571             public void onAnimationEnd(Animator animation) {
572                 endLatch.countDown();
573             }
574         });
575 
576         TypeConverter<PointF, PointF> converter3 = new TypeConverter<PointF, PointF>(
577                 PointF.class, PointF.class) {
578             PointF mValue = new PointF();
579             @Override
580             public PointF convert(PointF value) {
581                 mValue.x = -value.x;
582                 mValue.y = -value.y;
583                 return mValue;
584             }
585         };
586         final PropertyValuesHolder pvh3 =
587                 PropertyValuesHolder.ofObject("position", converter3, path);
588         final ValueAnimator anim3 = ValueAnimator.ofPropertyValuesHolder(pvh3);
589         anim3.setDuration(100);
590         anim3.setInterpolator(null);
591         anim3.addListener(new AnimatorListenerAdapter() {
592             @Override
593             public void onAnimationEnd(Animator animation) {
594                 endLatch.countDown();
595             }
596         });
597 
598         anim3.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
599             // Set the initial value of the distance to the distance between the first point on
600             // the path to the origin.
601             float mLastDistance = (float) (32 * Math.sqrt(2));
602             float mLastFraction = 0f;
603             @Override
604             public void onAnimationUpdate(ValueAnimator animation) {
605                 float fraction = anim1.getAnimatedFraction();
606                 assertEquals(fraction, anim2.getAnimatedFraction(), 0.0f);
607                 assertEquals(fraction, anim3.getAnimatedFraction(), 0.0f);
608                 float distance = (Float) anim1.getAnimatedValue();
609                 PointF position = (PointF) anim2.getAnimatedValue();
610                 PointF positionReverseSign = (PointF) anim3.getAnimatedValue();
611                 assertEquals(position.x, -positionReverseSign.x, 0.0f);
612                 assertEquals(position.y, -positionReverseSign.y, 0.0f);
613 
614                 // Manually calculate the distance for the animator that doesn't have a
615                 // TypeConverter, and expect the result to be the same as the animation value from
616                 // the type converter.
617                 float distanceFromPosition = (float) Math.sqrt(
618                         position.x * position.x + position.y * position.y);
619                 assertEquals(distance, distanceFromPosition, 0.0001f);
620 
621                 if (mLastFraction > 0.75) {
622                     // In the 2nd line segment of the path, distance to origin should be increasing.
623                     assertTrue(distance >= mLastDistance);
624                 } else if (fraction < 0.75) {
625                     assertTrue(distance <= mLastDistance);
626                 }
627                 mLastDistance = distance;
628                 mLastFraction = fraction;
629             }
630         });
631 
632         mActivityRule.runOnUiThread(() -> {
633             anim1.start();
634             anim2.start();
635             anim3.start();
636         });
637 
638         // Wait until both of the animations finish
639         assertTrue(endLatch.await(200, TimeUnit.MILLISECONDS));
640     }
641 
642     @Test
testSetConverter()643     public void testSetConverter() throws Throwable {
644         // Test for PropertyValuesHolder.setConverter()
645         PropertyValuesHolder pvh = PropertyValuesHolder.ofObject("", null, 0f, 1f);
646         // Reverse the sign of the float in the converter, and use that value as the new type
647         // PointF's x value.
648         pvh.setConverter(new TypeConverter<Float, PointF>(Float.class, PointF.class) {
649             PointF mValue = new PointF();
650             @Override
651             public PointF convert(Float value) {
652                 mValue.x = value * (-1f);
653                 mValue.y = 0f;
654                 return mValue;
655             }
656         });
657         final CountDownLatch endLatch = new CountDownLatch(2);
658 
659         final ValueAnimator anim1 = ValueAnimator.ofPropertyValuesHolder(pvh);
660         anim1.setInterpolator(null);
661         anim1.setDuration(100);
662         anim1.addListener(new AnimatorListenerAdapter() {
663             @Override
664             public void onAnimationEnd(Animator animation) {
665                 endLatch.countDown();
666             }
667         });
668 
669         final ValueAnimator anim2 = ValueAnimator.ofFloat(0f, 1f);
670         anim2.setInterpolator(null);
671         anim2.addUpdateListener((ValueAnimator animation) -> {
672             assertEquals(anim1.getAnimatedFraction(), anim2.getAnimatedFraction(), 0.0f);
673             // Check that the pvh with type converter did reverse the sign of float, and set
674             // the x value of the PointF with it.
675             PointF value1 = (PointF) anim1.getAnimatedValue();
676             float value2 = (Float) anim2.getAnimatedValue();
677             assertEquals(value2, -value1.x, 0.0f);
678             assertEquals(0f, value1.y, 0.0f);
679         });
680         anim2.setDuration(100);
681         anim2.addListener(new AnimatorListenerAdapter() {
682             @Override
683             public void onAnimationEnd(Animator animation) {
684                 endLatch.countDown();
685             }
686         });
687 
688         mActivityRule.runOnUiThread(() -> {
689             anim1.start();
690             anim2.start();
691         });
692 
693         // Wait until both of the animations finish
694         assertTrue(endLatch.await(200, TimeUnit.MILLISECONDS));
695     }
696 
697     @FlakyTest
698     @Test
testSetProperty()699     public void testSetProperty() throws Throwable {
700         float[] values = {mStartY, mEndY};
701         ShapeHolderYProperty property=new ShapeHolderYProperty(ShapeHolder.class,"y");
702         property.setObject(mObject);
703         PropertyValuesHolder pVHolder = PropertyValuesHolder.ofFloat("", values);
704         pVHolder.setProperty(property);
705         ObjectAnimator objAnimator = ObjectAnimator.ofPropertyValuesHolder(mObject,pVHolder);
706         setAnimatorProperties(objAnimator);
707         startAnimation(objAnimator);
708         assertTrue(objAnimator != null);
709         float[] yArray = getYPosition();
710         assertResults(yArray, mStartY, mEndY);
711     }
712 
713     class ShapeHolderYProperty extends Property {
714         private ShapeHolder shapeHolder ;
715         private Class type = Float.class;
716         private String name = "y";
717         @SuppressWarnings("unchecked")
ShapeHolderYProperty(Class type, String name)718         public ShapeHolderYProperty(Class type, String name) throws Exception {
719             super(Float.class, name );
720             if(!( type.equals(this.type) || ( name.equals(this.name))) ){
721                 throw new Exception("Type or name provided does not match with " +
722                         this.type.getName() + " or " + this.name);
723             }
724         }
725 
setObject(Object object)726         public void setObject(Object object){
727             shapeHolder = (ShapeHolder) object;
728         }
729 
730         @Override
get(Object object)731         public Object get(Object object) {
732             return shapeHolder;
733         }
734 
735         @Override
getName()736         public String getName() {
737             return "y";
738         }
739 
740         @Override
getType()741         public Class getType() {
742             return super.getType();
743         }
744 
745         @Override
isReadOnly()746         public boolean isReadOnly() {
747             return false;
748         }
749 
750         @Override
set(Object object, Object value)751         public void set(Object object, Object value) {
752             shapeHolder.setY((Float)value);
753         }
754 
755     }
756 
757     class ViewColorProperty extends Property {
758         private View view ;
759         private Class type = Integer.class;
760         private String name = "backgroundColor";
761         @SuppressWarnings("unchecked")
ViewColorProperty(Class type, String name)762         public ViewColorProperty(Class type, String name) throws Exception {
763             super(Integer.class, name );
764             if(!( type.equals(this.type) || ( name.equals(this.name))) ){
765                 throw new Exception("Type or name provided does not match with " +
766                         this.type.getName() + " or " + this.name);
767             }
768         }
769 
setObject(Object object)770         public void setObject(Object object){
771             view = (View) object;
772         }
773 
774         @Override
get(Object object)775         public Object get(Object object) {
776             return view;
777         }
778 
779         @Override
getName()780         public String getName() {
781             return name;
782         }
783 
784         @Override
getType()785         public Class getType() {
786             return super.getType();
787         }
788 
789         @Override
isReadOnly()790         public boolean isReadOnly() {
791             return false;
792         }
793 
794         @Override
set(Object object, Object value)795         public void set(Object object, Object value) {
796             view.setBackgroundColor((Integer)value);
797         }
798     }
799 
setAnimatorProperties(ObjectAnimator objAnimator)800     private void setAnimatorProperties(ObjectAnimator objAnimator) {
801         objAnimator.setDuration(5000);
802         objAnimator.setRepeatCount(ValueAnimator.INFINITE);
803         objAnimator.setInterpolator(new AccelerateInterpolator());
804         objAnimator.setRepeatMode(ValueAnimator.REVERSE);
805     }
806 
getYPosition()807     public float[] getYPosition() throws Throwable{
808         float[] yArray = new float[3];
809         for(int i = 0; i < 3; i++) {
810             float y = mActivity.view.newBall.getY();
811             yArray[i] = y;
812             SystemClock.sleep(1300);
813         }
814         return yArray;
815     }
816 
assertResults(float[] yArray,float startY, float endY)817     public void assertResults(float[] yArray,float startY, float endY) {
818         for(int i = 0; i < 3; i++){
819             float y = yArray[i];
820             assertTrue(y >= startY);
821             assertTrue(y <= endY);
822         }
823     }
824 
startAnimation(final Animator animator)825     private void startAnimation(final Animator animator) throws Throwable {
826         mActivityRule.runOnUiThread(() -> mActivity.startAnimation(animator));
827     }
828 
startAnimation(final ObjectAnimator mObjectAnimator, final ObjectAnimator colorAnimator)829     private void startAnimation(final ObjectAnimator mObjectAnimator,
830             final ObjectAnimator colorAnimator) throws Throwable {
831         mActivityRule.runOnUiThread(() -> mActivity.startAnimation(mObjectAnimator, colorAnimator));
832     }
833 }
834 
835