diff --git a/litho-it/src/test/java/com/facebook/litho/sections/SectionTreeTest.java b/litho-it/src/test/java/com/facebook/litho/sections/SectionTreeTest.java index 734ca833d5b..e48356a8250 100644 --- a/litho-it/src/test/java/com/facebook/litho/sections/SectionTreeTest.java +++ b/litho-it/src/test/java/com/facebook/litho/sections/SectionTreeTest.java @@ -1125,5 +1125,8 @@ public void requestFocusWithOffset(int index, int offset) {} public boolean supportsBackgroundChangeSets() { return mSupportsBackgroundChangeSets; } + + @Override + public void changeConfig(DynamicConfig dynamicConfig) {} } } diff --git a/litho-it/src/test/java/com/facebook/litho/widget/RecyclerBinderTest.java b/litho-it/src/test/java/com/facebook/litho/widget/RecyclerBinderTest.java index c2965d5a848..1b522147df0 100644 --- a/litho-it/src/test/java/com/facebook/litho/widget/RecyclerBinderTest.java +++ b/litho-it/src/test/java/com/facebook/litho/widget/RecyclerBinderTest.java @@ -2526,6 +2526,52 @@ public void testInsertAsync_AsyncMode() { assertThat(holder.isTreeValid()).isTrue(); } + @Test + public void testInsertAsync_PolicyChangeMode() { + final RecyclerBinder recyclerBinder = + new RecyclerBinder.Builder().rangeRatio(RANGE_RATIO).build(mComponentContext); + recyclerBinder.setCommitPolicy(RecyclerBinder.CommitPolicy.LAYOUT_BEFORE_INSERT); + + final Component component = + TestDrawableComponent.create(mComponentContext).widthPx(100).heightPx(100).build(); + final ComponentRenderInfo renderInfo = + ComponentRenderInfo.create().component(component).build(); + + recyclerBinder.measure( + new Size(), makeSizeSpec(1000, EXACTLY), makeSizeSpec(1000, EXACTLY), null); + recyclerBinder.insertItemAtAsync(0, renderInfo); + + final Component secondComponent = + TestDrawableComponent.create(mComponentContext).widthPx(100).heightPx(100).build(); + final ComponentRenderInfo secondRenderInfo = + ComponentRenderInfo.create().component(secondComponent).build(); + recyclerBinder.setCommitPolicy(RecyclerBinder.CommitPolicy.IMMEDIATE); + recyclerBinder.insertItemAtAsync(1, secondRenderInfo); + + recyclerBinder.notifyChangeSetCompleteAsync(true, NO_OP_CHANGE_SET_COMPLETE_CALLBACK); + + assertThat(recyclerBinder.getItemCount()).isEqualTo(0); + assertThat(recyclerBinder.getRangeCalculationResult()).isNull(); + + mLayoutThreadShadowLooper.runToEndOfTasks(); + + assertThat(recyclerBinder.getItemCount()).isEqualTo(2); + assertThat(recyclerBinder.getRangeCalculationResult()).isNotNull(); + + final ComponentTreeHolder holder = recyclerBinder.getComponentTreeHolderAt(0); + assertThat(holder.getRenderInfo().getComponent()).isEqualTo(component); + assertThat(holder.hasCompletedLatestLayout()).isTrue(); + assertThat(holder.isTreeValid()).isTrue(); + + final Component thirdComponent = + TestDrawableComponent.create(mComponentContext).widthPx(100).heightPx(100).build(); + final ComponentRenderInfo thirdRenderInfo = + ComponentRenderInfo.create().component(thirdComponent).build(); + recyclerBinder.insertItemAtAsync(2, thirdRenderInfo); + recyclerBinder.notifyChangeSetCompleteAsync(true, NO_OP_CHANGE_SET_COMPLETE_CALLBACK); + assertThat(recyclerBinder.getItemCount()).isEqualTo(3); + } + @Test public void testMultipleInsertAsyncs_AsyncMode() { final RecyclerBinder recyclerBinder = diff --git a/litho-sections-core/src/main/java/com/facebook/litho/sections/BatchedTarget.java b/litho-sections-core/src/main/java/com/facebook/litho/sections/BatchedTarget.java index 7863ac11095..5adb2232220 100644 --- a/litho-sections-core/src/main/java/com/facebook/litho/sections/BatchedTarget.java +++ b/litho-sections-core/src/main/java/com/facebook/litho/sections/BatchedTarget.java @@ -171,6 +171,11 @@ public boolean supportsBackgroundChangeSets() { return mTarget.supportsBackgroundChangeSets(); } + @Override + public void changeConfig(DynamicConfig dynamicConfig) { + mTarget.changeConfig(dynamicConfig); + } + private void maybeLogRequestFocusWithOffset(int index, int offset) { if (ENABLE_LOGGER && mComponentInfoSparseArray.size() != 0) { mSectionsDebugLogger.logRequestFocusWithOffset( diff --git a/litho-sections-core/src/main/java/com/facebook/litho/sections/SectionTree.java b/litho-sections-core/src/main/java/com/facebook/litho/sections/SectionTree.java index 55536b61a6f..de724fdaf42 100644 --- a/litho-sections-core/src/main/java/com/facebook/litho/sections/SectionTree.java +++ b/litho-sections-core/src/main/java/com/facebook/litho/sections/SectionTree.java @@ -55,6 +55,7 @@ import com.facebook.litho.sections.config.SectionsConfiguration; import com.facebook.litho.sections.logger.SectionsDebugLogger; import com.facebook.litho.widget.ChangeSetCompleteCallback; +import com.facebook.litho.widget.RecyclerBinder.CommitPolicy; import com.facebook.litho.widget.RenderInfo; import com.facebook.litho.widget.SectionsDebug; import com.facebook.litho.widget.SmoothScrollAlignmentType; @@ -142,6 +143,18 @@ void notifyChangeSetComplete( * @return whether this target supports applying change sets from a background thread. */ boolean supportsBackgroundChangeSets(); + + /** Notify this target that a new set of configurations is applied. */ + void changeConfig(DynamicConfig dynamicConfig); + + class DynamicConfig { + + public final @CommitPolicy int mChangeSetsCommitPolicy; + + public DynamicConfig(@CommitPolicy int changeSetsCommitPolicy) { + mChangeSetsCommitPolicy = changeSetsCommitPolicy; + } + } } private static final String EMPTY_STRING = ""; @@ -426,6 +439,20 @@ public void setLoadEventsHandler(LoadEventsHandler loadEventsHandler) { mLoadEventsHandler = loadEventsHandler; } + /** + * Set a new set of configurations to the {@link Target}. Only those allowed to be modified will + * be included in {@link Target.DynamicConfig}. + */ + public void setTargetConfig(Target.DynamicConfig dynamicConfig) { + if (mUseBackgroundChangeSets) { + synchronized (this) { + // applyChangeSetsToTargetUnchecked() in background will be computed in a synchronized + // block, and the config is only updated after the ongoing computation is completed. + mTarget.changeConfig(dynamicConfig); + } + } + } + private void refreshRecursive(Section section) { section.refresh(section.getScopedContext()); diff --git a/litho-sections-widget/src/main/java/com/facebook/litho/sections/widget/RecyclerCollectionComponentSpec.java b/litho-sections-widget/src/main/java/com/facebook/litho/sections/widget/RecyclerCollectionComponentSpec.java index 9a2e85fc289..da315f01a9c 100644 --- a/litho-sections-widget/src/main/java/com/facebook/litho/sections/widget/RecyclerCollectionComponentSpec.java +++ b/litho-sections-widget/src/main/java/com/facebook/litho/sections/widget/RecyclerCollectionComponentSpec.java @@ -63,6 +63,7 @@ import com.facebook.litho.widget.PTRRefreshEvent; import com.facebook.litho.widget.Recycler; import com.facebook.litho.widget.RecyclerBinder; +import com.facebook.litho.widget.RecyclerBinder.CommitPolicy; import com.facebook.litho.widget.RecyclerEventsController; import com.facebook.litho.widget.StickyHeaderControllerFactory; import com.facebook.litho.widget.ViewportInfo; @@ -407,6 +408,14 @@ static void onScroll( sectionTree.requestFocusOnRoot(position); } + @OnTrigger(RecyclerDynamicConfigEvent.class) + static void onRecyclerConfigChanged( + ComponentContext c, + @FromTrigger @CommitPolicy int commitPolicy, + @State SectionTree sectionTree) { + sectionTree.setTargetConfig(new SectionTree.Target.DynamicConfig(commitPolicy)); + } + @OnDetached static void onDetached(ComponentContext c, @State Binder binder) { binder.detach(); diff --git a/litho-sections-widget/src/main/java/com/facebook/litho/sections/widget/RecyclerDynamicConfigEvent.java b/litho-sections-widget/src/main/java/com/facebook/litho/sections/widget/RecyclerDynamicConfigEvent.java new file mode 100644 index 00000000000..f0eaa7329a6 --- /dev/null +++ b/litho-sections-widget/src/main/java/com/facebook/litho/sections/widget/RecyclerDynamicConfigEvent.java @@ -0,0 +1,28 @@ +/* + * Copyright 2019-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.litho.sections.widget; + +import com.facebook.litho.annotations.Event; +import com.facebook.litho.widget.RecyclerBinder.CommitPolicy; + +/** + * An event that is triggered when a new set of configurations, which are allowed to be dynamic, is + * applied to {@link com.facebook.litho.sections.SectionTree.Target}. + */ +@Event +public class RecyclerDynamicConfigEvent { + public @CommitPolicy int commitPolicy; +} diff --git a/litho-sections-widget/src/main/java/com/facebook/litho/sections/widget/SectionBinderTarget.java b/litho-sections-widget/src/main/java/com/facebook/litho/sections/widget/SectionBinderTarget.java index 719ef4d4bf6..04c55ac3a10 100644 --- a/litho-sections-widget/src/main/java/com/facebook/litho/sections/widget/SectionBinderTarget.java +++ b/litho-sections-widget/src/main/java/com/facebook/litho/sections/widget/SectionBinderTarget.java @@ -202,6 +202,11 @@ public boolean supportsBackgroundChangeSets() { return mUseBackgroundChangeSets; } + @Override + public void changeConfig(DynamicConfig dynamicConfig) { + mRecyclerBinder.setCommitPolicy(dynamicConfig.mChangeSetsCommitPolicy); + } + @Override public void detach() { mRecyclerBinder.detach(); diff --git a/litho-testing/src/main/java/com/facebook/litho/testing/sections/TestTarget.java b/litho-testing/src/main/java/com/facebook/litho/testing/sections/TestTarget.java index 3acde651231..18217083a04 100644 --- a/litho-testing/src/main/java/com/facebook/litho/testing/sections/TestTarget.java +++ b/litho-testing/src/main/java/com/facebook/litho/testing/sections/TestTarget.java @@ -197,6 +197,9 @@ public boolean supportsBackgroundChangeSets() { return false; } + @Override + public void changeConfig(DynamicConfig dynamicConfig) {} + public void clear() { mOperations.clear(); mNumChanges = 0; diff --git a/litho-widget/src/main/java/com/facebook/litho/widget/RecyclerBinder.java b/litho-widget/src/main/java/com/facebook/litho/widget/RecyclerBinder.java index fe371aeac2f..0e20b92a9c9 100644 --- a/litho-widget/src/main/java/com/facebook/litho/widget/RecyclerBinder.java +++ b/litho-widget/src/main/java/com/facebook/litho/widget/RecyclerBinder.java @@ -34,6 +34,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; +import androidx.annotation.AnyThread; import androidx.annotation.IntDef; import androidx.annotation.UiThread; import androidx.annotation.VisibleForTesting; @@ -3273,8 +3274,8 @@ private int getActualChildrenHeightSpec(final ComponentTreeHolder treeHolder) { return mLayoutInfo.getChildHeightSpec(mLastHeightSpec, treeHolder.getRenderInfo()); } - @VisibleForTesting - void setCommitPolicy(@CommitPolicy int commitPolicy) { + @AnyThread + public void setCommitPolicy(@CommitPolicy int commitPolicy) { mCommitPolicy = commitPolicy; } @@ -3310,7 +3311,7 @@ private AsyncInsertOperation createAsyncInsertOperation(int position, RenderInfo */ @IntDef({CommitPolicy.IMMEDIATE, CommitPolicy.LAYOUT_BEFORE_INSERT}) @Retention(RetentionPolicy.SOURCE) - @interface CommitPolicy { + public @interface CommitPolicy { int IMMEDIATE = 0; int LAYOUT_BEFORE_INSERT = 1; }