• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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.uirendering.cts.testclasses;
17 
18 import static android.uirendering.cts.util.MockVsyncHelper.nextFrame;
19 
20 import static org.junit.Assert.assertEquals;
21 import static org.junit.Assert.assertFalse;
22 import static org.junit.Assert.assertTrue;
23 
24 import android.animation.ValueAnimator;
25 import android.content.Context;
26 import android.graphics.Bitmap;
27 import android.graphics.BlendMode;
28 import android.graphics.Canvas;
29 import android.graphics.Color;
30 import android.graphics.ColorFilter;
31 import android.graphics.Paint;
32 import android.graphics.PixelFormat;
33 import android.graphics.RecordingCanvas;
34 import android.graphics.Rect;
35 import android.graphics.RenderNode;
36 import android.graphics.drawable.Drawable;
37 import android.uirendering.cts.bitmapverifiers.ColorVerifier;
38 import android.uirendering.cts.bitmapverifiers.PerPixelBitmapVerifier;
39 import android.uirendering.cts.bitmapverifiers.RegionVerifier;
40 import android.uirendering.cts.testinfrastructure.ActivityTestBase;
41 import android.uirendering.cts.testinfrastructure.Tracer;
42 import android.uirendering.cts.util.MockVsyncHelper;
43 import android.view.ContextThemeWrapper;
44 import android.view.animation.AnimationUtils;
45 import android.widget.EdgeEffect;
46 
47 import androidx.test.InstrumentationRegistry;
48 import androidx.test.filters.LargeTest;
49 import androidx.test.filters.SmallTest;
50 import androidx.test.runner.AndroidJUnit4;
51 
52 import org.junit.After;
53 import org.junit.Before;
54 import org.junit.Rule;
55 import org.junit.Test;
56 import org.junit.runner.RunWith;
57 
58 @SmallTest
59 @RunWith(AndroidJUnit4.class)
60 public class EdgeEffectTests extends ActivityTestBase {
61     private static final int WIDTH = 90;
62     private static final int HEIGHT = 90;
63 
64     @Rule
65     public Tracer name = new Tracer();
66 
67     private Context mThemeContext;
68     private float mPreviousDurationScale;
69 
70     interface EdgeEffectInitializer {
initialize(EdgeEffect edgeEffect)71         void initialize(EdgeEffect edgeEffect);
72     }
73 
getContext()74     private Context getContext() {
75         return mThemeContext;
76     }
77 
78     @Before
setUp()79     public void setUp() {
80         final Context targetContext = InstrumentationRegistry.getTargetContext();
81         mThemeContext = new ContextThemeWrapper(targetContext,
82                 android.R.style.Theme_Material_Light);
83         mPreviousDurationScale = ValueAnimator.getDurationScale();
84         ValueAnimator.setDurationScale(1.0f);
85     }
86 
87     @After
tearDown()88     public void tearDown() {
89         ValueAnimator.setDurationScale(mPreviousDurationScale);
90     }
91 
92     private static class EdgeEffectValidator extends PerPixelBitmapVerifier {
93         public float stretch; // in pixels, vertically
94 
EdgeEffectValidator()95         EdgeEffectValidator() {
96         }
97 
98         @Override
verifyPixel(int x, int y, int observedColor)99         protected boolean verifyPixel(int x, int y, int observedColor) {
100             if (y < HEIGHT / 2) {
101                 // Top half should always be the top color
102                 return observedColor == Color.WHITE;
103             }
104 
105             // This may be either bottom or top color, depending on the stretch
106             stretch += Color.red(observedColor) / 255f / WIDTH;
107             return true;
108         }
109     }
110 
assertEdgeEffect(EdgeEffectInitializer initializer)111     private void assertEdgeEffect(EdgeEffectInitializer initializer) {
112         EdgeEffect edgeEffect = new EdgeEffect(getContext());
113         edgeEffect.setSize(WIDTH, HEIGHT);
114         initializer.initialize(edgeEffect);
115 
116         RenderNode renderNode = drawEdgeEffect(edgeEffect, 0, 0f);
117 
118         float stretchPixelCount = getStretchDownPixelCount(renderNode);
119 
120         // at least 1 pixel stretch
121         assertTrue(stretchPixelCount > 1);
122     }
123 
getStretchDownPixelCount( RenderNode renderNode )124     private float getStretchDownPixelCount(
125             RenderNode renderNode
126     ) {
127         EdgeEffectValidator verifier = new EdgeEffectValidator();
128         createTest()
129                 .addCanvasClientWithoutUsingPicture((canvas, width, height) -> {
130                     canvas.drawRenderNode(renderNode);
131                 }, true)
132                 .runWithVerifier(verifier);
133 
134         return verifier.stretch;
135     }
136 
137     @Test
testOnPull()138     public void testOnPull() {
139         assertEdgeEffect(edgeEffect -> {
140             edgeEffect.onPull(1);
141         });
142     }
143 
144     @Test
testSetSize()145     public void testSetSize() {
146         EdgeEffect edgeEffect = new EdgeEffect(getContext());
147         edgeEffect.setSize(WIDTH, HEIGHT / 2);
148 
149         RenderNode renderNode = drawEdgeEffect(edgeEffect, HEIGHT / 2f, 0f);
150 
151         float stretchPixelCount = getStretchDownPixelCount(renderNode);
152 
153         // The top half is not in the stretched area, so the only thing being stretched is the
154         // bottom half
155         assertEquals(0f, stretchPixelCount, 0.01f);
156     }
157 
158     @Test
testIsFinished()159     public void testIsFinished() {
160         EdgeEffect effect = new EdgeEffect(getContext());
161         assertTrue(effect.isFinished());
162         effect.onPull(0.5f);
163         assertFalse(effect.isFinished());
164     }
165 
166     @Test
testFinish()167     public void testFinish() {
168         EdgeEffect effect = new EdgeEffect(getContext());
169         effect.onPull(1);
170         effect.finish();
171         assertTrue(effect.isFinished());
172 
173         effect.onAbsorb(1000);
174         effect.finish();
175         assertFalse(effect.draw(new Canvas()));
176     }
177 
178     @Test
testGetMaxHeight()179     public void testGetMaxHeight() {
180         EdgeEffect edgeEffect = new EdgeEffect(getContext());
181         edgeEffect.setSize(200, 200);
182         assertEquals(200, edgeEffect.getMaxHeight());
183         edgeEffect.setSize(200, 0);
184         assertEquals(0, edgeEffect.getMaxHeight());
185     }
186 
187     @Test
testDistance()188     public void testDistance() {
189         EdgeEffect effect = new EdgeEffect(getContext());
190 
191         assertEquals(0f, effect.getDistance(), 0.001f);
192 
193         assertEquals(0.1f, effect.onPullDistance(0.1f, 0.5f), 0.001f);
194 
195         assertEquals(0.1f, effect.getDistance(), 0.001f);
196 
197         assertEquals(-0.05f, effect.onPullDistance(-0.05f, 0.5f), 0.001f);
198 
199         assertEquals(0.05f, effect.getDistance(), 0.001f);
200 
201         assertEquals(-0.05f, effect.onPullDistance(-0.2f, 0.5f), 0.001f);
202 
203         assertEquals(0f, effect.getDistance(), 0.001f);
204     }
205 
206     @Test
testPullToZeroReleases()207     public void testPullToZeroReleases() {
208         EdgeEffect edgeEffect = new EdgeEffect(getContext());
209         edgeEffect.setSize(200, 200);
210         assertEquals(0.5f, edgeEffect.onPullDistance(0.5f, 0.5f), 0f);
211         assertFalse(edgeEffect.isFinished());
212         assertEquals(-0.5f, edgeEffect.onPullDistance(-0.5f, 0.5f), 0f);
213         assertTrue(edgeEffect.isFinished());
214     }
215 
216     @Test
testFlingNegativeReleases()217     public void testFlingNegativeReleases() throws Throwable {
218         MockVsyncHelper.runOnVsyncThread(() -> {
219             EdgeEffect edgeEffect = new EdgeEffect(getContext());
220             edgeEffect.setSize(WIDTH, HEIGHT);
221             assertEquals(0.01f, edgeEffect.onPullDistance(0.01f, 0.5f), 0f);
222             assertFalse(edgeEffect.isFinished());
223             edgeEffect.onAbsorb(-10000);
224             nextFrame();
225             drawEdgeEffect(edgeEffect, 0f, 0f);
226             // It should have flung past 0 in one frame
227             assertTrue(edgeEffect.isFinished());
228         });
229     }
230 
231     /**
232      * Can't stretch past 1.0
233      */
234     @Test
testMaxStretch()235     public void testMaxStretch() throws Throwable {
236         EdgeEffect edgeEffect = new EdgeEffect(getContext());
237         edgeEffect.setSize(WIDTH, HEIGHT);
238         float pulled = edgeEffect.onPullDistance(1f, 0.5f);
239         assertEquals(1f, pulled, 0.001f);
240         assertEquals(1f, edgeEffect.getDistance(), 0.001f);
241         pulled = edgeEffect.onPullDistance(0.1f, 0.5f);
242         assertEquals(0.1f, pulled, 0.001f);
243         assertEquals(1f, edgeEffect.getDistance(), 0.001f);
244     }
245 
246     /**
247      * A fling past 1.0 results in stopping at 1 and then retracting.
248      */
249     @Test
testMaxFling()250     public void testMaxFling() throws Throwable {
251         MockVsyncHelper.runOnVsyncThread(() -> {
252             EdgeEffect edgeEffect = new EdgeEffect(getContext());
253             edgeEffect.setSize(WIDTH, HEIGHT);
254             assertEquals(0.99f, edgeEffect.onPullDistance(0.99f, 0.5f), 0.001f);
255             edgeEffect.onAbsorb(10000);
256             nextFrame();
257             drawEdgeEffect(edgeEffect, 0f, 0f);
258             assertEquals(1f, edgeEffect.getDistance(), 0.001f);
259             nextFrame();
260             drawEdgeEffect(edgeEffect, 0f, 0f);
261             assertTrue(edgeEffect.getDistance() < 1f);
262         });
263     }
264 
265     private RenderNode drawStretchEffect(float rotation) {
266         EdgeEffect edgeEffect = new EdgeEffect(getContext());
267         edgeEffect.setSize(WIDTH, HEIGHT);
268         edgeEffect.onPullDistance(1f, 0.5f);
269 
270         return drawEdgeEffect(edgeEffect, 0, rotation);
271     }
272 
273     private RenderNode drawEdgeEffect(EdgeEffect edgeEffect, float verticalOffset, float rotation) {
274         RenderNode renderNode = new RenderNode("");
275         renderNode.setPosition(0, 0, WIDTH, HEIGHT);
276         RecordingCanvas recordingCanvas = renderNode.beginRecording();
277         Paint paint = new Paint();
278         paint.setColor(Color.WHITE);
279         recordingCanvas.drawRect(0f, 0f, WIDTH, HEIGHT / 2f, paint);
280         paint.setColor(Color.BLACK);
281         recordingCanvas.drawRect(0, HEIGHT / 2f, WIDTH, HEIGHT, paint);
282         renderNode.endRecording();
283 
284         RenderNode outer = new RenderNode("outer");
285         outer.setPosition(0, 0, WIDTH, HEIGHT);
286         RecordingCanvas outerRecordingCanvas = outer.beginRecording();
287         outerRecordingCanvas.drawRenderNode(renderNode);
288         recordingCanvas.translate(0f, verticalOffset);
289         recordingCanvas.rotate(rotation, WIDTH / 2f, HEIGHT / 2f);
290         edgeEffect.draw(outerRecordingCanvas);
291         outer.endRecording();
292         return outer;
293     }
294 
295     @Test
296     public void testStretchTop() {
297         RenderNode renderNode = drawStretchEffect(0f);
298         Rect innerRect = new Rect(0, 0, WIDTH, HEIGHT / 2 + 1);
299         Rect outerRect = new Rect(0, HEIGHT / 2 + 10, WIDTH, HEIGHT);
300         createTest()
301                 .addCanvasClientWithoutUsingPicture((canvas, width, height) -> {
302                     canvas.drawRenderNode(renderNode);
303                 }, true)
304                 .runWithVerifier(
305                         new RegionVerifier().addVerifier(
306                                 innerRect,
307                                 new ColorVerifier(Color.WHITE)
308                         ).addVerifier(
309                                 outerRect,
310                                 new ColorVerifier(Color.BLACK)
311                         ));
312     }
313 
314     @Test
testStretchBottom()315     public void testStretchBottom() {
316         RenderNode renderNode = drawStretchEffect(180f);
317         Rect innerRect = new Rect(0, 0, WIDTH, 1);
318         Rect outerRect = new Rect(0, (HEIGHT / 2) - 1, WIDTH, HEIGHT / 2);
319         createTest()
320                 .addCanvasClientWithoutUsingPicture((canvas, width, height) -> {
321                     canvas.drawRenderNode(renderNode);
322                 }, true)
323                 .runWithVerifier(
324                         new RegionVerifier().addVerifier(
325                                 innerRect,
326                                 new ColorVerifier(Color.WHITE)
327                         ).addVerifier(
328                                 outerRect,
329                                 new ColorVerifier(Color.BLACK)
330                         ));
331     }
332 
333     @Test
testNoSetSizeCallDoesNotCrash()334     public void testNoSetSizeCallDoesNotCrash() {
335         EdgeEffect edgeEffect = new EdgeEffect(getContext());
336         edgeEffect.onPullDistance(1f, 1f);
337         edgeEffect.onAbsorb(100);
338         edgeEffect.onRelease();
339 
340         RenderNode node = new RenderNode("");
341         RecordingCanvas canvas = node.beginRecording();
342         edgeEffect.draw(canvas);
343         node.endRecording();
344     }
345 
346     @Test
testInvalidPullDistanceDoesNotCrash()347     public void testInvalidPullDistanceDoesNotCrash() {
348         EdgeEffect edgeEffect = new EdgeEffect(getContext());
349         // Verify that bad inputs to onPull do not crash
350         edgeEffect.onPull(Float.NaN, Float.NaN);
351 
352         edgeEffect.setSize(TEST_WIDTH, TEST_HEIGHT);
353         RenderNode node = new RenderNode("");
354         node.setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT);
355         RecordingCanvas canvas = node.beginRecording();
356 
357         edgeEffect.draw(canvas);
358         node.endRecording();
359     }
360 
361     @Test
testAbsorbThenDrawDoesNotCrash()362     public void testAbsorbThenDrawDoesNotCrash() {
363         MockVsyncHelper.runOnVsyncThread(() -> {
364             EdgeEffect edgeEffect = new EdgeEffect(getContext());
365             edgeEffect.onPullDistance(1f, 1f);
366             edgeEffect.onAbsorb(100);
367             edgeEffect.onRelease();
368 
369             nextFrame();
370 
371             edgeEffect.setSize(10, 10);
372             RenderNode node = new RenderNode("");
373             node.setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT);
374             RecordingCanvas canvas = node.beginRecording();
375             Paint paint = new Paint();
376             paint.setColor(Color.RED);
377             canvas.drawRect(0f, 0f, TEST_WIDTH, TEST_HEIGHT, paint);
378 
379             canvas.rotate(90, TEST_WIDTH / 2f, TEST_HEIGHT / 2f);
380             edgeEffect.draw(canvas);
381             node.endRecording();
382         });
383     }
384 
385     @Test
testStretchEffectUsesLayer()386     public void testStretchEffectUsesLayer() {
387         // Verify that any overscroll effect leverages a layer for rendering to ensure
388         // consistent graphics behavior.
389         EdgeEffect effect = new EdgeEffect(getContext());
390         OverscrollDrawable drawable = new OverscrollDrawable(effect, Color.CYAN);
391         drawable.setBounds(0, 0, TEST_WIDTH, TEST_HEIGHT);
392 
393         // Verify that the black background is visible when rendering without overscroll
394         createTest()
395                 .addCanvasClientWithoutUsingPicture((canvas, width, height) -> {
396                     canvas.drawColor(Color.BLACK);
397                     drawable.setOverscroll(false);
398                     drawable.draw(canvas);
399                 }, true)
400                 .runWithVerifier(
401                         new RegionVerifier().addVerifier(
402                                 new Rect(0, 0, TEST_WIDTH, TEST_HEIGHT),
403                                 new ColorVerifier(Color.BLACK)
404                         ));
405 
406         // Verify cyan color is visible when rendering with overscroll enabled
407         createTest()
408                 .addCanvasClientWithoutUsingPicture((canvas, width, height) -> {
409                     canvas.drawColor(Color.BLACK);
410                     drawable.setOverscroll(true);
411                     drawable.draw(canvas);
412                 }, true)
413                 .runWithVerifier(
414                         new RegionVerifier().addVerifier(
415                                 new Rect(0, 0, TEST_WIDTH, TEST_HEIGHT),
416                                 new ColorVerifier(Color.CYAN)
417                         ));
418     }
419 
420     /**
421      * Test drawable class that renders content using {@link BlendMode#DST_OVER} which
422      * requires a compositing layer in order to draw pixels, otherwise nothing is drawn.
423      */
424     private static class OverscrollDrawable extends Drawable {
425 
426         private final int mChildColor;
427 
428         private final EdgeEffect mEdgeEffect;
429 
430         private final RenderNode mParentNode = new RenderNode("");
431         private final RenderNode mChildNode = new RenderNode("");
432 
433         private boolean mOverscroll = false;
434 
OverscrollDrawable(EdgeEffect edgeEffect, int color)435         public OverscrollDrawable(EdgeEffect edgeEffect, int color) {
436             mChildColor = color;
437             mEdgeEffect = edgeEffect;
438         }
439 
setOverscroll(boolean overscroll)440         public void setOverscroll(boolean overscroll) {
441             mOverscroll = overscroll;
442             invalidateSelf();
443         }
444 
onBoundsChange(Rect bounds)445         protected void onBoundsChange(Rect bounds) {
446             super.onBoundsChange(bounds);
447             mEdgeEffect.setSize(bounds.width(), bounds.height());
448             mParentNode.setPosition(0, 0, bounds.width(), bounds.height());
449             mChildNode.setPosition(0, 0, bounds.width(), bounds.height());
450         }
451 
452         @Override
draw(Canvas canvas)453         public void draw(Canvas canvas) {
454             if (mOverscroll) {
455                 mEdgeEffect.onPullDistance(1.0f, 0.5f);
456             } else {
457                 mEdgeEffect.finish();
458             }
459 
460             RecordingCanvas parentCanvas = mParentNode.beginRecording();
461             {
462                 RecordingCanvas childCanvas = mChildNode.beginRecording();
463                 {
464                     childCanvas.drawColor(mChildColor, BlendMode.DST_OVER);
465                 }
466                 mChildNode.endRecording();
467 
468                 parentCanvas.drawRenderNode(mChildNode);
469                 mEdgeEffect.draw(parentCanvas);
470             }
471             mParentNode.endRecording();
472 
473             canvas.drawRenderNode(mParentNode);
474         }
475 
476         @Override
setAlpha(int alpha)477         public void setAlpha(int alpha) {
478             // NO-OP
479         }
480 
481         @Override
setColorFilter(ColorFilter colorFilter)482         public void setColorFilter(ColorFilter colorFilter) {
483             // NO-OP
484         }
485 
486         @Override
getOpacity()487         public int getOpacity() {
488             return PixelFormat.TRANSLUCENT;
489         }
490     }
491 
492     /**
493      * A held pull should not retract.
494      */
495     @Test
496     @LargeTest
testStretchPullAndHold()497     public void testStretchPullAndHold() throws Exception {
498         MockVsyncHelper.runOnVsyncThread(() -> {
499             EdgeEffect edgeEffect = createEdgeEffectWithPull();
500             assertEquals(0.25f, edgeEffect.getDistance(), 0.001f);
501 
502             // We must wait until the EdgeEffect would normally start receding (167 ms)
503             sleepAnimationTime(200);
504 
505             // Drawing will cause updates of the distance if it is animating
506             RenderNode renderNode = new RenderNode(null);
507             Canvas canvas = renderNode.beginRecording();
508             edgeEffect.draw(canvas);
509 
510             // A glow effect would start receding now, so let's be sure it doesn't:
511             sleepAnimationTime(200);
512             edgeEffect.draw(canvas);
513 
514             // It should not be updating now
515             assertEquals(0.25f, edgeEffect.getDistance(), 0.001f);
516 
517             // Now let's release it and it should start animating
518             edgeEffect.onRelease();
519 
520             sleepAnimationTime(20);
521 
522             // Now that it should be animating, the draw should update the distance
523             edgeEffect.draw(canvas);
524 
525             assertTrue(edgeEffect.getDistance() < 0.25f);
526         });
527     }
528 
529     /**
530      * It should be possible to catch the stretch effect during an animation.
531      */
532     @Test
533     @LargeTest
534     public void testCatchStretchDuringAnimation() throws Exception {
535         MockVsyncHelper.runOnVsyncThread(() -> {
536             EdgeEffect edgeEffect = createEdgeEffectWithPull();
537             assertEquals(0.25f, edgeEffect.getDistance(), 0.001f);
538             edgeEffect.onRelease();
539 
540             // Wait some time to be sure it is animating away.
541             sleepAnimationTime(20);
542 
543             // Drawing will cause updates of the distance if it is animating
544             RenderNode renderNode = new RenderNode(null);
545             Canvas canvas = renderNode.beginRecording();
546             edgeEffect.draw(canvas);
547 
548             // It should have started retracting. Now catch it.
549             float consumed = edgeEffect.onPullDistance(0f, 0.5f);
550             assertEquals(0f, consumed, 0f);
551 
552             float distanceAfterAnimation = edgeEffect.getDistance();
553             assertTrue(distanceAfterAnimation < 0.25f);
554 
555             sleepAnimationTime(50);
556 
557             // There should be no change once it has been caught.
558             edgeEffect.draw(canvas);
559             assertEquals(distanceAfterAnimation, edgeEffect.getDistance(), 0f);
560         });
561     }
562 
563     /**
564      * When an EdgeEffect is drawn on a non-RecordingCanvas, the animation
565      * should immediately end.
566      */
567     @Test
568     public void testStretchOnBitmapCanvas() throws Throwable {
569         EdgeEffect edgeEffect = createEdgeEffectWithPull();
570         Bitmap bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888);
571         Canvas canvas = new Canvas(bitmap);
572         edgeEffect.draw(canvas);
573         assertTrue(edgeEffect.isFinished());
574         assertEquals(0f, edgeEffect.getDistance(), 0f);
575     }
576 
577     @Test
578     public void testEdgeEffectWithAnimationsDisabled() {
579         float previousDuration = ValueAnimator.getDurationScale();
580         ValueAnimator.setDurationScale(0);
581         try {
582             EdgeEffect edgeEffect = createEdgeEffectWithPull();
583             RenderNode renderNode = new RenderNode("");
584             renderNode.setPosition(0, 0, WIDTH, HEIGHT);
585 
586             RecordingCanvas recordingCanvas = renderNode.beginRecording();
587             Paint paint = new Paint();
588             paint.setColor(Color.WHITE);
589             recordingCanvas.drawRect(0f, 0f, WIDTH, HEIGHT / 2f, paint);
590             paint.setColor(Color.BLACK);
591             recordingCanvas.drawRect(0, HEIGHT / 2f, WIDTH, HEIGHT, paint);
592             edgeEffect.draw(recordingCanvas);
593 
594             renderNode.endRecording();
595 
596             assertTrue(edgeEffect.isFinished());
597             assertEquals(0f, edgeEffect.getDistance(), 0f);
598 
599             Rect innerRect = new Rect(0, 0, WIDTH, HEIGHT / 2);
600             Rect outerRect = new Rect(0, HEIGHT / 2, WIDTH, HEIGHT);
601             createTest()
602                     .addCanvasClientWithoutUsingPicture((canvas, width, height) -> {
603                         canvas.drawRenderNode(renderNode);
604                     }, true)
605                     .runWithVerifier(
606                             new RegionVerifier().addVerifier(
607                                     innerRect,
608                                     new ColorVerifier(Color.WHITE)
609                             ).addVerifier(
610                                     outerRect,
611                                     new ColorVerifier(Color.BLACK)
612                             ));
613         } finally {
614             ValueAnimator.setDurationScale(previousDuration);
615         }
616     }
617 
618     @Test
testEdgeEffectAnimationDisabledMidAnimation()619     public void testEdgeEffectAnimationDisabledMidAnimation() {
620         float previousDuration = ValueAnimator.getDurationScale();
621         try {
622             EdgeEffect edgeEffect = new EdgeEffect(getContext());
623             edgeEffect.setSize(WIDTH, HEIGHT);
624             edgeEffect.onPullDistance(0.5f, 0.5f);
625             edgeEffect.onAbsorb(100);
626 
627             // Explicitly disable animations post stretch
628             ValueAnimator.setDurationScale(0);
629 
630             RenderNode renderNode = new RenderNode("");
631             renderNode.setPosition(0, 0, WIDTH, HEIGHT);
632 
633             RecordingCanvas recordingCanvas = renderNode.beginRecording();
634             Paint paint = new Paint();
635             paint.setColor(Color.WHITE);
636             recordingCanvas.drawRect(0f, 0f, WIDTH, HEIGHT / 2f, paint);
637             paint.setColor(Color.BLACK);
638             recordingCanvas.drawRect(0, HEIGHT / 2f, WIDTH, HEIGHT, paint);
639             edgeEffect.draw(recordingCanvas);
640 
641             renderNode.endRecording();
642 
643             assertTrue(edgeEffect.isFinished());
644             assertEquals(0f, edgeEffect.getDistance(), 0f);
645 
646             Rect innerRect = new Rect(0, 0, WIDTH, HEIGHT / 2);
647             Rect outerRect = new Rect(0, HEIGHT / 2, WIDTH, HEIGHT);
648             createTest()
649                     .addCanvasClientWithoutUsingPicture((canvas, width, height) -> {
650                         canvas.drawRenderNode(renderNode);
651                     }, true)
652                     .runWithVerifier(
653                             new RegionVerifier().addVerifier(
654                                     innerRect,
655                                     new ColorVerifier(Color.WHITE)
656                             ).addVerifier(
657                                     outerRect,
658                                     new ColorVerifier(Color.BLACK)
659                             ));
660         } finally {
661             ValueAnimator.setDurationScale(previousDuration);
662         }
663     }
664 
665     @Test
testOnAborbAfterStretch()666     public void testOnAborbAfterStretch() throws Throwable {
667         EdgeEffect edgeEffect = new EdgeEffect(getContext());
668         edgeEffect.setSize(100, 100);
669         float distance = edgeEffect.onPullDistance(0.5f, 0.5f);
670         edgeEffect.onAbsorb(100);
671         assertEquals(distance, edgeEffect.getDistance(), 0.01f);
672     }
673 
createEdgeEffectWithPull()674     private EdgeEffect createEdgeEffectWithPull() {
675         EdgeEffect edgeEffect = new EdgeEffect(getContext());
676         edgeEffect.setSize(100, 100);
677         edgeEffect.onPullDistance(0.25f, 0.5f);
678         return edgeEffect;
679     }
680 
681     /**
682      * This sleeps until the {@link AnimationUtils#currentAnimationTimeMillis()} changes
683      * by at least <code>durationMillis</code> milliseconds. This is useful for EdgeEffect because
684      * it uses that mechanism to determine the animation duration.
685      *
686      * Must be in a {@link MockVsyncHelper#runOnVsyncThread(MockVsyncHelper.CallableVoid)}
687      *
688      * @param durationMillis The time to sleep in milliseconds.
689      */
sleepAnimationTime(long durationMillis)690     private void sleepAnimationTime(long durationMillis) {
691         AnimationUtils.lockAnimationClock(
692                 AnimationUtils.currentAnimationTimeMillis() + durationMillis);
693     }
694 
695     private interface StretchVerifier {
696         void verify(float oldStretch, float newStretch);
697     }
698 
699     // validates changes to the stretch over the course of an animation
verifyStretch( long timeBetweenFrames, EdgeEffectInitializer initializer, StretchVerifier stretchVerifier )700     private void verifyStretch(
701             long timeBetweenFrames,
702             EdgeEffectInitializer initializer,
703             StretchVerifier stretchVerifier
704     ) {
705         MockVsyncHelper.runOnVsyncThread(() -> {
706             EdgeEffect edgeEffect = new EdgeEffect(getContext());
707             edgeEffect.setSize(WIDTH, HEIGHT);
708             initializer.initialize(edgeEffect);
709             RenderNode renderNode1 = drawEdgeEffect(edgeEffect, 0, 0);
710             float oldStretch = getStretchDownPixelCount(renderNode1);
711             for (int i = 0; i < 3; i++) {
712                 sleepAnimationTime(timeBetweenFrames);
713                 RenderNode renderNode2 = drawEdgeEffect(edgeEffect, 0, 0);
714                 float newStretch = getStretchDownPixelCount(renderNode2);
715                 stretchVerifier.verify(oldStretch, newStretch);
716                 oldStretch = newStretch;
717             }
718         });
719     }
720 
721     @Test
testOnAbsorb()722     public void testOnAbsorb() {
723         verifyStretch(
724                 8, // The spring will bounce back very quickly
725                 edgeEffect -> edgeEffect.onAbsorb(300),
726                 (oldStretch, newStretch) -> {
727                     assertTrue("Stretch should grow",
728                             oldStretch < newStretch);
729                 }
730         );
731     }
732 
733     @Test
testOnRelease()734     public void testOnRelease() {
735         verifyStretch(
736                 16,
737                 edgeEffect -> {
738                     edgeEffect.onPull(1);
739                     edgeEffect.onRelease();
740                 }, (oldStretch, newStretch) ->
741                         assertTrue("Stretch should decrease", oldStretch > newStretch)
742         );
743     }
744 }
745