Compose 点击/按压事件全解析:从基础到进阶,新手也能秒懂

作为 Android 开发者,我们每天都在和「点击事件」打交道------按钮点击、卡片点击、长按弹出菜单、双击放大图片,这些都是 App 交互的核心。而 Jetpack Compose 作为 Google 主推的声明式 UI 框架,其事件处理方式和传统 View 体系有很大不同,更简洁、更灵活,但也有不少新手容易踩坑的细节。

今天这篇文章,就带大家从零开始,吃透 Compose 中所有常见的点击、按压事件,从最基础的单击,到复杂的长按、双击、按压反馈,再到自定义事件,每一个知识点都配 完整可运行示例,通俗易懂,新手也能跟着敲、跟着学,看完直接上手项目!

话不多说,直奔主题,先梳理一下我们今天要覆盖的核心内容:

  • 基础单击事件:最常用,3种实现方式(内置组件、clickable修饰符、pointerInput)
  • 复合点击事件:长按、双击,一次性搞定多类手势
  • 按压反馈:自定义波纹效果、取消反馈,提升交互体验
  • 事件传递与拦截:解决多个组件嵌套时的事件冲突
  • 实战场景:结合 ViewModel 处理点击事件(避免内存泄漏)
  • 新手避坑指南:那些容易写错的细节(状态管理、事件消费)

一、基础单击事件:3种实现方式,按需选择

单击事件是最常用的交互,Compose 提供了多种实现方式,核心区别在于「便捷性」和「灵活性」,我们按「从简单到复杂」的顺序讲解,新手优先掌握前两种。

1. 方式一:内置可点击组件(最便捷)

Compose 中有很多内置组件本身就支持 onClick 回调,比如 Button、IconButton、TextButton 等,无需额外添加修饰符,直接传入 onClick 即可,适合快速开发。

示例(按钮单击+计数):

kotlin 复制代码
@Composable
fun BasicClickDemo() {
    // 用 remember 保存点击计数,重组时不会重置
    var clickCount by remember { mutableStateOf(0) }
    
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        // 内置 Button,直接使用 onClick
        Button(
            onClick = { 
                // 点击时执行的逻辑:计数+1
                clickCount++ 
                Log.d("ClickDemo", "按钮被点击,当前计数:$clickCount")
            }
        ) {
            Text(text = "点击我(已点击$clickCount次)")
        }
        
        // 间隔8dp
        Spacer(modifier = Modifier.height(8.dp))
        
        // IconButton 示例(常用于图标点击)
        IconButton(onClick = { clickCount = 0 }) {
            Icon(Icons.Default.Refresh, contentDescription = "重置计数")
        }
    }
}

关键说明:

  • 内置组件(Button、IconButton)默认自带「波纹反馈」(按压时的水波纹效果),无需额外配置,符合 Material Design 规范。
  • remember + mutableStateOf:保存点击计数,确保 Compose 重组时,计数不会被重置(这是 Compose 状态管理的基础,新手一定要记住)。
  • 适用场景:简单的按钮、图标点击,无需自定义反馈效果。

2. 方式二:clickable 修饰符(最灵活)

如果我们想让「非内置可点击组件」支持点击(比如 Text、Box、Image),就需要用到 clickable 修饰符------这是 Compose 中最常用的点击事件实现方式,几乎适用于所有组件。

示例(文本点击、卡片点击):

