Skip to content

Commit

Permalink
[Snapshots] Fix device specs (#285)
Browse files Browse the repository at this point in the history
* Fix device specs

* Fixes

* Fix preview params and tweak views

* Fix tests, prioritize width/height overrides

* Tweaks

* Rounding and orientation

* Lint

* Lint and cleanups

* Fix device specs

* Fix
  • Loading branch information
rbro112 authored Oct 23, 2024
1 parent 5fae30f commit cc54780
Show file tree
Hide file tree
Showing 5 changed files with 305 additions and 276 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.emergetools.snapshots.sample.ui

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Composable
fun PortraitLandscapeView() {
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val aspectRatio = maxWidth / maxHeight

// Repro of client case with different layouts for different screen sizes
if (maxWidth < 840.dp) {
Column(
modifier = Modifier.fillMaxSize()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.background(Color.Blue)
)

Card(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.padding(16.dp)
) {
Box(modifier = Modifier.fillMaxSize()) // Placeholder for card content
}
}
} else {
if (aspectRatio >= 2) {
Row(
modifier = Modifier.fillMaxSize()
) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.background(Color.Blue)
)

Card(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.padding(16.dp)
) {
Box(modifier = Modifier.fillMaxSize()) // Placeholder for card content
}
}
} else {
Column(
modifier = Modifier.fillMaxSize()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.background(Color.Blue)
)

Card(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.padding(16.dp)
) {
Box(modifier = Modifier.fillMaxSize()) // Placeholder for card content
}
}
}
}
}
}

