compose UI(六)一个跑酷demo小游戏讲解分层复杂布局

本文示例代码API基于compose UI 1.0.0-bate08

小游戏示例

分层布局

compose ui 中一个Column,Row都可以看成一个Layout,在这些布局中modifier都有自己的边界,想让一个组件覆盖在另一个上面是比较难以实现的。如果遇到复杂的布局,就可以采用分层布局。原理就是先画一层layout后,再画一层,或者再画n层(性能很有问题)。
as bate版本新建一个compose项目,示例代码:

@Composable
fun Demo() {
    ComplexlayoutTheme {
        // A surface container using the 'background' color from the theme
        Surface(color = MaterialTheme.colors.background) {
            //第一层背景
            Canvas(modifier = Modifier.fillMaxSize()) {
                drawRect(Color.Gray)
            }
            //第二层布局
            LazyColumn {
                //为了凑长度
                item{ Greeting("markrenChina love Android!!!") }
                item{ Greeting("markrenChina love Android!!!") }
                item{ Greeting("markrenChina love Android!!!") }
                item{ Greeting("markrenChina love Android!!!") }
                item{ Greeting("markrenChina love Android!!!") }
                item{ Greeting("markrenChina love Android!!!") }
                item{ Greeting("markrenChina love Android!!!") }
                item{ Greeting("markrenChina love Android!!!") }
                item{ Greeting("markrenChina love Android!!!") }
                item{ Greeting("markrenChina love Android!!!") }
                item{ Greeting("markrenChina love Android!!!") }
                item{ Greeting("markrenChina love Android!!!") }
                item{ Greeting("markrenChina love Android!!!") }
                item{ Greeting("markrenChina love Android!!!") }
                item{ Greeting("markrenChina love Android!!!") }
                item{ Greeting("markrenChina love Android!!!") }
                item{ Greeting("markrenChina love Android!!!") }
            }
            //第三层布局
            LazyRow {
                item{ Greeting("Android") }
                item{ Greeting("Android") }
                item{ Greeting("Android") }
                item{ Greeting("Android") }
                item{ Greeting("Android") }
                item{ Greeting("Android") }
                item{ Greeting("Android") }
                item{ Greeting("Android") }
            }
            //第四层布局 右下角放一个圆圈
            Canvas(modifier = Modifier
                .layout { measurable, constraints ->
                    Log.i("with * height", "${constraints.maxWidth}  * ${constraints.maxHeight}")
                    val p = measurable.measure(constraints)
                    layout(constraints.maxWidth, constraints.maxHeight) {
                        //荣耀20 1dp = 3
                        p.placeRelative(constraints.maxWidth- 300, constraints.maxHeight - (50+80)*3)
                    }
                }
                .size(80.dp)
            ) {
                Log.i("with * height", "${size.width}  * ${size.height}")
                drawCircle(
                    Color.Red//.copy(0.4f)
                )
            }
        }
    }
}

效果大致是
分层布局示例
示例中,灰色背景是最下层,LazyColumn是第二层,LazyRow是第三层,因为他的第一个列覆盖了第二层的第一行,所以第二层第一行的滑动将失效。第四层,是右下角的小圆圈,最外层将覆盖所以层显示包括事件。

跑酷小游戏(Demo)

简单小demo,我们用全部基于第4篇的图形和动画去实现,因为将UI,就没有实现计分系统和碰撞检查。友情提示,本demo是为了学习compose api,用java搞游戏不科学。本游戏原型来源我的安卓入门书籍 《Android游戏开发详解》美James S Cho
game.kt

/**
 * 游戏入口
 */
@Composable
fun Game() {
    //先画背景
    GameBackground()
    //主要内容
    GameView()
}

GameBackground.kt
背景非常简单,主要是背景,草坪,太阳

@Composable
fun GameBackground(){
    Canvas(modifier = Modifier.fillMaxSize()) {
        //画背景
        drawRect(
            Color.Blue.copy(0.5f),
            size = Size(size.width,size.height*0.8F)
        )
        //画草坪
        drawRect(
            Color.Green.copy(0.8f),
            Offset(0F,size.height*0.8F),
            size = Size(size.width,size.height*0.2F)
        )
        //画太阳
        drawCircle(
            Color.Yellow,
            radius = 120F,
            center = Offset(size.width,0F)
        )
    }
}

