Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2019 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.android.systemui.bubbles.animation; |
| 18 | |
| 19 | import static org.junit.Assert.assertEquals; |
| 20 | import static org.junit.Assert.assertFalse; |
| 21 | import static org.junit.Assert.assertTrue; |
| 22 | import static org.mockito.ArgumentMatchers.any; |
| 23 | import static org.mockito.ArgumentMatchers.anyBoolean; |
| 24 | import static org.mockito.ArgumentMatchers.anyFloat; |
| 25 | import static org.mockito.ArgumentMatchers.anyInt; |
| 26 | import static org.mockito.ArgumentMatchers.eq; |
| 27 | import static org.mockito.Mockito.inOrder; |
Joshua Tsuji | 1575e6b | 2019-01-30 13:43:28 -0500 | [diff] [blame] | 28 | import static org.mockito.Mockito.never; |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 29 | |
| 30 | import android.os.SystemClock; |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 31 | import android.testing.AndroidTestingRunner; |
| 32 | import android.view.View; |
| 33 | import android.widget.FrameLayout; |
| 34 | |
| 35 | import androidx.dynamicanimation.animation.DynamicAnimation; |
| 36 | import androidx.dynamicanimation.animation.SpringForce; |
Brett Chabot | 84151d9 | 2019-02-27 15:37:59 -0800 | [diff] [blame] | 37 | import androidx.test.filters.SmallTest; |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 38 | |
| 39 | import com.google.android.collect.Sets; |
| 40 | |
| 41 | import org.junit.Before; |
| 42 | import org.junit.Test; |
| 43 | import org.junit.runner.RunWith; |
| 44 | import org.mockito.InOrder; |
| 45 | import org.mockito.Mockito; |
| 46 | import org.mockito.Spy; |
| 47 | |
| 48 | import java.util.HashMap; |
| 49 | import java.util.HashSet; |
| 50 | import java.util.Set; |
| 51 | import java.util.concurrent.CountDownLatch; |
| 52 | import java.util.concurrent.TimeUnit; |
| 53 | |
| 54 | @SmallTest |
| 55 | @RunWith(AndroidTestingRunner.class) |
| 56 | /** Tests the PhysicsAnimationLayout itself, with a basic test animation controller. */ |
| 57 | public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { |
| 58 | static final float TEST_TRANSLATION_X_OFFSET = 15f; |
| 59 | |
| 60 | @Spy |
| 61 | private TestableAnimationController mTestableController = new TestableAnimationController(); |
| 62 | |
| 63 | @Before |
| 64 | public void setUp() throws Exception { |
| 65 | super.setUp(); |
| 66 | |
| 67 | // By default, use translation animations, chain the X animations with the default |
| 68 | // offset, and don't actually remove views immediately (since most implementations will wait |
| 69 | // to animate child views out before actually removing them). |
| 70 | mTestableController.setAnimatedProperties(Sets.newHashSet( |
| 71 | DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)); |
| 72 | mTestableController.setChainedProperties(Sets.newHashSet(DynamicAnimation.TRANSLATION_X)); |
| 73 | mTestableController.setOffsetForProperty( |
| 74 | DynamicAnimation.TRANSLATION_X, TEST_TRANSLATION_X_OFFSET); |
| 75 | mTestableController.setRemoveImmediately(false); |
| 76 | } |
| 77 | |
| 78 | @Test |
Joshua Tsuji | a08b6d3 | 2019-01-29 16:15:52 -0500 | [diff] [blame] | 79 | public void testRenderVisibility() throws InterruptedException { |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 80 | mLayout.setController(mTestableController); |
| 81 | addOneMoreThanRenderLimitBubbles(); |
| 82 | |
| 83 | // The last child should be GONE, the rest VISIBLE. |
| 84 | for (int i = 0; i < mMaxRenderedBubbles + 1; i++) { |
| 85 | assertEquals(i == mMaxRenderedBubbles ? View.GONE : View.VISIBLE, |
| 86 | mLayout.getChildAt(i).getVisibility()); |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | @Test |
Joshua Tsuji | a08b6d3 | 2019-01-29 16:15:52 -0500 | [diff] [blame] | 91 | public void testHierarchyChanges() throws InterruptedException { |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 92 | mLayout.setController(mTestableController); |
| 93 | addOneMoreThanRenderLimitBubbles(); |
| 94 | |
| 95 | // Make sure the controller was notified of all the views we added. |
| 96 | for (View mView : mViews) { |
| 97 | Mockito.verify(mTestableController).onChildAdded(mView, 0); |
| 98 | } |
| 99 | |
| 100 | // Remove some views and ensure the controller was notified, with the proper indices. |
| 101 | mTestableController.setRemoveImmediately(true); |
| 102 | mLayout.removeView(mViews.get(1)); |
| 103 | mLayout.removeView(mViews.get(2)); |
Joshua Tsuji | 1575e6b | 2019-01-30 13:43:28 -0500 | [diff] [blame] | 104 | Mockito.verify(mTestableController).onChildRemoved( |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 105 | eq(mViews.get(1)), eq(1), any()); |
Joshua Tsuji | 1575e6b | 2019-01-30 13:43:28 -0500 | [diff] [blame] | 106 | Mockito.verify(mTestableController).onChildRemoved( |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 107 | eq(mViews.get(2)), eq(1), any()); |
| 108 | |
| 109 | // Make sure we still get view added notifications after doing some removals. |
| 110 | final View newBubble = new FrameLayout(mContext); |
| 111 | mLayout.addView(newBubble, 0); |
| 112 | Mockito.verify(mTestableController).onChildAdded(newBubble, 0); |
| 113 | } |
| 114 | |
| 115 | @Test |
| 116 | public void testUpdateValueNotChained() throws InterruptedException { |
| 117 | mLayout.setController(mTestableController); |
| 118 | addOneMoreThanRenderLimitBubbles(); |
| 119 | |
| 120 | // Don't chain any values. |
| 121 | mTestableController.setChainedProperties(Sets.newHashSet()); |
| 122 | |
| 123 | // Child views should not be translated. |
| 124 | assertEquals(0, mLayout.getChildAt(0).getTranslationX(), .1f); |
| 125 | assertEquals(0, mLayout.getChildAt(1).getTranslationX(), .1f); |
| 126 | |
| 127 | // Animate the first child's translation X. |
| 128 | final CountDownLatch animLatch = new CountDownLatch(1); |
| 129 | mLayout.animateValueForChildAtIndex( |
| 130 | DynamicAnimation.TRANSLATION_X, |
| 131 | 0, |
| 132 | 100, |
| 133 | animLatch::countDown); |
| 134 | animLatch.await(1, TimeUnit.SECONDS); |
| 135 | |
| 136 | // Ensure that the first view has been translated, but not the second one. |
| 137 | assertEquals(100, mLayout.getChildAt(0).getTranslationX(), .1f); |
| 138 | assertEquals(0, mLayout.getChildAt(1).getTranslationX(), .1f); |
| 139 | } |
| 140 | |
| 141 | @Test |
| 142 | public void testUpdateValueXChained() throws InterruptedException { |
| 143 | mLayout.setController(mTestableController); |
| 144 | addOneMoreThanRenderLimitBubbles(); |
| 145 | testChainedTranslationAnimations(); |
| 146 | } |
| 147 | |
| 148 | @Test |
| 149 | public void testSetEndListeners() throws InterruptedException { |
| 150 | mLayout.setController(mTestableController); |
| 151 | addOneMoreThanRenderLimitBubbles(); |
| 152 | mTestableController.setChainedProperties(Sets.newHashSet()); |
| 153 | |
| 154 | final CountDownLatch xLatch = new CountDownLatch(1); |
| 155 | OneTimeEndListener xEndListener = Mockito.spy(new OneTimeEndListener() { |
| 156 | @Override |
| 157 | public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value, |
| 158 | float velocity) { |
| 159 | super.onAnimationEnd(animation, canceled, value, velocity); |
| 160 | xLatch.countDown(); |
| 161 | } |
| 162 | }); |
| 163 | |
| 164 | final CountDownLatch yLatch = new CountDownLatch(1); |
| 165 | final OneTimeEndListener yEndListener = Mockito.spy(new OneTimeEndListener() { |
| 166 | @Override |
| 167 | public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value, |
| 168 | float velocity) { |
| 169 | super.onAnimationEnd(animation, canceled, value, velocity); |
| 170 | yLatch.countDown(); |
| 171 | } |
| 172 | }); |
| 173 | |
| 174 | // Set end listeners for both x and y. |
| 175 | mLayout.setEndListenerForProperty(xEndListener, DynamicAnimation.TRANSLATION_X); |
| 176 | mLayout.setEndListenerForProperty(yEndListener, DynamicAnimation.TRANSLATION_Y); |
| 177 | |
| 178 | // Animate x, and wait for it to finish. |
| 179 | mLayout.animateValueForChildAtIndex( |
| 180 | DynamicAnimation.TRANSLATION_X, |
| 181 | 0, |
| 182 | 100); |
| 183 | xLatch.await(); |
| 184 | yLatch.await(1, TimeUnit.SECONDS); |
| 185 | |
| 186 | // Make sure the x end listener was called only one time, and the y listener was never |
| 187 | // called since we didn't animate y. Wait 1 second after the original animation end trigger |
| 188 | // to make sure it doesn't get called again. |
| 189 | Mockito.verify(xEndListener, Mockito.after(1000).times(1)) |
| 190 | .onAnimationEnd( |
| 191 | any(), |
| 192 | eq(false), |
| 193 | eq(100f), |
| 194 | anyFloat()); |
| 195 | Mockito.verify(yEndListener, Mockito.after(1000).never()) |
| 196 | .onAnimationEnd(any(), anyBoolean(), anyFloat(), anyFloat()); |
| 197 | } |
| 198 | |
| 199 | @Test |
| 200 | public void testRemoveEndListeners() throws InterruptedException { |
| 201 | mLayout.setController(mTestableController); |
| 202 | addOneMoreThanRenderLimitBubbles(); |
| 203 | mTestableController.setChainedProperties(Sets.newHashSet()); |
| 204 | |
| 205 | final CountDownLatch xLatch = new CountDownLatch(1); |
| 206 | OneTimeEndListener xEndListener = Mockito.spy(new OneTimeEndListener() { |
| 207 | @Override |
| 208 | public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value, |
| 209 | float velocity) { |
| 210 | super.onAnimationEnd(animation, canceled, value, velocity); |
| 211 | xLatch.countDown(); |
| 212 | } |
| 213 | }); |
| 214 | |
| 215 | // Set the end listener. |
| 216 | mLayout.setEndListenerForProperty(xEndListener, DynamicAnimation.TRANSLATION_X); |
| 217 | |
| 218 | // Animate x, and wait for it to finish. |
| 219 | mLayout.animateValueForChildAtIndex( |
| 220 | DynamicAnimation.TRANSLATION_X, |
| 221 | 0, |
| 222 | 100); |
| 223 | xLatch.await(); |
| 224 | |
| 225 | InOrder endListenerCalls = inOrder(xEndListener); |
| 226 | endListenerCalls.verify(xEndListener, Mockito.times(1)) |
| 227 | .onAnimationEnd( |
| 228 | any(), |
| 229 | eq(false), |
| 230 | eq(100f), |
| 231 | anyFloat()); |
| 232 | |
| 233 | // Animate X again, remove the end listener. |
| 234 | mLayout.animateValueForChildAtIndex( |
| 235 | DynamicAnimation.TRANSLATION_X, |
| 236 | 0, |
| 237 | 1000); |
| 238 | mLayout.removeEndListenerForProperty(DynamicAnimation.TRANSLATION_X); |
| 239 | xLatch.await(1, TimeUnit.SECONDS); |
| 240 | |
| 241 | // Make sure the end listener was not called. |
| 242 | endListenerCalls.verifyNoMoreInteractions(); |
| 243 | } |
| 244 | |
| 245 | @Test |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 246 | public void testSetController() throws InterruptedException { |
| 247 | // Add the bubbles, then set the controller, to make sure that a controller added to an |
| 248 | // already-initialized view works correctly. |
| 249 | addOneMoreThanRenderLimitBubbles(); |
| 250 | mLayout.setController(mTestableController); |
| 251 | testChainedTranslationAnimations(); |
| 252 | |
| 253 | TestableAnimationController secondController = |
| 254 | Mockito.spy(new TestableAnimationController()); |
| 255 | secondController.setAnimatedProperties(Sets.newHashSet( |
| 256 | DynamicAnimation.SCALE_X, DynamicAnimation.SCALE_Y)); |
| 257 | secondController.setChainedProperties(Sets.newHashSet( |
| 258 | DynamicAnimation.SCALE_X)); |
| 259 | secondController.setOffsetForProperty( |
| 260 | DynamicAnimation.SCALE_X, 10f); |
| 261 | secondController.setRemoveImmediately(true); |
| 262 | |
| 263 | mLayout.setController(secondController); |
| 264 | mLayout.animateValueForChildAtIndex( |
| 265 | DynamicAnimation.SCALE_X, |
| 266 | 0, |
| 267 | 1.5f); |
| 268 | |
| 269 | waitForPropertyAnimations(DynamicAnimation.SCALE_X); |
| 270 | |
| 271 | // Make sure we never asked the original controller about any SCALE animations, that would |
| 272 | // mean the controller wasn't switched over properly. |
| 273 | Mockito.verify(mTestableController, Mockito.never()) |
| 274 | .getNextAnimationInChain(eq(DynamicAnimation.SCALE_X), anyInt()); |
| 275 | Mockito.verify(mTestableController, Mockito.never()) |
| 276 | .getOffsetForChainedPropertyAnimation(eq(DynamicAnimation.SCALE_X)); |
| 277 | |
| 278 | // Make sure we asked the new controller about its animated properties, and configuration |
| 279 | // options. |
| 280 | Mockito.verify(secondController, Mockito.atLeastOnce()) |
| 281 | .getAnimatedProperties(); |
| 282 | Mockito.verify(secondController, Mockito.atLeastOnce()) |
| 283 | .getNextAnimationInChain(eq(DynamicAnimation.SCALE_X), anyInt()); |
| 284 | Mockito.verify(secondController, Mockito.atLeastOnce()) |
| 285 | .getOffsetForChainedPropertyAnimation(eq(DynamicAnimation.SCALE_X)); |
| 286 | |
| 287 | mLayout.setController(mTestableController); |
| 288 | mLayout.animateValueForChildAtIndex( |
| 289 | DynamicAnimation.TRANSLATION_X, |
| 290 | 0, |
| 291 | 100f); |
| 292 | |
| 293 | waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X); |
| 294 | |
| 295 | // Make sure we never asked the second controller about the TRANSLATION_X animation. |
| 296 | Mockito.verify(secondController, Mockito.never()) |
| 297 | .getNextAnimationInChain(eq(DynamicAnimation.TRANSLATION_X), anyInt()); |
| 298 | Mockito.verify(secondController, Mockito.never()) |
| 299 | .getOffsetForChainedPropertyAnimation(eq(DynamicAnimation.TRANSLATION_X)); |
| 300 | |
| 301 | } |
| 302 | |
| 303 | @Test |
| 304 | public void testArePropertiesAnimating() throws InterruptedException { |
| 305 | mLayout.setController(mTestableController); |
| 306 | addOneMoreThanRenderLimitBubbles(); |
| 307 | |
| 308 | assertFalse(mLayout.arePropertiesAnimating( |
| 309 | DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)); |
| 310 | |
| 311 | mLayout.animateValueForChildAtIndex( |
| 312 | DynamicAnimation.TRANSLATION_X, |
| 313 | 0, |
| 314 | 100); |
| 315 | |
| 316 | // Wait for the animations to get underway. |
| 317 | SystemClock.sleep(50); |
| 318 | |
| 319 | assertTrue(mLayout.arePropertiesAnimating(DynamicAnimation.TRANSLATION_X)); |
| 320 | assertFalse(mLayout.arePropertiesAnimating(DynamicAnimation.TRANSLATION_Y)); |
| 321 | |
| 322 | waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X); |
| 323 | |
| 324 | assertFalse(mLayout.arePropertiesAnimating( |
| 325 | DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)); |
| 326 | } |
| 327 | |
| 328 | @Test |
| 329 | public void testCancelAllAnimations() throws InterruptedException { |
| 330 | mLayout.setController(mTestableController); |
| 331 | addOneMoreThanRenderLimitBubbles(); |
| 332 | |
| 333 | mLayout.animateValueForChildAtIndex( |
| 334 | DynamicAnimation.TRANSLATION_X, |
| 335 | 0, |
| 336 | 1000); |
| 337 | mLayout.animateValueForChildAtIndex( |
| 338 | DynamicAnimation.TRANSLATION_Y, |
| 339 | 0, |
| 340 | 1000); |
| 341 | |
| 342 | mLayout.cancelAllAnimations(); |
| 343 | |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 344 | // Animations should be somewhere before their end point. |
| 345 | assertTrue(mViews.get(0).getTranslationX() < 1000); |
| 346 | assertTrue(mViews.get(0).getTranslationY() < 1000); |
| 347 | } |
| 348 | |
Joshua Tsuji | 1575e6b | 2019-01-30 13:43:28 -0500 | [diff] [blame] | 349 | @Test |
| 350 | public void testSetChildVisibility() throws InterruptedException { |
| 351 | mLayout.setController(mTestableController); |
| 352 | addOneMoreThanRenderLimitBubbles(); |
| 353 | |
| 354 | // The last view should have been set to GONE by the controller, since we added one more |
| 355 | // than the limit and it got pushed off. None of the first children should have been set |
| 356 | // VISIBLE, since they would have been animated in by onChildAdded. |
| 357 | Mockito.verify(mTestableController).setChildVisibility( |
| 358 | mViews.get(mViews.size() - 1), 5, View.GONE); |
| 359 | Mockito.verify(mTestableController, never()).setChildVisibility( |
| 360 | any(View.class), anyInt(), eq(View.VISIBLE)); |
| 361 | |
| 362 | // Remove the first view, which should cause the last view to become visible again. |
| 363 | mLayout.removeView(mViews.get(0)); |
| 364 | Mockito.verify(mTestableController).setChildVisibility( |
| 365 | mViews.get(mViews.size() - 1), 4, View.VISIBLE); |
| 366 | } |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 367 | |
| 368 | /** Standard test of chained translation animations. */ |
| 369 | private void testChainedTranslationAnimations() throws InterruptedException { |
| 370 | assertEquals(0, mLayout.getChildAt(0).getTranslationX(), .1f); |
| 371 | assertEquals(0, mLayout.getChildAt(1).getTranslationX(), .1f); |
| 372 | |
| 373 | mLayout.animateValueForChildAtIndex( |
| 374 | DynamicAnimation.TRANSLATION_X, |
| 375 | 0, |
| 376 | 100); |
| 377 | |
| 378 | waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X); |
| 379 | |
| 380 | // Since we enabled chaining, animating the first view to 100 should animate the second to |
| 381 | // 115 (since we set the offset to 15) and the third to 130, etc. Despite the sixth bubble |
| 382 | // not being visible, or animated, make sure that it has the appropriate chained |
| 383 | // translation. |
| 384 | for (int i = 0; i < mMaxRenderedBubbles + 1; i++) { |
| 385 | assertEquals( |
| 386 | 100 + i * TEST_TRANSLATION_X_OFFSET, |
| 387 | mLayout.getChildAt(i).getTranslationX(), .1f); |
| 388 | } |
| 389 | |
| 390 | // Ensure that the Y translations were unaffected. |
| 391 | assertEquals(0, mLayout.getChildAt(0).getTranslationY(), .1f); |
| 392 | assertEquals(0, mLayout.getChildAt(1).getTranslationY(), .1f); |
| 393 | |
| 394 | // Animate the first child's Y translation. |
| 395 | mLayout.animateValueForChildAtIndex( |
| 396 | DynamicAnimation.TRANSLATION_Y, |
| 397 | 0, |
| 398 | 100); |
| 399 | |
| 400 | waitForPropertyAnimations(DynamicAnimation.TRANSLATION_Y); |
| 401 | |
| 402 | // Ensure that only the first view's Y translation chained, since we only chained X |
| 403 | // translations. |
| 404 | assertEquals(100, mLayout.getChildAt(0).getTranslationY(), .1f); |
| 405 | assertEquals(0, mLayout.getChildAt(1).getTranslationY(), .1f); |
| 406 | } |
| 407 | |
| 408 | /** |
| 409 | * Animation controller with configuration methods whose return values can be set by individual |
| 410 | * tests. |
| 411 | */ |
| 412 | private class TestableAnimationController |
| 413 | extends PhysicsAnimationLayout.PhysicsAnimationController { |
| 414 | private Set<DynamicAnimation.ViewProperty> mAnimatedProperties = new HashSet<>(); |
| 415 | private Set<DynamicAnimation.ViewProperty> mChainedProperties = new HashSet<>(); |
| 416 | private HashMap<DynamicAnimation.ViewProperty, Float> mOffsetForProperty = new HashMap<>(); |
| 417 | private boolean mRemoveImmediately = false; |
| 418 | |
| 419 | void setAnimatedProperties( |
| 420 | Set<DynamicAnimation.ViewProperty> animatedProperties) { |
| 421 | mAnimatedProperties = animatedProperties; |
| 422 | } |
| 423 | |
| 424 | void setChainedProperties( |
| 425 | Set<DynamicAnimation.ViewProperty> chainedProperties) { |
| 426 | mChainedProperties = chainedProperties; |
| 427 | } |
| 428 | |
| 429 | void setOffsetForProperty( |
| 430 | DynamicAnimation.ViewProperty property, float offset) { |
| 431 | mOffsetForProperty.put(property, offset); |
| 432 | } |
| 433 | |
| 434 | public void setRemoveImmediately(boolean removeImmediately) { |
| 435 | mRemoveImmediately = removeImmediately; |
| 436 | } |
| 437 | |
| 438 | @Override |
| 439 | Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { |
| 440 | return mAnimatedProperties; |
| 441 | } |
| 442 | |
| 443 | @Override |
| 444 | int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) { |
| 445 | return mChainedProperties.contains(property) ? index + 1 : NONE; |
| 446 | } |
| 447 | |
| 448 | @Override |
| 449 | float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) { |
| 450 | return mOffsetForProperty.getOrDefault(property, 0f); |
| 451 | } |
| 452 | |
| 453 | @Override |
| 454 | SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) { |
| 455 | return new SpringForce(); |
| 456 | } |
| 457 | |
| 458 | @Override |
| 459 | void onChildAdded(View child, int index) {} |
| 460 | |
| 461 | @Override |
Joshua Tsuji | 1575e6b | 2019-01-30 13:43:28 -0500 | [diff] [blame] | 462 | void onChildRemoved(View child, int index, Runnable finishRemoval) { |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 463 | if (mRemoveImmediately) { |
Joshua Tsuji | 1575e6b | 2019-01-30 13:43:28 -0500 | [diff] [blame] | 464 | finishRemoval.run(); |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 465 | } |
| 466 | } |
Joshua Tsuji | 1575e6b | 2019-01-30 13:43:28 -0500 | [diff] [blame] | 467 | |
| 468 | @Override |
| 469 | protected void setChildVisibility(View child, int index, int visibility) { |
| 470 | super.setChildVisibility(child, index, visibility); |
| 471 | } |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 472 | } |
| 473 | } |