JetPack WindowManager for XML

JetPack WindowManager is an api published in android 34

the library enables developers to query screen info on more complicated modern device

WindowManager is mainly used in foldable or multi-screen devices

api("androidx.window:window:1.3.0")
api("androidx.window:window-java:1.3.0")
api("androidx.compose.material3.adaptive:adaptive-android:1.0.0")
lifecycleScope.launch { registerWindowTracker() }

suspend fun registerWindowTracker() {
    val tracker = WindowInfoTracker.getOrCreate(this)
    tracker.windowLayoutInfo(this).collect { info ->
        val summary = StringBuilder()
        val calculator = WindowMetricsCalculator.getOrCreate()
        val currentMetrics = calculator.computeCurrentWindowMetrics(this).bounds
        val maximumMetrics = calculator.computeMaximumWindowMetrics(this).bounds
        summary.append(currentMetrics).appendLine()
        summary.append(maximumMetrics).appendLine()
        val feature = info.displayFeatures.filterIsInstance<FoldingFeature>().firstOrNull()
        feature?.let {
            val state = feature.state
            val orientation = feature.orientation
            val separating = feature.isSeparating
            summary.append(state).appendLine()
            summary.append(orientation).appendLine()
            summary.append(separating).appendLine()
        }
        println(summary.toString())
        updateUI()
    }
}
WindowManager with Compose

this is a simple demo, in formal project, you can create different composables, depending on window size

@Composable
fun rememberWindowSize(): Size {
    val configuration = LocalConfiguration.current
    val context = LocalContext.current
    val windowMetrics = remember(configuration) {
        WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(context)
    }
    return windowMetrics.bounds.toComposeRect().size
}

@Composable
fun WindowSizeAwareText() {
    val windowSize = rememberWindowSize()
    Text("${windowSize.toIntSize().width}-${windowSize.toIntSize().height}")
}
WindowSizeClass

library window size classify width and height size into 3 levels

we can create different composables for different level sizes

@Composable
fun WindowSizeClass() {
    val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
    val widthClass = windowSizeClass.windowWidthSizeClass
    when (widthClass) {
        COMPACT -> Text("COMPACT")
        MEDIUM -> Text("MEDIUM")
        EXPANDED -> Text("EXPANDED")
    }
}

@Composable
fun Compose09() {
    WindowSizeClass()
}
BoxWithConstraints

BoxWithConstraintsScope provides ability to access layout limits

such as maxWidth, minWidth, maxHeight and minHeight

then we can dynamicly calculate layout attributes

this is a photo gallery control, that only show part images, with a badge display remained image counts

private val imagesInternal = listOf(
    "https://images.unsplash.com/photo-1511044568932-338cba0ad803",
    "https://images.unsplash.com/photo-1511044568932-338cba0ad803",
    "https://images.unsplash.com/photo-1511044568932-338cba0ad803",
    "https://images.unsplash.com/photo-1511044568932-338cba0ad803",
    "https://images.unsplash.com/photo-1511044568932-338cba0ad803"
)

@Composable
private fun responsiveGallery(
    images: List<String> = imagesInternal,
    modifier: Modifier = Modifier
) {
    BoxWithConstraints(modifier.wrapContentSize().background(Color.Red)) {
        val itemSpacing = 10.dp
        val itemWidth = 50.dp
        val visibleCount = maxWidth.div(itemWidth + itemSpacing).toInt().minus(1)
        Row(
            modifier = Modifier.fillMaxWidth().background(Color.Yellow),
            horizontalArrangement = Arrangement.spacedBy(itemSpacing),
            verticalAlignment = Alignment.CenterVertically
        ) {
            images.take(visibleCount).forEach {
                Image(
                    painter = rememberAsyncImagePainter(it),
                    contentDescription = null,
                    modifier = Modifier.width(itemWidth).aspectRatio(1f).background(Color.Green)
                )
            }
            val remainCount = images.size - visibleCount
            if (remainCount > 0) {
                Badge {
                    Text("+$remainCount")
                }
            }
        }
    }
}

@Preview
@Composable
fun Compose04() {
    responsiveGallery(modifier = Modifier.width(250.dp).height(100.dp))
}
Fixed Size LazyRow

this time, we create a LazyRow with minimum item size ensured, by computing with BoxWithConstraints

private val imagesInternal = listOf(
    "https://images.unsplash.com/photo-1511044568932-338cba0ad803?t=1",
    "https://images.unsplash.com/photo-1511044568932-338cba0ad803?t=2",
    "https://images.unsplash.com/photo-1511044568932-338cba0ad803?t=3",
    "https://images.unsplash.com/photo-1511044568932-338cba0ad803?t=4",
    "https://images.unsplash.com/photo-1511044568932-338cba0ad803?t=5"
)