ini 复制代码
@Composable
fun ClickableModifierDemo() {
    var isClicked by remember { mutableStateOf(false) }
    
    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        verticalArrangement = Arrangement.Center
    ) {
        // 1. 文本点击(无默认波纹,需手动配置)
        Text(
            text = if (isClicked) "已点击" else "点击我(文本)",
            fontSize = 20.sp,
            modifier = Modifier
                .clickable(
                    // 可选:是否启用点击(比如根据状态动态控制)
                    enabled = true,
                    // 可选:无障碍标签(优化 accessibility)
                    onClickLabel = "文本点击示例",
                    // 核心:点击回调
                    onClick = { isClicked = !isClicked }
                )
                .padding(12.dp)
                .background(if (isClicked) Color.LightGray else Color.Transparent)
        )
        
        Spacer(modifier = Modifier.height(16.dp))
        
        // 2. 卡片点击(带波纹反馈)
        Box(
            modifier = Modifier
                .size(200.dp)
                .background(Color.Blue)
                // 配置波纹反馈(MaterialTheme 主题色)
                .clickable(
                    indication = rememberRipple(color = Color.White, radius = 10.dp),
                    interactionSource = remember { MutableInteractionSource() },
                    onClick = { Log.d("ClickDemo", "卡片被点击") }
                ),
            contentAlignment = Alignment.Center
        ) {
            Text(text = "卡片点击", color = Color.White, fontSize = 18.sp)
        }
    }
}

关键说明(重点!新手必看):

  • clickable 修饰符的核心参数:

    • enabled:控制组件是否可点击(true 可点击,false 禁用,禁用时不会触发 onClick)。
    • indication:点击反馈效果(默认是主题波纹,可自定义或取消)。
    • interactionSource:用于跟踪组件的交互状态(比如是否被按压),配合 indication 使用。
  • 取消波纹反馈:如果不需要波纹,可将 indication 设为 null,示例:clickable(indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = {})。

  • 适用场景:自定义组件点击(文本、图片、卡片等),需要灵活控制反馈效果。

3. 方式三:pointerInput 修饰符(底层实现,最灵活)

clickable 修饰符本质上是对 pointerInput 的封装------pointerInput 是 Compose 中处理所有触摸事件的底层 API,支持更精细的事件控制(比如按压、抬起、取消),适合复杂场景。

示例(监听按压、抬起、取消事件):

kotlin 复制代码
@Composable
fun PointerInputDemo() {
    var pressState by remember { mutableStateOf("未按压") }
    
    Box(
        modifier = Modifier
            .size(200.dp)
            .background(Color.Green)
            .pointerInput(Unit) {
                // 监听触摸事件
                detectTapGestures(
                    // 按压时触发(手指按下瞬间)
                    onPress = {
                        pressState = "正在按压"
                        // awaitRelease():等待手指抬起,返回 true 表示正常抬起,false 表示取消(比如滑动离开)
                        val isReleased = awaitRelease()
                        pressState = if (isReleased) "按压结束" else "按压取消"
                    },
                    // 单击时触发(手指按下并抬起,无长按、双击)
                    onTap = {
                        Log.d("ClickDemo", "单击触发,坐标:$it")
                    }
                )
            },
        contentAlignment = Alignment.Center
    ) {
        Text(text = pressState, color = Color.White, fontSize = 18.sp)
    }
}

关键说明:

  • pointerInput 接收一个「键值」(示例中用 Unit,表示不依赖外部状态),当键值变化时,会重新创建事件监听。
  • detectTapGestures 是常用的触摸事件监听器,支持 onPress、onTap、onLongPress、onDoubleTap 等回调(后面会详细讲)。
  • onPress 中的 awaitRelease():必须调用,否则会导致事件异常;返回值表示按压是否正常结束(手指抬起)。除此之外,还有一个常用的 tryAwaitRelease() 方法,两者用法相似但有核心区别,新手容易混淆,这里重点区分。
  • 适用场景:需要监听按压、抬起、取消等精细事件,或者自定义手势(比如长按+拖动)。
  • 补充:tryAwaitRelease() 与 awaitRelease() 的区别(重点!) :两者都是用于等待手指抬起,但核心差异在于「是否阻塞协程」,具体对比如下:
  • awaitRelease():阻塞协程,直到手指抬起(正常抬起或取消)才会继续执行后续代码,必须在 onPress 中调用,否则会抛出异常。适合需要严格等待按压结束后再做后续操作的场景(比如按压结束后修改状态)。
  • tryAwaitRelease():不阻塞协程,会立即返回一个 Boolean? 值:返回 true 表示正常抬起,false 表示按压取消,null 表示当前没有按压事件可等待。无需强制在 onPress 中调用,灵活性更高,适合不需要阻塞协程的场景(比如同时监听多个手势)。
  • 示例(tryAwaitRelease() 用法):
