From 92943012e0db9649bd5c7e3fbea72e5806d27543 Mon Sep 17 00:00:00 2001 From: Varun Gupta Date: Wed, 27 Apr 2022 10:28:13 -0700 Subject: [PATCH 1/9] Add support for Async Layout inflation with EPoxy --- blessedDeps.gradle | 2 + .../com/airbnb/epoxy/AsyncFrameLayout.java | 49 ++++++ .../com/airbnb/epoxy/BaseEpoxyAdapter.java | 25 ++- .../airbnb/epoxy/EpoxyControllerAdapter.java | 6 +- .../airbnb/epoxy/EpoxyVisibilityTracker.kt | 12 +- kotlinsample/build.gradle | 1 + .../airbnb/epoxy/kotlinsample/MainActivity.kt | 21 +++ .../models/AsyncItemCustomView.kt | 142 ++++++++++++++++++ .../res/layout/async_custom_view_item.xml | 17 +++ 9 files changed, 267 insertions(+), 8 deletions(-) create mode 100644 epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncFrameLayout.java create mode 100644 kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt create mode 100644 kotlinsample/src/main/res/layout/async_custom_view_item.xml diff --git a/blessedDeps.gradle b/blessedDeps.gradle index f8066624f9..966515fa70 100644 --- a/blessedDeps.gradle +++ b/blessedDeps.gradle @@ -37,6 +37,7 @@ rootProject.ext.ANDROID_DATA_BINDING = "1.3.1" rootProject.ext.ANDROID_RUNTIME_VERSION = "4.1.1.4" rootProject.ext.ANDROID_TEST_RUNNER = "1.4.0" rootProject.ext.ASSERTJ_VERSION = "1.7.1" +rootProject.ext.ASYNCLAYOUTINFLATOR_VERSION = "1.0.0" rootProject.ext.AUTO_VALUE_VERSION = "1.7.4" rootProject.ext.GLIDE_VERSION = "4.12.0" rootProject.ext.GOOGLE_TESTING_COMPILE_VERSION = "0.19" @@ -78,6 +79,7 @@ rootProject.ext.deps = [ androidTestRules : "androidx.test:rules:$ANDROID_TEST_RUNNER", androidTestRunner : "androidx.test:runner:$ANDROID_TEST_RUNNER", assertj : "org.assertj:assertj-core:$ASSERTJ_VERSION", + asyncLayoutInflater : "androidx.asynclayoutinflater:asynclayoutinflater:$ASYNCLAYOUTINFLATOR_VERSION", autoValue : "com.google.auto.value:auto-value:$AUTO_VALUE_VERSION", composeMaterial : "androidx.compose.material:material:$COMPOSE_VERSION", composeUi : "androidx.compose.ui:ui:$COMPOSE_VERSION", diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncFrameLayout.java b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncFrameLayout.java new file mode 100644 index 0000000000..e860e5737d --- /dev/null +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncFrameLayout.java @@ -0,0 +1,49 @@ +package com.airbnb.epoxy; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.Nullable; + +/** + * Base class to support Async layout inflation with EPoxy + */ +public abstract class AsyncFrameLayout extends FrameLayout { + protected boolean isInflated = false; + protected List pendingFunctions = new ArrayList<>(); + + public AsyncFrameLayout(Context context) { + super(context); + } + + public AsyncFrameLayout(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public AsyncFrameLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + /** + * Call this method once the inflation is completed + */ + protected void onInflationComplete() { + isInflated = true; + for (Runnable runnable: pendingFunctions) { + runnable.run(); + } + pendingFunctions.clear(); + } + + public void executeWhenInflated(Runnable runnable) { + if (isInflated) { + runnable.run(); + } else { + pendingFunctions.add(runnable); + } + } +} diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/BaseEpoxyAdapter.java b/epoxy-adapter/src/main/java/com/airbnb/epoxy/BaseEpoxyAdapter.java index 9b9e32394a..e89d6f511d 100644 --- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/BaseEpoxyAdapter.java +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/BaseEpoxyAdapter.java @@ -108,6 +108,11 @@ public void onBindViewHolder(EpoxyViewHolder holder, int position) { @Override public void onBindViewHolder(EpoxyViewHolder holder, int position, List payloads) { + executeWhenInflated(holder, + () -> onBindViewHolderInternal(holder, holder.getBindingAdapterPosition(), payloads)); + } + + private void onBindViewHolderInternal(EpoxyViewHolder holder, int position, List payloads) { EpoxyModel modelToShow = getModelForPosition(position); EpoxyModel previouslyBoundModel = null; @@ -209,15 +214,19 @@ public boolean onFailedToRecycleView(EpoxyViewHolder holder) { @CallSuper @Override public void onViewAttachedToWindow(EpoxyViewHolder holder) { - //noinspection unchecked,rawtypes - ((EpoxyModel) holder.getModel()).onViewAttachedToWindow(holder.objectToBind()); + executeWhenInflated(holder, + () -> + //noinspection unchecked,rawtypes + ((EpoxyModel) holder.getModel()).onViewAttachedToWindow(holder.objectToBind())); } @CallSuper @Override public void onViewDetachedFromWindow(EpoxyViewHolder holder) { - //noinspection unchecked,rawtypes - ((EpoxyModel) holder.getModel()).onViewDetachedFromWindow(holder.objectToBind()); + executeWhenInflated(holder, + () -> + //noinspection unchecked,rawtypes + ((EpoxyModel) holder.getModel()).onViewDetachedFromWindow(holder.objectToBind())); } public void onSaveInstanceState(Bundle outState) { @@ -339,4 +348,12 @@ public boolean isStickyHeader(int position) { } //endregion + + protected void executeWhenInflated(EpoxyViewHolder holder, Runnable runnable) { + if (holder.itemView instanceof AsyncFrameLayout) { + ((AsyncFrameLayout)holder.itemView).executeWhenInflated(runnable); + } else { + runnable.run(); + } + } } diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyControllerAdapter.java b/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyControllerAdapter.java index e8dd9e70d1..296170b91d 100644 --- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyControllerAdapter.java +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyControllerAdapter.java @@ -125,13 +125,15 @@ public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { @Override public void onViewAttachedToWindow(@NonNull EpoxyViewHolder holder) { super.onViewAttachedToWindow(holder); - epoxyController.onViewAttachedToWindow(holder, holder.getModel()); + executeWhenInflated(holder, + () -> epoxyController.onViewAttachedToWindow(holder, holder.getModel())); } @Override public void onViewDetachedFromWindow(@NonNull EpoxyViewHolder holder) { super.onViewDetachedFromWindow(holder); - epoxyController.onViewDetachedFromWindow(holder, holder.getModel()); + executeWhenInflated(holder, + () -> epoxyController.onViewDetachedFromWindow(holder, holder.getModel())); } @Override diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyVisibilityTracker.kt b/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyVisibilityTracker.kt index 16b6b731d7..0e9ad87638 100644 --- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyVisibilityTracker.kt +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyVisibilityTracker.kt @@ -7,8 +7,6 @@ import androidx.annotation.IdRes import androidx.annotation.IntRange import androidx.recyclerview.widget.RecyclerView import com.airbnb.viewmodeladapter.R -import java.util.ArrayList -import java.util.HashMap /** * A simple way to track visibility events on [com.airbnb.epoxy.EpoxyModel]. @@ -206,6 +204,16 @@ class EpoxyVisibilityTracker { * @param eventOriginForDebug a debug strings used for logs */ private fun processChild(child: View, detachEvent: Boolean, eventOriginForDebug: String) { + if (child is AsyncFrameLayout) { + child.executeWhenInflated { + processChildInternal(child, detachEvent, eventOriginForDebug) + } + } else { + processChildInternal(child, detachEvent, eventOriginForDebug) + } + } + + private fun processChildInternal(child: View, detachEvent: Boolean, eventOriginForDebug: String) { // Only if attached val recyclerView = attachedRecyclerView ?: return diff --git a/kotlinsample/build.gradle b/kotlinsample/build.gradle index 49743e42d8..7f46ff60b1 100644 --- a/kotlinsample/build.gradle +++ b/kotlinsample/build.gradle @@ -30,6 +30,7 @@ android { dependencies { implementation rootProject.deps.androidRecyclerView implementation rootProject.deps.androidAppcompat + implementation rootProject.deps.asyncLayoutInflater implementation project(':epoxy-databinding') implementation project(':epoxy-adapter') implementation project(':epoxy-annotations') diff --git a/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/MainActivity.kt b/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/MainActivity.kt index d191b3c1c6..4c630726c3 100644 --- a/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/MainActivity.kt +++ b/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/MainActivity.kt @@ -13,6 +13,7 @@ import com.airbnb.epoxy.group import com.airbnb.epoxy.kotlinsample.helpers.carouselNoSnapBuilder import com.airbnb.epoxy.kotlinsample.models.ItemDataClass import com.airbnb.epoxy.kotlinsample.models.ItemViewBindingDataClass +import com.airbnb.epoxy.kotlinsample.models.asyncItemCustomView import com.airbnb.epoxy.kotlinsample.models.carouselItemCustomView import com.airbnb.epoxy.kotlinsample.models.coloredSquareView import com.airbnb.epoxy.kotlinsample.models.decoratedLinearGroup @@ -141,6 +142,16 @@ class MainActivity : AppCompatActivity() { } } + asyncItemCustomView { + id("custom view $i") + color(Color.GREEN) + title("Async Row - Open sticky header activity") + listener { _ -> + Toast.makeText(this@MainActivity, "clicked", Toast.LENGTH_LONG).show() + startActivity(Intent(this@MainActivity, StickyHeaderActivity::class.java)) + } + } + itemCustomView { id("custom view $i") color(Color.MAGENTA) @@ -151,6 +162,16 @@ class MainActivity : AppCompatActivity() { } } + asyncItemCustomView { + id("custom view $i") + color(Color.MAGENTA) + title("Async Row - Open Drag and Dropt activity") + listener { _ -> + Toast.makeText(this@MainActivity, "clicked", Toast.LENGTH_LONG).show() + startActivity(Intent(this@MainActivity, DragAndDropActivity::class.java)) + } + } + itemEpoxyHolder { id("view holder $i") title("this is a View Holder item") diff --git a/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt b/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt new file mode 100644 index 0000000000..10cad6e177 --- /dev/null +++ b/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt @@ -0,0 +1,142 @@ +package com.airbnb.epoxy.kotlinsample.models + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.util.Log +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import androidx.annotation.ColorInt +import com.airbnb.epoxy.AfterPropsSet +import com.airbnb.epoxy.AsyncFrameLayout +import com.airbnb.epoxy.CallbackProp +import com.airbnb.epoxy.ModelProp +import com.airbnb.epoxy.ModelView +import com.airbnb.epoxy.OnViewRecycled +import com.airbnb.epoxy.OnVisibilityChanged +import com.airbnb.epoxy.OnVisibilityStateChanged +import com.airbnb.epoxy.TextProp +import com.airbnb.epoxy.VisibilityState +import com.airbnb.epoxy.kotlinsample.R +import androidx.asynclayoutinflater.view.AsyncLayoutInflater + +// The ModelView annotation is used on Views to have models generated from those views. +// This is pretty straightforward with Kotlin, but properties need some special handling. +@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT) +class AsyncItemCustomView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : AsyncFrameLayout(context, attrs, defStyleAttr) { + + private val onVisibilityEventDrawable = OnVisibilityEventDrawable(context) + + private lateinit var textView: TextView + + init { + AsyncLayoutInflater(context).inflate(R.layout.async_custom_view_item, this) { + view, _, _ -> + addView(view) + textView = (findViewById(R.id.title)) + textView.setCompoundDrawables(null, null, onVisibilityEventDrawable, null) + textView.compoundDrawablePadding = (4 * resources.displayMetrics.density).toInt() + onInflationComplete() + } + } + + // You can annotate your methods with @ModelProp + // A default model property value can be set by using Kotlin default arguments, but you + // must use JvmOverloads for Epoxy to handle it correctly. + @JvmOverloads + @ModelProp + fun color(@ColorInt color: Int = Color.RED) { + textView.setTextColor(color) + } + + // Or if you need to store data in properties there are two options + + // 1.You can make it nullable like this and annotate the setter + var listener: View.OnClickListener? = null + @CallbackProp set + + // 2. Or you can use lateinit + @TextProp + lateinit var title: CharSequence + + @AfterPropsSet + fun useProps() { + // This is optional, and is called after the annotated properties above are set. + // This is useful for using several properties in one method to guarantee they are all set first. + textView.text = title + textView.setOnClickListener(listener) + } + + @OnVisibilityStateChanged + fun onVisibilityStateChanged( + @VisibilityState.Visibility visibilityState: Int + ) { + when (visibilityState) { + VisibilityState.VISIBLE -> { + Log.d(TAG, "$title Visible") + onVisibilityEventDrawable.visible = true + } + VisibilityState.INVISIBLE -> { + Log.d(TAG, "$title Invisible") + onVisibilityEventDrawable.visible = false + } + VisibilityState.FOCUSED_VISIBLE -> { + Log.d(TAG, "$title FocusedVisible") + onVisibilityEventDrawable.focusedVisible = true + } + VisibilityState.UNFOCUSED_VISIBLE -> { + Log.d(TAG, "$title UnfocusedVisible") + onVisibilityEventDrawable.focusedVisible = false + } + VisibilityState.PARTIAL_IMPRESSION_VISIBLE -> { + Log.d(TAG, "$title PartialImpressionVisible") + onVisibilityEventDrawable.partialImpression = true + } + VisibilityState.PARTIAL_IMPRESSION_INVISIBLE -> { + Log.d(TAG, "$title PartialImpressionInVisible") + onVisibilityEventDrawable.partialImpression = false + } + VisibilityState.FULL_IMPRESSION_VISIBLE -> { + Log.d(TAG, "$title FullImpressionVisible") + onVisibilityEventDrawable.fullImpression = true + } + } + } + + @OnVisibilityChanged + fun onVisibilityChanged( + percentVisibleHeight: Float, + percentVisibleWidth: Float, + visibleHeight: Int, + visibleWidth: Int + ) { + Log.d( + TAG, + "$title onChanged ${percentVisibleHeight.toInt()} ${percentVisibleWidth.toInt()} " + + "$visibleHeight $visibleWidth ${System.identityHashCode( + this + )}" + ) + with(onVisibilityEventDrawable) { + if ((percentVisibleHeight < 100 || percentVisibleWidth < 100) && fullImpression) { + fullImpression = false + } + percentHeight = percentVisibleHeight + percentWidth = percentVisibleWidth + } + } + + @OnViewRecycled + fun clear() { + onVisibilityEventDrawable.reset() + } + + companion object { + private const val TAG = "ItemCustomView" + } +} diff --git a/kotlinsample/src/main/res/layout/async_custom_view_item.xml b/kotlinsample/src/main/res/layout/async_custom_view_item.xml new file mode 100644 index 0000000000..ba9e00d06c --- /dev/null +++ b/kotlinsample/src/main/res/layout/async_custom_view_item.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file From 389eae81534e4731f86126c7d5daadf9aed99d71 Mon Sep 17 00:00:00 2001 From: Varun Gupta Date: Tue, 3 May 2022 16:05:50 -0700 Subject: [PATCH 2/9] Use interface instead of abstract class for AsyncView --- .../com/airbnb/epoxy/AsyncFrameLayout.java | 49 ------------------- .../com/airbnb/epoxy/AsyncInflatedView.kt | 22 +++++++++ .../com/airbnb/epoxy/BaseEpoxyAdapter.java | 4 +- .../airbnb/epoxy/EpoxyVisibilityTracker.kt | 2 +- .../models/AsyncItemCustomView.kt | 8 ++- 5 files changed, 31 insertions(+), 54 deletions(-) delete mode 100644 epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncFrameLayout.java create mode 100644 epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncFrameLayout.java b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncFrameLayout.java deleted file mode 100644 index e860e5737d..0000000000 --- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncFrameLayout.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.airbnb.epoxy; - -import android.content.Context; -import android.util.AttributeSet; -import android.widget.FrameLayout; - -import java.util.ArrayList; -import java.util.List; - -import androidx.annotation.Nullable; - -/** - * Base class to support Async layout inflation with EPoxy - */ -public abstract class AsyncFrameLayout extends FrameLayout { - protected boolean isInflated = false; - protected List pendingFunctions = new ArrayList<>(); - - public AsyncFrameLayout(Context context) { - super(context); - } - - public AsyncFrameLayout(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - public AsyncFrameLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - /** - * Call this method once the inflation is completed - */ - protected void onInflationComplete() { - isInflated = true; - for (Runnable runnable: pendingFunctions) { - runnable.run(); - } - pendingFunctions.clear(); - } - - public void executeWhenInflated(Runnable runnable) { - if (isInflated) { - runnable.run(); - } else { - pendingFunctions.add(runnable); - } - } -} diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt new file mode 100644 index 0000000000..5443fe6061 --- /dev/null +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt @@ -0,0 +1,22 @@ +package com.airbnb.epoxy + +interface AsyncInflatedView { + var isInflated : Boolean + var pendingFunctions : ArrayList + + fun onInflationComplete() { + isInflated = true + for (runnable in pendingFunctions) { + runnable.run() + } + pendingFunctions.clear() + } + + fun executeWhenInflated(runnable: Runnable) { + if (isInflated) { + runnable.run() + } else { + pendingFunctions.add(runnable) + } + } +} \ No newline at end of file diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/BaseEpoxyAdapter.java b/epoxy-adapter/src/main/java/com/airbnb/epoxy/BaseEpoxyAdapter.java index e89d6f511d..b8f547683a 100644 --- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/BaseEpoxyAdapter.java +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/BaseEpoxyAdapter.java @@ -350,8 +350,8 @@ public boolean isStickyHeader(int position) { //endregion protected void executeWhenInflated(EpoxyViewHolder holder, Runnable runnable) { - if (holder.itemView instanceof AsyncFrameLayout) { - ((AsyncFrameLayout)holder.itemView).executeWhenInflated(runnable); + if (holder.itemView instanceof AsyncInflatedView) { + ((AsyncInflatedView)holder.itemView).executeWhenInflated(runnable); } else { runnable.run(); } diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyVisibilityTracker.kt b/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyVisibilityTracker.kt index 0e9ad87638..36cdf833d5 100644 --- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyVisibilityTracker.kt +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyVisibilityTracker.kt @@ -204,7 +204,7 @@ class EpoxyVisibilityTracker { * @param eventOriginForDebug a debug strings used for logs */ private fun processChild(child: View, detachEvent: Boolean, eventOriginForDebug: String) { - if (child is AsyncFrameLayout) { + if (child is AsyncInflatedView) { child.executeWhenInflated { processChildInternal(child, detachEvent, eventOriginForDebug) } diff --git a/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt b/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt index 10cad6e177..080f84d94c 100644 --- a/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt +++ b/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt @@ -5,11 +5,11 @@ import android.graphics.Color import android.util.AttributeSet import android.util.Log import android.view.View +import android.widget.FrameLayout import android.widget.LinearLayout import android.widget.TextView import androidx.annotation.ColorInt import com.airbnb.epoxy.AfterPropsSet -import com.airbnb.epoxy.AsyncFrameLayout import com.airbnb.epoxy.CallbackProp import com.airbnb.epoxy.ModelProp import com.airbnb.epoxy.ModelView @@ -20,6 +20,7 @@ import com.airbnb.epoxy.TextProp import com.airbnb.epoxy.VisibilityState import com.airbnb.epoxy.kotlinsample.R import androidx.asynclayoutinflater.view.AsyncLayoutInflater +import com.airbnb.epoxy.AsyncInflatedView // The ModelView annotation is used on Views to have models generated from those views. // This is pretty straightforward with Kotlin, but properties need some special handling. @@ -28,7 +29,10 @@ class AsyncItemCustomView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : AsyncFrameLayout(context, attrs, defStyleAttr) { +) : FrameLayout(context, attrs, defStyleAttr), AsyncInflatedView { + + override var isInflated : Boolean = false + override var pendingFunctions : ArrayList = ArrayList() private val onVisibilityEventDrawable = OnVisibilityEventDrawable(context) From 0f78ebde53e762ed640f572eabdda1367638bcbf Mon Sep 17 00:00:00 2001 From: Varun Gupta Date: Thu, 5 May 2022 13:29:50 -0700 Subject: [PATCH 3/9] Add abstract class AsyncFrameLayout that can be used as a base class to support Async view inflation with EPoxy --- .../java/com/airbnb/epoxy/AsyncFrameLayout.kt | 17 +++++++++++ .../com/airbnb/epoxy/AsyncInflatedView.kt | 29 ++++++++++++++++--- .../models/AsyncItemCustomView.kt | 7 ++--- 3 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncFrameLayout.kt diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncFrameLayout.kt b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncFrameLayout.kt new file mode 100644 index 0000000000..b8a9b8b020 --- /dev/null +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncFrameLayout.kt @@ -0,0 +1,17 @@ +package com.airbnb.epoxy + +import android.content.Context +import android.util.AttributeSet +import android.widget.FrameLayout + +/** + * Base class to support Async layout inflation with EPoxy. + */ +abstract class AsyncFrameLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr), AsyncInflatedView { + override var isInflated : Boolean = false + override var pendingRunnables : ArrayList = ArrayList() +} \ No newline at end of file diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt index 5443fe6061..bad55f6029 100644 --- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt @@ -1,22 +1,43 @@ package com.airbnb.epoxy +/** + * Interface to support Async layout inflation with EPoxy + */ interface AsyncInflatedView { + /** + * isInflated flag is set to true once the async inflation is completed. + * It is used to run any methods/runnables that requires the view to be inflated. + */ var isInflated : Boolean - var pendingFunctions : ArrayList + /** + * pendingRunnables keep a list of runnables, in order, that are waiting for the view to be + * inflated. + */ + var pendingRunnables : ArrayList + + /** + * onInflationComplete method that MUST be called after the view is asyncronously inflated. + * It runs all pending runnables waiting for view inflation. + */ fun onInflationComplete() { isInflated = true - for (runnable in pendingFunctions) { + for (runnable in pendingRunnables) { runnable.run() } - pendingFunctions.clear() + pendingRunnables.clear() } + /** + * executeWhenInflated method is called by EPoxy to execute a runnable that depend on the + * inflated view. If the view is already inflated, the runnable will immediately run, + * otherwise it is added to the list of pending runnables. + */ fun executeWhenInflated(runnable: Runnable) { if (isInflated) { runnable.run() } else { - pendingFunctions.add(runnable) + pendingRunnables.add(runnable) } } } \ No newline at end of file diff --git a/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt b/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt index 080f84d94c..5e30909c49 100644 --- a/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt +++ b/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt @@ -20,7 +20,7 @@ import com.airbnb.epoxy.TextProp import com.airbnb.epoxy.VisibilityState import com.airbnb.epoxy.kotlinsample.R import androidx.asynclayoutinflater.view.AsyncLayoutInflater -import com.airbnb.epoxy.AsyncInflatedView +import com.airbnb.epoxy.AsyncFrameLayout // The ModelView annotation is used on Views to have models generated from those views. // This is pretty straightforward with Kotlin, but properties need some special handling. @@ -29,10 +29,7 @@ class AsyncItemCustomView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : FrameLayout(context, attrs, defStyleAttr), AsyncInflatedView { - - override var isInflated : Boolean = false - override var pendingFunctions : ArrayList = ArrayList() +) : AsyncFrameLayout(context, attrs, defStyleAttr) { private val onVisibilityEventDrawable = OnVisibilityEventDrawable(context) From 4ee66030c0249eb33547961407c899560eb85877 Mon Sep 17 00:00:00 2001 From: Varun Gupta Date: Thu, 5 May 2022 16:02:02 -0700 Subject: [PATCH 4/9] Incorporate code review comments --- .../java/com/airbnb/epoxy/AsyncFrameLayout.kt | 8 +++++++- .../java/com/airbnb/epoxy/AsyncInflatedView.kt | 18 ++++++++++++++++-- gradle.properties | 2 +- .../kotlinsample/models/AsyncItemCustomView.kt | 1 + 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncFrameLayout.kt b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncFrameLayout.kt index b8a9b8b020..f669504f6b 100644 --- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncFrameLayout.kt +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncFrameLayout.kt @@ -5,8 +5,9 @@ import android.util.AttributeSet import android.widget.FrameLayout /** - * Base class to support Async layout inflation with EPoxy. + * Base class to support Async layout inflation with Epoxy. */ +@ModelView abstract class AsyncFrameLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -14,4 +15,9 @@ abstract class AsyncFrameLayout @JvmOverloads constructor( ) : FrameLayout(context, attrs, defStyleAttr), AsyncInflatedView { override var isInflated : Boolean = false override var pendingRunnables : ArrayList = ArrayList() + + @OnViewRecycled + fun onRecycle() { + onViewRecycled() + } } \ No newline at end of file diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt index bad55f6029..5fe4184f6b 100644 --- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt @@ -1,7 +1,13 @@ package com.airbnb.epoxy /** - * Interface to support Async layout inflation with EPoxy + * Interface to support Async layout inflation with Epoxy + * This is useful if we want to inflate views asyncronously, thereby improving the first layout time + * + * For example, let's say you have a page with first view as a header and then a video. The recycler + * view will inflate both views before showing the first frame. Since video is a heavy view and take + * a longer time to inflate, we can inflate it asyncronously and improve the time taken to show the + * first frame. */ interface AsyncInflatedView { /** @@ -29,7 +35,7 @@ interface AsyncInflatedView { } /** - * executeWhenInflated method is called by EPoxy to execute a runnable that depend on the + * executeWhenInflated method is called by Epoxy to execute a runnable that depend on the * inflated view. If the view is already inflated, the runnable will immediately run, * otherwise it is added to the list of pending runnables. */ @@ -40,4 +46,12 @@ interface AsyncInflatedView { pendingRunnables.add(runnable) } } + + /** + * onViewRecycled method that MUST be called when the view is recycled. It clears pending + * runnable It runs all pending runnables waiting for view inflation. + */ + fun onViewRecycled() { + pendingRunnables.clear() + } } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 19f9a8817f..754149e8b8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=5.0.0-beta06 +VERSION_NAME=5.0.0-beta05 GROUP=com.airbnb.android POM_DESCRIPTION=Epoxy is a system for composing complex screens with a ReyclerView in Android. POM_URL=https://github.com/airbnb/epoxy diff --git a/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt b/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt index 5e30909c49..79e3de053e 100644 --- a/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt +++ b/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt @@ -134,6 +134,7 @@ class AsyncItemCustomView @JvmOverloads constructor( @OnViewRecycled fun clear() { + onRecycle() onVisibilityEventDrawable.reset() } From 302604cbdf3949d57008e5ca5168416ce35efaab Mon Sep 17 00:00:00 2001 From: Varun Gupta Date: Thu, 5 May 2022 16:51:59 -0700 Subject: [PATCH 5/9] Fix comments --- .../src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt index 5fe4184f6b..703d5128bd 100644 --- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt @@ -23,7 +23,7 @@ interface AsyncInflatedView { var pendingRunnables : ArrayList /** - * onInflationComplete method that MUST be called after the view is asyncronously inflated. + * onInflationComplete method MUST be called after the view is asyncronously inflated. * It runs all pending runnables waiting for view inflation. */ fun onInflationComplete() { @@ -48,7 +48,7 @@ interface AsyncInflatedView { } /** - * onViewRecycled method that MUST be called when the view is recycled. It clears pending + * onViewRecycled method MUST be called when the view is recycled. It clears pending * runnable It runs all pending runnables waiting for view inflation. */ fun onViewRecycled() { From 5a219bc1dfb2cb49dcb61be364b1efdf74bbb94f Mon Sep 17 00:00:00 2001 From: Varun Gupta Date: Mon, 16 May 2022 17:07:50 -0700 Subject: [PATCH 6/9] Do not create runnable to synchronously inflated views --- .../com/airbnb/epoxy/BaseEpoxyAdapter.java | 40 ++++++++++--------- .../airbnb/epoxy/EpoxyControllerAdapter.java | 16 ++++++-- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/BaseEpoxyAdapter.java b/epoxy-adapter/src/main/java/com/airbnb/epoxy/BaseEpoxyAdapter.java index b8f547683a..34a0706414 100644 --- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/BaseEpoxyAdapter.java +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/BaseEpoxyAdapter.java @@ -108,8 +108,12 @@ public void onBindViewHolder(EpoxyViewHolder holder, int position) { @Override public void onBindViewHolder(EpoxyViewHolder holder, int position, List payloads) { - executeWhenInflated(holder, - () -> onBindViewHolderInternal(holder, holder.getBindingAdapterPosition(), payloads)); + if (holder.itemView instanceof AsyncInflatedView) { + ((AsyncInflatedView)holder.itemView).executeWhenInflated(() -> + onBindViewHolderInternal(holder, holder.getBindingAdapterPosition(), payloads)); + } else { + onBindViewHolderInternal(holder, holder.getBindingAdapterPosition(), payloads); + } } private void onBindViewHolderInternal(EpoxyViewHolder holder, int position, List payloads) { @@ -214,19 +218,27 @@ public boolean onFailedToRecycleView(EpoxyViewHolder holder) { @CallSuper @Override public void onViewAttachedToWindow(EpoxyViewHolder holder) { - executeWhenInflated(holder, - () -> - //noinspection unchecked,rawtypes - ((EpoxyModel) holder.getModel()).onViewAttachedToWindow(holder.objectToBind())); + if (holder.itemView instanceof AsyncInflatedView) { + ((AsyncInflatedView)holder.itemView).executeWhenInflated(() -> + //noinspection unchecked,rawtypes + ((EpoxyModel) holder.getModel()).onViewAttachedToWindow(holder.objectToBind())); + } else { + //noinspection unchecked,rawtypes + ((EpoxyModel) holder.getModel()).onViewAttachedToWindow(holder.objectToBind()); + } } @CallSuper @Override public void onViewDetachedFromWindow(EpoxyViewHolder holder) { - executeWhenInflated(holder, - () -> - //noinspection unchecked,rawtypes - ((EpoxyModel) holder.getModel()).onViewDetachedFromWindow(holder.objectToBind())); + if (holder.itemView instanceof AsyncInflatedView) { + ((AsyncInflatedView)holder.itemView).executeWhenInflated(() -> + //noinspection unchecked,rawtypes + ((EpoxyModel) holder.getModel()).onViewDetachedFromWindow(holder.objectToBind())); + } else { + //noinspection unchecked,rawtypes + ((EpoxyModel) holder.getModel()).onViewDetachedFromWindow(holder.objectToBind()); + } } public void onSaveInstanceState(Bundle outState) { @@ -348,12 +360,4 @@ public boolean isStickyHeader(int position) { } //endregion - - protected void executeWhenInflated(EpoxyViewHolder holder, Runnable runnable) { - if (holder.itemView instanceof AsyncInflatedView) { - ((AsyncInflatedView)holder.itemView).executeWhenInflated(runnable); - } else { - runnable.run(); - } - } } diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyControllerAdapter.java b/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyControllerAdapter.java index 296170b91d..91c1ec8c83 100644 --- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyControllerAdapter.java +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyControllerAdapter.java @@ -125,15 +125,23 @@ public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { @Override public void onViewAttachedToWindow(@NonNull EpoxyViewHolder holder) { super.onViewAttachedToWindow(holder); - executeWhenInflated(holder, - () -> epoxyController.onViewAttachedToWindow(holder, holder.getModel())); + if (holder.itemView instanceof AsyncInflatedView) { + ((AsyncInflatedView)holder.itemView).executeWhenInflated(() -> + epoxyController.onViewAttachedToWindow(holder, holder.getModel())); + } else { + epoxyController.onViewAttachedToWindow(holder, holder.getModel()); + } } @Override public void onViewDetachedFromWindow(@NonNull EpoxyViewHolder holder) { super.onViewDetachedFromWindow(holder); - executeWhenInflated(holder, - () -> epoxyController.onViewDetachedFromWindow(holder, holder.getModel())); + if (holder.itemView instanceof AsyncInflatedView) { + ((AsyncInflatedView)holder.itemView).executeWhenInflated(() -> + epoxyController.onViewDetachedFromWindow(holder, holder.getModel())); + } else { + epoxyController.onViewDetachedFromWindow(holder, holder.getModel()); + } } @Override From df629f528b4a70eb7800aa092f89aad0fca63f57 Mon Sep 17 00:00:00 2001 From: Varun Gupta Date: Tue, 17 May 2022 11:42:18 -0700 Subject: [PATCH 7/9] Fix Ktlint errors --- .../src/main/java/com/airbnb/epoxy/AsyncFrameLayout.kt | 6 +++--- .../src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncFrameLayout.kt b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncFrameLayout.kt index f669504f6b..093ae136af 100644 --- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncFrameLayout.kt +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncFrameLayout.kt @@ -13,11 +13,11 @@ abstract class AsyncFrameLayout @JvmOverloads constructor( attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr), AsyncInflatedView { - override var isInflated : Boolean = false - override var pendingRunnables : ArrayList = ArrayList() + override var isInflated: Boolean = false + override var pendingRunnables: ArrayList = ArrayList() @OnViewRecycled fun onRecycle() { onViewRecycled() } -} \ No newline at end of file +} diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt index 703d5128bd..f6d89eca79 100644 --- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncInflatedView.kt @@ -14,13 +14,13 @@ interface AsyncInflatedView { * isInflated flag is set to true once the async inflation is completed. * It is used to run any methods/runnables that requires the view to be inflated. */ - var isInflated : Boolean + var isInflated: Boolean /** * pendingRunnables keep a list of runnables, in order, that are waiting for the view to be * inflated. */ - var pendingRunnables : ArrayList + var pendingRunnables: ArrayList /** * onInflationComplete method MUST be called after the view is asyncronously inflated. @@ -54,4 +54,4 @@ interface AsyncInflatedView { fun onViewRecycled() { pendingRunnables.clear() } -} \ No newline at end of file +} From 1c939071b78c2c8f987e5bd523a24de9a58aef39 Mon Sep 17 00:00:00 2001 From: Varun Gupta Date: Tue, 17 May 2022 11:52:38 -0700 Subject: [PATCH 8/9] Another round of Ktlint fixes --- .../airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt b/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt index 79e3de053e..d52cf085f0 100644 --- a/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt +++ b/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/AsyncItemCustomView.kt @@ -5,11 +5,11 @@ import android.graphics.Color import android.util.AttributeSet import android.util.Log import android.view.View -import android.widget.FrameLayout -import android.widget.LinearLayout import android.widget.TextView import androidx.annotation.ColorInt +import androidx.asynclayoutinflater.view.AsyncLayoutInflater import com.airbnb.epoxy.AfterPropsSet +import com.airbnb.epoxy.AsyncFrameLayout import com.airbnb.epoxy.CallbackProp import com.airbnb.epoxy.ModelProp import com.airbnb.epoxy.ModelView @@ -19,8 +19,6 @@ import com.airbnb.epoxy.OnVisibilityStateChanged import com.airbnb.epoxy.TextProp import com.airbnb.epoxy.VisibilityState import com.airbnb.epoxy.kotlinsample.R -import androidx.asynclayoutinflater.view.AsyncLayoutInflater -import com.airbnb.epoxy.AsyncFrameLayout // The ModelView annotation is used on Views to have models generated from those views. // This is pretty straightforward with Kotlin, but properties need some special handling. From 5aa0d75596aa65a5ccd31abfb6c1e99a155a9522 Mon Sep 17 00:00:00 2001 From: Varun Gupta Date: Mon, 23 May 2022 13:43:33 -0700 Subject: [PATCH 9/9] Use position passed in onBindViewHolder instead of using it from holder.getBindingAdapterPosition --- .../src/main/java/com/airbnb/epoxy/BaseEpoxyAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/BaseEpoxyAdapter.java b/epoxy-adapter/src/main/java/com/airbnb/epoxy/BaseEpoxyAdapter.java index 34a0706414..8e153752bb 100644 --- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/BaseEpoxyAdapter.java +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/BaseEpoxyAdapter.java @@ -112,7 +112,7 @@ public void onBindViewHolder(EpoxyViewHolder holder, int position, List ((AsyncInflatedView)holder.itemView).executeWhenInflated(() -> onBindViewHolderInternal(holder, holder.getBindingAdapterPosition(), payloads)); } else { - onBindViewHolderInternal(holder, holder.getBindingAdapterPosition(), payloads); + onBindViewHolderInternal(holder, position, payloads); } }