[go: nahoru, domu]

blob: 4c54bbe46843fd7f0cdf196b25fa61aa6193124c [file] [log] [blame]
/*
* 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.viewinterop
import android.os.Build
import android.util.DisplayMetrics
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.testutils.assertPixels
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.platform.findViewTreeCompositionContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.R
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.test.espresso.Espresso
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withClassName
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
import org.hamcrest.CoreMatchers.endsWith
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.CoreMatchers.instanceOf
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.math.roundToInt
@MediumTest
@RunWith(AndroidJUnit4::class)
class AndroidViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun androidViewWithConstructor() {
rule.setContent {
AndroidView({ TextView(it).apply { text = "Test" } })
}
Espresso
.onView(instanceOf(TextView::class.java))
.check(matches(isDisplayed()))
}
@Test
fun androidViewWithResourceTest() {
rule.setContent {
AndroidView({ LayoutInflater.from(it).inflate(R.layout.test_layout, null) })
}
Espresso
.onView(instanceOf(RelativeLayout::class.java))
.check(matches(isDisplayed()))
}
@Test
fun androidViewWithViewTest() {
lateinit var frameLayout: FrameLayout
rule.activityRule.scenario.onActivity { activity ->
frameLayout = FrameLayout(activity).apply {
layoutParams = ViewGroup.LayoutParams(300, 300)
}
}
rule.setContent {
AndroidView({ frameLayout })
}
Espresso
.onView(equalTo(frameLayout))
.check(matches(isDisplayed()))
}
@Test
fun androidViewWithResourceTest_preservesLayoutParams() {
rule.setContent {
AndroidView({
LayoutInflater.from(it).inflate(R.layout.test_layout, FrameLayout(it), false)
})
}
Espresso
.onView(withClassName(endsWith("RelativeLayout")))
.check(matches(isDisplayed()))
.check { view, exception ->
if (view.layoutParams.width != 300.dp.toPx(view.context.resources.displayMetrics)) {
throw exception
}
if (view.layoutParams.height != WRAP_CONTENT) {
throw exception
}
}
}
@Test
fun androidViewProperlyDetached() {
lateinit var frameLayout: FrameLayout
rule.activityRule.scenario.onActivity { activity ->
frameLayout = FrameLayout(activity).apply {
layoutParams = ViewGroup.LayoutParams(300, 300)
}
}
var emit by mutableStateOf(true)
rule.setContent {
if (emit) {
AndroidView({ frameLayout })
}
}
rule.runOnUiThread {
assertThat(frameLayout.parent).isNotNull()
emit = false
}
rule.runOnIdle {
assertThat(frameLayout.parent).isNull()
}
}
@Test
@LargeTest
fun androidView_attachedAfterDetached_addsViewBack() {
lateinit var root: FrameLayout
lateinit var composeView: ComposeView
lateinit var viewInsideCompose: View
rule.activityRule.scenario.onActivity { activity ->
root = FrameLayout(activity)
composeView = ComposeView(activity)
composeView.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnLifecycleDestroyed(activity)
)
viewInsideCompose = View(activity)
activity.setContentView(root)
root.addView(composeView)
composeView.setContent {
AndroidView({ viewInsideCompose })
}
}
var viewInsideComposeHolder: ViewGroup? = null
rule.runOnUiThread {
assertThat(viewInsideCompose.parent).isNotNull()
viewInsideComposeHolder = viewInsideCompose.parent as ViewGroup
root.removeView(composeView)
}
rule.runOnIdle {
// Views don't detach from the parent when the parent is detached
assertThat(viewInsideCompose.parent).isNotNull()
assertThat(viewInsideComposeHolder?.childCount).isEqualTo(1)
root.addView(composeView)
}
rule.runOnIdle {
assertThat(viewInsideCompose.parent).isEqualTo(viewInsideComposeHolder)
assertThat(viewInsideComposeHolder?.childCount).isEqualTo(1)
}
}
@Test
fun androidViewWithResource_modifierIsApplied() {
val size = 20.dp
rule.setContent {
AndroidView(
{ LayoutInflater.from(it).inflate(R.layout.test_layout, null) },
Modifier.size(size)
)
}
Espresso
.onView(instanceOf(RelativeLayout::class.java))
.check(matches(isDisplayed()))
.check { view, exception ->
val expectedSize = size.toPx(view.context.resources.displayMetrics)
if (view.width != expectedSize || view.height != expectedSize) {
throw exception
}
}
}
@Test
fun androidViewWithView_modifierIsApplied() {
val size = 20.dp
lateinit var frameLayout: FrameLayout
rule.activityRule.scenario.onActivity { activity ->
frameLayout = FrameLayout(activity)
}
rule.setContent {
AndroidView({ frameLayout }, Modifier.size(size))
}
Espresso
.onView(equalTo(frameLayout))
.check(matches(isDisplayed()))
.check { view, exception ->
val expectedSize = size.toPx(view.context.resources.displayMetrics)
if (view.width != expectedSize || view.height != expectedSize) {
throw exception
}
}
}
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun androidViewWithView_drawModifierIsApplied() {
val size = 300
lateinit var frameLayout: FrameLayout
rule.activityRule.scenario.onActivity { activity ->
frameLayout = FrameLayout(activity).apply {
layoutParams = ViewGroup.LayoutParams(size, size)
}
}
rule.setContent {
AndroidView({ frameLayout }, Modifier.testTag("view").background(color = Color.Blue))
}
rule.onNodeWithTag("view").captureToImage().assertPixels(IntSize(size, size)) {
Color.Blue
}
}
@Test
fun androidViewWithResource_modifierIsCorrectlyChanged() {
val size = mutableStateOf(20.dp)
rule.setContent {
AndroidView(
{ LayoutInflater.from(it).inflate(R.layout.test_layout, null) },
Modifier.size(size.value)
)
}
Espresso
.onView(instanceOf(RelativeLayout::class.java))
.check(matches(isDisplayed()))
.check { view, exception ->
val expectedSize = size.value.toPx(view.context.resources.displayMetrics)
if (view.width != expectedSize || view.height != expectedSize) {
throw exception
}
}
rule.runOnIdle { size.value = 30.dp }
Espresso
.onView(instanceOf(RelativeLayout::class.java))
.check(matches(isDisplayed()))
.check { view, exception ->
val expectedSize = size.value.toPx(view.context.resources.displayMetrics)
if (view.width != expectedSize || view.height != expectedSize) {
throw exception
}
}
}
@Test
fun androidView_notDetachedFromWindowTwice() {
// Should not crash.
rule.setContent {
Box {
AndroidView(::ComposeView) {
it.setContent {
Box(Modifier)
}
}
}
}
}
@Test
fun androidView_updateObservesStateChanges() {
var size by mutableStateOf(20)
var obtainedSize: IntSize = IntSize.Zero
rule.setContent {
Box {
AndroidView(
::View,
Modifier.onGloballyPositioned { obtainedSize = it.size }
) { view ->
view.layoutParams = ViewGroup.LayoutParams(size, size)
}
}
}
rule.runOnIdle {
assertThat(obtainedSize).isEqualTo(IntSize(size, size))
size = 40
}
rule.runOnIdle {
assertThat(obtainedSize).isEqualTo(IntSize(size, size))
}
}
@Test
fun androidView_propagatesDensity() {
rule.setContent {
val size = 50.dp
val density = Density(3f)
val sizeIpx = with(density) { size.roundToPx() }
CompositionLocalProvider(LocalDensity provides density) {
AndroidView(
{ FrameLayout(it) },
Modifier.size(size).onGloballyPositioned {
assertThat(it.size).isEqualTo(IntSize(sizeIpx, sizeIpx))
}
)
}
}
rule.waitForIdle()
}
@Test
fun androidView_propagatesViewTreeCompositionContext() {
lateinit var parentComposeView: ComposeView
lateinit var compositionChildView: View
rule.activityRule.scenario.onActivity { activity ->
parentComposeView = ComposeView(activity).apply {
setContent {
AndroidView(::View) {
compositionChildView = it
}
}
activity.setContentView(this)
}
}
rule.runOnIdle {
assertThat(compositionChildView.findViewTreeCompositionContext())
.isNotEqualTo(parentComposeView.findViewTreeCompositionContext())
}
}
@Test
fun androidView_propagatesAmbientsToComposeViewChildren() {
val ambient = compositionLocalOf { "unset" }
var childComposedAmbientValue = "uncomposed"
rule.setContent {
CompositionLocalProvider(ambient provides "setByParent") {
AndroidView(
viewBlock = {
ComposeView(it).apply {
setContent {
childComposedAmbientValue = ambient.current
}
}
}
)
}
}
rule.runOnIdle {
assertThat(childComposedAmbientValue).isEqualTo("setByParent")
}
}
@Test
fun androidView_propagatesLayoutDirectionToComposeViewChildren() {
var childViewLayoutDirection: Int = Int.MIN_VALUE
var childCompositionLayoutDirection: LayoutDirection? = null
rule.setContent {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
AndroidView(
viewBlock = {
FrameLayout(it).apply {
addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
childViewLayoutDirection = layoutDirection
}
addView(
ComposeView(it).apply {
// The view hierarchy's layout direction should always override
// the ambient layout direction from the parent composition.
layoutDirection = android.util.LayoutDirection.LTR
setContent {
childCompositionLayoutDirection =
LocalLayoutDirection.current
}
},
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
)
}
}
)
}
}
rule.runOnIdle {
assertThat(childViewLayoutDirection).isEqualTo(android.util.LayoutDirection.RTL)
assertThat(childCompositionLayoutDirection).isEqualTo(LayoutDirection.Ltr)
}
}
private fun Dp.toPx(displayMetrics: DisplayMetrics) =
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
value,
displayMetrics
).roundToInt()
}