diff --git a/app/src/main/java/com/google/android/apps/flexbox/MainActivity.java b/app/src/main/java/com/google/android/apps/flexbox/MainActivity.java index c591b07b8c57608110345eb2e8fdcb84efa17a4e..07a180f0370d7525819ee6a2a0d23ac7cf39f971 100644 --- a/app/src/main/java/com/google/android/apps/flexbox/MainActivity.java +++ b/app/src/main/java/com/google/android/apps/flexbox/MainActivity.java @@ -532,8 +532,6 @@ public class MainActivity extends AppCompatActivity View view = mFlexboxLayout.getChildAt(flexItem.index); FlexboxLayout.LayoutParams lp = flexItem.toLayoutParams(MainActivity.this); view.setLayoutParams(lp); - // TODO: Update the layout only related views - mFlexboxLayout.requestLayout(); } } 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 a8443bf0e55b14edddf6df31275a9a97cd0e8c13..bacd2e4b5083d5413c58b796ec5d5521a30c2807 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 @@ -157,6 +157,60 @@ public class FlexboxAndroidTest { is(String.valueOf(1))); } + @Test + @FlakyTest(tolerance = TOLERANCE) + public void testChangeOrder_fromChildSetLayoutParams() throws Throwable { + final FlexboxTestActivity activity = mActivityRule.getActivity(); + mActivityRule.runOnUiThread(new Runnable() { + @Override + public void run() { + activity.setContentView(R.layout.activity_order_test); + } + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + final FlexboxLayout flexboxLayout = (FlexboxLayout) activity + .findViewById(R.id.flexbox_layout); + assertThat(flexboxLayout.getChildCount(), is(4)); + // order: -1, index 1 + assertThat(((TextView) flexboxLayout.getReorderedChildAt(0)).getText().toString(), + is(String.valueOf(2))); + // order: 0, index 2 + assertThat(((TextView) flexboxLayout.getReorderedChildAt(1)).getText().toString(), + is(String.valueOf(3))); + // order: 1, index 3 + assertThat(((TextView) flexboxLayout.getReorderedChildAt(2)).getText().toString(), + is(String.valueOf(4))); + // order: 2, index 0 + assertThat(((TextView) flexboxLayout.getReorderedChildAt(3)).getText().toString(), + is(String.valueOf(1))); + + // By changing the order and calling the setLayoutParams, the reordered array in the + // FlexboxLayout (mReordereredIndices) will be recreated without adding a new View. + mActivityRule.runOnUiThread(new Runnable() { + @Override + public void run() { + View view1 = flexboxLayout.getChildAt(0); + FlexboxLayout.LayoutParams lp = (FlexboxLayout.LayoutParams) + view1.getLayoutParams(); + lp.order = -3; + view1.setLayoutParams(lp); + } + }); + // order: -3, index 0 + assertThat(((TextView) flexboxLayout.getReorderedChildAt(3)).getText().toString(), + is(String.valueOf(1))); + // order: -1, index 1 + assertThat(((TextView) flexboxLayout.getReorderedChildAt(0)).getText().toString(), + is(String.valueOf(2))); + // order: 0, index 2 + assertThat(((TextView) flexboxLayout.getReorderedChildAt(1)).getText().toString(), + is(String.valueOf(3))); + // order: 1, index 3 + assertThat(((TextView) flexboxLayout.getReorderedChildAt(2)).getText().toString(), + is(String.valueOf(4))); + } + @Test @FlakyTest(tolerance = TOLERANCE) public void testFlexWrap_wrap() throws Throwable { 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 9560f385ebe12aba1524bf27a7850fc81c7bdd96..93c7681d519214a8de5c78d28b3d2b4c3e8d2ba9 100644 --- a/flexbox/src/main/java/com/google/android/flexbox/FlexboxLayout.java +++ b/flexbox/src/main/java/com/google/android/flexbox/FlexboxLayout.java @@ -22,6 +22,7 @@ import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; +import android.util.SparseIntArray; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; @@ -30,9 +31,8 @@ import android.widget.RelativeLayout; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.SortedSet; -import java.util.TreeSet; /** * A layout that arranges its children in a way its attributes can be specified like the @@ -210,6 +210,13 @@ public class FlexboxLayout extends ViewGroup { */ private int[] mReorderedIndices; + /** + * Caches the {@link LayoutParams#order} attributes for children views. + * Key: the index of the view ({@link #mReorderedIndices} isn't taken into account) + * Value: the value for the order attribute + */ + private SparseIntArray mOrderCache; + private List mFlexLines = new ArrayList<>(); public FlexboxLayout(Context context) { @@ -237,7 +244,9 @@ public class FlexboxLayout extends ViewGroup { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); - mReorderedIndices = createReorderedIndices(); + if (isOrderChangedFromLastMeasurement()) { + mReorderedIndices = createReorderedIndices(); + } // TODO: Only calculate the children views which are affected from the last measure. switch (mFlexDirection) { @@ -271,31 +280,122 @@ public class FlexboxLayout extends ViewGroup { return getChildAt(mReorderedIndices[index]); } + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + // Create an array for the reordered indices before the View is added in the parent + // ViewGroup since otherwise reordered indices won't be in effect before the + // FlexboxLayout's onMeasure is called. + // Because requestLayout is requested in the super.addView method. + mReorderedIndices = createReorderedIndices(child, index, params); + super.addView(child, index, params); + } + /** * Create an array, which indicates the reordered indices that {@link LayoutParams#order} - * attributes are taken into account. + * attributes are taken into account. This method takes a View before that is added as the + * parent ViewGroup's children. * + * @param viewBeforeAdded the View instance before added to the array of children + * Views of the parent ViewGroup + * @param indexForViewBeforeAdded the index for the View before added to the array of the + * parent ViewGroup + * @param paramsForViewBeforeAdded the layout parameters for the View before added to the array + * of the parent ViewGroup * @return an array which have the reordered indices */ + private int[] createReorderedIndices(View viewBeforeAdded, int indexForViewBeforeAdded, + ViewGroup.LayoutParams paramsForViewBeforeAdded) { + int childCount = getChildCount(); + List orders = createOrders(childCount); + Order orderForViewToBeAdded = new Order(); + if (viewBeforeAdded != null + && paramsForViewBeforeAdded instanceof FlexboxLayout.LayoutParams) { + orderForViewToBeAdded.order = ((LayoutParams) paramsForViewBeforeAdded).order; + } else { + orderForViewToBeAdded.order = LayoutParams.ORDER_DEFAULT; + } + + if (indexForViewBeforeAdded == -1 || indexForViewBeforeAdded == childCount) { + orderForViewToBeAdded.index = childCount; + } else if (indexForViewBeforeAdded < getChildCount()) { + orderForViewToBeAdded.index = indexForViewBeforeAdded; + for (int i = indexForViewBeforeAdded; i < childCount; i++) { + orders.get(i).index++; + } + } else { + // This path is not expected since OutOfBoundException will be thrown in the ViewGroup + // But setting the index for fail-safe + orderForViewToBeAdded.index = childCount; + } + orders.add(orderForViewToBeAdded); + + return sortOrdersIntoReorderedIndices(childCount + 1, orders); + } + + /** + * Create an array, which indicates the reordered indices that {@link LayoutParams#order} + * attributes are taken into account. + * + * @return @return an array which have the reordered indices + */ private int[] createReorderedIndices() { - int[] reorderedIndex = new int[getChildCount()]; - int count = getChildCount(); - SortedSet orderSet = new TreeSet<>(); - for (int i = 0; i < count; i++) { + int childCount = getChildCount(); + List orders = createOrders(childCount); + return sortOrdersIntoReorderedIndices(childCount, orders); + } + + private int[] sortOrdersIntoReorderedIndices(int childCount, List orders) { + Collections.sort(orders); + if (mOrderCache == null) { + mOrderCache = new SparseIntArray(childCount); + } + mOrderCache.clear(); + int[] reorderedIndices = new int[childCount]; + int i = 0; + for (Order order : orders) { + reorderedIndices[i] = order.index; + mOrderCache.append(i, order.order); + i++; + } + return reorderedIndices; + } + + @NonNull + private List createOrders(int childCount) { + List orders = new ArrayList<>(); + for (int i = 0; i < childCount; i++) { View child = getChildAt(i); LayoutParams params = (LayoutParams) child.getLayoutParams(); Order order = new Order(); order.order = params.order; order.index = i; - orderSet.add(order); + orders.add(order); } + return orders; + } - int i = 0; - for (Order order : orderSet) { - reorderedIndex[i] = order.index; - i++; + /** + * Returns if any of the children's {@link LayoutParams#order} attributes are changed + * from the last measurement. + * + * @return {@code true} if changed from the last measurement, {@code false} otherwise. + */ + private boolean isOrderChangedFromLastMeasurement() { + int childCount = getChildCount(); + if (mOrderCache.size() != childCount) { + return true; + } + for (int i = 0; i < childCount; i++) { + View view = getChildAt(i); + if (view == null) { + continue; + } + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + if (lp.order != mOrderCache.get(i)) { + return true; + } } - return reorderedIndex; + return false; } /**