为了适配,所有的尺寸都基于最大尺寸换算,这种换算在实际生成环境应该放置于remember引用而不是每次都计算。这里提一点compose的缺点,因为是ui fun,所以其实是一个隐藏的死循环线程中的代码。对于对象的创建,计算必须非常谨慎,循环,循环,循环,重要的事情说三遍。内存抖动,泄露可能就在不经意间。

创建障碍物

为了不让逻辑混乱,我们把障碍物的绘制从GameView分离出来,障碍物的移动就利用入参一个x,y的坐标。

 /**
 * 障碍物
 */
@Composable
fun BlockView(
    offset: Pair<Float, Float>,
) {
    Canvas(modifier = Modifier.fillMaxSize()) {
        drawRect(
            Color.Yellow,
            topLeft = Offset(offset.first * size.width, offset.second * size.height),
            size = Size(size.width*0.03F, size.height*0.15F)
        )
    }
}

这里用Pair不直接传Offset是因为Offset的x,y是val,x ,y每次都不一样,避免过多创建对象。
补充一个线性拟合方法:

//线性拟合
fun lerp(start: Float, stop: Float, fraction: Float): Float {
    return (1 - fraction) * start + fraction * stop
}

在GameView 去绘制障碍物

const val BLOCK_NUMBER = 5

@Composable
fun GameView() {
	//创建一个随机发生器
    val random by remember { mutableStateOf(ThreadLocalRandom.current()) }
	//添加动画
    val infiniteTransition = rememberInfiniteTransition()
    val animatedProgress by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = tween(25000, easing = LinearEasing),
            repeatMode = RepeatMode.Restart
        )
    )
	//创建障碍物
    val blockList = remember {
        val offsets = mutableListOf<Block>()
        //不用随机数直接均匀分布
        val between = 1.0F / BLOCK_NUMBER
        (1..BLOCK_NUMBER).forEach { i ->
            val block = Block(Pair((between * i), if(random.nextBoolean()) 0.5F else 0.65F))
            offsets.add(block)
        }
        offsets
    }
	//画出障碍物
    repeat(BLOCK_NUMBER) {
        //计算当前地址
        var x = lerp(blockList[it].offset.first,blockList[it].offset.first - 1F,animatedProgress)
        if (x < 0F) {
            x += 1F
        }
        val y = blockList[it].offset.second
        Log.i("TAG", "")
        BlockView(offset = Pair(x,y))
    }}

简单解释一下,infiniteTransition是一个无限循环动画,由他每次去产生一个animatedProgress(动画进度)障碍物创建时我们随机产生了高度,0.5F,0.65F值都是适配而来。在传入BlockView前我们计算每一次最新的位置,如果他移动到了左边超出屏幕了,就让他会到右边,复用这个对象。计算位置需要原始位置信息,所以再定义一个对象去保存:

data class Block(
    val offset: Pair<Float, Float>
)

绘制角色

角色绘制,需要使用图片,我们引入assets文件夹放资源文件,然后导入资源
导入图片
HeroView

@Composable
fun HeroView() {
    val assets = LocalContext.current.assets
    val runBitmaps by remember {
        val bitmaps = mutableListOf<ImageBitmap>()
        (1..5).forEach {
            assets.open("run_anim$it.webp").use { ins->
                bitmaps.add(BitmapFactory.decodeStream(ins).asImageBitmap())
            }
        }
        mutableStateOf(bitmaps)
    }
    Canvas(modifier = Modifier.fillMaxSize()) {
            drawImage(
                image = runBitmaps[0],
                dstOffset = IntOffset((0.05F* size.width).toInt(),(0.6f* size.height).toInt()),
                dstSize = IntSize((size.width*0.1f).toInt(),(size.height*0.2F).toInt())
            )
    }
}

让角色走路

run图片我们导入了5张,只要让他们交替播放就可以,前面我们有一个animatedProgress的动画发生器,但是太慢了,当然你可以算法换算animatedProgress成0-4即可,简单点就是再建一个,修改GameView,加入一个:

	val heroProgress by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = tween(1500, easing = LinearEasing),
            repeatMode = RepeatMode.Restart
        )
    )
    ...
    //修改HeroView加入一个入参
    HeroView((heroProgress / 0.2F).toInt())

