如何用Kotlin Compose实现适配手机与平板的响应式登录UI
使用单一Compose UI适配手机与平板横竖屏场景
现有面向手机开发的登录界面,因原Modifier设置仅适配手机,在平板上出现视图遮挡问题。需基于屏幕比例调整尺寸,保持UI样式不变,实现跨设备无差异适配。
当前代码
@Composable fun TestLogin( ) { val userNameFieldValue = remember { mutableStateOf("") } val passwordFieldValue = remember { mutableStateOf("") } val passwordVisibility = remember { mutableStateOf(false) } val isLoginError = remember { mutableStateOf(false) } val isInternetError = remember { mutableStateOf(false) } val configuration = LocalConfiguration.current Box( Modifier .fillMaxSize() .background(color = colorResource(id = R.color.white)), contentAlignment = Alignment.Center) { Card( shape = RoundedCornerShape(20.dp), modifier = Modifier .padding(start = 10.dp, end = 10.dp) ) { Column( modifier = Modifier .padding(start = 16.dp, end = 16.dp, bottom = 10.dp, top = 10.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { Icon( painter = painterResource(id = R.drawable.ic_launcher_background), tint = Color.Unspecified, contentDescription = "success", modifier = Modifier .align(Alignment.CenterHorizontally) ) Text( text = "Welcome to the App \n Hi There", fontWeight = FontWeight.ExtraBold, fontSize = 32.sp, color = colorResource(R.color.black), modifier = Modifier .padding(bottom = 24.dp) .fillMaxWidth(), textAlign = TextAlign.Center ) Text( text = "Login", fontWeight = FontWeight.Bold, fontSize = 22.sp, color = colorResource(id = R.color.black), modifier = Modifier .padding(top = 50.dp, bottom = 24.dp) .fillMaxWidth(), textAlign = TextAlign.Center ) OutlinedTextField( modifier = Modifier .padding(bottom = 40.dp) .fillMaxWidth(), value = userNameFieldValue.value, textStyle = TextStyle( fontSize = 16.sp, fontWeight = FontWeight.Normal ), singleLine = true, onValueChange = { isLoginError.value = false userNameFieldValue.value = it }, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Next ), enabled = true, label = { Text(text = "username") }, colors = TextFieldDefaults.outlinedTextFieldColors( focusedBorderColor = colorResource(id = R.color.black), unfocusedBorderColor = colorResource(id = R.color.black), cursorColor = colorResource(id = R.color.black) ) ) OutlinedTextField( modifier = Modifier .padding(bottom = 32.dp) .fillMaxWidth(), value = passwordFieldValue.value, textStyle = TextStyle( fontSize = 16.sp, fontWeight = FontWeight.Normal ), enabled = true, singleLine = true, onValueChange = { isLoginError.value = false passwordFieldValue.value = it }, label = { Text(text = "password") }, visualTransformation = if (passwordVisibility.value) { VisualTransformation.None } else { PasswordVisualTransformation() }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Password ), colors = TextFieldDefaults.outlinedTextFieldColors( focusedBorderColor = colorResource(id = R.color.black), unfocusedBorderColor = colorResource(id = R.color.black), cursorColor = colorResource(id = R.color.black) ) ) Row( Modifier .background( colorResource( id = getLoginButtonColor( userNameFieldValue, passwordFieldValue, isInternetError ) ), RoundedCornerShape(5.dp) ) .align(Alignment.CenterHorizontally) .fillMaxWidth() .padding(15.dp) .clickable( enabled = userNameFieldValue.value.isNotEmpty() && passwordFieldValue.value.isNotEmpty() && !isInternetError.value, onClick = {} ), content = { CircularProgressIndicator( modifier = Modifier .padding(end = 10.dp) .size(20.dp) .align(Alignment.CenterVertically), color = colorResource(id = R.color.white), strokeWidth = 3.dp ) Text( "Login", fontWeight = FontWeight.Medium, fontSize = 14.sp, textAlign = TextAlign.Center, color = colorResource(id = getLoginButtonTextColor(userNameFieldValue, passwordFieldValue, isInternetError))) }, horizontalArrangement = Arrangement.Center ) Text( text = "Forgot Password", fontWeight = FontWeight.ExtraBold, color = colorResource(id = R.color.black), modifier = Modifier .padding(top = 28.dp), textAlign = TextAlign.Center ) Text( text = "Not a User ", fontWeight = FontWeight.ExtraBold, color = colorResource(id = R.color.black), modifier = Modifier .padding(top = 24.dp) .clickable { }, textAlign = TextAlign.Center ) } } } } fun getLoginButtonColor( userNameFieldValue: MutableState<String>, passwordFieldValue: MutableState<String>, isInternetError: MutableState<Boolean> ): Int { return if(!isInternetError.value && userNameFieldValue.value.isNotEmpty() && passwordFieldValue.value.isNotEmpty()) R.color.black else R.color.purple_200 } fun getLoginButtonTextColor( userNameFieldValue: MutableState<String>, passwordFieldValue: MutableState<String>, isInternetError: MutableState<Boolean> ): Int { return if(!isInternetError.value && userNameFieldValue.value.isNotEmpty() && passwordFieldValue.value.isNotEmpty()) R.color.white else R.color.white } @Preview @Composable fun previewView() { TestLogin() }
当前显示状态
- 手机端显示正常:

- 平板端视图被遮挡:

适配解决方案
1. 限制Card最大宽度,避免大屏过度拉伸
给Card添加maxWidth修饰符,固定最大宽度为适合的数值(如400dp),同时让高度自适应内容,防止平板上Card过宽导致布局异常:
Card( shape = RoundedCornerShape(20.dp), modifier = Modifier .padding(10.dp) .maxWidth(400.dp) .wrapContentHeight() )
2. 为Column添加滚动支持
在小屏幕竖屏或特殊场景下,确保内容可滚动不被遮挡,给Column添加verticalScroll修饰符:
Column( modifier = Modifier .padding(16.dp) .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, )
3. 动态调整字体与间距(可选)
基于屏幕宽度判断是否为平板,动态调整字体大小和间距,让UI在不同设备上保持比例一致:
val isTablet = configuration.screenWidthDp > 600 val titleFontSize = if (isTablet) 36.sp else 32.sp val sectionPadding = if (isTablet) 32.dp else 24.dp
将代码中固定的32.sp、24.dp等替换为这些动态变量即可。
完整适配后代码
@Composable fun TestLogin( ) { val userNameFieldValue = remember { mutableStateOf("") } val passwordFieldValue = remember { mutableStateOf("") } val passwordVisibility = remember { mutableStateOf(false) } val isLoginError = remember { mutableStateOf(false) } val isInternetError = remember { mutableStateOf(false) } val configuration = LocalConfiguration.current val isTablet = configuration.screenWidthDp > 600 val titleFontSize = if (isTablet) 36.sp else 32.sp val sectionPadding = if (isTablet) 32.dp else 24.dp Box( Modifier .fillMaxSize() .background(color = colorResource(id = R.color.white)), contentAlignment = Alignment.Center ) { Card( shape = RoundedCornerShape(20.dp), modifier = Modifier .padding(10.dp) .maxWidth(400.dp) .wrapContentHeight() ) { Column( modifier = Modifier .padding(16.dp) .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { Icon( painter = painterResource(id = R.drawable.ic_launcher_background), tint = Color.Unspecified, contentDescription = "App Icon", modifier = Modifier.align(Alignment.CenterHorizontally) ) Text( text = "Welcome to the App \n Hi There", fontWeight = FontWeight.ExtraBold, fontSize = titleFontSize, color = colorResource(R.color.black), modifier = Modifier .padding(bottom = sectionPadding) .fillMaxWidth(), textAlign = TextAlign.Center ) Text( text = "Login", fontWeight = FontWeight.Bold, fontSize = if (isTablet) 26.sp else 22.sp, color = colorResource(id = R.color.black), modifier = Modifier .padding(top = if (isTablet) 60.dp else 50.dp, bottom = sectionPadding) .fillMaxWidth(), textAlign = TextAlign.Center ) OutlinedTextField( modifier = Modifier .padding(bottom = if (isTablet) 48.dp else 40.dp) .fillMaxWidth(), value = userNameFieldValue.value, textStyle = TextStyle( fontSize = if (isTablet) 18.sp else 16.sp, fontWeight = FontWeight.Normal ), singleLine = true, onValueChange = { isLoginError.value = false userNameFieldValue.value = it }, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Next ), enabled = true, label = { Text(text = "username") }, colors = TextFieldDefaults.outlinedTextFieldColors( focusedBorderColor = colorResource(id = R.color.black), unfocusedBorderColor = colorResource(id = R.color.black), cursorColor = colorResource(id = R.color.black) ) ) OutlinedTextField( modifier = Modifier .padding(bottom = if (isTablet) 40.dp else 32.dp) .fillMaxWidth(), value = passwordFieldValue.value, textStyle = TextStyle( fontSize = if (isTablet) 18.sp else 16.sp, fontWeight = FontWeight.Normal ), enabled = true, singleLine = true, onValueChange = { isLoginError.value = false passwordFieldValue.value = it }, label = { Text(text = "password") }, visualTransformation = if (passwordVisibility.value) { VisualTransformation.None } else { PasswordVisualTransformation() }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Password ), colors = TextFieldDefaults.outlinedTextFieldColors( focusedBorderColor = colorResource(id = R.color.black), unfocusedBorderColor = colorResource(id = R.color.black), cursorColor = colorResource(id = R.color.black) ) ) Row( Modifier .background( colorResource( id = getLoginButtonColor( userNameFieldValue, passwordFieldValue, isInternetError ) ), RoundedCornerShape(5.dp) ) .align(Alignment.CenterHorizontally) .fillMaxWidth() .padding(if (isTablet) 18.dp else 15.dp) .clickable( enabled = userNameFieldValue.value.isNotEmpty() && passwordFieldValue.value.isNotEmpty() && !isInternetError.value, onClick = {} ), content = { CircularProgressIndicator( modifier = Modifier .padding(end = 10.dp) .size(if (isTablet) 24.dp else 20.dp) .align(Alignment.CenterVertically), color = colorResource(id = R.color.white), strokeWidth = 3.dp ) Text( "Login", fontWeight = FontWeight.Medium, fontSize = if (isTablet) 16.sp else 14.sp, textAlign = TextAlign.Center, color = colorResource(id = getLoginButtonTextColor(userNameFieldValue, passwordFieldValue, isInternetError)) ) }, horizontalArrangement = Arrangement.Center ) Text( text = "Forgot Password", fontWeight = FontWeight.ExtraBold, color = colorResource(id = R.color.black), modifier = Modifier .padding(top = if (isTablet) 36.dp else 28.dp), textAlign = TextAlign.Center ) Text( text = "Not a User ", fontWeight = FontWeight.ExtraBold, color = colorResource(id = R.color.black), modifier = Modifier .padding(top = if (isTablet) 32.dp else 24.dp) .padding(bottom = 16.dp) .clickable { }, textAlign = TextAlign.Center ) } } } } fun getLoginButtonColor( userNameFieldValue: MutableState<String>, passwordFieldValue: MutableState<String>, isInternetError: MutableState<Boolean> ): Int { return if (!isInternetError.value && userNameFieldValue.value.isNotEmpty() && passwordFieldValue.value.isNotEmpty()) R.color.black else R.color.purple_200 } fun getLoginButtonTextColor( userNameFieldValue: MutableState<String>, passwordFieldValue: MutableState<String>, isInternetError: MutableState<Boolean> ): Int { return if (!isInternetError.value && userNameFieldValue.value.isNotEmpty() && passwordFieldValue.value.isNotEmpty()) R.color.white else R.color.white } @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 720) @Composable fun previewView() { TestLogin() }
内容的提问来源于stack exchange,提问作者arun
相关产品推荐
相关产品推荐