@Composable
private fun FixedSizeLazyRow(
    images: List<String> = imagesInternal,
    modifier: Modifier = Modifier
) {
    BoxWithConstraints(modifier.background(Color.Red), contentAlignment = Alignment.Center) {
        val itemSpacing = 10.dp
        var itemWidth = 50.dp
        val visibleCount = maxWidth.div(itemWidth + itemSpacing).toInt()
        itemWidth = maxWidth.minus(itemSpacing.times(visibleCount - 1)).div(visibleCount)
        val items = remember { images }
        LazyRow(
            modifier = Modifier.fillMaxWidth().wrapContentHeight().background(Color.Yellow),
            horizontalArrangement = Arrangement.spacedBy(itemSpacing),
            verticalAlignment = Alignment.CenterVertically
        ) {
            items(items, key = { it }) {
                Image(
                    painter = rememberAsyncImagePainter(it),
                    contentDescription = null,
                    contentScale = ContentScale.FillBounds,
                    modifier = Modifier.width(itemWidth).aspectRatio(1f).background(Color.Green)
                )
            }
        }
    }
}

@Preview
@Composable
fun Compose05() {
    FixedSizeLazyRow(modifier = Modifier.width(250.dp).height(100.dp))
}
ConstrainLayout for Compose
typealias ConstrainBlock = ConstrainScope.() -> Unit

val ConstrainScope.left get() = run { absoluteLeft }
val ConstrainScope.right get() = run { absoluteRight }
val ConstrainedLayoutReference.left get() = run { absoluteLeft }
val ConstrainedLayoutReference.right get() = run { absoluteRight }

@Composable
private fun ConstraintLayoutCompose(modifier: Modifier) {
    ConstraintLayout(modifier) {
        val (e1, e2, e3) = createRefs()
        val guideline = createGuidelineFromTop(50.dp)
        val constrain1: ConstrainBlock = {
            left.linkTo(parent.left)
            top.linkTo(parent.top)
        }
        val constrain2: ConstrainBlock = {
            left.linkTo(e1.right)
            top.linkTo(guideline)
        }
        val constrain3: ConstrainBlock = {
            top.linkTo(e2.bottom)
            left.linkTo(e1.left)
            right.linkTo(e2.right)
            width = Dimension.fillToConstraints
        }
        Box(Modifier.constrainAs(e1, constrain1).width(100.dp).height(100.dp).background(Color.Yellow))
        Box(Modifier.constrainAs(e2, constrain2).width(100.dp).height(100.dp).background(Color.Green))
        Box(Modifier.constrainAs(e3, constrain3).width(100.dp).height(100.dp).background(Color.Cyan))
    }
}

@Preview
@Composable
fun Compose06() {
    ConstraintLayoutCompose(Modifier.width(250.dp).height(250.dp).background(Color(0.2f, 0.2f, 0.2f, 0.2f)))
}
ConstrainSet

if multiple layout has same elements

we can share single layout, while dynamically apply constrains by ConstraintSet

ConstraintSet only define element names, then bind actual element until runtimes

private val ConstrainScope.left get() = run { absoluteLeft }
private val ConstrainScope.right get() = run { absoluteRight }
private val ConstrainedLayoutReference.left get() = run { absoluteLeft }
private val ConstrainedLayoutReference.right get() = run { absoluteRight }

private fun createConstraintSet(guidelinePosition: Dp) = ConstraintSet {
    val (e1, e2, e3) = createRefsFor("e1", "e2", "e3")
    val guideline = createGuidelineFromTop(guidelinePosition)
    constrain(e1) {
        left.linkTo(parent.left)
        top.linkTo(parent.top)
    }
    constrain(e2) {
        left.linkTo(e1.right)
        top.linkTo(guideline)
    }
    constrain(e3) {
        top.linkTo(e2.bottom)
        left.linkTo(e1.left)
        right.linkTo(e2.right)
        width = Dimension.fillToConstraints
    }
}

@Composable
private fun ConstraintSet(modifier: Modifier) {
    val constrains = createConstraintSet(50.dp)
    ConstraintLayout(constrains, modifier) {
        Box(Modifier.layoutId("e1").width(100.dp).height(100.dp).background(Color.Yellow))
        Box(Modifier.layoutId("e2").width(100.dp).height(100.dp).background(Color.Green))
        Box(Modifier.layoutId("e3").width(100.dp).height(100.dp).background(Color.Cyan))
    }
}

@Preview
@Composable
fun Compose07() {
    ConstraintSet(Modifier.width(250.dp).height(250.dp).background(Color(0.2f, 0.2f, 0.2f, 0.2f)))
}
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