diff --git a/android/plugins/litho/build.gradle b/android/plugins/litho/build.gradle index eabe18840..33b61a525 100644 --- a/android/plugins/litho/build.gradle +++ b/android/plugins/litho/build.gradle @@ -20,6 +20,7 @@ android { dependencies { compileOnly deps.lithoAnnotations implementation project(':android') + implementation deps.kotlinCoroutinesAndroid implementation deps.lithoCore api deps.lithoEditorCore api(deps.lithoEditorFlipper) { diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/LithoObserver.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/LithoObserver.kt index b3b9fb837..5fd229dfe 100644 --- a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/LithoObserver.kt +++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/LithoObserver.kt @@ -8,38 +8,92 @@ package com.facebook.flipper.plugins.uidebugger.litho import android.util.Log +import android.view.ViewTreeObserver import com.facebook.flipper.plugins.uidebugger.LogTag import com.facebook.flipper.plugins.uidebugger.core.Context import com.facebook.flipper.plugins.uidebugger.descriptors.nodeId import com.facebook.flipper.plugins.uidebugger.observers.TreeObserver import com.facebook.flipper.plugins.uidebugger.observers.TreeObserverBuilder +import com.facebook.flipper.plugins.uidebugger.scheduler.throttleLatest import com.facebook.litho.LithoView +import com.facebook.rendercore.extensions.ExtensionState +import com.facebook.rendercore.extensions.MountExtension +import kotlinx.coroutines.* +/** + * There are 2 ways a litho view can update: + * 1. a view was added / updated / removed through a mount ,we use the mount extension to capture + * these + * 2. The user scrolled. This does not cause a mount to the litho view but it may cause new + * components to mount as they come on screen On the native side we capture scrolls as it causes the + * draw listener to first but but the layout traversal would stop once it sees the lithoview. + * + * Therefore we need a way to capture the changes in the position of views in a litho view hierarchy + * as they are scrolled. A property that seems to hold for litho is if there is a scrolling view in + * the heierachy, its direct children are lithoview. + * + * Given that we are observing a litho view in this class for mount extension we can also attach a + * on scroll changed listener to it to be notified by android when it is scrolled. We just need to + * then update the bounds for this view as nothing else has changed. If this scroll does lead to a + * mount this will be picked up by the mount extension + */ class LithoViewTreeObserver(val context: Context) : TreeObserver() { override val type = "Litho" + private val throttleTimeMs = 500L - private var nodeRef: LithoView? = null + private val waitScope = CoroutineScope(Dispatchers.IO) + private val mainScope = CoroutineScope(Dispatchers.Main) + var nodeRef: LithoView? = null + var onScrollChangedListener: ViewTreeObserver.OnScrollChangedListener? = null override fun subscribe(node: Any) { - Log.i(LogTag, "Subscribing to litho view ${node.nodeId()}") + Log.d(LogTag, "Subscribing to litho view ${node.nodeId()}") nodeRef = node as LithoView - val listener: (view: LithoView) -> Unit = { processUpdate(context, node) } - node.setOnDirtyMountListener(listener) + val lithoDebuggerExtension = LithoDebuggerExtension(this) + node.registerUIDebugger(lithoDebuggerExtension) - listener(node) + val throttledUpdate = + throttleLatest(throttleTimeMs, waitScope, mainScope) { node -> + // todo only send bounds for the view rather than the entire hierachy + processUpdate(context, node) + } + + onScrollChangedListener = ViewTreeObserver.OnScrollChangedListener({ throttledUpdate(node) }) + node.viewTreeObserver.addOnScrollChangedListener(onScrollChangedListener) + + // we have already missed the first mount so we trigger it manually on subscribe + processUpdate(context, node) } override fun unsubscribe() { - Log.i(LogTag, "Unsubscribing from litho view") - nodeRef?.setOnDirtyMountListener(null) + Log.d(LogTag, "Unsubscribing from litho view ${nodeRef?.nodeId()}") + nodeRef?.viewTreeObserver?.removeOnScrollChangedListener(onScrollChangedListener) + nodeRef?.unregisterUIDebugger() nodeRef = null } } +class LithoDebuggerExtension(val observer: LithoViewTreeObserver) : MountExtension() { + + override fun createState(): Void? { + return null + } + + /** + * The call guaranteed to be called after new layout mounted completely on the main thread. + * mounting includes adding updating or removing views from the heriachy + */ + override fun afterMount(state: ExtensionState) { + Log.i(LogTag, "After mount called for litho view ${observer.nodeRef?.nodeId()}") + // todo sparse update + observer.processUpdate(observer.context, state.rootHost as Any) + } +} + object LithoViewTreeObserverBuilder : TreeObserverBuilder { override fun canBuildFor(node: Any): Boolean { return node is LithoView diff --git a/gradle.properties b/gradle.properties index 6ad794d6b..4c5dde1d8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,6 @@ # # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. - # POM publishing constants VERSION_NAME=0.171.2-SNAPSHOT GROUP=com.facebook.flipper @@ -17,18 +16,15 @@ POM_LICENCE_DIST=repo POM_DEVELOPER_ID=facebook POM_DEVELOPER_NAME=facebook POM_ISSUES_URL=https://github.com/facebook/flipper/issues/ - # Shared version numbers -LITHO_VERSION=0.41.1 +LITHO_VERSION=0.43.0 ANDROIDX_VERSION=1.3.0 KOTLIN_VERSION=1.6.20 - # Gradle internals org.gradle.internal.repository.max.retries=10 org.gradle.internal.repository.initial.backoff=1250 org.gradle.jvmargs=-Xmx2g -Xms512m -XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled systemProp.org.gradle.internal.http.connectionTimeout=120000 systemProp.org.gradle.internal.http.socketTimeout=120000 - android.useAndroidX=true android.enableJetifier=true