Commit b4e2368f authored by Mugurell's avatar Mugurell Committed by mergify[bot]
Browse files

For #9932 - Make ExpandableLayout aware of the sticky footer item index

If the menu starts as collapsed and it has a sticky footer set the bottom item
until which the menu should be collapsed must be shown on top of the sticky
item's view.
As an edgecase, if the same item is the limit of the collapsed menu and also
the sticky footer that will be the last item shown in the collapsed menu but
will be shown with full height.
parent 44604be8
Loading
Loading
Loading
Loading
+37 −13
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import androidx.core.view.marginLeft
import androidx.core.view.marginRight
import androidx.core.view.marginTop
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView

/**
 * ViewGroup intended to wrap another to then allow for the following automatic behavior:
@@ -47,6 +48,12 @@ internal class ExpandableLayout private constructor(context: Context) : FrameLay
    @VisibleForTesting
    internal var lastVisibleItemIndexWhenCollapsed: Int = Int.MAX_VALUE

    /**
     * Index of the sticky footer, if such an item is set.
     */
    @VisibleForTesting
    internal var stickyItemIndex: Int = RecyclerView.NO_POSITION

    /**
     * Height of wrapped view when collapsed.
     * Calculated once based on the position of the "isCollapsingMenuLimit" BrowserMenuItem.
@@ -321,6 +328,7 @@ internal class ExpandableLayout private constructor(context: Context) : FrameLay
        result += listView.paddingTop
        result += listView.paddingBottom

        run loop@ {
            listView.children.forEachIndexed { index, view ->
                if (index < lastVisibleItemIndexWhenCollapsed) {
                    result += view.marginTop
@@ -331,9 +339,23 @@ internal class ExpandableLayout private constructor(context: Context) : FrameLay
                } else if (index == lastVisibleItemIndexWhenCollapsed) {
                    result += view.marginTop
                    result += view.paddingTop
                result += view.measuredHeight / 2

                return@forEachIndexed
                    // Edgecase: if the same item is the sticky footer and the lastVisibleItemIndexWhenCollapsed
                    // the menu will be collapsed to this item but shown with full height.
                    if (index == stickyItemIndex) {
                        result += view.measuredHeight
                        return@loop
                    } else {
                        result += view.measuredHeight / 2
                    }
                } else {
                    // If there is a sticky item below we need to add it's height as an offset.
                    // Otherwise the sticky item will cover the the view of lastVisibleItemIndexWhenCollapsed.
                    if (index <= stickyItemIndex) {
                        result += listView.getChildAt(stickyItemIndex).measuredHeight
                    }
                    return@loop
                }
            }
        }

@@ -362,6 +384,7 @@ internal class ExpandableLayout private constructor(context: Context) : FrameLay
        internal fun wrapContentInExpandableView(
            contentView: ViewGroup,
            lastVisibleItemIndexWhenCollapsed: Int = Int.MAX_VALUE,
            stickyFooterItemIndex: Int = RecyclerView.NO_POSITION,
            blankTouchListener: (() -> Unit)? = null
        ): ExpandableLayout {

@@ -376,6 +399,7 @@ internal class ExpandableLayout private constructor(context: Context) : FrameLay
            expandableView.addView(contentView, params)

            expandableView.wrappedView = contentView
            expandableView.stickyItemIndex = stickyFooterItemIndex
            expandableView.blankTouchListener = blankTouchListener
            expandableView.lastVisibleItemIndexWhenCollapsed = lastVisibleItemIndexWhenCollapsed

+65 −7
Original line number Diff line number Diff line
@@ -49,7 +49,7 @@ class ExpandableLayoutTest {
        }

        val result = ExpandableLayout.wrapContentInExpandableView(
            wrappedView, 42, blankTouchListener
            wrappedView, 42, 33, blankTouchListener
        )

        assertEquals(FrameLayout.LayoutParams.WRAP_CONTENT, result.wrappedView.layoutParams.height)
@@ -64,6 +64,7 @@ class ExpandableLayoutTest {

        // Also test the default configuration of a newly built ExpandableLayout.
        assertEquals(42, result.lastVisibleItemIndexWhenCollapsed)
        assertEquals(33, result.stickyItemIndex)
        assertEquals(ExpandableLayout.NOT_CALCULATED_DEFAULT_HEIGHT, result.collapsedHeight)
        assertEquals(ExpandableLayout.NOT_CALCULATED_DEFAULT_HEIGHT, result.expandedHeight)
        assertEquals(ExpandableLayout.NOT_CALCULATED_DEFAULT_HEIGHT, result.parentHeight)
@@ -145,7 +146,7 @@ class ExpandableLayoutTest {
    fun `GIVEN an expanded menu WHEN onInterceptTouchEvent is called for a touch on the menu THEN super is called`() {
        val blankTouchListener = spy {}
        val expandableLayout = spy(ExpandableLayout.wrapContentInExpandableView(
            FrameLayout(testContext), 1, blankTouchListener)
            FrameLayout(testContext), 1, blankTouchListener = blankTouchListener)
        )
        val event: MotionEvent = mock()
        doReturn(false).`when`(expandableLayout).shouldInterceptTouches()
@@ -161,7 +162,7 @@ class ExpandableLayoutTest {
    fun `GIVEN a menu currently expanding WHEN onInterceptTouchEvent is called for a touch on the menu THEN the touch is swallowed`() {
        val blankTouchListener = spy {}
        val expandableLayout = spy(ExpandableLayout.wrapContentInExpandableView(
            FrameLayout(testContext), 1, blankTouchListener)
            FrameLayout(testContext), 1, blankTouchListener = blankTouchListener)
        )
        val event: MotionEvent = mock()
        doReturn(false).`when`(expandableLayout).shouldInterceptTouches()
@@ -178,7 +179,7 @@ class ExpandableLayoutTest {
    fun `GIVEN an expanded menu WHEN onInterceptTouchEvent is called for a outside the menu THEN super blankTouchListener is invoked`() {
        val blankTouchListener = spy {}
        val expandableLayout = spy(ExpandableLayout.wrapContentInExpandableView(
            FrameLayout(testContext), 1, blankTouchListener)
            FrameLayout(testContext), 1, blankTouchListener = blankTouchListener)
        )
        val event: MotionEvent = mock()
        doReturn(false).`when`(expandableLayout).shouldInterceptTouches()
@@ -236,7 +237,7 @@ class ExpandableLayoutTest {
        var listenerCalled = false
        val listener = spy { listenerCalled = true }
        val expandableLayout = spy(ExpandableLayout.wrapContentInExpandableView(
            FrameLayout(testContext), 1, listener)
            FrameLayout(testContext), 1, blankTouchListener = listener)
        )
        doReturn(false).`when`(expandableLayout).isTouchingTheWrappedView(any())
        val actionDown = MotionEvent.obtain(0, 0, ACTION_DOWN, 0f, 0f, 0)
@@ -251,7 +252,7 @@ class ExpandableLayoutTest {
        var listenerCalled = false
        val listener = spy { listenerCalled = true }
        val expandableLayout = spy(ExpandableLayout.wrapContentInExpandableView(
            FrameLayout(testContext), 1, listener)
            FrameLayout(testContext), 1, blankTouchListener = listener)
        )
        doReturn(true).`when`(expandableLayout).isTouchingTheWrappedView(any())
        val actionDown = MotionEvent.obtain(0, 0, ACTION_DOWN, 0f, 0f, 0)
@@ -567,7 +568,7 @@ class ExpandableLayoutTest {
    }

    @Test
    fun `GIVEN calculateCollapsedHeight() WHEN called THEN it returns the distance between parent top and half of the SpecialView`() {
    fun `GIVEN calculateCollapsedHeight() WHEN called without a sticky footer index THEN it returns the distance between parent top and half of the SpecialView`() {
        val viewHeightForEachProperty = 1_000
        val listHeightForEachProperty = 100
        val itemHeightForEachProperty = 10
@@ -593,6 +594,63 @@ class ExpandableLayoutTest {
        expected += itemHeightForEachProperty / 2 // as per the specs, show only half of the special view
        assertEquals(expected, result)
    }

    @Test
    fun `GIVEN calculateCollapsedHeight() WHEN called with a sticky footer index THEN it returns the distance between parent top and half of the SpecialView + height of sticky`() {
        val viewHeightForEachProperty = 1_000
        val listHeightForEachProperty = 100
        val itemHeightForEachProperty = 10
        // Adding Views and creating spies in two stages because otherwise
        // the addView call for a spy will not get us the expected result.
        var list = FrameLayout(testContext).apply {
            addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty))
            addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty))
            addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty))
        }
        list = spy(list).configureWithHeight(listHeightForEachProperty)
        var wrappedView = FrameLayout(testContext).apply { addView(list) }
        wrappedView = spy(wrappedView).configureWithHeight(viewHeightForEachProperty)
        val expandableLayout = ExpandableLayout.wrapContentInExpandableView(wrappedView, 1, 2) { }

        val result = expandableLayout.calculateCollapsedHeight()

        var expected = 0
        expected += viewHeightForEachProperty * 4 // marginTop + marginBottom + paddingTop + paddingBottom
        expected += listHeightForEachProperty * 4 // marginTop + marginBottom + paddingTop + paddingBottom
        expected += itemHeightForEachProperty * 5 // height + marginTop + marginBottom + paddingTop + paddingBottom for the top item shown in entirety
        expected += itemHeightForEachProperty * 2 // marginTop + paddingTop for the special view
        expected += itemHeightForEachProperty / 2 // as per the specs, show only half of the special view
        expected += itemHeightForEachProperty     // height of the sticky item
        assertEquals(expected, result)
    }

    @Test
    fun `GIVEN calculateCollapsedHeight() WHEN called the same item as limit and sticky THEN it returns the distance between parent top and bottom of sticky`() {
        val viewHeightForEachProperty = 1_000
        val listHeightForEachProperty = 100
        val itemHeightForEachProperty = 10
        // Adding Views and creating spies in two stages because otherwise
        // the addView call for a spy will not get us the expected result.
        var list = FrameLayout(testContext).apply {
            addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty))
            addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty))
            addView(spy(View(testContext)).configureWithHeight(itemHeightForEachProperty))
        }
        list = spy(list).configureWithHeight(listHeightForEachProperty)
        var wrappedView = FrameLayout(testContext).apply { addView(list) }
        wrappedView = spy(wrappedView).configureWithHeight(viewHeightForEachProperty)
        val expandableLayout = ExpandableLayout.wrapContentInExpandableView(wrappedView, 1, 1) { }

        val result = expandableLayout.calculateCollapsedHeight()

        var expected = 0
        expected += viewHeightForEachProperty * 4 // marginTop + marginBottom + paddingTop + paddingBottom
        expected += listHeightForEachProperty * 4 // marginTop + marginBottom + paddingTop + paddingBottom
        expected += itemHeightForEachProperty * 5 // height + marginTop + marginBottom + paddingTop + paddingBottom for the top item shown in entirety
        expected += itemHeightForEachProperty * 2 // marginTop + paddingTop for the special view
        expected += itemHeightForEachProperty     // height of the sticky item
        assertEquals(expected, result)
    }
}

/**