AndroidViewsHandler.android.kt

/*
 * Copyright 2020 The Android Open Source Project
 *
 * 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 androidx.compose.ui.platform

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.viewinterop.AndroidViewHolder

/**
 * Used by [AndroidComposeView] to handle the Android [View]s attached to its hierarchy.
 * The [AndroidComposeView] has one direct [AndroidViewsHandler], which is responsible
 * of intercepting [requestLayout]s, [onMeasure]s, [invalidate]s, etc. sent from or towards
 * children.
 */
internal class AndroidViewsHandler(context: Context) : ViewGroup(context) {
    init {
        clipChildren = false
    }

    val holderToLayoutNode = hashMapOf<AndroidViewHolder, LayoutNode>()
    val layoutNodeToHolder = hashMapOf<LayoutNode, AndroidViewHolder>()

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // Layout will be handled by component nodes. However, we act like proper measurement
        // here in case ViewRootImpl did forceLayout().
        require(MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY)
        require(MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY)
        setMeasuredDimension(
            MeasureSpec.getSize(widthMeasureSpec),
            MeasureSpec.getSize(heightMeasureSpec)
        )
        // Remeasure children, such that, if ViewRootImpl did forceLayout(), the holders
        // will be set PFLAG_LAYOUT_REQUIRED and they will be relaid out during the next layout.
        // This will ensure that the need relayout flags will be cleared correctly.
        holderToLayoutNode.keys.forEach { it.remeasure() }
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        // Layout was already handled by component nodes, but replace here because
        // the View system has forced relayout on children. This method will only be called
        // when forceLayout is called on the Views hierarchy.
        holderToLayoutNode.keys.forEach { it.layout(it.left, it.top, it.right, it.bottom) }
    }

    // No call to super to avoid invalidating the AndroidComposeView and the handler, and rely on
    // component nodes logic. The layer invalidation will have been already done by the holder.
    @SuppressLint("MissingSuperCall")
    override fun onDescendantInvalidated(child: View, target: View) { }

    override fun invalidateChildInParent(location: IntArray?, dirty: Rect?) = null

    fun drawView(view: AndroidViewHolder, canvas: Canvas) {
        view.draw(canvas)
    }

    // Touch events forwarding will be handled by component nodes.
    override fun dispatchTouchEvent(ev: MotionEvent?) = true

    // No call to super to avoid invalidating the AndroidComposeView and rely on
    // component nodes logic.
    @SuppressLint("MissingSuperCall")
    override fun requestLayout() {
        // Hack to cleanup the dirty layout flag on ourselves, such that this method continues
        // to be called for further children requestLayout().
        cleanupLayoutState(this)
        // requestLayout() was called by a child, so we have to request remeasurement for
        // their corresponding layout node.
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            val node = holderToLayoutNode[child]
            if (child.isLayoutRequested && node != null) {
                node.requestRemeasure()
            }
        }
    }

    override fun shouldDelayChildPressedState(): Boolean = false
}