diff --git a/README.md b/README.md index db79b24ee04e8ce905d3667aba44a58a5efbdada..cf7d4fb6da987213b8cbbdc679299d35e71873fb 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,16 @@ Also you can specify the following attributes for the children of a FlexboxLayou `flexDirection` attribute as to which attribute imposes the size constraint along the main axis) regardless of the `layout_flexGrow` attribute. +* layout_wrapBefore + * This attribute forces a flex line wrapping. i.e. if this is set to `true` for a + flex item, the item will become the first item of a flex line. (A wrapping happens + regardless of the flex items being processed in the the previous flex line) + This attribute is ignored if the `flex_wrap` attribute is set to `nowrap`. + The equivalent attribute isn't defined in the original CSS Flexible Box Module + specification, but having this attribute is useful for Android developers to flatten + the layouts when building a grid like layout or for a situation where developers want + to put a new flex line to make a semantic difference from the previous one, etc. + ## Known differences from the original CSS specification This library tries to achieve the same capabilities of the original [Flexible Box specification](https://www.w3.org/TR/css-flexbox-1) as much as possible, @@ -194,6 +204,11 @@ equivalent attribute accepts percentage values, which can't be done through layout_width (or layout_height) for simplicity. +(4) `layout_wrapBefore` is introduced. + * The equivalent attribute doesn't exist in the CSS Flexible Box Module speicification, + but as explained above, Android developers will benefit by having this attribute for having + more control over when a wrapping happens. + ## How to make contributions Please read and follow the steps in [CONTRIBUTING.md](https://github.com/google/flexbox-layout/blob/master/CONTRIBUTING.md) diff --git a/app/src/main/java/com/google/android/apps/flexbox/FlexItem.java b/app/src/main/java/com/google/android/apps/flexbox/FlexItem.java index b5fb5462a02827ac3ee004e67e1f331fe310f2f3..0ab83bcf8ecc3aa49ee384cdbdbe926f16b2a445 100644 --- a/app/src/main/java/com/google/android/apps/flexbox/FlexItem.java +++ b/app/src/main/java/com/google/android/apps/flexbox/FlexItem.java @@ -76,6 +76,8 @@ public class FlexItem implements Parcelable { /** Maximum height in DP */ public int maxHeight; + public boolean wrapBefore; + public FlexItem() { } @@ -106,6 +108,7 @@ public class FlexItem implements Parcelable { dest.writeInt(this.minHeight); dest.writeInt(this.maxWidth); dest.writeInt(this.maxHeight); + dest.writeByte((byte) (wrapBefore ? 1 : 0)); } protected FlexItem(Parcel in) { @@ -129,6 +132,7 @@ public class FlexItem implements Parcelable { this.minHeight = in.readInt(); this.maxWidth = in.readInt(); this.maxHeight = in.readInt(); + this.wrapBefore = in.readByte() != 0; } public FlexboxLayout.LayoutParams toLayoutParams(Context context) { @@ -148,6 +152,7 @@ public class FlexItem implements Parcelable { lp.minHeight = Util.dpToPixel(context, minHeight); lp.maxWidth = Util.dpToPixel(context, maxWidth); lp.maxHeight = Util.dpToPixel(context, maxHeight); + lp.wrapBefore = wrapBefore; return lp; } @@ -174,6 +179,7 @@ public class FlexItem implements Parcelable { flexItem.minHeight = Util.pixelToDp(view.getContext(), lp.minHeight); flexItem.maxWidth = Util.pixelToDp(view.getContext(), lp.maxWidth); flexItem.maxHeight = Util.pixelToDp(view.getContext(), lp.maxHeight); + flexItem.wrapBefore = lp.wrapBefore; return flexItem; } diff --git a/app/src/main/java/com/google/android/apps/flexbox/FlexItemEditFragment.java b/app/src/main/java/com/google/android/apps/flexbox/FlexItemEditFragment.java index 356e9fcccffad72d7896fb113164b857df5371d0..bb1108f60a0d809b64a51f8b8f58682db5181440 100644 --- a/app/src/main/java/com/google/android/apps/flexbox/FlexItemEditFragment.java +++ b/app/src/main/java/com/google/android/apps/flexbox/FlexItemEditFragment.java @@ -43,6 +43,8 @@ import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; import android.widget.EditText; import android.widget.Spinner; import android.widget.TextView; @@ -228,6 +230,15 @@ public class FlexItemEditFragment extends DialogFragment { // No op } }); + + CheckBox wrapBeforeCheckBox = (CheckBox) view.findViewById(R.id.checkbox_wrap_before); + wrapBeforeCheckBox.setChecked(mFlexItem.wrapBefore); + wrapBeforeCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + mFlexItem.wrapBefore = isChecked; + } + }); int alignSelfPosition = arrayAdapter.getPosition(alignSelfAsString(mFlexItem.alignSelf)); alignSelfSpinner.setSelection(alignSelfPosition); diff --git a/app/src/main/res/layout/fragment_flex_item_edit.xml b/app/src/main/res/layout/fragment_flex_item_edit.xml index c6d58105cc0b3a9f6761a6104e0a2cdb63612087..46062ecd643ddcaccff62d76341089b8b6a64c03 100644 --- a/app/src/main/res/layout/fragment_flex_item_edit.xml +++ b/app/src/main/res/layout/fragment_flex_item_edit.xml @@ -20,7 +20,7 @@ limitations under the License. android:layout_marginEnd="@dimen/activity_horizontal_margin" android:layout_marginStart="@dimen/activity_horizontal_margin" android:layout_marginTop="@dimen/activity_vertical_margin" - android:layout_marginBottom="@dimen/activity_vertical_margin" > + android:layout_marginBottom="@dimen/activity_vertical_margin"> + app:layout_flexGrow="1"> + app:layout_flexGrow="1"> + app:layout_flexGrow="1"> + app:layout_flexGrow="1"> + + + android:orientation="horizontal" + app:layout_flexGrow="1"> + Min Height Max Width Max Height + Wrap Before Must be a non-negative float value Must be a non-negative integer value diff --git a/flexbox/src/androidTest/java/com/google/android/flexbox/test/FlexboxAndroidTest.java b/flexbox/src/androidTest/java/com/google/android/flexbox/test/FlexboxAndroidTest.java index 58e37ac8fc86299a4a545ed74f265838a99fb90a..5539b543634408bed7e8b4b6560198875dc49a7e 100644 --- a/flexbox/src/androidTest/java/com/google/android/flexbox/test/FlexboxAndroidTest.java +++ b/flexbox/src/androidTest/java/com/google/android/flexbox/test/FlexboxAndroidTest.java @@ -2757,6 +2757,83 @@ public class FlexboxAndroidTest { assertThat(textView3.getTop(), is(textView1.getBottom())); } + @Test + @FlakyTest(tolerance = TOLERANCE) + public void testWrapBefore() throws Throwable { + final FlexboxTestActivity activity = mActivityRule.getActivity(); + mActivityRule.runOnUiThread(new Runnable() { + @Override + public void run() { + activity.setContentView(R.layout.activity_wrap_before_test); + } + }); + FlexboxLayout flexboxLayout = (FlexboxLayout) activity.findViewById(R.id.flexbox_layout); + + // layout_wrapBefore for the text2 and text3 are set to true, the text2 and text3 should + // be the first item for each flex line. + assertThat(flexboxLayout.getFlexWrap(), is(FlexboxLayout.FLEX_WRAP_WRAP)); + assertThat(flexboxLayout.getFlexDirection(), is(FlexboxLayout.FLEX_DIRECTION_ROW)); + onView(withId(R.id.text1)).check(isTopAlignedWith(withId(R.id.flexbox_layout))); + onView(withId(R.id.text1)).check(isLeftAlignedWith(withId(R.id.flexbox_layout))); + onView(withId(R.id.text2)).check(isLeftAlignedWith(withId(R.id.flexbox_layout))); + onView(withId(R.id.text2)).check(isBelow(withId(R.id.text1))); + onView(withId(R.id.text3)).check(isLeftAlignedWith(withId(R.id.flexbox_layout))); + onView(withId(R.id.text3)).check(isBelow(withId(R.id.text2))); + TextView textView1 = (TextView) activity.findViewById(R.id.text1); + final TextView textView2 = (TextView) activity.findViewById(R.id.text2); + TextView textView3 = (TextView) activity.findViewById(R.id.text3); + assertThat(flexboxLayout.getHeight(), is(textView1.getHeight() + textView2.getHeight() + + textView3.getHeight())); + + mActivityRule.runOnUiThread(new Runnable() { + @Override + public void run() { + FlexboxLayout.LayoutParams lp2 = (FlexboxLayout.LayoutParams) + textView2.getLayoutParams(); + lp2.wrapBefore = false; + textView2.setLayoutParams(lp2); + } + }); + + onView(withId(R.id.text1)).check(isTopAlignedWith(withId(R.id.flexbox_layout))); + onView(withId(R.id.text1)).check(isLeftAlignedWith(withId(R.id.flexbox_layout))); + onView(withId(R.id.text2)).check(isTopAlignedWith(withId(R.id.flexbox_layout))); + onView(withId(R.id.text2)).check(isRightOf(withId(R.id.text1))); + onView(withId(R.id.text3)).check(isLeftAlignedWith(withId(R.id.flexbox_layout))); + onView(withId(R.id.text3)).check(isBelow(withId(R.id.text1))); + onView(withId(R.id.text3)).check(isBelow(withId(R.id.text2))); + assertThat(flexboxLayout.getHeight(), is(textView1.getHeight() + textView3.getHeight())); + } + + @Test + @FlakyTest(tolerance = TOLERANCE) + public void testWrapBefore_nowrap() throws Throwable { + final FlexboxTestActivity activity = mActivityRule.getActivity(); + mActivityRule.runOnUiThread(new Runnable() { + @Override + public void run() { + activity.setContentView(R.layout.activity_wrap_before_test); + FlexboxLayout flexboxLayout = (FlexboxLayout) activity.findViewById(R.id.flexbox_layout); + flexboxLayout.setFlexWrap(FlexboxLayout.FLEX_WRAP_NOWRAP); + } + }); + FlexboxLayout flexboxLayout = (FlexboxLayout) activity.findViewById(R.id.flexbox_layout); + + // layout_wrapBefore for the text2 and text3 are set to true, but the flexWrap is set to + // FLEX_WRAP_NOWRAP, three text views should not be wrapped. + assertThat(flexboxLayout.getFlexWrap(), is(FlexboxLayout.FLEX_WRAP_NOWRAP)); + assertThat(flexboxLayout.getFlexDirection(), is(FlexboxLayout.FLEX_DIRECTION_ROW)); + onView(withId(R.id.text1)).check(isTopAlignedWith(withId(R.id.flexbox_layout))); + onView(withId(R.id.text1)).check(isLeftAlignedWith(withId(R.id.flexbox_layout))); + onView(withId(R.id.text1)).check(isBottomAlignedWith(withId(R.id.flexbox_layout))); + onView(withId(R.id.text2)).check(isTopAlignedWith(withId(R.id.flexbox_layout))); + onView(withId(R.id.text2)).check(isRightOf(withId(R.id.text1))); + onView(withId(R.id.text2)).check(isBottomAlignedWith(withId(R.id.flexbox_layout))); + onView(withId(R.id.text3)).check(isTopAlignedWith(withId(R.id.flexbox_layout))); + onView(withId(R.id.text3)).check(isRightOf(withId(R.id.text2))); + onView(withId(R.id.text3)).check(isBottomAlignedWith(withId(R.id.flexbox_layout))); + } + private TextView createTextView(Context context, String text, int order) { TextView textView = new TextView(context); textView.setText(text); diff --git a/flexbox/src/androidTest/res/layout/activity_wrap_before_test.xml b/flexbox/src/androidTest/res/layout/activity_wrap_before_test.xml new file mode 100644 index 0000000000000000000000000000000000000000..0df906cd8a6478b681caaa9f8edd57eec81e7bf7 --- /dev/null +++ b/flexbox/src/androidTest/res/layout/activity_wrap_before_test.xml @@ -0,0 +1,43 @@ + + + + + + + + + \ No newline at end of file diff --git a/flexbox/src/main/java/com/google/android/flexbox/FlexboxLayout.java b/flexbox/src/main/java/com/google/android/flexbox/FlexboxLayout.java index fda0e4d42885532ce172fe84002ad132eec80591..7a2c868d6647fcf33bff1602465045fdf4a88b34 100644 --- a/flexbox/src/main/java/com/google/android/flexbox/FlexboxLayout.java +++ b/flexbox/src/main/java/com/google/android/flexbox/FlexboxLayout.java @@ -494,7 +494,7 @@ public class FlexboxLayout extends ViewGroup { child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); if (isWrapRequired(mFlexWrap, widthMode, widthSize, flexLine.mainSize, - child.getMeasuredWidth())) { + child.getMeasuredWidth(), lp)) { flexLine.mainSize += paddingEnd; mFlexLines.add(flexLine); @@ -645,7 +645,7 @@ public class FlexboxLayout extends ViewGroup { child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); if (isWrapRequired(mFlexWrap, heightMode, heightSize, flexLine.mainSize, - child.getMeasuredHeight())) { + child.getMeasuredHeight(), lp)) { flexLine.mainSize += paddingBottom; mFlexLines.add(flexLine); @@ -1354,14 +1354,21 @@ public class FlexboxLayout extends ViewGroup { * @param maxSize the max size along the main axis direction * @param currentLength the accumulated current length * @param childLength the length of a child view which is to be collected to the flex line + * @param lp the LayoutParams for the view being determined whether a new flex line + * is needed * @return {@code true} if a wrap is required, {@code false} otherwise * @see #getFlexWrap() * @see #setFlexWrap(int) */ private boolean isWrapRequired(int flexWrap, int mode, int maxSize, - int currentLength, int childLength) { - return flexWrap != FLEX_WRAP_NOWRAP && - (mode == MeasureSpec.EXACTLY || mode == MeasureSpec.AT_MOST) && + int currentLength, int childLength, LayoutParams lp) { + if (flexWrap == FLEX_WRAP_NOWRAP) { + return false; + } + if (lp.wrapBefore) { + return true; + } + return (mode == MeasureSpec.EXACTLY || mode == MeasureSpec.AT_MOST) && maxSize < currentLength + childLength; } @@ -1976,6 +1983,18 @@ public class FlexboxLayout extends ViewGroup { */ public int maxHeight = MAX_SIZE; + /** + * This attribute forces a flex line wrapping. i.e. if this is set to {@code true} for a + * flex item, the item will become the first item of the new flex line. (A wrapping happens + * regardless of the flex items being processed in the the previous flex line) + * This attribute is ignored if the flex_wrap attribute is set as nowrap. + * The equivalent attribute isn't defined in the original CSS Flexible Box Module + * specification, but having this attribute is useful for Android developers to flatten + * the layouts when building a grid like layout or for a situation where developers want + * to put a new flex line to make a semantic difference from the previous one, etc. + */ + public boolean wrapBefore; + public LayoutParams(Context context, AttributeSet attrs) { super(context, attrs); @@ -1998,6 +2017,7 @@ public class FlexboxLayout extends ViewGroup { MAX_SIZE); maxHeight = a.getDimensionPixelSize(R.styleable.FlexboxLayout_Layout_layout_maxHeight, MAX_SIZE); + wrapBefore = a.getBoolean(R.styleable.FlexboxLayout_Layout_layout_wrapBefore, false); a.recycle(); } diff --git a/flexbox/src/main/res/values/attrs.xml b/flexbox/src/main/res/values/attrs.xml index 461016f01d577e9f20dfcff04f776fdc52368b01..5159b32ad1a0a03deb5213fecc859809b864e301 100644 --- a/flexbox/src/main/res/values/attrs.xml +++ b/flexbox/src/main/res/values/attrs.xml @@ -104,5 +104,17 @@ limitations under the License. + + + \ No newline at end of file