kotlin 复制代码
@Composable
fun TryAwaitReleaseDemo() {
    var pressState by remember { mutableStateOf("未按压") }
    
    Box(
        modifier = Modifier
            .size(200.dp)
            .background(Color.Green)
            .pointerInput(Unit) {
                detectTapGestures(
                    onPress = {
                        pressState = "正在按压"
                        // 用 tryAwaitRelease(),不阻塞协程
                        val result = tryAwaitRelease()
                        // 立即执行后续代码,无需等待手指抬起
                        pressState = when(result) {
                            true -> "按压正常结束"
                            false -> "按压取消"
                            null -> "无按压事件"
                        }
                    },
                    onTap = {
                        Log.d("ClickDemo", "单击触发,坐标:$it")
                    }
                )
            },
        contentAlignment = Alignment.Center
    ) {
        Text(text = pressState, color = Color.White, fontSize = 18.sp)
    }
}
  • 关键注意:tryAwaitRelease() 虽然不阻塞协程,但仍需在触摸事件回调(如 onPress)中使用,否则无法获取到正确的按压状态;且它不会抛出异常,即使未调用也不会导致程序崩溃,比 awaitRelease() 更灵活,但需根据场景选择。

二、复合点击事件:长按、双击,一次性搞定

实际开发中,我们经常需要同时处理多种点击手势------比如「单击打开详情、长按弹出菜单、双击放大图片」,这时候用上面的基础方式就比较繁琐,Compose 提供了两种便捷方式:combinedClickable 修饰符 和 detectTapGestures 回调。

1. 方式一:combinedClickable 修饰符(推荐,简洁)

combinedClickable 是 clickable 的增强版,支持同时监听单击、长按、双击事件,用法和 clickable 类似,无需额外配置,适合大多数复合场景。

示例(单击+长按+双击):

ini 复制代码
@Composable
fun CombinedClickDemo() {
    var tip by remember { mutableStateOf("请操作(单击/长按/双击)") }
    // 触觉反馈(长按震动,提升交互体验)
    val haptics = LocalHapticFeedback.current
    
    Box(
        modifier = Modifier
            .size(250.dp)
            .background(Color.Purple)
            .combinedClickable(
                onClick = {
                    // 单击
                    tip = "单击触发"
                },
                onLongClick = {
                    // 长按(手指按下超过500ms,默认值,可自定义)
                    haptics.performHapticFeedback(HapticFeedbackType.LongPress) // 震动反馈
                    tip = "长按触发"
                },
                onDoubleClick = {
                    // 双击(快速点击两次)
                    tip = "双击触发"
                }
            ),
        contentAlignment = Alignment.Center
    ) {
        Text(text = tip, color = Color.White, fontSize = 18.sp, textAlign = TextAlign.Center)
    }
}

关键说明:

  • 优先级:双击 > 长按 > 单击(比如快速双击时,不会触发两次单击)。
  • 长按震动反馈:LocalHapticFeedback 是 Compose 提供的触觉反馈 API,长按添加震动,能让用户更直观感受到交互,推荐添加。
  • 禁用某类事件:只需将对应的回调设为 null(比如不需要双击,就写 onDoubleClick = null)。

2. 方式二:detectTapGestures 回调(底层,精细控制)

如果需要更精细的控制(比如自定义长按时间、双击间隔),可以使用 pointerInput + detectTapGestures,支持的回调更全面。

示例(自定义长按时间+双击间隔):

