Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 127479eb authored by Ibrahim Yilmaz's avatar Ibrahim Yilmaz Committed by Android (Google) Code Review
Browse files

Merge "OptimizedLinearLayout: Handle negative margins for Vertical measurement" into main

parents 5f57cbc3 d398ad4a
Loading
Loading
Loading
Loading
+83 −69
Original line number Diff line number Diff line
@@ -53,6 +53,7 @@ import java.util.List;
 * - LinearLayout doesn't have <code>weightSum</code>.
 * - Horizontal LinearLayout's width should be measured EXACTLY.
 * - Horizontal LinearLayout shouldn't need baseLineAlignment.
 * - Horizontal LinearLayout shouldn't have any child that has negative left or right margin.
 * - Vertical LinearLayout shouldn't have MATCH_PARENT children when it is not measured EXACTLY.
 *
 * @hide
@@ -88,7 +89,7 @@ public class NotificationOptimizedLinearLayout extends LinearLayout {
        final View weightedChildView = getSingleWeightedChild();
        mShouldUseOptimizedLayout =
                isUseOptimizedLinearLayoutFlagEnabled() && weightedChildView != null
                        && isLinearLayoutUsable(widthMeasureSpec, heightMeasureSpec);
                        && isOptimizationPossible(widthMeasureSpec, heightMeasureSpec);

        if (mShouldUseOptimizedLayout) {
            onMeasureOptimized(weightedChildView, widthMeasureSpec, heightMeasureSpec);
@@ -118,7 +119,7 @@ public class NotificationOptimizedLinearLayout extends LinearLayout {
     * @param heightMeasureSpec The height measurement specification.
     * @return `true` if optimization is possible, `false` otherwise.
     */
    private boolean isLinearLayoutUsable(int widthMeasureSpec, int heightMeasureSpec) {
    private boolean isOptimizationPossible(int widthMeasureSpec, int heightMeasureSpec) {
        final boolean hasWeightSum = getWeightSum() > 0.0f;
        if (hasWeightSum) {
            logSkipOptimizedOnMeasure("Has weightSum.");
@@ -142,9 +143,35 @@ public class NotificationOptimizedLinearLayout extends LinearLayout {
            logSkipOptimizedOnMeasure("Need to apply baseline.");
            return false;
        }

        if (requiresNegativeMarginHandlingForHorizontalLinearLayout()) {
            logSkipOptimizedOnMeasure("Need to handle negative margins.");
            return false;
        }
        return true;
    }

    /**
     * @return if the horizontal linearlayout requires to handle negative margins in its children.
     * In that case, we can't use excessSpace because LinearLayout negative margin handling for
     * excess space and WRAP_CONTENT is different.
     */
    private boolean requiresNegativeMarginHandlingForHorizontalLinearLayout() {
        if (getOrientation() == VERTICAL) {
            return false;
        }

        final List<View> activeChildren = getActiveChildren();
        for (int i = 0; i < activeChildren.size(); i++) {
            final View child = activeChildren.get(i);
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            if (lp.leftMargin < 0 || lp.rightMargin < 0) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return if the vertical linearlayout requires match_parent children remeasure
     */
@@ -337,94 +364,81 @@ public class NotificationOptimizedLinearLayout extends LinearLayout {
     */
    private void measureVerticalOptimized(@NonNull View weightedChildView, int widthMeasureSpec,
            int heightMeasureSpec) {
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int totalLength = 0;
        int maxWidth = 0;
        int usedHeight = 0;
        final List<View> activeChildren = getActiveChildren();
        final int activeChildCount = activeChildren.size();

        final boolean isContentFirstItem = !activeChildren.isEmpty() && activeChildren.get(0)
                == weightedChildView;

        final boolean isContentLastItem = !activeChildren.isEmpty() && activeChildren.get(
                activeChildCount - 1) == weightedChildView;

        final int horizontalPaddings = getPaddingLeft() + getPaddingRight();
        final int availableHeight = MeasureSpec.getSize(heightMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        // 1. Measure other child views.
        for (int i = 0; i < activeChildCount; i++) {
            final View child = activeChildren.get(i);
            if (child == weightedChildView) {
        // 1. Measure all unweighted children
        for (int i = 0; i < getChildCount(); i++) {
            final View child = getChildAt(i);
            if (child == null || child.getVisibility() == GONE) {
                continue;
            }

            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

            int requiredVerticalPadding = lp.topMargin + lp.bottomMargin;
            if (!isContentFirstItem && i == 0) {
                requiredVerticalPadding += getPaddingTop();
            if (child == weightedChildView) {
                // In excessMode, LinearLayout add  weighted child top and bottom margins to
                // totalLength when their sum is positive.
                if (lp.height == 0 && heightMode == MeasureSpec.EXACTLY) {
                    totalLength = Math.max(totalLength, totalLength + lp.topMargin
                            + lp.bottomMargin);
                }
            if (!isContentLastItem && i == activeChildCount - 1) {
                requiredVerticalPadding += getPaddingBottom();
                continue;
            }

            child.measure(ViewGroup.getChildMeasureSpec(widthMeasureSpec,
                            horizontalPaddings + lp.leftMargin + lp.rightMargin,
                            child.getLayoutParams().width),
                    ViewGroup.getChildMeasureSpec(heightMeasureSpec, requiredVerticalPadding,
                            lp.height));
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            // LinearLayout only adds measured children heights and its top and bottom margins
            // to totalLength when their sum is positive.
            totalLength = Math.max(totalLength,
                    totalLength + child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            maxWidth = Math.max(maxWidth,
                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            usedHeight += child.getMeasuredHeight() + requiredVerticalPadding;
        }

        // measure content
        final MarginLayoutParams lp = (MarginLayoutParams) weightedChildView.getLayoutParams();
        // Add padding to totalLength that we are going to use for remaining space.
        totalLength += mPaddingTop + mPaddingBottom;

        int usedSpace = usedHeight + lp.topMargin + lp.bottomMargin;
        if (isContentFirstItem) {
            usedSpace += getPaddingTop();
        }
        if (isContentLastItem) {
            usedSpace += getPaddingBottom();
        // 2. generate measure spec for weightedChildView.
        final MarginLayoutParams lp = (MarginLayoutParams) weightedChildView.getLayoutParams();
        // height should be AT_MOST for non EXACT cases.
        final int childHeightMeasureMode =
                heightMode == MeasureSpec.EXACTLY ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST;
        final int childHeightMeasureSpec;

        // In excess mode, LinearLayout measures weighted children with remaining space. Otherwise,
        // it is measured with remaining space just like other children.
        if (lp.height == 0 && heightMode == MeasureSpec.EXACTLY) {
            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                    Math.max(0, availableHeight - totalLength), childHeightMeasureMode);
        } else {
            final int usedHeight = lp.topMargin + lp.bottomMargin + totalLength;
            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                    Math.max(0, availableHeight - usedHeight), childHeightMeasureMode);
        }
        final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin, lp.width);

        final int availableWidth = MeasureSpec.getSize(widthMeasureSpec);
        final int availableHeight = MeasureSpec.getSize(heightMeasureSpec);

        final int childWidthMeasureSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
                horizontalPaddings + lp.leftMargin + lp.rightMargin, lp.width);
        // 3. Measure weightedChildView with the remaining space.
        weightedChildView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

        // 2. Calculate remaining height for weightedChildView.
        final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                Math.max(0, availableHeight - usedSpace), MeasureSpec.AT_MOST);
        totalLength = Math.max(totalLength,
                totalLength + weightedChildView.getMeasuredHeight() + lp.topMargin
                        + lp.bottomMargin);

        // 3. Measure weightedChildView with the remaining remaining space.
        weightedChildView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        maxWidth = Math.max(maxWidth,
                weightedChildView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);

        final int totalUsedHeight = usedSpace + weightedChildView.getMeasuredHeight();

        final int measuredWidth;
        if (widthMode == MeasureSpec.EXACTLY) {
            measuredWidth = availableWidth;
        } else {
            measuredWidth = maxWidth + getPaddingStart() + getPaddingEnd();
        }

        final int measuredHeight;
        if (heightMode == MeasureSpec.EXACTLY) {
            measuredHeight = availableHeight;
        } else {
            measuredHeight = totalUsedHeight;
        }
        // Add padding to width
        maxWidth += getPaddingLeft() + getPaddingRight();

        // 4. Set the container size
        setMeasuredDimension(
                resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth),
                        widthMeasureSpec),
                Math.max(getSuggestedMinimumHeight(), measuredHeight));
        // Resolve final dimensions
        final int finalWidth = resolveSizeAndState(Math.max(maxWidth, getSuggestedMinimumWidth()),
                widthMeasureSpec, 0);
        final int finalHeight = resolveSizeAndState(
                Math.max(totalLength, getSuggestedMinimumHeight()), heightMeasureSpec, 0);
        setMeasuredDimension(finalWidth, finalHeight);
    }

    @NonNull
+107 −87
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.flags.Flags;

import androidx.test.InstrumentationRegistry;
@@ -73,7 +74,7 @@ public class NotificationOptimizedLinearLayoutComparisonTest {

    private static final int[] LAYOUT_PARAMS = {MATCH_PARENT, WRAP_CONTENT, 0, 50};
    private static final int[] CHILD_WEIGHTS = {0, 1};

    private static final int[] CHILD_MARGINS = {0, 10, -10};
    @Rule
    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();

@@ -84,20 +85,75 @@ public class NotificationOptimizedLinearLayoutComparisonTest {
        mContext = InstrumentationRegistry.getTargetContext();
    }


    @Test
    public void test() throws Throwable {
        final List<View> controlChildren =
                new ArrayList<>();
        final List<View> testChildren =
                new ArrayList<>();

        final View controlChild1 = buildChildView();
        final View controlChild2 = buildChildView();
        controlChildren.add(controlChild1);
        controlChildren.add(controlChild2);

        final View testChild1 = buildChildView();
        final View testChild2 = buildChildView();
        testChildren.add(testChild1);
        testChildren.add(testChild2);

        final LinearLayout controlContainer = buildLayout(false, controlChildren);

        final LinearLayout testContainer = buildLayout(true, testChildren);

        final LinearLayout.LayoutParams firstChildLayoutParams = new LinearLayout.LayoutParams(0,
                0);
        final LinearLayout.LayoutParams secondChildLayoutParams = new LinearLayout.LayoutParams(0,
                0);
        controlChild1.setLayoutParams(firstChildLayoutParams);
        controlChild2.setLayoutParams(secondChildLayoutParams);
        testChild1.setLayoutParams(firstChildLayoutParams);
        testChild2.setLayoutParams(secondChildLayoutParams);

        for (int orientation : ORIENTATIONS) {
            for (int widthSpec : MEASURE_SPECS) {
                for (int heightSpec : MEASURE_SPECS) {
                    for (int firstChildGravity : GRAVITIES) {
                        for (int secondChildGravity : GRAVITIES) {
            controlContainer.setOrientation(orientation);
            testContainer.setOrientation(orientation);

            for (int firstChildLayoutWidth : LAYOUT_PARAMS) {
                firstChildLayoutParams.width = firstChildLayoutWidth;
                for (int firstChildLayoutHeight : LAYOUT_PARAMS) {
                    firstChildLayoutParams.height = firstChildLayoutHeight;

                    for (int secondChildLayoutWidth : LAYOUT_PARAMS) {
                        secondChildLayoutParams.width = secondChildLayoutWidth;
                        for (int secondChildLayoutHeight : LAYOUT_PARAMS) {
                            secondChildLayoutParams.height = secondChildLayoutHeight;

                            for (int firstChildMargin : CHILD_MARGINS) {
                                firstChildLayoutParams.setMargins(firstChildMargin,
                                        firstChildMargin, firstChildMargin, firstChildMargin);
                                for (int secondChildMargin : CHILD_MARGINS) {
                                    secondChildLayoutParams.setMargins(secondChildMargin,
                                            secondChildMargin, secondChildMargin,
                                            secondChildMargin);

                                    for (int firstChildGravity : GRAVITIES) {
                                        firstChildLayoutParams.gravity = firstChildGravity;
                                        for (int secondChildGravity : GRAVITIES) {
                                            secondChildLayoutParams.gravity = secondChildGravity;

                                            for (int firstChildWeight : CHILD_WEIGHTS) {
                                                firstChildLayoutParams.weight = firstChildWeight;
                                                for (int secondChildWeight : CHILD_WEIGHTS) {
                                                    executeTest(/*testSpec =*/createTestSpec(
                                                    secondChildLayoutParams.weight =
                                                            secondChildWeight;

                                                    for (int widthSpec : MEASURE_SPECS) {
                                                        for (int heightSpec : MEASURE_SPECS) {
                                                            executeTest(controlContainer,
                                                                    testContainer,
                                                                    createTestSpec(
                                                                            orientation,
                                                                            widthSpec, heightSpec,
                                                                            firstChildLayoutWidth,
@@ -107,7 +163,10 @@ public class NotificationOptimizedLinearLayoutComparisonTest {
                                                                            firstChildGravity,
                                                                            secondChildGravity,
                                                                            firstChildWeight,
                                                            secondChildWeight));
                                                                            secondChildWeight,
                                                                            firstChildMargin,
                                                                            secondChildMargin)
                                                            );
                                                        }
                                                    }
                                                }
@@ -115,53 +174,17 @@ public class NotificationOptimizedLinearLayoutComparisonTest {
                                        }
                                    }
                                }

                            }
                        }
                    }
                }
            }
        }
    }

    private void executeTest(TestSpec testSpec) {
        // GIVEN
        final List<View> controlChildren =
                new ArrayList<>();
        final List<View> testChildren =
                new ArrayList<>();

        controlChildren.add(
                buildChildView(
                        testSpec.mFirstChildLayoutWidth,
                        testSpec.mFirstChildLayoutHeight,
                        testSpec.mFirstChildGravity,
                        testSpec.mFirstChildWeight));
        controlChildren.add(
                buildChildView(
                        testSpec.mSecondChildLayoutWidth,
                        testSpec.mSecondChildLayoutHeight,
                        testSpec.mSecondChildGravity,
                        testSpec.mSecondChildWeight));

        testChildren.add(
                buildChildView(
                        testSpec.mFirstChildLayoutWidth,
                        testSpec.mFirstChildLayoutHeight,
                        testSpec.mFirstChildGravity,
                        testSpec.mFirstChildWeight));
        testChildren.add(
                buildChildView(
                        testSpec.mSecondChildLayoutWidth,
                        testSpec.mSecondChildLayoutHeight,
                        testSpec.mSecondChildGravity,
                        testSpec.mSecondChildWeight));

        final LinearLayout controlContainer = buildLayout(false,
                testSpec.mOrientation,
                controlChildren);

        final LinearLayout testContainer = buildLayout(true,
                testSpec.mOrientation,
                testChildren);

    private void executeTest(LinearLayout controlContainer, LinearLayout testContainer,
            TestSpec testSpec) {
        // WHEN
        controlContainer.measure(testSpec.mWidthSpec, testSpec.mHeightSpec);
        testContainer.measure(testSpec.mWidthSpec, testSpec.mHeightSpec);
@@ -171,6 +194,7 @@ public class NotificationOptimizedLinearLayoutComparisonTest {
        assertLayoutsEqual("Test Case:" + testSpec, controlContainer, testContainer);
    }


    private static class TestSpec {
        private final int mOrientation;
        private final int mWidthSpec;
@@ -183,6 +207,8 @@ public class NotificationOptimizedLinearLayoutComparisonTest {
        private final int mSecondChildGravity;
        private final int mFirstChildWeight;
        private final int mSecondChildWeight;
        private final int mFirstChildMargin;
        private final int mSecondChildMargin;

        TestSpec(
                int orientation,
@@ -195,7 +221,9 @@ public class NotificationOptimizedLinearLayoutComparisonTest {
                int firstChildGravity,
                int secondChildGravity,
                int firstChildWeight,
                int secondChildWeight) {
                int secondChildWeight,
                int firstChildMargin,
                int secondChildMargin) {
            mOrientation = orientation;
            mWidthSpec = widthSpec;
            mHeightSpec = heightSpec;
@@ -207,6 +235,8 @@ public class NotificationOptimizedLinearLayoutComparisonTest {
            mSecondChildGravity = secondChildGravity;
            mFirstChildWeight = firstChildWeight;
            mSecondChildWeight = secondChildWeight;
            mFirstChildMargin = firstChildMargin;
            mSecondChildMargin = secondChildMargin;
        }

        @Override
@@ -223,6 +253,8 @@ public class NotificationOptimizedLinearLayoutComparisonTest {
                    + ", mSecondChildGravity=" + mSecondChildGravity
                    + ", mFirstChildWeight=" + mFirstChildWeight
                    + ", mSecondChildWeight=" + mSecondChildWeight
                    + ", mFirstChildMargin=" + mFirstChildMargin
                    + ", mSecondChildMargin=" + mSecondChildMargin
                    + '}';
        }

@@ -246,15 +278,13 @@ public class NotificationOptimizedLinearLayoutComparisonTest {
        }
    }

    private LinearLayout buildLayout(boolean isNotificationOptimized,
            @LinearLayout.OrientationMode int orientation, List<View> children) {
    private LinearLayout buildLayout(boolean isNotificationOptimized, List<View> children) {
        final LinearLayout linearLayout;
        if (isNotificationOptimized) {
            linearLayout = new NotificationOptimizedLinearLayout(mContext);
        } else {
            linearLayout = new LinearLayout(mContext);
        }
        linearLayout.setOrientation(orientation);
        for (int i = 0; i < children.size(); i++) {
            linearLayout.addView(children.get(i));
        }
@@ -262,7 +292,8 @@ public class NotificationOptimizedLinearLayoutComparisonTest {
    }

    private void assertLayoutsEqual(String testCase, View controlView, View testView) {
        mExpect.withMessage("MeasuredWidths are not equal. Test Case:" + testCase)
        mExpect.withMessage(
                        "MeasuredWidths are not equal. Test Case:" + testCase)
                .that(testView.getMeasuredWidth()).isEqualTo(controlView.getMeasuredWidth());
        mExpect.withMessage("MeasuredHeights are not equal. Test Case:" + testCase)
                .that(testView.getMeasuredHeight()).isEqualTo(controlView.getMeasuredHeight());
@@ -286,23 +317,12 @@ public class NotificationOptimizedLinearLayoutComparisonTest {
        }
    }

    private static class TestView extends View {
        TestView(Context context) {
            super(context);
        }

        @Override
        public int getBaseline() {
            return 5;
        }
    }


    private TestSpec createTestSpec(int orientation,
            int widthSpec, int heightSpec,
            int firstChildLayoutWidth, int firstChildLayoutHeight, int secondChildLayoutWidth,
            int secondChildLayoutHeight, int firstChildGravity, int secondChildGravity,
            int firstChildWeight, int secondChildWeight) {
            int firstChildWeight, int secondChildWeight, int firstChildMargin,
            int secondChildMargin) {

        return new TestSpec(
                orientation,
@@ -314,16 +334,16 @@ public class NotificationOptimizedLinearLayoutComparisonTest {
                firstChildGravity,
                secondChildGravity,
                firstChildWeight,
                secondChildWeight);
                secondChildWeight,
                firstChildMargin,
                secondChildMargin);
    }

    private View buildChildView(int childLayoutWidth, int childLayoutHeight,
            int childGravity, int childWeight) {
        final View childView = new TestView(mContext);
        // Set desired size using LayoutParams
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(childLayoutWidth,
                childLayoutHeight, childWeight);
        params.gravity = childGravity;
    private View buildChildView() {
        final View childView = new TextView(mContext);
        // this is initial value. We are going to mutate this layout params during the test.
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(MATCH_PARENT,
                WRAP_CONTENT);
        childView.setLayoutParams(params);
        return childView;
    }