@Preview(device = Devices.PIXEL_5)
@Preview(name = "Phone (Landscape)", device = "spec:width=430dp,height=860dp,orientation=landscape")
@Preview(name = "Phone (Portrait)", device = "spec:width=430dp,height=860dp")
@Composable
fun PortraitLandscapeViewPreview() {
Surface {
PortraitLandscapeView()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import com.emergetools.snapshots.SnapshotErrorType
import com.emergetools.snapshots.shared.ComposePreviewSnapshotConfig
import java.lang.reflect.Modifier
import java.lang.reflect.ParameterizedType
import kotlin.math.max

@Suppress("TooGenericExceptionCaught", "ThrowsCount")
fun snapshotComposable(
Expand Down Expand Up @@ -125,14 +124,12 @@ private fun getPreviewProviderParameters(
.singleOrNull { it.parameterTypes.isEmpty() }
?.apply { isAccessible = true }
?: throw IllegalArgumentException(
"PreviewParameterProvider constructor can not" + " have parameters"
"PreviewParameterProvider constructor can not have parameters"
)
val params = constructor.newInstance() as PreviewParameterProvider<*>
return params.values.toList()
}

const val DEFAULT_DENSITY_PPI = 160

private fun snapshot(
activity: Activity,
snapshotRule: EmergeSnapshots,
Expand All @@ -155,8 +152,19 @@ private fun snapshot(
add(0)
}.toTypedArray()

val deviceSpec = configToDeviceSpec(previewConfig)

val saveablePreviewConfig = previewConfig.copy(
previewParameter = previewConfig.previewParameter?.copy(index = index)
)

// Update activity window size if device is specified
if (deviceSpec != null) {
updateActivityBounds(activity, deviceSpec)
}

composeView.setContent {
SnapshotVariantProvider(previewConfig) {
SnapshotVariantProvider(previewConfig, deviceSpec?.scalingFactor) {
@Suppress("SpreadOperator")
if (Modifier.isStatic(composableMethod.asMethod().modifiers)) {
// This is a top level or static method
Expand All @@ -170,87 +178,88 @@ private fun snapshot(
}
}

activity.addContentView(composeView, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT))

// Need to update to accommodate param index in case when preview param is present
val saveablePreviewConfig = previewConfig.copy(
previewParameter = previewConfig.previewParameter?.copy(index = index)
// Add the ComposeView to the activity
activity.addContentView(
composeView,
LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
)

composeView.post {
// Measure the composable agnostic of the parent constraints to layout properly in activity
val composableSize = measureComposableSize(composeView, previewConfig)
val bitmap = captureBitmap(
view = composeView,
width = composableSize.width,
height = composableSize.height,
)
val size = measureViewSize(composeView, previewConfig)
val bitmap = captureBitmap(composeView, size.width, size.height)

bitmap?.let {
snapshotRule.take(bitmap, saveablePreviewConfig)
snapshotRule.take(it, saveablePreviewConfig)
} ?: run {
snapshotRule.saveError(
errorType = SnapshotErrorType.EMPTY_SNAPSHOT,
composePreviewSnapshotConfig = saveablePreviewConfig,
composePreviewSnapshotConfig = saveablePreviewConfig
)
}

// Remove the view from the activity to ensure it doesn't interfere with the next preview param
// Reset activity content view
(composeView.parent as? ViewGroup)?.removeView(composeView)
}
}
}

private fun measureComposableSize(
view: ComposeView,
previewConfig: ComposePreviewSnapshotConfig,
private fun measureViewSize(
view: View,
previewConfig: ComposePreviewSnapshotConfig
): IntSize {
if (previewConfig.device != null) {
val deviceSpec = configToDeviceSpec(previewConfig)
if (deviceSpec != null) {
// Measure the composable with the device dimensions
// Override the width and height if set in preview annotation
val deviceDpScale = deviceSpec.densityPpi / DEFAULT_DENSITY_PPI
val widthPixels = previewConfig.widthDp?.let { it * deviceDpScale } ?: deviceSpec.widthPixels
val heightPixels =
previewConfig.heightDp?.let { it * deviceDpScale } ?: deviceSpec.heightPixels
Log.i(
EmergeComposeSnapshotReflectiveParameterizedInvoker.TAG,
"Measuring composable with device dimensions: $widthPixels x $heightPixels"
)
view.measure(
View.MeasureSpec.makeMeasureSpec(widthPixels, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(heightPixels, View.MeasureSpec.EXACTLY),
val deviceSpec = configToDeviceSpec(previewConfig)

// Use exact measurements when we have them
val scalingFactor = deviceSpec?.scalingFactor ?: view.resources.displayMetrics.density

val widthMeasureSpec = when {
previewConfig.widthDp != null -> {
View.MeasureSpec.makeMeasureSpec(
dpToPx(previewConfig.widthDp!!, scalingFactor),
View.MeasureSpec.EXACTLY
)
return IntSize(view.measuredWidth, view.measuredHeight)
} else {
Log.e(
EmergeComposeSnapshotReflectiveParameterizedInvoker.TAG,
"Device not found for preview annotation: ${previewConfig.device}"
}

deviceSpec?.widthPixels != null && deviceSpec.widthPixels > 0 ->
View.MeasureSpec.makeMeasureSpec(deviceSpec.widthPixels, View.MeasureSpec.EXACTLY)

else ->
View.MeasureSpec.makeMeasureSpec(view.width, View.MeasureSpec.AT_MOST)
}

val heightMeasureSpec = when {
previewConfig.heightDp != null -> {
View.MeasureSpec.makeMeasureSpec(
dpToPx(previewConfig.heightDp!!, scalingFactor),
View.MeasureSpec.EXACTLY
)
}

deviceSpec?.heightPixels != null && deviceSpec.heightPixels > 0 ->
View.MeasureSpec.makeMeasureSpec(deviceSpec.heightPixels, View.MeasureSpec.EXACTLY)

else ->
View.MeasureSpec.makeMeasureSpec(view.height, View.MeasureSpec.AT_MOST)
}

// Default to 0 if not set which will allow parent to impose constraints on child when
// AT_MOST set.
val emulatorDensity = view.resources.displayMetrics.density
val heightPx = (previewConfig.heightDp ?: 0) * emulatorDensity
val widthPx = (previewConfig.widthDp ?: 0) * emulatorDensity

// If width or height is set in preview annotation, measure with unspecified to allow
// stretching past bounds of parent.
// Otherwise, use AT_MOST to allow parent to impose constraints on child.
val widthMeasureSpec =
previewConfig.widthDp?.let { View.MeasureSpec.UNSPECIFIED } ?: View.MeasureSpec.AT_MOST
val heightMeasureSpec =
previewConfig.heightDp?.let { View.MeasureSpec.UNSPECIFIED } ?: View.MeasureSpec.AT_MOST

view.measure(
View.MeasureSpec.makeMeasureSpec(max(view.width, widthPx.toInt()), widthMeasureSpec),
View.MeasureSpec.makeMeasureSpec(max(view.height, heightPx.toInt()), heightMeasureSpec),
)
view.measure(widthMeasureSpec, heightMeasureSpec)
return IntSize(view.measuredWidth, view.measuredHeight)
}

private fun updateActivityBounds(activity: Activity, deviceSpec: DeviceSpec) {
// Apply the device spec dimensions to the activity window
val width = deviceSpec.widthPixels
val height = deviceSpec.heightPixels

if (width > 0 && height > 0) {
activity.window.setLayout(width, height)
}
}

private fun dpToPx(dp: Int, scalingFactor: Float): Int {
return (dp * scalingFactor).toInt()
}

fun captureBitmap(
view: View,
width: Int,
Expand Down
Loading

0 comments on commit cc54780

Please sign in to comment.