ini 复制代码
@Composable
fun DetectTapGesturesDemo() {
    var tip by remember { mutableStateOf("请操作") }
    
    Box(
        modifier = Modifier
            .size(250.dp)
            .background(Color.Orange)
            .pointerInput(Unit) {
                // 自定义长按时间(默认500ms,这里改为800ms)
                val longPressTimeout = 800L
                // 自定义双击间隔(默认300ms,这里改为500ms)
                val doubleTapTimeout = 500L
                
                detectTapGestures(
                    onTap = { tip = "单击触发,坐标:$it" },
                    onLongPress = { tip = "长按触发(800ms),坐标:$it" },
                    onDoubleTap = { tip = "双击触发(间隔500ms),坐标:$it" },
                    // 按压时触发(和前面的 pointerInput 示例一致)
                    onPress = {
                        tip = "正在按压"
                        awaitRelease()
                        tip = "按压结束"
                    }
                )
            },
        contentAlignment = Alignment.Center
    ) {
        Text(text = tip, color = Color.White, fontSize = 18.sp, textAlign = TextAlign.Center)
    }
}

关键说明:

  • 自定义长按时间、双击间隔:需要通过修改 detectTapGestures 内部的参数(示例中直接指定了时间,实际可通过源码扩展)。
  • onPress 和 onTap 的区别:onPress 是「手指按下就触发」,onTap 是「手指按下并抬起,且没有触发长按、双击才触发」。

三、按压反馈:自定义波纹,提升交互体验

点击反馈(比如波纹效果)是提升 App 交互体验的关键,Compose 默认提供了 Material Design 风格的波纹反馈,但我们也可以根据需求自定义------比如修改波纹颜色、形状、大小,或者取消反馈。

1. 自定义波纹反馈(clickable / combinedClickable)

通过 indication 参数自定义波纹,常用的是 rememberRipple(Compose 提供的波纹工厂),支持颜色、半径、是否有边界等配置。

示例(自定义波纹):

ini 复制代码
@Composable
fun CustomRippleDemo() {
    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        // 1. 圆形波纹(默认)
        Box(
            modifier = Modifier
                .size(150.dp)
                .background(Color.Blue)
                .clickable(
                    indication = rememberRipple(
                        color = Color.White, // 波纹颜色
                        radius = 75.dp, // 波纹半径(和Box大小一致,就是全屏波纹)
                        bounded = true // 是否有边界(true:波纹不超出组件范围;false:超出)
                    ),
                    interactionSource = remember { MutableInteractionSource() },
                    onClick = {}
                ),
            contentAlignment = Alignment.Center
        ) {
            Text(text = "圆形波纹", color = Color.White)
        }
        
        Spacer(modifier = Modifier.height(16.dp))
        
        // 2. 无边界波纹(超出组件范围)
        Box(
            modifier = Modifier
                .size(150.dp)
                .background(Color.Green)
                .clickable(
                    indication = rememberRipple(
                        color = Color.Red,
                        bounded = false,
                        rippleAlpha = RippleAlpha(0.5f) // 波纹透明度
                    ),
                    interactionSource = remember { MutableInteractionSource() },
                    onClick = {}
                ),
            contentAlignment = Alignment.Center
        ) {
            Text(text = "无边界波纹", color = Color.White)
        }
    }
}

2. 取消反馈(无波纹)

有些场景(比如页面背景点击、遮罩点击)不需要波纹反馈,只需将 indication 设为 null,同时必须传入 interactionSource(否则会有默认反馈)。

示例(无波纹点击):

ini 复制代码
@Composable
fun NoRippleDemo() {
    var isShow by remember { mutableStateOf(false) }
    
    Box(modifier = Modifier.fillMaxSize()) {
        // 遮罩(点击遮罩关闭弹窗)
        if (isShow) {
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .background(Color.Black.copy(alpha = 0.5f))
                    // 取消波纹反馈
                    .clickable(
                        indication = null,
                        interactionSource = remember { MutableInteractionSource() },
                        onClick = { isShow = false }
                    )
            )
        }
        
        // 按钮(打开弹窗)
        Button(
            onClick = { isShow = true },
            modifier = Modifier.align(Alignment.Center)
        ) {
            Text(text = "打开弹窗")
        }
    }
}

四、事件传递与拦截:解决嵌套组件的事件冲突

实际开发中,我们经常会遇到「组件嵌套」的情况------比如一个可点击的卡片里面,有一个可点击的按钮,这时候就会出现「事件冲突」(点击按钮时,卡片也会触发点击)。