修改HeroView:

	drawImage(
                image = runBitmaps[progress],
                dstOffset = IntOffset((0.05F* size.width).toInt(),(0.6f* size.height).toInt()),
                dstSize = IntSize((size.width*0.1f).toInt(),(size.height*0.2F).toInt())
            )

让角色下蹲

下蹲躲避上方的障碍物和跳跃躲避下方的障碍物都需要引入手势检查(上划跳跃 ,下划下蹲),跳跃要实现一个缓速上升,再下降,相对实现更复杂,我们先来实现下蹲。
引入手势,还需要是全屏手势,方便用户操作,所以我们来修改HeroView的Canvas

	val duckBitmap by remember {
        mutableStateOf( assets.open("duck.webp").use {
            BitmapFactory.decodeStream(it).asImageBitmap()
        })
    }
    ...
    //手势判断
    var offset by remember { mutableStateOf(0f) }
    ...
    Canvas(modifier = Modifier
        .fillMaxSize()
        .scrollable(
            orientation = Orientation.Vertical,
            state = rememberScrollableState { delta ->
                offset += delta
                delta
            }
        )) {
        ...
        }

角色的状态需要切换,我们还要状态属性,状态切换后,我们画蹲的图片,然后给定一个时间将状态切回run即可

    //动画状态,只有run(0)可以触发 跳(1),蹲(2)
    var state by remember { mutableStateOf(0)}
    LaunchedEffect(key1 = offset){
        if (state == 0) {
            if (offset < -100) {
                Log.i("TAG", "jump")
                state = 1
                offset = 0f
            } else if (offset > 100) {
                Log.i("TAG", "duck")
                state = 2
                offset = 0f
            }else state = 0
        }
    }
    LaunchedEffect(key1 = state){
        if (state == 2) {
            delay(1000)
            offset = 0f
            state = 0
        }
    }
	Canvas(modifier = Modifier
        .fillMaxSize()
        .scrollable(
            orientation = Orientation.Vertical,
            state = rememberScrollableState { delta ->
                offset += delta
                delta
            }
        )) {
        if (state == 0){
            //run
            drawImage(
                image = runBitmaps[progress],
                dstOffset = IntOffset((0.05F* size.width).toInt(),(0.6f* size.height).toInt()),
                dstSize = IntSize((size.width*0.1f).toInt(),(size.height*0.2F).toInt())
            )
        }else if (state == 1){
            //jump
        }else{
            //duck
            drawImage(
                image = duckBitmap,
                dstOffset = IntOffset((0.05F* size.width).toInt(),(0.65F* size.height).toInt()),
                dstSize = IntSize((size.width*0.1f).toInt(),(size.height*0.15F).toInt())
            )
        }
    }

让角色跳起来

跳起来比下蹲只是多一个上升和下降的过渡动画:
HeroView

	val jumpBitmap by remember {
        mutableStateOf( assets.open("jump.webp").use {
            BitmapFactory.decodeStream(it).asImageBitmap()
        })
    }
    val jump = remember { Animatable(0.6f) }

修改LaunchedEffect(key1 = state)

		else if (state == 1){
            jump.animateTo(
                targetValue = 0.27f,
                animationSpec =
                //tween(durationMillis = 400, easing = LinearEasing)
                spring(
                    stiffness = 30f//Spring.StiffnessVeryLow,
                )
            )
            //这里会浮空一段时间
            delay(600)
            jump.animateTo(
                targetValue = 0.6f,
                animationSpec = spring(
                    stiffness = 30f//Spring.StiffnessVeryLow,
                )
            )
            delay(400)
            state = 0
            offset = 0f
        }

最后修改画的部分

		else if (state == 1){
            //jump
            drawImage(
                image = jumpBitmap,
                dstOffset = IntOffset((0.05F* size.width ).toInt(),(jump.value* size.height).toInt()),
                dstSize = IntSize((size.width*0.1f).toInt(),(size.height*0.2F).toInt())
            )
        }

最后

demo代码
写demo周日下午在家一边带儿子一边写花了几个小时,晚上写博文又是2小时,看完点个赞。要是写UI要是没什么人看,大概这就是最后一篇了!

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值