| /* |
| * 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.ui.node |
| |
| import android.content.Context |
| import android.view.View |
| import android.view.ViewGroup |
| import android.widget.FrameLayout |
| import androidx.annotation.RestrictTo |
| import androidx.ui.core.AndroidOwner |
| import androidx.ui.core.ComponentNode |
| import androidx.ui.core.Constraints |
| import androidx.ui.core.LayoutDirection |
| import androidx.ui.core.LayoutNode |
| import androidx.ui.core.Measurable |
| import androidx.ui.core.MeasureScope |
| import androidx.ui.core.Modifier |
| import androidx.ui.core.drawBehind |
| import androidx.ui.graphics.painter.drawCanvas |
| import androidx.ui.unit.IntPx |
| import androidx.ui.unit.ipx |
| import androidx.ui.unit.isFinite |
| import androidx.ui.viewinterop.AndroidViewHolder |
| |
| /** |
| * @suppress |
| */ |
| // TODO(b/150806128): We should decide if we want to make this public API or not. Right now it is needed |
| // for convenient LayoutParams usage in compose with views. |
| @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) |
| interface ViewAdapter { |
| val id: Int |
| fun willInsert(view: View, parent: ViewGroup) |
| fun didInsert(view: View, parent: ViewGroup) |
| fun didUpdate(view: View, parent: ViewGroup) |
| } |
| |
| /** |
| * @suppress |
| */ |
| // TODO(b/150806128): We should decide if we want to make this public API or not. Right now it is needed |
| // for convenient LayoutParams usage in compose with views. |
| @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) |
| fun <T : ViewAdapter> View.getOrAddAdapter(id: Int, factory: () -> T): T { |
| return getViewAdapter().get(id, factory) |
| } |
| |
| /** |
| * Intersects [Constraints] and [View] LayoutParams to obtain the suitable [View.MeasureSpec] |
| * for measuring the [View]. |
| */ |
| private fun obtainMeasureSpec( |
| min: IntPx, |
| max: IntPx, |
| preferred: Int |
| ): Int = when { |
| preferred >= 0 || min == max -> { |
| // Fixed size due to fixed size layout param or fixed constraints. |
| View.MeasureSpec.makeMeasureSpec( |
| preferred.coerceIn(min.value, max.value), |
| View.MeasureSpec.EXACTLY |
| ) |
| } |
| preferred == ViewGroup.LayoutParams.WRAP_CONTENT && max.isFinite() -> { |
| // Wrap content layout param with finite max constraint. If max constraint is infinite, |
| // we will measure the child with UNSPECIFIED. |
| View.MeasureSpec.makeMeasureSpec(max.value, View.MeasureSpec.AT_MOST) |
| } |
| preferred == ViewGroup.LayoutParams.MATCH_PARENT && max.isFinite() -> { |
| // Match parent layout param, so we force the child to fill the available space. |
| View.MeasureSpec.makeMeasureSpec(max.value, View.MeasureSpec.EXACTLY) |
| } |
| else -> { |
| // max constraint is infinite and layout param is WRAP_CONTENT or MATCH_PARENT. |
| View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) |
| } |
| } |
| |
| /** |
| * Builds a [ComponentNode] tree representation for an Android [View]. |
| * The component nodes will proxy the Compose core calls to the [View]. |
| */ |
| internal fun AndroidViewHolder.toComponentNode(): ComponentNode { |
| // TODO(soboleva): add layout direction here? |
| // TODO(popam): forward pointer input, accessibility, focus |
| // Prepare layout node that proxies measure and layout passes to the View. |
| val layoutNode = LayoutNode() |
| layoutNode.modifier = Modifier |
| .pointerInteropModifier(this) |
| .drawBehind { |
| drawCanvas { canvas, _ -> draw(canvas.nativeCanvas) } |
| } |
| layoutNode.onAttach = { owner -> |
| (owner as? AndroidOwner)?.addAndroidView(this, layoutNode) |
| } |
| layoutNode.onDetach = { owner -> |
| (owner as? AndroidOwner)?.removeAndroidView(this) |
| } |
| layoutNode.measureBlocks = object : LayoutNode.NoIntrinsicsMeasureBlocks( |
| "Intrinsics not supported for Android views" |
| ) { |
| override fun measure( |
| measureScope: MeasureScope, |
| measurables: List<Measurable>, |
| constraints: Constraints, |
| layoutDirection: LayoutDirection |
| ): MeasureScope.MeasureResult { |
| if (constraints.minWidth != 0.ipx) { |
| getChildAt(0).minimumWidth = constraints.minWidth.value |
| } |
| if (constraints.minHeight != 0.ipx) { |
| getChildAt(0).minimumHeight = constraints.minHeight.value |
| } |
| // TODO (soboleva): native view should get LD value from Compose? |
| |
| // TODO(shepshapard): !! necessary? |
| measure( |
| obtainMeasureSpec( |
| constraints.minWidth, |
| constraints.maxWidth, |
| layoutParams!!.width |
| ), |
| obtainMeasureSpec( |
| constraints.minHeight, |
| constraints.maxHeight, |
| layoutParams!!.height |
| ) |
| ) |
| return measureScope.layout(measuredWidth.ipx, measuredHeight.ipx) { |
| layout( |
| 0, |
| 0, |
| measuredWidth, |
| measuredHeight |
| ) |
| } |
| } |
| } |
| return layoutNode |
| } |
| |
| internal class MergedViewAdapter : ViewAdapter { |
| override val id = 0 |
| val adapters = mutableListOf<ViewAdapter>() |
| |
| inline fun <T : ViewAdapter> get(id: Int, factory: () -> T): T { |
| @Suppress("UNCHECKED_CAST") |
| val existing = adapters.firstOrNull { it.id == id } as? T |
| if (existing != null) return existing |
| val next = factory() |
| adapters.add(next) |
| return next |
| } |
| |
| override fun willInsert(view: View, parent: ViewGroup) { |
| for (adapter in adapters) adapter.willInsert(view, parent) |
| } |
| |
| override fun didInsert(view: View, parent: ViewGroup) { |
| for (adapter in adapters) adapter.didInsert(view, parent) |
| } |
| |
| override fun didUpdate(view: View, parent: ViewGroup) { |
| for (adapter in adapters) adapter.didUpdate(view, parent) |
| } |
| } |
| |
| /** |
| * This function will take in a string and pass back a valid resource identifier for |
| * View.setTag(...). We should eventually move this to a resource id that's actually generated via |
| * AAPT but doing that in this project is proving to be complicated, so for now I'm just doing this |
| * as a stop-gap. |
| */ |
| internal fun tagKey(key: String): Int { |
| return (3 shl 24) or key.hashCode() |
| } |
| |
| private val viewAdaptersKey = tagKey("ViewAdapter") |
| |
| internal fun View.getViewAdapterIfExists(): MergedViewAdapter? { |
| return getTag(viewAdaptersKey) as? MergedViewAdapter |
| } |
| |
| internal fun View.getViewAdapter(): MergedViewAdapter { |
| var adapter = getTag(viewAdaptersKey) as? MergedViewAdapter |
| if (adapter == null) { |
| adapter = MergedViewAdapter() |
| setTag(viewAdaptersKey, adapter) |
| } |
| return adapter |
| } |