Compose 中事件传递的规则和传统 View 类似:从子组件到父组件(冒泡机制) ,但我们可以通过「事件消费」来拦截事件,避免冲突。

示例(解决嵌套事件冲突):

less 复制代码
@Composable
fun EventInterceptDemo() {
    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        verticalArrangement = Arrangement.Center
    ) {
        // 父组件:卡片(可点击)
        Box(
            modifier = Modifier
                .size(300.dp)
                .background(Color.LightGray)
                .clickable { Log.d("EventDemo", "父组件(卡片)被点击") }
                .padding(16.dp),
            contentAlignment = Alignment.Center
        ) {
            // 子组件:按钮(可点击)
            Button(
                onClick = {
                    Log.d("EventDemo", "子组件(按钮)被点击")
                    // 事件消费:不传递给父组件(无需额外操作,默认消费)
                }
            ) {
                Text(text = "点击我(子组件)")
            }
        }
    }
}

关键说明:

  • 默认行为:子组件的点击事件会「消费」,不会传递给父组件(比如点击按钮,只会触发按钮的 onClick,不会触发卡片的 onClick)。
  • 手动拦截事件:如果需要让父组件也触发,可以在子组件的 onClick 中,手动调用父组件的点击逻辑(不推荐,会导致交互混乱)。
  • 特殊场景:如果子组件没有消费事件(比如子组件的 clickable 被禁用),事件会冒泡到父组件,触发父组件的点击。

五、实战场景:结合 ViewModel 处理点击事件

新手最容易犯的错误:将点击事件的业务逻辑(比如网络请求、数据修改)直接写在 Composable 中,这样会导致 Composable 臃肿、难以复用,还可能出现内存泄漏。

正确做法:将业务逻辑放在 ViewModel 中,Composable 只负责「触发事件」和「展示状态」,实现 UI 与业务逻辑分离。

示例(ViewModel + 点击事件):

kotlin 复制代码
// 1. ViewModel(处理业务逻辑)
class ClickViewModel : ViewModel() {
    // 状态(供 UI 观察)
    private val _clickCount = MutableStateFlow(0)
    val clickCount: StateFlow<Int> = _clickCount.asStateFlow()
    
    // 点击事件逻辑(业务逻辑放在这里)
    fun onButtonClick() {
        _clickCount.value += 1
        // 模拟网络请求、数据存储等业务逻辑
        Log.d("ClickViewModel", "按钮点击,计数:${_clickCount.value}")
    }
    
    fun onResetClick() {
        _clickCount.value = 0
    }
}

// 2. Composable(只负责 UI 和事件触发)
@Composable
fun ViewModelClickDemo(viewModel: ClickViewModel = viewModel()) {
    // 观察 ViewModel 中的状态
    val clickCount by viewModel.clickCount.collectAsStateWithLifecycle()
    
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "当前计数:$clickCount", fontSize = 20.sp)
        Spacer(modifier = Modifier.height(16.dp))
        
        Button(onClick = viewModel::onButtonClick) {
            Text(text = "点击计数")
        }
        
        Spacer(modifier = Modifier.height(8.dp))
        
        Button(onClick = viewModel::onResetClick) {
            Text(text = "重置计数")
        }
    }
}

关键说明(重点!):

  • 状态流转:Composable 触发点击事件 → ViewModel 处理业务逻辑 → ViewModel 更新状态 → Composable 观察状态并刷新 UI。
  • collectAsStateWithLifecycle:用于观察 StateFlow,自动跟随生命周期(避免后台时仍在收集,导致内存泄漏)。
  • 好处:业务逻辑可复用、可测试,Composable 更简洁,后续修改业务逻辑无需改动 UI。

六、新手避坑指南:这些错误千万别犯

结合平时开发中遇到的问题,总结了 5 个新手最容易踩的坑,看完少走弯路!

坑1:忘记用 remember 保存状态,导致点击后状态不更新

错误示例:

ini 复制代码
// 错误:没有用 remember,重组时 count 会重置为 0
var count = 0 
Button(onClick = { count++ }) {
    Text(text = "点击$count次")
}

正确示例:

ini 复制代码
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
    Text(text = "点击$count次")
}

坑2:clickable 修饰符顺序错误,导致点击范围异常

错误示例(点击范围只有文本,不是整个背景):

less 复制代码
Box(
    modifier = Modifier
        .background(Color.Blue)
        .clickable { } // 放在 background 后面,点击范围是 background 的范围(正确)?不,顺序不影响范围,但推荐放在最后
        .padding(16.dp) // 错误:padding 放在 clickable 后面,点击范围不包含 padding 区域
) {
    Text(text = "点击我")
}

正确示例(点击范围包含 padding 区域):

less 复制代码
Box(
    modifier = Modifier
        .padding(16.dp)
        .background(Color.Blue)
        .clickable { } // 推荐放在最后,点击范围包含前面所有修饰符的范围
) {
    Text(text = "点击我")
}

坑3:同时使用 clickable 和 pointerInput,导致事件冲突

错误示例(两个事件都会触发,导致逻辑混乱):

ini 复制代码
Box(
    modifier = Modifier
        .clickable { Log.d("Demo", "clickable 触发") }
        .pointerInput(Unit) {
            detectTapGestures(onTap = { Log.d("Demo", "pointerInput 触发") })
        }
)

正确做法:二选一,根据需求选择 clickable(简单场景)或 pointerInput(复杂场景)。

坑4:在 onClick 中直接修改 ViewModel 中的可变状态(非 StateFlow)

错误示例(状态更新不被 UI 观察,导致 UI 不刷新):

kotlin 复制代码
// ViewModel 中
var count = 0 // 不是可观察状态
fun onButtonClick() { count++ }

// Composable 中
Button(onClick = viewModel::onButtonClick) {
    Text(text = "点击${viewModel.count}次") // 不会刷新
}

正确做法:使用 StateFlow、LiveData 等可观察状态,Composable 用 collectAsStateWithLifecycle 观察。

坑5:忽略无障碍支持(onClickLabel 未配置)

clickable 修饰符的 onClickLabel 参数,用于给无障碍服务提供描述,不配置会影响 App 的无障碍体验(比如盲人用户无法知道这个组件的作用)。

正确示例:

ini 复制代码
Text(
    text = "重置",
    modifier = Modifier.clickable(
        onClickLabel = "重置计数,将当前计数恢复为0",
        onClick = { viewModel.onResetClick() }
    )
)

七、总结

到这里,Compose 中所有常见的点击、按压事件就讲解完毕了,我们再来回顾一下核心要点:

  1. 基础单击:优先用内置组件(Button)或 clickable 修饰符,复杂场景用 pointerInput。
  2. 复合点击:用 combinedClickable(简洁)或 detectTapGestures(精细控制),支持长按、双击。
  3. 按压反馈:用 rememberRipple 自定义波纹,不需要反馈就设为 null + 传入 interactionSource。
  4. 事件冲突:子组件默认消费事件,避免父组件误触发。
  5. 实战规范:业务逻辑放在 ViewModel 中,Composable 只负责 UI 和事件触发,避免内存泄漏。

其实 Compose 的事件处理并不复杂,核心就是「状态驱动」和「分层设计」------UI 负责展示和触发事件,业务逻辑负责处理事件和更新状态,两者分离,代码才会更简洁、更易维护。

如果这篇文章对你有帮助,欢迎点赞、收藏、评论,关注我,后续会分享更多 Compose 实战技巧~ 如有疑问,也可以在评论区留言,我会一一回复!

相关推荐
Dovis(誓平步青云)1 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
晚风叙码3 小时前
MySQL 数据类型详解:从数值到字符串,一篇讲透
android·mysql·adb
传奇开心果编程3 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程6 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程7 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程8 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程8 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓8 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程8 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
supabc1238 小时前
Celium:连接 Windows、Mac、Linux 与 Android,让远程访问和设备管理更简单
android·linux·windows·macos·远程访问·网络管理·celium