package com.android.systemui.animation

import android.animation.ObjectAnimator
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.test.filters.SmallTest
import com.android.app.animation.Interpolators
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.children
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertNotNull
import junit.framework.Assert.assertNull
import junit.framework.Assert.assertTrue
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class
ViewHierarchyAnimatorTest : SysuiTestCase() {
    companion object {
        private const val TEST_DURATION = 1000L
        private val TEST_INTERPOLATOR = Interpolators.LINEAR
    }

    private lateinit var rootView: ViewGroup

    @Before
    fun setUp() {
        rootView = LinearLayout(mContext)
    }

    @After
    fun tearDown() {
        endAnimation(rootView)
        ViewHierarchyAnimator.stopAnimating(rootView)
    }

    @Test
    fun respectsAnimationParameters() {
        rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)

        // animate()
        var success = ViewHierarchyAnimator.animate(
            rootView, interpolator = TEST_INTERPOLATOR, duration = TEST_DURATION
        )
        rootView.layout(0 /* l */, 0 /* t */, 100 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        var animator = rootView.getTag(R.id.tag_animator) as ObjectAnimator
        assertEquals(animator.interpolator, TEST_INTERPOLATOR)
        assertEquals(animator.duration, TEST_DURATION)

        endAnimation(rootView)
        ViewHierarchyAnimator.stopAnimating(rootView)

        // animateNextUpdate()
        success = ViewHierarchyAnimator.animateNextUpdate(
            rootView, interpolator = TEST_INTERPOLATOR, duration = TEST_DURATION
        )
        rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        animator = rootView.getTag(R.id.tag_animator) as ObjectAnimator
        assertEquals(animator.interpolator, TEST_INTERPOLATOR)
        assertEquals(animator.duration, TEST_DURATION)

        endAnimation(rootView)

        // animateAddition()
        rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
        success = ViewHierarchyAnimator.animateAddition(
            rootView, interpolator = TEST_INTERPOLATOR, duration = TEST_DURATION
        )
        rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        animator = rootView.getTag(R.id.tag_animator) as ObjectAnimator
        assertEquals(animator.interpolator, TEST_INTERPOLATOR)
        assertEquals(animator.duration, TEST_DURATION)

        // animateRemoval()
        setUpRootWithChildren()
        val child = rootView.getChildAt(0)
        success = ViewHierarchyAnimator.animateRemoval(
            child, interpolator = TEST_INTERPOLATOR, duration = TEST_DURATION
        )

        assertTrue(success)
        assertNotNull(child.getTag(R.id.tag_animator))
        animator = child.getTag(R.id.tag_animator) as ObjectAnimator
        assertEquals(animator.interpolator, TEST_INTERPOLATOR)
        assertEquals(animator.duration, TEST_DURATION)
    }

    @Test
    fun animatesFromStartToEnd() {
        rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)

        val success = ViewHierarchyAnimator.animate(rootView)
        // Change all bounds.
        rootView.layout(0 /* l */, 15 /* t */, 70 /* r */, 80 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        // The initial values should be those of the previous layout.
        checkBounds(rootView, l = 10, t = 10, r = 50, b = 50)
        endAnimation(rootView)
        assertNull(rootView.getTag(R.id.tag_animator))
        // The end values should be those of the latest layout.
        checkBounds(rootView, l = 0, t = 15, r = 70, b = 80)
    }

    @Test
    fun animatesSuccessiveLayoutChanges() {
        rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)

        val success = ViewHierarchyAnimator.animate(rootView)
        // Change all bounds.
        rootView.layout(0 /* l */, 15 /* t */, 70 /* r */, 80 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        endAnimation(rootView)
        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 0, t = 15, r = 70, b = 80)

        // Change only top and right.
        rootView.layout(0 /* l */, 20 /* t */, 60 /* r */, 80 /* b */)

        assertNotNull(rootView.getTag(R.id.tag_animator))
        endAnimation(rootView)
        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 0, t = 20, r = 60, b = 80)

        // Change all bounds again.
        rootView.layout(5 /* l */, 25 /* t */, 55 /* r */, 95 /* b */)

        assertNotNull(rootView.getTag(R.id.tag_animator))
        endAnimation(rootView)
        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 5, t = 25, r = 55, b = 95)
    }

    @Test
    fun animatesFromPreviousAnimationProgress() {
        rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)

        val success =
            ViewHierarchyAnimator.animateNextUpdate(rootView, interpolator = TEST_INTERPOLATOR)
        // Change all bounds.
        rootView.layout(0 /* l */, 20 /* t */, 70 /* r */, 80 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        advanceAnimation(rootView, fraction = 0.5f)
        checkBounds(rootView, l = 5, t = 15, r = 60, b = 65)

        // Change all bounds again.
        rootView.layout(25 /* l */, 25 /* t */, 55 /* r */, 60 /* b */)

        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 5, t = 15, r = 60, b = 65)
        endAnimation(rootView)
        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 25, t = 25, r = 55, b = 60)
    }

    @Test
    fun animatesRootAndChildren_withoutExcludedViews() {
        setUpRootWithChildren()

        val success = ViewHierarchyAnimator.animate(rootView)
        // Change all bounds.
        rootView.measure(
            View.MeasureSpec.makeMeasureSpec(190, View.MeasureSpec.EXACTLY),
            View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
        )
        rootView.layout(10 /* l */, 20 /* t */, 200 /* r */, 120 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        assertNotNull(rootView.getChildAt(0).getTag(R.id.tag_animator))
        assertNotNull(rootView.getChildAt(1).getTag(R.id.tag_animator))
        // The initial values should be those of the previous layout.
        checkBounds(rootView, l = 0, t = 0, r = 200, b = 100)
        checkBounds(rootView.getChildAt(0), l = 0, t = 0, r = 100, b = 100)
        checkBounds(rootView.getChildAt(1), l = 100, t = 0, r = 200, b = 100)
        endAnimation(rootView)
        assertNull(rootView.getTag(R.id.tag_animator))
        assertNull(rootView.getChildAt(0).getTag(R.id.tag_animator))
        assertNull(rootView.getChildAt(1).getTag(R.id.tag_animator))
        // The end values should be those of the latest layout.
        checkBounds(rootView, l = 10, t = 20, r = 200, b = 120)
        checkBounds(rootView.getChildAt(0), l = 0, t = 0, r = 95, b = 100)
        checkBounds(rootView.getChildAt(1), l = 95, t = 0, r = 190, b = 100)
    }

    @Test
    fun animatesRootAndChildren_withExcludedViews() {
        setUpRootWithChildren()

        val success = ViewHierarchyAnimator.animate(
            rootView,
            excludedViews = setOf(rootView.getChildAt(0))
        )
        // Change all bounds.
        rootView.measure(
                View.MeasureSpec.makeMeasureSpec(180, View.MeasureSpec.EXACTLY),
                View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
        )
        rootView.layout(10 /* l */, 20 /* t */, 200 /* r */, 120 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        assertNull(rootView.getChildAt(0).getTag(R.id.tag_animator))
        assertNotNull(rootView.getChildAt(1).getTag(R.id.tag_animator))
        // The initial values for the affected views should be those of the previous layout, while
        // the excluded view should be at the final values from the beginning.
        checkBounds(rootView, l = 0, t = 0, r = 200, b = 100)
        checkBounds(rootView.getChildAt(0), l = 0, t = 0, r = 90, b = 100)
        checkBounds(rootView.getChildAt(1), l = 100, t = 0, r = 200, b = 100)
        endAnimation(rootView)
        assertNull(rootView.getTag(R.id.tag_animator))
        assertNull(rootView.getChildAt(0).getTag(R.id.tag_animator))
        assertNull(rootView.getChildAt(1).getTag(R.id.tag_animator))
        // The end values should be those of the latest layout.
        checkBounds(rootView, l = 10, t = 20, r = 200, b = 120)
        checkBounds(rootView.getChildAt(0), l = 0, t = 0, r = 90, b = 100)
        checkBounds(rootView.getChildAt(1), l = 90, t = 0, r = 180, b = 100)
    }

    @Test
    fun animatesRootOnly() {
        setUpRootWithChildren()

        val success = ViewHierarchyAnimator.animate(
                rootView,
                animateChildren = false
        )
        // Change all bounds.
        rootView.measure(
                View.MeasureSpec.makeMeasureSpec(180, View.MeasureSpec.EXACTLY),
                View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
        )
        rootView.layout(10 /* l */, 20 /* t */, 200 /* r */, 120 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        assertNull(rootView.getChildAt(0).getTag(R.id.tag_animator))
        assertNull(rootView.getChildAt(1).getTag(R.id.tag_animator))
        // The initial values for the root view should be those of the previous layout, while the
        // children views should be at the final values from the beginning.
        checkBounds(rootView, l = 0, t = 0, r = 200, b = 100)
        checkBounds(rootView.getChildAt(0), l = 0, t = 0, r = 90, b = 100)
        checkBounds(rootView.getChildAt(1), l = 90, t = 0, r = 180, b = 100)
        endAnimation(rootView)
        assertNull(rootView.getTag(R.id.tag_animator))
        assertNull(rootView.getChildAt(0).getTag(R.id.tag_animator))
        assertNull(rootView.getChildAt(1).getTag(R.id.tag_animator))
        // The end values should be those of the latest layout.
        checkBounds(rootView, l = 10, t = 20, r = 200, b = 120)
        checkBounds(rootView.getChildAt(0), l = 0, t = 0, r = 90, b = 100)
        checkBounds(rootView.getChildAt(1), l = 90, t = 0, r = 180, b = 100)
    }

    @Test
    fun animatesInvisibleViews() {
        rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
        rootView.visibility = View.INVISIBLE

        val success = ViewHierarchyAnimator.animate(rootView)
        // Change all bounds.
        rootView.layout(0 /* l */, 15 /* t */, 70 /* r */, 80 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        // The initial values should be those of the previous layout.
        checkBounds(rootView, l = 10, t = 10, r = 50, b = 50)
        endAnimation(rootView)
        assertNull(rootView.getTag(R.id.tag_animator))
        // The end values should be those of the latest layout.
        checkBounds(rootView, l = 0, t = 15, r = 70, b = 80)
    }

    @Test
    fun animatesAppearingViewsFromStartToEnd() {
        // Starting GONE.
        rootView.visibility = View.GONE
        rootView.layout(0 /* l */, 50 /* t */, 50 /* r */, 100 /* b */)
        var success = ViewHierarchyAnimator.animateAddition(rootView)
        rootView.visibility = View.VISIBLE
        rootView.layout(0 /* l */, 100 /* t */, 100 /* r */, 200 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 50, t = 150, r = 50, b = 150)
        endAnimation(rootView)
        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 0, t = 100, r = 100, b = 200)

        // Starting with nothing.
        rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
        success = ViewHierarchyAnimator.animateAddition(rootView)
        rootView.layout(0 /* l */, 20 /* t */, 50 /* r */, 80 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 25, t = 50, r = 25, b = 50)
        endAnimation(rootView)
        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 0, t = 20, r = 50, b = 80)

        // Starting with 0 width.
        rootView.layout(0 /* l */, 50 /* t */, 0 /* r */, 100 /* b */)
        success = ViewHierarchyAnimator.animateAddition(rootView)
        rootView.layout(0 /* l */, 50 /* t */, 50 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 25, t = 75, r = 25, b = 75)
        endAnimation(rootView)
        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 0, t = 50, r = 50, b = 100)

        // Starting with 0 height.
        rootView.layout(0 /* l */, 50 /* t */, 50 /* r */, 50 /* b */)
        success = ViewHierarchyAnimator.animateAddition(rootView)
        rootView.layout(0 /* l */, 50 /* t */, 50 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 25, t = 75, r = 25, b = 75)
        endAnimation(rootView)
        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 0, t = 50, r = 50, b = 100)
    }

    @Test
    fun animatesAppearingViewsRespectingOrigin() {
        // CENTER.
        rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
        var success = ViewHierarchyAnimator.animateAddition(
            rootView,
            origin = ViewHierarchyAnimator.Hotspot.CENTER
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 75, t = 75, r = 75, b = 75)
        endAnimation(rootView)

        // LEFT.
        rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
        success = ViewHierarchyAnimator.animateAddition(
            rootView,
            origin = ViewHierarchyAnimator.Hotspot.LEFT
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 50, t = 50, r = 50, b = 100)
        endAnimation(rootView)

        // TOP_LEFT.
        rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
        success = ViewHierarchyAnimator.animateAddition(
            rootView,
            origin = ViewHierarchyAnimator.Hotspot.TOP_LEFT
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 50, t = 50, r = 50, b = 50)
        endAnimation(rootView)

        // TOP.
        rootView.layout(150 /* l */, 0 /* t */, 150 /* r */, 0 /* b */)
        success = ViewHierarchyAnimator.animateAddition(
            rootView,
            origin = ViewHierarchyAnimator.Hotspot.TOP
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 50, t = 50, r = 100, b = 50)
        endAnimation(rootView)

        // TOP_RIGHT.
        rootView.layout(150 /* l */, 0 /* t */, 150 /* r */, 0 /* b */)
        success = ViewHierarchyAnimator.animateAddition(
            rootView,
            origin = ViewHierarchyAnimator.Hotspot.TOP_RIGHT
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 100, t = 50, r = 100, b = 50)
        endAnimation(rootView)

        // RIGHT.
        rootView.layout(150 /* l */, 150 /* t */, 150 /* r */, 150 /* b */)
        success = ViewHierarchyAnimator.animateAddition(
            rootView,
            origin = ViewHierarchyAnimator.Hotspot.RIGHT
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 100, t = 50, r = 100, b = 100)
        endAnimation(rootView)

        // BOTTOM_RIGHT.
        rootView.layout(150 /* l */, 150 /* t */, 150 /* r */, 150 /* b */)
        success = ViewHierarchyAnimator.animateAddition(
            rootView,
            origin = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 100, t = 100, r = 100, b = 100)
        endAnimation(rootView)

        // BOTTOM.
        rootView.layout(0 /* l */, 150 /* t */, 0 /* r */, 150 /* b */)
        success = ViewHierarchyAnimator.animateAddition(
            rootView,
            origin = ViewHierarchyAnimator.Hotspot.BOTTOM
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 50, t = 100, r = 100, b = 100)
        endAnimation(rootView)

        // BOTTOM_LEFT.
        rootView.layout(0 /* l */, 150 /* t */, 0 /* r */, 150 /* b */)
        success = ViewHierarchyAnimator.animateAddition(
            rootView,
            origin = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 50, t = 100, r = 50, b = 100)
        endAnimation(rootView)
    }

    @Test
    fun animatesAppearingViewsRespectingMargins() {
        // CENTER.
        rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
        var success = ViewHierarchyAnimator.animateAddition(
            rootView,
            origin = ViewHierarchyAnimator.Hotspot.CENTER,
            includeMargins = true
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 75, t = 75, r = 75, b = 75)
        endAnimation(rootView)

        // LEFT.
        rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
        success = ViewHierarchyAnimator.animateAddition(
            rootView, origin = ViewHierarchyAnimator.Hotspot.LEFT,
            includeMargins = true
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 0, t = 50, r = 0, b = 100)
        endAnimation(rootView)

        // TOP_LEFT.
        rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
        success = ViewHierarchyAnimator.animateAddition(
            rootView,
            origin = ViewHierarchyAnimator.Hotspot.TOP_LEFT,
            includeMargins = true
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 0, t = 0, r = 0, b = 0)
        endAnimation(rootView)

        // TOP.
        rootView.layout(150 /* l */, 0 /* t */, 150 /* r */, 0 /* b */)
        success = ViewHierarchyAnimator.animateAddition(
            rootView, origin = ViewHierarchyAnimator.Hotspot.TOP,
            includeMargins = true
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 50, t = 0, r = 100, b = 0)
        endAnimation(rootView)

        // TOP_RIGHT.
        rootView.layout(150 /* l */, 0 /* t */, 150 /* r */, 0 /* b */)
        success = ViewHierarchyAnimator.animateAddition(
            rootView,
            origin = ViewHierarchyAnimator.Hotspot.TOP_RIGHT,
            includeMargins = true
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 150, t = 0, r = 150, b = 0)
        endAnimation(rootView)

        // RIGHT.
        rootView.layout(150 /* l */, 150 /* t */, 150 /* r */, 150 /* b */)
        success = ViewHierarchyAnimator.animateAddition(
            rootView,
            origin = ViewHierarchyAnimator.Hotspot.RIGHT,
            includeMargins = true
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 150, t = 50, r = 150, b = 100)
        endAnimation(rootView)

        // BOTTOM_RIGHT.
        rootView.layout(150 /* l */, 150 /* t */, 150 /* r */, 150 /* b */)
        success = ViewHierarchyAnimator.animateAddition(
            rootView,
            origin = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT,
            includeMargins = true
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 150, t = 150, r = 150, b = 150)
        endAnimation(rootView)

        // BOTTOM.
        rootView.layout(0 /* l */, 150 /* t */, 0 /* r */, 150 /* b */)
        success = ViewHierarchyAnimator.animateAddition(
            rootView,
            origin = ViewHierarchyAnimator.Hotspot.BOTTOM,
            includeMargins = true
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 50, t = 150, r = 100, b = 150)
        endAnimation(rootView)

        // BOTTOM_LEFT.
        rootView.layout(0 /* l */, 150 /* t */, 0 /* r */, 150 /* b */)
        success = ViewHierarchyAnimator.animateAddition(
            rootView,
            origin = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT,
            includeMargins = true
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 0, t = 150, r = 0, b = 150)
        endAnimation(rootView)
    }

    @Test
    fun animatesAppearingViewsFadeIn_alphaStartsAtZero_endsAtOne() {
        rootView.alpha = 0f
        ViewHierarchyAnimator.animateAddition(rootView, includeFadeIn = true)
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        advanceFadeInAnimation(rootView, fraction = 1f)
        endFadeInAnimation(rootView)

        assertNull(rootView.getTag(R.id.tag_alpha_animator))
        assertEquals(1f, rootView.alpha)
    }

    @Test
    fun animatesAppearingViewsFadeIn_alphaStartsAboveZero_endsAtOne() {
        rootView.alpha = 0.2f
        ViewHierarchyAnimator.animateAddition(rootView, includeFadeIn = true)
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        advanceFadeInAnimation(rootView, fraction = 1f)
        endFadeInAnimation(rootView)

        assertNull(rootView.getTag(R.id.tag_alpha_animator))
        assertEquals(1f, rootView.alpha)
    }

    @Test
    fun animatesAppearingViewsFadeIn_alphaStartsAsZero_alphaUpdatedMidAnimation() {
        rootView.alpha = 0f
        ViewHierarchyAnimator.animateAddition(
            rootView,
            includeFadeIn = true,
            fadeInInterpolator = Interpolators.LINEAR
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        advanceFadeInAnimation(rootView, fraction = 0.42f)

        assertEquals(0.42f, rootView.alpha)
    }

    @Test
    fun animatesAppearingViewsFadeIn_alphaStartsAboveZero_alphaUpdatedMidAnimation() {
        rootView.alpha = 0.6f
        ViewHierarchyAnimator.animateAddition(
            rootView,
            includeFadeIn = true,
            fadeInInterpolator = Interpolators.LINEAR
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        advanceFadeInAnimation(rootView, fraction = 0.5f)

        assertEquals(0.8f, rootView.alpha)
    }

    @Test
    fun animatesAppearingViewsFadeIn_childViewAlphasAlsoAnimated() {
        rootView.alpha = 0f
        val firstChild = View(context)
        firstChild.alpha = 0f
        val secondChild = View(context)
        secondChild.alpha = 0f
        rootView.addView(firstChild)
        rootView.addView(secondChild)

        ViewHierarchyAnimator.animateAddition(
            rootView,
            includeFadeIn = true,
            fadeInInterpolator = Interpolators.LINEAR
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        advanceFadeInAnimation(rootView, fraction = 0.5f)

        assertEquals(0.5f, rootView.alpha)
        assertEquals(0.5f, firstChild.alpha)
        assertEquals(0.5f, secondChild.alpha)
    }

    @Test
    fun animatesAppearingViewsFadeIn_animatesFromPreviousAnimationProgress() {
        rootView.alpha = 0f
        ViewHierarchyAnimator.animateAddition(
            rootView,
            includeFadeIn = true,
            fadeInInterpolator = Interpolators.LINEAR
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        advanceFadeInAnimation(rootView, fraction = 0.5f)
        assertEquals(0.5f, rootView.alpha)
        assertNotNull(rootView.getTag(R.id.tag_alpha_animator))

        // IF we request animation again
        ViewHierarchyAnimator.animateAddition(
            rootView,
            includeFadeIn = true,
            fadeInInterpolator = Interpolators.LINEAR
        )

        // THEN the alpha remains at its current value (it doesn't get reset to 0)
        assertNotNull(rootView.getTag(R.id.tag_alpha_animator))
        assertEquals(0.5f, rootView.alpha)

        // IF we advance the new animation to the end
        advanceFadeInAnimation(rootView, fraction = 1f)
        endFadeInAnimation(rootView)

        // THEN we still end at the correct value
        assertNull(rootView.getTag(R.id.tag_alpha_animator))
        assertEquals(1f, rootView.alpha)
    }

    @Test
    fun animatesAppearingViews_fadeInFalse_alphasNotUpdated() {
        rootView.alpha = 0.3f
        val firstChild = View(context)
        firstChild.alpha = 0.4f
        val secondChild = View(context)
        secondChild.alpha = 0.5f
        rootView.addView(firstChild)
        rootView.addView(secondChild)

        ViewHierarchyAnimator.animateAddition(
            rootView,
            includeFadeIn = false,
            fadeInInterpolator = Interpolators.LINEAR
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        advanceFadeInAnimation(rootView, fraction = 1f)

        assertEquals(0.3f, rootView.alpha)
        assertEquals(0.4f, firstChild.alpha)
        assertEquals(0.5f, secondChild.alpha)
    }

    @Test
    fun animateAddition_runnableRunsWhenAnimationEnds() {
        var runnableRun = false
        val onAnimationEndRunnable = { runnableRun = true }

        ViewHierarchyAnimator.animateAddition(
                rootView,
                origin = ViewHierarchyAnimator.Hotspot.CENTER,
                includeMargins = true,
                onAnimationEnd = onAnimationEndRunnable
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        endAnimation(rootView)

        assertEquals(true, runnableRun)
    }

    @Test
    fun animateAddition_runnableDoesNotRunWhenAnimationCancelled() {
        var runnableRun = false
        val onAnimationEndRunnable = { runnableRun = true }

        ViewHierarchyAnimator.animateAddition(
            rootView,
            origin = ViewHierarchyAnimator.Hotspot.CENTER,
            includeMargins = true,
            onAnimationEnd = onAnimationEndRunnable
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        cancelAnimation(rootView)

        assertEquals(false, runnableRun)
    }

    @Test
    fun animationAddition_runnableDoesNotRunWhenOnlyPartwayThroughAnimation() {
        var runnableRun = false
        val onAnimationEndRunnable = { runnableRun = true }

        ViewHierarchyAnimator.animateAddition(
            rootView,
            origin = ViewHierarchyAnimator.Hotspot.CENTER,
            includeMargins = true,
            onAnimationEnd = onAnimationEndRunnable
        )
        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)

        advanceAnimation(rootView, 0.5f)

        assertEquals(false, runnableRun)
    }

    @Test
    fun animatesViewRemovalFromStartToEnd_viewHasSiblings() {
        setUpRootWithChildren()

        val child = rootView.getChildAt(0)
        val success = ViewHierarchyAnimator.animateRemoval(
            child,
            destination = ViewHierarchyAnimator.Hotspot.LEFT,
            interpolator = Interpolators.LINEAR
        )

        assertTrue(success)
        assertNotNull(child.getTag(R.id.tag_animator))
        checkBounds(child, l = 0, t = 0, r = 100, b = 100)
        advanceAnimation(child, 0.5f)
        checkBounds(child, l = 0, t = 0, r = 50, b = 100)
        advanceAnimation(child, 1.0f)
        checkBounds(child, l = 0, t = 0, r = 0, b = 100)
        endAnimation(rootView)
        endAnimation(child)
        assertEquals(1, rootView.childCount)
        assertFalse(child in rootView.children)
    }

    @Test
    fun animatesViewRemovalFromStartToEnd_viewHasNoSiblings() {
        rootView = LinearLayout(mContext)
        (rootView as LinearLayout).orientation = LinearLayout.HORIZONTAL
        (rootView as LinearLayout).weightSum = 1f

        val onlyChild = View(mContext)
        rootView.addView(onlyChild)
        forceLayout()

        val success = ViewHierarchyAnimator.animateRemoval(
            onlyChild,
            destination = ViewHierarchyAnimator.Hotspot.LEFT,
            interpolator = Interpolators.LINEAR
        )

        assertTrue(success)
        assertNotNull(onlyChild.getTag(R.id.tag_animator))
        checkBounds(onlyChild, l = 0, t = 0, r = 200, b = 100)
        advanceAnimation(onlyChild, 0.5f)
        checkBounds(onlyChild, l = 0, t = 0, r = 100, b = 100)
        advanceAnimation(onlyChild, 1.0f)
        checkBounds(onlyChild, l = 0, t = 0, r = 0, b = 100)
        endAnimation(rootView)
        endAnimation(onlyChild)
        assertEquals(0, rootView.childCount)
        assertFalse(onlyChild in rootView.children)
    }

    @Test
    fun animatesViewRemovalRespectingDestination() {
        // CENTER
        setUpRootWithChildren()
        var removedChild = rootView.getChildAt(0)
        var remainingChild = rootView.getChildAt(1)
        var success = ViewHierarchyAnimator.animateRemoval(
            removedChild, destination = ViewHierarchyAnimator.Hotspot.CENTER
        )
        // Ensure that the layout happens before the checks.
        forceLayout()

        assertTrue(success)
        assertNotNull(removedChild.getTag(R.id.tag_animator))
        advanceAnimation(removedChild, 1.0f)
        checkBounds(removedChild, l = 50, t = 50, r = 50, b = 50)
        endAnimation(rootView)
        endAnimation(removedChild)
        checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)

        // LEFT
        setUpRootWithChildren()
        removedChild = rootView.getChildAt(0)
        remainingChild = rootView.getChildAt(1)
        success = ViewHierarchyAnimator.animateRemoval(
            removedChild, destination = ViewHierarchyAnimator.Hotspot.LEFT
        )
        // Ensure that the layout happens before the checks.
        forceLayout()

        assertTrue(success)
        assertNotNull(removedChild.getTag(R.id.tag_animator))
        advanceAnimation(removedChild, 1.0f)
        checkBounds(removedChild, l = 0, t = 0, r = 0, b = 100)
        endAnimation(rootView)
        endAnimation(removedChild)
        checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)

        // TOP_LEFT
        setUpRootWithChildren()
        removedChild = rootView.getChildAt(0)
        remainingChild = rootView.getChildAt(1)
        success = ViewHierarchyAnimator.animateRemoval(
            removedChild, destination = ViewHierarchyAnimator.Hotspot.TOP_LEFT
        )
        // Ensure that the layout happens before the checks.
        forceLayout()

        assertTrue(success)
        assertNotNull(removedChild.getTag(R.id.tag_animator))
        advanceAnimation(removedChild, 1.0f)
        checkBounds(removedChild, l = 0, t = 0, r = 0, b = 0)
        endAnimation(rootView)
        endAnimation(removedChild)
        checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)

        // TOP
        setUpRootWithChildren()
        removedChild = rootView.getChildAt(0)
        remainingChild = rootView.getChildAt(1)
        success = ViewHierarchyAnimator.animateRemoval(
            removedChild, destination = ViewHierarchyAnimator.Hotspot.TOP
        )
        // Ensure that the layout happens before the checks.
        forceLayout()

        assertTrue(success)
        assertNotNull(removedChild.getTag(R.id.tag_animator))
        advanceAnimation(removedChild, 1.0f)
        checkBounds(removedChild, l = 0, t = 0, r = 100, b = 0)
        endAnimation(rootView)
        endAnimation(removedChild)
        checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)

        // TOP_RIGHT
        setUpRootWithChildren()
        removedChild = rootView.getChildAt(0)
        remainingChild = rootView.getChildAt(1)
        success = ViewHierarchyAnimator.animateRemoval(
            removedChild, destination = ViewHierarchyAnimator.Hotspot.TOP_RIGHT
        )
        // Ensure that the layout happens before the checks.
        forceLayout()

        assertTrue(success)
        assertNotNull(removedChild.getTag(R.id.tag_animator))
        advanceAnimation(removedChild, 1.0f)
        checkBounds(removedChild, l = 100, t = 0, r = 100, b = 0)
        endAnimation(rootView)
        endAnimation(removedChild)
        checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)

        // RIGHT
        setUpRootWithChildren()
        removedChild = rootView.getChildAt(0)
        remainingChild = rootView.getChildAt(1)
        success = ViewHierarchyAnimator.animateRemoval(
            removedChild, destination = ViewHierarchyAnimator.Hotspot.RIGHT
        )
        // Ensure that the layout happens before the checks.
        forceLayout()

        assertTrue(success)
        assertNotNull(removedChild.getTag(R.id.tag_animator))
        advanceAnimation(removedChild, 1.0f)
        checkBounds(removedChild, l = 100, t = 0, r = 100, b = 100)
        endAnimation(rootView)
        endAnimation(removedChild)
        checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)

        // BOTTOM_RIGHT
        setUpRootWithChildren()
        removedChild = rootView.getChildAt(0)
        remainingChild = rootView.getChildAt(1)
        success = ViewHierarchyAnimator.animateRemoval(
            removedChild, destination = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT
        )
        // Ensure that the layout happens before the checks.
        forceLayout()

        assertTrue(success)
        assertNotNull(removedChild.getTag(R.id.tag_animator))
        advanceAnimation(removedChild, 1.0f)
        checkBounds(removedChild, l = 100, t = 100, r = 100, b = 100)
        endAnimation(rootView)
        endAnimation(removedChild)
        checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)

        // BOTTOM
        setUpRootWithChildren()
        removedChild = rootView.getChildAt(0)
        remainingChild = rootView.getChildAt(1)
        success = ViewHierarchyAnimator.animateRemoval(
            removedChild, destination = ViewHierarchyAnimator.Hotspot.BOTTOM
        )
        // Ensure that the layout happens before the checks.
        forceLayout()

        assertTrue(success)
        assertNotNull(removedChild.getTag(R.id.tag_animator))
        advanceAnimation(removedChild, 1.0f)
        checkBounds(removedChild, l = 0, t = 100, r = 100, b = 100)
        endAnimation(rootView)
        endAnimation(removedChild)
        checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)

        // BOTTOM_LEFT
        setUpRootWithChildren()
        removedChild = rootView.getChildAt(0)
        remainingChild = rootView.getChildAt(1)
        success = ViewHierarchyAnimator.animateRemoval(
            removedChild, destination = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT
        )
        // Ensure that the layout happens before the checks.
        forceLayout()

        assertTrue(success)
        assertNotNull(removedChild.getTag(R.id.tag_animator))
        advanceAnimation(removedChild, 1.0f)
        checkBounds(removedChild, l = 0, t = 100, r = 0, b = 100)
        endAnimation(rootView)
        endAnimation(removedChild)
        checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
    }

    /* ******** start of animatesViewRemoval_includeMarginsTrue tests ******** */
    @Test
    fun animatesViewRemoval_includeMarginsTrue_center() {
        setUpRootWithChildren(includeMarginsOnFirstChild = true)
        val removedChild = rootView.getChildAt(0)
        val originalLeft = removedChild.left
        val originalTop = removedChild.top
        val originalRight = removedChild.right
        val originalBottom = removedChild.bottom

        val success = ViewHierarchyAnimator.animateRemoval(
            removedChild,
            destination = ViewHierarchyAnimator.Hotspot.CENTER,
            includeMargins = true,
        )
        forceLayout()

        assertTrue(success)
        assertNotNull(removedChild.getTag(R.id.tag_animator))
        advanceAnimation(removedChild, 1.0f)
        val expectedX = ((originalLeft - M_LEFT) + (originalRight + M_RIGHT)) / 2
        val expectedY = ((originalTop - M_TOP) + (originalBottom + M_BOTTOM)) / 2

        checkBounds(
            removedChild,
            l = expectedX,
            t = expectedY,
            r = expectedX,
            b = expectedY
        )
    }

    @Test
    fun animatesViewRemoval_includeMarginsTrue_left() {
        setUpRootWithChildren(includeMarginsOnFirstChild = true)
        val removedChild = rootView.getChildAt(0)
        val originalLeft = removedChild.left
        val originalTop = removedChild.top
        val originalBottom = removedChild.bottom

        val success = ViewHierarchyAnimator.animateRemoval(
            removedChild,
            destination = ViewHierarchyAnimator.Hotspot.LEFT,
            includeMargins = true,
        )
        forceLayout()

        assertTrue(success)
        assertNotNull(removedChild.getTag(R.id.tag_animator))
        advanceAnimation(removedChild, 1.0f)
        checkBounds(
            removedChild,
            l = originalLeft - M_LEFT,
            t = originalTop,
            r = originalLeft - M_LEFT,
            b = originalBottom
        )
    }

    @Test
    fun animatesViewRemoval_includeMarginsTrue_topLeft() {
        setUpRootWithChildren(includeMarginsOnFirstChild = true)
        val removedChild = rootView.getChildAt(0)
        val originalLeft = removedChild.left
        val originalTop = removedChild.top

        val success = ViewHierarchyAnimator.animateRemoval(
            removedChild,
            destination = ViewHierarchyAnimator.Hotspot.TOP_LEFT,
            includeMargins = true,
        )
        forceLayout()

        assertTrue(success)
        assertNotNull(removedChild.getTag(R.id.tag_animator))
        advanceAnimation(removedChild, 1.0f)
        checkBounds(
            removedChild,
            l = originalLeft - M_LEFT,
            t = originalTop - M_TOP,
            r = originalLeft - M_LEFT,
            b = originalTop - M_TOP
        )
    }

    @Test
    fun animatesViewRemoval_includeMarginsTrue_top() {
        setUpRootWithChildren(includeMarginsOnFirstChild = true)
        val removedChild = rootView.getChildAt(0)
        val originalLeft = removedChild.left
        val originalTop = removedChild.top
        val originalRight = removedChild.right

        val success = ViewHierarchyAnimator.animateRemoval(
            removedChild,
            destination = ViewHierarchyAnimator.Hotspot.TOP,
            includeMargins = true,
        )
        forceLayout()

        assertTrue(success)
        assertNotNull(removedChild.getTag(R.id.tag_animator))
        advanceAnimation(removedChild, 1.0f)
        checkBounds(
            removedChild,
            l = originalLeft,
            t = originalTop - M_TOP,
            r = originalRight,
            b = originalTop - M_TOP
        )
    }

    @Test
    fun animatesViewRemoval_includeMarginsTrue_topRight() {
        setUpRootWithChildren(includeMarginsOnFirstChild = true)
        val removedChild = rootView.getChildAt(0)
        val originalTop = removedChild.top
        val originalRight = removedChild.right

        val success = ViewHierarchyAnimator.animateRemoval(
            removedChild,
            destination = ViewHierarchyAnimator.Hotspot.TOP_RIGHT,
            includeMargins = true,
        )
        forceLayout()

        assertTrue(success)
        assertNotNull(removedChild.getTag(R.id.tag_animator))
        advanceAnimation(removedChild, 1.0f)
        checkBounds(
            removedChild,
            l = originalRight + M_RIGHT,
            t = originalTop - M_TOP,
            r = originalRight + M_RIGHT,
            b = originalTop - M_TOP
        )
    }

    @Test
    fun animatesViewRemoval_includeMarginsTrue_right() {
        setUpRootWithChildren(includeMarginsOnFirstChild = true)
        val removedChild = rootView.getChildAt(0)
        val originalTop = removedChild.top
        val originalRight = removedChild.right
        val originalBottom = removedChild.bottom

        val success = ViewHierarchyAnimator.animateRemoval(
            removedChild,
            destination = ViewHierarchyAnimator.Hotspot.RIGHT,
            includeMargins = true,
        )
        forceLayout()

        assertTrue(success)
        assertNotNull(removedChild.getTag(R.id.tag_animator))
        advanceAnimation(removedChild, 1.0f)
        checkBounds(
            removedChild,
            l = originalRight + M_RIGHT,
            t = originalTop,
            r = originalRight + M_RIGHT,
            b = originalBottom
        )
    }

    @Test
    fun animatesViewRemoval_includeMarginsTrue_bottomRight() {
        setUpRootWithChildren(includeMarginsOnFirstChild = true)
        val removedChild = rootView.getChildAt(0)
        val originalRight = removedChild.right
        val originalBottom = removedChild.bottom

        val success = ViewHierarchyAnimator.animateRemoval(
            removedChild,
            destination = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT,
            includeMargins = true,
        )
        forceLayout()

        assertTrue(success)
        assertNotNull(removedChild.getTag(R.id.tag_animator))
        advanceAnimation(removedChild, 1.0f)
        checkBounds(
            removedChild,
            l = originalRight + M_RIGHT,
            t = originalBottom + M_BOTTOM,
            r = originalRight + M_RIGHT,
            b = originalBottom + M_BOTTOM
        )
    }

    @Test
    fun animatesViewRemoval_includeMarginsTrue_bottom() {
        setUpRootWithChildren(includeMarginsOnFirstChild = true)
        val removedChild = rootView.getChildAt(0)
        val originalLeft = removedChild.left
        val originalRight = removedChild.right
        val originalBottom = removedChild.bottom

        val success = ViewHierarchyAnimator.animateRemoval(
            removedChild,
            destination = ViewHierarchyAnimator.Hotspot.BOTTOM,
            includeMargins = true,
        )
        forceLayout()

        assertTrue(success)
        assertNotNull(removedChild.getTag(R.id.tag_animator))
        advanceAnimation(removedChild, 1.0f)
        checkBounds(
            removedChild,
            l = originalLeft,
            t = originalBottom + M_BOTTOM,
            r = originalRight,
            b = originalBottom + M_BOTTOM
        )
    }

    @Test
    fun animatesViewRemoval_includeMarginsTrue_bottomLeft() {
        setUpRootWithChildren(includeMarginsOnFirstChild = true)
        val removedChild = rootView.getChildAt(0)
        val originalLeft = removedChild.left
        val originalBottom = removedChild.bottom

        val success = ViewHierarchyAnimator.animateRemoval(
            removedChild,
            destination = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT,
            includeMargins = true,
        )
        forceLayout()

        assertTrue(success)
        assertNotNull(removedChild.getTag(R.id.tag_animator))
        advanceAnimation(removedChild, 1.0f)
        checkBounds(
            removedChild,
            l = originalLeft - M_LEFT,
            t = originalBottom + M_BOTTOM,
            r = originalLeft - M_LEFT,
            b = originalBottom + M_BOTTOM
        )
    }
    /* ******** end of animatesViewRemoval_includeMarginsTrue tests ******** */

    @Test
    fun animatesChildrenDuringViewRemoval() {
        setUpRootWithChildren()

        val child = rootView.getChildAt(0) as ViewGroup
        val firstGrandChild = child.getChildAt(0)
        val secondGrandChild = child.getChildAt(1)
        val success = ViewHierarchyAnimator.animateRemoval(
            child, interpolator = Interpolators.LINEAR
        )

        assertTrue(success)
        assertNotNull(child.getTag(R.id.tag_animator))
        assertNotNull(firstGrandChild.getTag(R.id.tag_animator))
        assertNotNull(secondGrandChild.getTag(R.id.tag_animator))
        checkBounds(child, l = 0, t = 0, r = 100, b = 100)
        checkBounds(firstGrandChild, l = 0, t = 0, r = 40, b = 40)
        checkBounds(secondGrandChild, l = 60, t = 60, r = 100, b = 100)

        advanceAnimation(child, 0.5f)
        checkBounds(child, l = 25, t = 25, r = 75, b = 75)
        checkBounds(firstGrandChild, l = -10, t = -10, r = 30, b = 30)
        checkBounds(secondGrandChild, l = 20, t = 20, r = 60, b = 60)

        advanceAnimation(child, 1.0f)
        checkBounds(child, l = 50, t = 50, r = 50, b = 50)
        checkBounds(firstGrandChild, l = -20, t = -20, r = 20, b = 20)
        checkBounds(secondGrandChild, l = -20, t = -20, r = 20, b = 20)

        endAnimation(rootView)
        endAnimation(child)
    }

    @Test
    fun animatesSiblingsDuringViewRemoval() {
        setUpRootWithChildren()

        val removedChild = rootView.getChildAt(0)
        val remainingChild = rootView.getChildAt(1)
        val success = ViewHierarchyAnimator.animateRemoval(
            removedChild, interpolator = Interpolators.LINEAR
        )
        // Ensure that the layout happens before the checks.
        forceLayout()

        assertTrue(success)
        assertNotNull(remainingChild.getTag(R.id.tag_animator))
        checkBounds(remainingChild, l = 100, t = 0, r = 200, b = 100)
        advanceAnimation(rootView, 0.5f)
        checkBounds(remainingChild, l = 50, t = 0, r = 150, b = 100)
        advanceAnimation(rootView, 1.0f)
        checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100)
        endAnimation(rootView)
        endAnimation(removedChild)
        assertNull(remainingChild.getTag(R.id.tag_animator))
    }

    @Test
    fun animateRemoval_runnableRunsWhenAnimationEnds() {
        var runnableRun = false
        val onAnimationEndRunnable = { runnableRun = true }

        setUpRootWithChildren()
        forceLayout()
        val removedView = rootView.getChildAt(0)

        ViewHierarchyAnimator.animateRemoval(
            removedView,
            onAnimationEnd = onAnimationEndRunnable
        )
        endAnimation(removedView)

        assertEquals(true, runnableRun)
    }

    @Test
    fun animateRemoval_runnableDoesNotRunWhenAnimationCancelled() {
        var runnableRun = false
        val onAnimationEndRunnable = { runnableRun = true }

        setUpRootWithChildren()
        forceLayout()
        val removedView = rootView.getChildAt(0)

        ViewHierarchyAnimator.animateRemoval(
            removedView,
            onAnimationEnd = onAnimationEndRunnable
        )
        cancelAnimation(removedView)

        assertEquals(false, runnableRun)
    }

    @Test
    fun animationRemoval_runnableDoesNotRunWhenOnlyPartwayThroughAnimation() {
        var runnableRun = false
        val onAnimationEndRunnable = { runnableRun = true }

        setUpRootWithChildren()
        forceLayout()
        val removedView = rootView.getChildAt(0)

        ViewHierarchyAnimator.animateRemoval(
            removedView,
            onAnimationEnd = onAnimationEndRunnable
        )
        advanceAnimation(removedView, 0.5f)

        assertEquals(false, runnableRun)
    }

    @Test
    fun cleansUpListenersCorrectly() {
        val firstChild = View(mContext)
        firstChild.layoutParams = LinearLayout.LayoutParams(50 /* width */, 100 /* height */)
        rootView.addView(firstChild)
        val secondChild = View(mContext)
        secondChild.layoutParams = LinearLayout.LayoutParams(50 /* width */, 100 /* height */)
        rootView.addView(secondChild)
        rootView.measure(
            View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY),
            View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
        )
        rootView.layout(0 /* l */, 0 /* t */, 100 /* r */, 100 /* b */)

        val success = ViewHierarchyAnimator.animateNextUpdate(rootView)
        // Change all bounds.
        rootView.measure(
            View.MeasureSpec.makeMeasureSpec(150, View.MeasureSpec.EXACTLY),
            View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
        )
        rootView.layout(0 /* l */, 0 /* t */, 150 /* r */, 100 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_layout_listener))
        assertNotNull(firstChild.getTag(R.id.tag_layout_listener))
        assertNotNull(secondChild.getTag(R.id.tag_layout_listener))
        endAnimation(rootView)
        assertNull(rootView.getTag(R.id.tag_layout_listener))
        assertNull(firstChild.getTag(R.id.tag_layout_listener))
        assertNull(secondChild.getTag(R.id.tag_layout_listener))
    }

    @Test
    fun doesNotAnimateGoneViews() {
        rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)

        // GONE
        rootView.visibility = View.GONE
        var success = ViewHierarchyAnimator.animate(rootView)
        rootView.layout(0 /* l */, 15 /* t */, 55 /* r */, 80 /* b */)

        assertFalse(success)
        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 0, t = 15, r = 55, b = 80)
    }

    @Test
    fun doesNotAnimateAppearingViews() {
        // Starting with nothing.
        rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
        var success = ViewHierarchyAnimator.animate(rootView)
        rootView.layout(0 /* l */, 15 /* t */, 55 /* r */, 80 /* b */)

        assertFalse(success)
        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 0, t = 15, r = 55, b = 80)

        // Starting with 0 width.
        rootView.layout(0 /* l */, 50 /* t */, 0 /* r */, 100 /* b */)
        success = ViewHierarchyAnimator.animate(rootView)
        rootView.layout(0 /* l */, 15 /* t */, 55 /* r */, 80 /* b */)

        assertFalse(success)
        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 0, t = 15, r = 55, b = 80)

        // Starting with 0 height.
        rootView.layout(0 /* l */, 50 /* t */, 50 /* r */, 50 /* b */)
        success = ViewHierarchyAnimator.animate(rootView)
        rootView.layout(0 /* l */, 15 /* t */, 55 /* r */, 80 /* b */)

        assertFalse(success)
        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 0, t = 15, r = 55, b = 80)
    }

    @Test
    fun doesNotAnimateDisappearingViews() {
        rootView.layout(0 /* l */, 0 /* t */, 100 /* r */, 100 /* b */)

        val success = ViewHierarchyAnimator.animate(rootView)
        // Ending with nothing.
        rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)

        assertTrue(success)
        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 0, t = 0, r = 0, b = 0)

        // Ending with 0 width.
        rootView.layout(0 /* l */, 50 /* t */, 50 /* r */, 100 /* b */)
        endAnimation(rootView)
        rootView.layout(0 /* l */, 15 /* t */, 0 /* r */, 80 /* b */)

        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 0, t = 15, r = 0, b = 80)

        // Ending with 0 height.
        rootView.layout(0 /* l */, 50 /* t */, 50 /* r */, 100 /* b */)
        endAnimation(rootView)
        rootView.layout(0 /* l */, 50 /* t */, 55 /* r */, 50 /* b */)

        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 0, t = 50, r = 55, b = 50)
    }

    @Test
    fun doesNotAnimateUnchangingBounds() {
        rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)

        val success = ViewHierarchyAnimator.animate(rootView)
        // No bounds are changed.
        rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)

        assertTrue(success)
        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 10, t = 10, r = 50, b = 50)

        // Change only right and bottom.
        rootView.layout(10 /* l */, 10 /* t */, 70 /* r */, 80 /* b */)

        assertNotNull(rootView.getTag(R.id.tag_animator))
        endAnimation(rootView)
        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 10, t = 10, r = 70, b = 80)
    }

    @Test
    fun stopsAnimatingAfterSingleLayout() {
        rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)

        val success = ViewHierarchyAnimator.animateNextUpdate(rootView)
        // Change all bounds.
        rootView.layout(0 /* l */, 15 /* t */, 70 /* r */, 80 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        endAnimation(rootView)
        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 0, t = 15, r = 70, b = 80)

        // Change all bounds again.
        rootView.layout(10 /* l */, 10 /* t */, 50/* r */, 50 /* b */)

        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 10, t = 10, r = 50, b = 50)
    }

    @Test
    fun stopsAnimatingWhenInstructed() {
        rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)

        val success = ViewHierarchyAnimator.animate(rootView)
        // Change all bounds.
        rootView.layout(0 /* l */, 15 /* t */, 70 /* r */, 80 /* b */)

        assertTrue(success)
        assertNotNull(rootView.getTag(R.id.tag_animator))
        endAnimation(rootView)
        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 0, t = 15, r = 70, b = 80)

        ViewHierarchyAnimator.stopAnimating(rootView)
        // Change all bounds again.
        rootView.layout(10 /* l */, 10 /* t */, 50/* r */, 50 /* b */)

        assertNull(rootView.getTag(R.id.tag_animator))
        checkBounds(rootView, l = 10, t = 10, r = 50, b = 50)
    }

    private fun setUpRootWithChildren(includeMarginsOnFirstChild: Boolean = false) {
        rootView = LinearLayout(mContext)
        (rootView as LinearLayout).orientation = LinearLayout.HORIZONTAL
        (rootView as LinearLayout).weightSum = 1f

        val firstChild = RelativeLayout(mContext)
        rootView.addView(firstChild)
        val firstGrandChild = View(mContext)
        firstChild.addView(firstGrandChild)
        val secondGrandChild = View(mContext)
        firstChild.addView(secondGrandChild)
        val secondChild = View(mContext)
        rootView.addView(secondChild)

        val firstChildParams = LinearLayout.LayoutParams(
            0 /* width */,
            LinearLayout.LayoutParams.MATCH_PARENT
        )
        firstChildParams.weight = 0.5f
        if (includeMarginsOnFirstChild) {
            firstChildParams.leftMargin = M_LEFT
            firstChildParams.topMargin = M_TOP
            firstChildParams.rightMargin = M_RIGHT
            firstChildParams.bottomMargin = M_BOTTOM
        }
        firstChild.layoutParams = firstChildParams

        val secondChildParams = LinearLayout.LayoutParams(
            0 /* width */,
            LinearLayout.LayoutParams.MATCH_PARENT
        )
        secondChildParams.weight = 0.5f
        secondChild.layoutParams = secondChildParams

        firstGrandChild.layoutParams = RelativeLayout.LayoutParams(40 /* width */, 40 /* height */)
        (firstGrandChild.layoutParams as RelativeLayout.LayoutParams)
            .addRule(RelativeLayout.ALIGN_PARENT_START)
        (firstGrandChild.layoutParams as RelativeLayout.LayoutParams)
            .addRule(RelativeLayout.ALIGN_PARENT_TOP)
        secondGrandChild.layoutParams = RelativeLayout.LayoutParams(40 /* width */, 40 /* height */)
        (secondGrandChild.layoutParams as RelativeLayout.LayoutParams)
            .addRule(RelativeLayout.ALIGN_PARENT_END)
        (secondGrandChild.layoutParams as RelativeLayout.LayoutParams)
            .addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)

        forceLayout()
    }

    private fun forceLayout() {
        rootView.measure(
            View.MeasureSpec.makeMeasureSpec(200 /* width */, View.MeasureSpec.AT_MOST),
            View.MeasureSpec.makeMeasureSpec(100 /* height */, View.MeasureSpec.AT_MOST)
        )
        rootView.layout(0 /* l */, 0 /* t */, 200 /* r */, 100 /* b */)
    }

    private fun checkBounds(v: View, l: Int, t: Int, r: Int, b: Int) {
        assertEquals(l, v.left)
        assertEquals(t, v.top)
        assertEquals(r, v.right)
        assertEquals(b, v.bottom)
    }

    private fun advanceAnimation(rootView: View, fraction: Float) {
        (rootView.getTag(R.id.tag_animator) as? ObjectAnimator)?.setCurrentFraction(fraction)

        if (rootView is ViewGroup) {
            for (i in 0 until rootView.childCount) {
                advanceAnimation(rootView.getChildAt(i), fraction)
            }
        }
    }

    private fun advanceFadeInAnimation(rootView: View, fraction: Float) {
        (rootView.getTag(R.id.tag_alpha_animator) as? ObjectAnimator)?.setCurrentFraction(fraction)

        if (rootView is ViewGroup) {
            for (i in 0 until rootView.childCount) {
                advanceFadeInAnimation(rootView.getChildAt(i), fraction)
            }
        }
    }

    private fun endAnimation(rootView: View) {
        (rootView.getTag(R.id.tag_animator) as? ObjectAnimator)?.end()

        if (rootView is ViewGroup) {
            for (i in 0 until rootView.childCount) {
                endAnimation(rootView.getChildAt(i))
            }
        }
    }

    private fun cancelAnimation(rootView: View) {
        (rootView.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel()

        if (rootView is ViewGroup) {
            for (i in 0 until rootView.childCount) {
                cancelAnimation(rootView.getChildAt(i))
            }
        }
    }

    private fun endFadeInAnimation(rootView: View) {
        (rootView.getTag(R.id.tag_alpha_animator) as? ObjectAnimator)?.end()

        if (rootView is ViewGroup) {
            for (i in 0 until rootView.childCount) {
                endFadeInAnimation(rootView.getChildAt(i))
            }
        }
    }
}

// Margin values.
private const val M_LEFT = 14
private const val M_TOP = 16
private const val M_RIGHT = 18
private const val M_BOTTOM = 20
