Jetpack Compose : 从改造你的登录页面开始

6,928 阅读5分钟

我正在参加「掘金·启航计划」

什么是Compose

Jetpack Compose 是用于构建原生 Android 界面的新工具包。它使用更少的代码、强大的工具和直观的 Kotlin API,可以帮助您简化并加快 Android 界面开发,打造生动而精彩的应用。

Compose的优势

  • 更少的代码: 编写代码只需要采用 Kotlin,而不必拆分成 Kotlin 和 XML 部分,所有代码都位于同一文件中(而不是在 Kotlin 和 XML 语言之间来回切换)时,跟踪变得更容易。
  • 直观: 利用 Compose,您可以构建不与特定 activity 或 fragment 相关联的小型无状态组件。
  • 加速开发: Compose 与您所有的现有代码兼容:您可以从 View 调用 Compose 代码,也可以从 Compose 调用 View。大多数常用库(如 Navigation、ViewModel 和 Kotlin 协程)都适用于 Compose,因此您可以随时随地开始采用。
  • 功能强大: Compose 不仅解决了声明性界面的问题,还改进了无障碍功能 API、布局等各种内容。

Compose的上手成本

XML代码如下:

<LinearLayout android:orientation="vertical"> 
    <TextView android:text="Hello" />
    <TextView android:text="World" />
</LinearLayout>

Compose代码如下:

Column { 
    Text("Hello")
    Text("World")
}

通过XML与Compose代码对比,我们发现Compose更直观,代码量更少,可以通过官方文档和示例快速上手(老安卓er狂喜 )。
接下来我们就编写一个简单的登录页面试一下吧。

将 Jetpack Compose 添加到应用中

为了您能正常运行本项目,请使用 Android Studio Chipmunk (2021.2.1) 🐿️Android Gradle 7.2.2 或者以上版本。
Download Android Studio | Android Developer

配置Android Gradle 插件,如下所示:

buildscript {
    ...
    dependencies {
        classpath "com.android.tools.build:gradle:7.2.2"
        ...
    }
}

在应用的 build.gradle 文件中启用 Jetpack Compose,如下所示:

android {

    buildFeatures {
        // Enables Jetpack Compose for this module
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion '1.3.0'
    }
}

在应用的 build.gradle 文件中添加 Jetpack Compose 工具包依赖项(按需导入),如下所示:

dependencies {
    // Integration with activities
    implementation 'androidx.activity:activity-compose:1.5.1'
    // Compose Material Design
    implementation 'androidx.compose.material:material:1.2.1'
    // Animations
    implementation 'androidx.compose.animation:animation:1.2.1'
    // Tooling support (Previews, etc.)
    implementation 'androidx.compose.ui:ui-tooling:1.2.1'
    // Integration with ViewModels
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'
    // UI Tests
    androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.2.1'
}

开始编写登录界面

这图是常见的登录界面设计图,如下所示:

Screenshot_20221019_1142191.png

看到设计图我首先想到的是通过ConstraintLayout来进行布局,并把按钮和输入框等组合项摆放到对应的位置,代码如下:

ConstraintLayout(
    modifier = Modifier
        .fillMaxSize()
        .paint(
            painter = painterResource(id = R.drawable.bg),
            contentScale = ContentScale.FillBounds
        )
        .padding(start = 40.dp, end = 40.dp)
        .systemBarsPadding() //设置系统状态栏Padding
) {
    //为每个可组合项创建引用
    val (black, welcome, wan, username, password, login, sign_in, sign_up) = createRefs()
        
    ...
    
    }

使用Image创建返回按钮,代码如下:

Image(
    //设置图片
    painter = painterResource(R.drawable.ic_back),
    contentDescription = null,
    modifier = Modifier
        //设置尺寸
        .size(15.dp)
        //将black分配给可组合按钮,并将其约束到ConstraintLayout的顶部
        .constrainAs(black) { 
            //top约束,间距15dp 
            top.linkTo(parent.top, margin = 15.dp)
        }
        //点击事件
        .clickable {
            navigation(Router.MAIN)
        }
)

使用Text创建"Welcome",代码如下:

Text(
    text = "Welcome",
    style = MaterialTheme.typography.h4,
    color = colorResource(R.color.white),
    //将welcome分配给可组合按钮,并将其约束到black的底部
    modifier = Modifier
        .constrainAs(welcome) {
            top.linkTo(black.bottom, 60.dp)
        }
)

使用TextField创建输入框,代码如下:

var usernameText by rememberSaveable { mutableStateOf("") }
TextField(
    value = usernameText,
    onValueChange = {
        usernameText = it
    },
    placeholder = {
        Text("请输入用户名")
    },
    colors = TextFieldDefaults.textFieldColors(
        backgroundColor = Color.Transparent,
        disabledIndicatorColor = colorResource(id = R.color.white),
        unfocusedIndicatorColor = colorResource(id = R.color.white),
        focusedIndicatorColor = colorResource(id = R.color.white),
        focusedLabelColor = colorResource(id = R.color.white),
        errorIndicatorColor = colorResource(id = R.color.white),
        placeholderColor = colorResource(id = R.color.text_ccc),
        textColor = colorResource(id = R.color.text_fff),
        cursorColor = colorResource(id = R.color.white)
    ),
    //将username分配给可组合按钮,并将其约束到password的顶部
    modifier = Modifier
        .fillMaxWidth()
        .height(50.dp)
        .constrainAs(username) {
            bottom.linkTo(password.top, 25.dp)
        }
)

使用Image创建登录按钮,代码如下:

Image(
    painter = painterResource(R.drawable.ic_right_arrow),
    contentDescription = null,
    modifier = Modifier
        //剪裁成圆形
        .clip(CircleShape)
        .background(colorResource(R.color.theme_orange))
        .size(75.dp)
        .padding(25.dp)
        //将login分配给可组合按钮,并将其约束到ConstraintLayout的右侧、sign_up的顶部
        .constrainAs(login) {
            end.linkTo(parent.end)
                bottom.linkTo(sign_up.top, 60.dp)
            }
        .clickable {
            if (checkParameter(usernameText, passwordText)) {
                 viewModel.login(usernameText, passwordText)
            }
        }
)

使用Text创建"登录"文字并使用屏障线进行约束,代码如下:

//创建登录按钮顶部的屏障线
val loginTopBarrier = createTopBarrier(login)
//创建登录按钮底部的屏障线
val loginBottomBarrier = createBottomBarrier(login)
Text(
    text = "登录",
    fontSize = 25.sp,
    color = colorResource(R.color.white),
    modifier = Modifier
        //将sign_in分配给可组合按钮,并将其约束到loginTopBarrier和loginBottomBarrier之间
        .constrainAs(sign_in) {
            top.linkTo(loginTopBarrier)
            bottom.linkTo(loginBottomBarrier)
        }
)

使用Text创建去注册,代码如下:

Text(
    text = "去注册",
    //设置下划线
    textDecoration = TextDecoration.Underline,
    fontSize = 16.sp,
    color = colorResource(R.color.white),
    //将sign_up分配给可组合按钮,并将其约束到ConstraintLayout的底部
    modifier = Modifier
        .constrainAs(sign_up) {
            bottom.linkTo(parent.bottom, 40.dp)
        }
        .clickable {
            navigation(Router.USER_REGISTER)
        }
)

最后完整代码如下:

@Composable
fun UserLoginPage() {
    var usernameText by rememberSaveable { mutableStateOf("") }
    var passwordText by rememberSaveable { mutableStateOf("") }
    ConstraintLayout(
        modifier = Modifier
            .fillMaxSize()
            .paint(
                painter = painterResource(id = R.drawable.bg),
                contentScale = ContentScale.FillBounds
            )
            .padding(start = 40.dp, end = 40.dp)
            .systemBarsPadding()
    ) {
        val (black, welcome, wan, username, password, login, sign_in, sign_up) = createRefs() 
        val loginTopBarrier = createTopBarrier(login)
        val loginBottomBarrier = createBottomBarrier(login)
        Image(
            painter = painterResource(R.drawable.ic_back),
            contentDescription = null,
            modifier = Modifier
                .size(15.dp)
                .constrainAs(black) {
                    top.linkTo(parent.top, margin = 15.dp) //top约束
                }
                .clickable { //点击事件
                    navigation(Router.MAIN)
                }
        )
        Text(
            text = "Welcome",
            style = MaterialTheme.typography.h4,
            color = colorResource(R.color.white),
            modifier = Modifier
                .constrainAs(welcome) {
                    top.linkTo(black.bottom, 60.dp)
                }
        )
        Text(
            text = "玩Android",
            style = MaterialTheme.typography.h5,
            color = colorResource(R.color.white),
            modifier = Modifier
                .constrainAs(wan) {
                    top.linkTo(welcome.bottom, 10.dp)
                }
        )
        TextField(
            value = usernameText,
            onValueChange = {
                usernameText = it
            },
            placeholder = {
                Text("请输入用户名")
            },
            colors = TextFieldDefaults.textFieldColors(
                backgroundColor = Color.Transparent,
                disabledIndicatorColor = colorResource(id = R.color.white),
                unfocusedIndicatorColor = colorResource(id = R.color.white),
                focusedIndicatorColor = colorResource(id = R.color.white),
                focusedLabelColor = colorResource(id = R.color.white),
                errorIndicatorColor = colorResource(id = R.color.white),
                placeholderColor = colorResource(id = R.color.text_ccc),
                textColor = colorResource(id = R.color.text_fff),
                cursorColor = colorResource(id = R.color.white)
            ),
            modifier = Modifier
                .fillMaxWidth()
                .height(50.dp)
                .constrainAs(username) {
                    bottom.linkTo(password.top, 25.dp)
                }
        )
        TextField(
            value = passwordText,
            onValueChange = {
                passwordText = it
            },
            placeholder = {
                Text("请输入用户密码")
            },
            colors = TextFieldDefaults.textFieldColors(
                backgroundColor = Color.Transparent,
                disabledIndicatorColor = colorResource(id = R.color.white),
                unfocusedIndicatorColor = colorResource(id = R.color.white),
                focusedIndicatorColor = colorResource(id = R.color.white),
                focusedLabelColor = colorResource(id = R.color.white),
                errorIndicatorColor = colorResource(id = R.color.white),
                placeholderColor = colorResource(id = R.color.text_ccc),
                textColor = colorResource(id = R.color.text_fff),
                cursorColor = colorResource(id = R.color.white)
            ),
            visualTransformation = PasswordVisualTransformation(),
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
            modifier = Modifier
                .fillMaxWidth()
                .height(50.dp)
                .constrainAs(password) {
                    bottom.linkTo(login.top, 25.dp)
                }
        )
        Text(
            text = "登录",
            fontSize = 25.sp,
            color = colorResource(R.color.white),
            modifier = Modifier
                .constrainAs(sign_in) {
                    top.linkTo(loginTopBarrier)
                    bottom.linkTo(loginBottomBarrier)
                }
        )
        Image(
            painter = painterResource(R.drawable.ic_right_arrow),
            contentDescription = null,
            modifier = Modifier
                .clip(CircleShape)
                .background(colorResource(R.color.theme_orange))
                .size(75.dp)
                .padding(25.dp)
                .constrainAs(login) {
                    end.linkTo(parent.end)
                    bottom.linkTo(sign_up.top, 60.dp)
                }
                .clickable {
                    if (checkParameter(usernameText, passwordText)) {
                        viewModel.login(usernameText, passwordText)
                    }
                }
        )
        Text(
            text = "去注册",
            textDecoration = TextDecoration.Underline,
            fontSize = 16.sp,
            color = colorResource(R.color.white),
            modifier = Modifier
                .constrainAs(sign_up) {
                    bottom.linkTo(parent.bottom, 40.dp)
                }
                .clickable {
                    navigation(Router.USER_REGISTER)
                }
        )
    }
}

如何预览

借助 @Preview 注解,您可以在 Android Studio 中预览可组合函数,而无需构建应用并将其安装到 Android 设备或模拟器中。
该注解必须用于不接受参数的可组合函数。
请在 @Composable 上方添加 @Preview 注解。

@Preview()
@Composable
fun UserLoginPagePreview() {
    MaterialTheme { 
        UserLoginPage()
    }
}

Thanks

以上就是本篇文章的全部内容,如有问题欢迎指出,我们一起进步。
如果觉得本篇文章对您有帮助的话请点个赞让更多人看到吧,您的鼓励是我前进的动力。
谢谢~~

源代码地址

推荐阅读