Android compose wanandroid app之导航规整以及登录页个人中心页实现

前言

在前面开发时只是注重了页面绘制,已经compose各种组件的使用,没有规整导航,所以页面跳转的操作很难实现;今天先规整一下页面导航,在页面跳转操作完成之后在绘制登录页以及个人中心页。

导航规整

在前面绘制页面的时候说到,compose打开页面的时候会在当前页面直击打开,所以就需要把要打开的页面都放在主页中进行打开,那么就要区分页面是首页,还是其他页面。

首先定义一个页面枚举,main代表首页,其他则是其他页面:

/**
 * 页面类
 * */
enum class RouteKey(val route:String){
    Main("main"),
    Login("login"),
    WebView("webview")
}

使用navhost进行导航,主页所有页面都用navigation进行包裹:

@ExperimentalMaterialApi
@ExperimentalCoilApi
@ExperimentalPagerApi
@Composable
fun RouteNavigation(navHostController: NavHostController,
           onFinish: () -> Unit
){
    val context = LocalContext.current

    NavHost(navController = navHostController, startDestination = RouteKey.Main.route){
		//主页面
        navigation(
            route = RouteKey.Main.route,
            startDestination = Nav.BottomNavScreen.HomeScreen.route
        ){

            composable(Nav.BottomNavScreen.HomeScreen.route){
                HomePage(navHostController = navHostController)
            }
            ....//其他要展示在主页面的paer
        }
        //要打开的新页面
        //登录页
        composable(RouteKey.Login.route) {
            LoginPage(navHostController = navHostController)
        }
    }
}

首页导航页面把四个页面都封装起来:

object Nav {
    sealed class BottomNavScreen(val route: String, @StringRes val resourceId: Int, @DrawableRes val id: Int) {
        object HomeScreen: BottomNavScreen("home", R.string.nav_home, R.drawable.home_unselected)
        object ProjectScreen: BottomNavScreen("project",R.string.nav_project,R.drawable.project_unselected)
        object ClassicScreen: BottomNavScreen("classic",R.string.nav_classic,R.drawable.classic_unselected)
        object MineScreen: BottomNavScreen("mine", R.string.nav_mine, R.drawable.mine_unselected)
    }
    //主页点击两次返回桌面
    var onMainBackPressed = false
    val bottomNavRoute = mutableStateOf<BottomNavScreen>(BottomNavScreen.HomeScreen)
}

将不同页面的展示封装到一个page,所有页面都在这个page打开,并加载到MainActivity里面去,但是要区分是主页,还是其他页面。

先判断是否是主页:

fun isMainScreen(route:String):Boolean = when(route){
    Nav.BottomNavScreen.HomeScreen.route,
        Nav.BottomNavScreen.ProjectScreen.route,
        Nav.BottomNavScreen.ClassicScreen.route,
        Nav.BottomNavScreen.MineScreen.route -> true

    else -> false
}

然后根据得到的结果加载页面:

@ExperimentalPagerApi
@ExperimentalMaterialApi
@Composable
fun MainPage(
    navHostController: NavHostController = rememberNavController(),
    onFinish:() -> Unit
){
    //返回back堆栈的顶部条目
    val navBackStackEntry by navHostController.currentBackStackEntryAsState()
    //返回当前route
    val currentRoute = navBackStackEntry?.destination?.route ?: Nav.BottomNavScreen.HomeScreen.route
    //加载主页内容
    if (isMainScreen(currentRoute)){
        Scaffold(
            contentColor = MaterialTheme.colors.background,
            //标题栏
            topBar = {
                Column {
                    Spacer(
                        modifier = Modifier
                            .background(MaterialTheme.colors.primary)
                            .statusBarsHeight()
                            .fillMaxWidth()
                    )
                }
            },
            //底部导航栏
            bottomBar = {
                Column {
                    BottomNavBar(Nav.bottomNavRoute.value, navHostController)
                    Spacer(
                        modifier = Modifier
                            .background(MaterialTheme.colors.primary)
                            .navigationBarsHeight()
                            .fillMaxWidth()
                    )
                }
            },
            //内容
            content = { paddingValues: PaddingValues ->
                //内容嵌套在Scaffold中
                RouteNavigation(navHostController, paddingValues, onFinish)

                OnBackClick(navHostController)
            })
    }else{
        //加载独立页面
        RouteNavigation(navHostController, onFinish = onFinish)
    }
}

到这里就完成了导航的规整,页面打开也没有问题,接下来就是个人中心页面以及登录页面的绘制和实现了。

个人中心的实现

目前个人中心比较简单,就展示了一个头像,昵称,用户id以及用户积分,更多的东西等到实现收藏等操作之后在添加,简单看一下效果图。
在这里插入图片描述
布局元素比较简单,这里就不贴布局文件了。

MineViewmodel获取数据

登录成功之后保存cookie,通过cookie调用用户信息接口,获取用户信息。

class MineViewModel : ViewModel() {
    //默认头像
    val defaultHead = "https://jusha-info.oss-cn-shenzhen.aliyuncs.com/obt/mall/upload/image/store/2021/08/06/1628250153533.png"

    private val _userInfo = MutableLiveData<UserConfigModule>()
    val userInfo = _userInfo
    fun getUserInfo(){
        Log.e("intoTAG","get user info")
        NetWork.service.getUserInfo().enqueue(object :  Callback<BaseResult<UserConfigModule>>{
            override fun onResponse(
                call: Call<BaseResult<UserConfigModule>>,response: Response<BaseResult<UserConfigModule>>) {
                Log.e("intoTAG","response")
                response.body()?.let {
                    _userInfo.value = it.data
                }
            }

            override fun onFailure(call: Call<BaseResult<UserConfigModule>>, t: Throwable) {
                Log.e("intoTAG","onFailure${t.message}")
            }
        })
    }
}

MinePage

获取信息并展示。

@Composable
fun MinePage(navHostController: NavHostController){
    val mineViewModel:MineViewModel = viewModel()
    val userInfo by mineViewModel.userInfo.observeAsState()
    mineViewModel.getUserInfo()
    Column(
        Modifier
            .fillMaxSize()
            .verticalScroll(rememberScrollState())) {
        com.yangchoi.composeuidemo.ui.bar.TopAppBar(title = "我的")
        Box(
            Modifier
                .background(Color.White)
                .fillMaxSize()
        ) {
            Column(Modifier.fillMaxSize()) {
                if (userInfo !== null){
                    //头像昵称
                    ConstraintLayout {
                        val (headImg,userName,userId)  = createRefs()

                        Image(painter = rememberImagePainter(mineViewModel.defaultHead),
                            contentDescription = "用户头像",
                            modifier = Modifier
                                .size(80.dp)
                                .padding(16.dp, 20.dp, 0.dp, 0.dp)
                                .clip(shape = RoundedCornerShape(50))
                                .constrainAs(headImg) {})

                        Text(text = "${userInfo!!.userInfo.nickname}",fontSize = 14.sp,color = Color.Black,modifier = Modifier
                            .padding(10.dp, 20.dp, 0.dp, 0.dp)
                            .constrainAs(userName) {
                                start.linkTo(headImg.end)
                                top.linkTo(headImg.top)
                            })

                        Text(text = "${userInfo!!.userInfo.id}",fontSize = 12.sp,color = Color.Gray,modifier = Modifier
                            .padding(10.dp, 20.dp, 0.dp, 0.dp)
                            .constrainAs(userId) {
                                start.linkTo(headImg.end)
                                bottom.linkTo(headImg.bottom)
                            })
                    }

                    ConstraintLayout(modifier = Modifier
                        .fillMaxWidth()
                        .padding(vertical = 40.dp)
                        .height(50.dp)) {
                        val (icons,title,integral,btmLine) = createRefs()

                        Row(Modifier
                            .constrainAs(icons) {}
                            .fillMaxHeight()
                            .padding(16.dp, 0.dp, 0.dp, 0.dp),
                            verticalAlignment = Alignment.CenterVertically) {
                            Image(painter = painterResource(id =R.drawable.icon_integral),
                                contentDescription = "积分", modifier = Modifier
                                    .height(20.dp)
                                    .width(20.dp))
                        }

                        Row(modifier = Modifier
                            .constrainAs(title) {
                                start.linkTo(icons.end)
                            }
                            .fillMaxHeight()
                            .padding(horizontal = 10.dp),
                            verticalAlignment = Alignment.CenterVertically) {
                            Text(text = "积分",fontSize = 12.sp,color = Color.Black,textAlign = TextAlign.Center)
                        }

                        Row(modifier = Modifier
                            .fillMaxHeight()
                            .constrainAs(integral) {
                                end.linkTo(parent.end)
                            }
                            .padding(horizontal = 16.dp),
                            verticalAlignment = Alignment.CenterVertically) {
                            Text(text = "${userInfo!!.coinInfo.coinCount}",fontSize = 12.sp,color = Color.Gray,textAlign = TextAlign.Center,)
                        }

                        Divider(
                            modifier = Modifier
                                .padding(0.dp, 0.dp, 16.dp, 0.dp,)
                                .constrainAs(btmLine) {
                                    bottom.linkTo(parent.bottom)
                                },
                            color = Color(229,224,227),
                            thickness = 1.dp,
                            startIndent = 16.dp)
                    }
                }else{
                    Row(modifier = Modifier
                        .padding(horizontal = 16.dp, vertical = 200.dp)
                        .fillMaxWidth()
                        .height(50.dp)
                        .border(
                            1.dp,
                            color = Color(114, 160, 240),
                            shape = RoundedCornerShape(20.dp)
                        ),verticalAlignment = Alignment.CenterVertically) {

                        Text(text = "登  录",
                            fontSize = 16.sp,
                            modifier = Modifier
                                .fillMaxWidth()
                                .clickable {
                                    navHostController.navigate("${RouteKey.Login.route}")
                                },
                            color = Color(114, 160, 240),
                            textAlign = TextAlign.Center)

                    }
                }


            }
        }
    }
}

请求头添加cookie

登录成功之后会返回一个cookie在请求头里面,只需要将cookie拦截并保存下来,就可以通过cookie去获取用户信息。

//创建OKhttp
private val client: OkHttpClient.Builder = OkHttpClient.Builder()
            .addInterceptor(LogInterceptor())
            .addInterceptor {
                val request = it.request()
                val response = it.proceed(request)
                val requestUrl = request.url.toString()
                val domain = request.url.host
                //cookie可能有多个,都保存下来
                if ((requestUrl.contains(SAVE_USER_LOGIN_KEY) || requestUrl.contains(SAVE_USER_REGISTER_KEY))) {
                    val cookies = response.headers(SET_COOKIE_KEY)
                    val cookie = encodeCookie(cookies)
                    saveCookie(requestUrl, domain, cookie)
                }
                response
            }
            //请求时设置cookie
            .addInterceptor {
                val request = it.request()
                val builder = request.newBuilder()
                val domain = request.url.host
                //获取domain内的cookie
                if (domain.isNotEmpty()) {
                    val sqDomain: String = DataStoreUtil.readStringData(domain, "")
                    val cookie: String = if (sqDomain.isNotEmpty()) sqDomain else ""
                    if (cookie.isNotEmpty()) {
                        builder.addHeader(COOKIE_NAME, cookie)
                    }
                }
                it.proceed(builder.build())
            }
            .connectTimeout(10, TimeUnit.SECONDS)
            .readTimeout(20, TimeUnit.SECONDS)
            .retryOnConnectionFailure(false)

在网络请求的位置添加以上两个拦截器就行了。

登录页面的实现

首先来看效果图。
在这里插入图片描述
简单的绘制了一个登录页面,UI就不要纠结了,丑是真的丑~

可以看到在输入框左边有一个图标,然后是提示内容,以及密码框右边的显示和隐藏密码的图标;选中的时候颜色发生改变,并且在左上角显示提示用户输入的内容。

OutlinedTextField 属性解析

在实现以上效果前,先要了解OutlinedTextField的属性,才能加以运用 ;先看一下属性列表。

@Composable
fun OutlinedTextField(
    value: String,
    onValueChange: (String) -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    readOnly: Boolean = false,
    textStyle: TextStyle = LocalTextStyle.current,
    label: @Composable (() -> Unit)? = null,
    placeholder: @Composable (() -> Unit)? = null,
    leadingIcon: @Composable (() -> Unit)? = null,
    trailingIcon: @Composable (() -> Unit)? = null,
    isError: Boolean = false,
    visualTransformation: VisualTransformation = VisualTransformation.None,
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
    keyboardActions: KeyboardActions = KeyboardActions.Default,
    singleLine: Boolean = false,
    maxLines: Int = Int.MAX_VALUE,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    shape: Shape = MaterialTheme.shapes.small,
    colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
) 
  • value: String 输入框显示的文本
  • onValueChange: (String) -> Unit 值发生改变之后触发的回调
  • modifier: Modifier = Modifier 修饰
  • enabled: Boolean = true 可用
  • readOnly: Boolean = false 是否只读
  • textStyle: TextStyle = LocalTextStyle.current
  • label: @Composable (() -> Unit)? = null 输入框获取焦点时左上角提示的内容
  • placeholder: @Composable (() -> Unit)? = null 输入框提示的内容
  • leadingIcon: @Composable (() -> Unit)? = null 输入框左侧的图标
  • trailingIcon: @Composable (() -> Unit)? = null 输入框右侧的图标
  • isError: Boolean = false 是否处于错误状态
  • visualTransformation: VisualTransformation = VisualTransformation.None, 转换输入值的视觉表示
  • keyboardOptions: KeyboardOptions = KeyboardOptions.Default 输入框输入类型
  • singleLine: Boolean = false, 是否单行显示
  • maxLines: Int = Int.MAX_VALUE 最大行数
  • colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors() 颜色集合,设置获取焦点,失去焦点以及光标等颜色

大致就是这些属性了,知道使用之后就可以封装输入框了。

封装输入框

定义状态枚举PwdShowState,通过状态设置密码是否可见。

通过value:String设置输入框显示的值。

通过placeholder:String设置输入框提示的值。

通过color:TextFieldColors设置对应状态下的颜色,获得焦点、失去焦点、以及光标时候的颜色。

通过leadingIcon:ImageVector设置左侧图标。

通过trailingIcon:ImageVector设置右侧图标,通过trailingtintIcon:Color设置图标颜色。

通过keyboardOptions: KeyboardOptions设置输入框输入类型。

通过visualTransformation: VisualTransformation = VisualTransformation.None改变设置密码是否可见。

通过onValueChange:(String) -> Unit获取输入框发生改变时值的回调。

//输入框
enum class PwdShowState{
    Show,Hide
}
@Composable
fun MyTextField(value:String,
                label:String,
                placeholder:String,
                color:TextFieldColors,
                leadingIcon:ImageVector,
                trailingIcon:ImageVector,
                trailingtintIcon:Color,
                modifier: Modifier,
                modifierTrailing: Modifier,
                keyboardOptions: KeyboardOptions,
                visualTransformation: VisualTransformation = VisualTransformation.None,
                onValueChange:(String) -> Unit){

    val showState = remember {
        mutableStateOf(PwdShowState.Hide)
    }

    val icon =  if (showState.value === PwdShowState.Hide){
        painterResource(id = R.drawable.pwd_look)
    }else{
        painterResource(id = R.drawable.pwd_hide)
    }

    OutlinedTextField(value = value,
        colors = color,
        label = {
            Text(text = label)
        },
        placeholder = {
            Text(text = placeholder)
        },
        modifier = modifier,
        keyboardOptions = keyboardOptions,
        leadingIcon = {
            Icon(leadingIcon,"左边图标",modifierTrailing,trailingtintIcon)
        },
        trailingIcon = {
            if (label.equals("密码")){
                IconButton(onClick = {
                    if (showState.value === PwdShowState.Hide){
                        showState.value = PwdShowState.Show
                    }else{
                        showState.value = PwdShowState.Hide
                    }
                }) {
                    if (showState.value === PwdShowState.Hide){
                        Icon(icon, contentDescription = "点击密码可见",modifier = Modifier.size(30.dp))
                    }else{
                        Icon(icon, contentDescription = "点击密码隐藏",modifier = Modifier.size(30.dp))
                    }
                }
            }
        },
        visualTransformation = if (label.equals("密码")){
            if (showState.value === PwdShowState.Hide){ PasswordVisualTransformation()} else visualTransformation
        }else{
            visualTransformation
        },
        singleLine = true,
        onValueChange = onValueChange)
}

输入框的使用

根据不同的使用场景,设置不同的参数。

账号:

val userName = remember {
         mutableStateOf("")
}
val colors = TextFieldDefaults.outlinedTextFieldColors(
           focusedBorderColor = Color(68,84,246),
           unfocusedBorderColor = Color.Gray,
           cursorColor = Color(68,84,246)
)
MyTextField(
           value = userName.value,
           label = "账号",
           placeholder = "请输入账号",
           color = colors,
           leadingIcon = Icons.Default.Phone,
           trailingIcon = Icons.Default.Phone,
           trailingtintIcon = Color(68,84,246),
           modifier = Modifier
                 .padding(12.dp, 0.dp, 12.dp, 0.dp)
                 .fillMaxWidth(),
           modifierTrailing = Modifier,
           keyboardOptions = KeyboardOptions(
                keyboardType = KeyboardType.Text
           ),
           onValueChange = {
                 userName.value = it
          }
)

密码:

val password = remember {
        mutableStateOf("")
}
val colors = TextFieldDefaults.outlinedTextFieldColors(
           focusedBorderColor = Color(68,84,246),
           unfocusedBorderColor = Color.Gray,
           cursorColor = Color(68,84,246)
)
MyTextField(
       value = password.value,
       label = "密码",
       placeholder = "请输入密码",
       color = colors,
       leadingIcon = Icons.Default.Lock,
       trailingIcon = Icons.Default.Lock,
       trailingtintIcon = Color(68,84,246),
       modifier = Modifier
               .padding(12.dp, 0.dp, 12.dp, 0.dp)
               .fillMaxWidth(),
       modifierTrailing = Modifier,
       keyboardOptions = KeyboardOptions(
               keyboardType = KeyboardType.Password
       ),
       onValueChange = {
         password.value = it
       }
)

输入框实现完成~

登录按钮实现

在点击登录按钮的时候,登录接口请求过程中加载一个简单的动画,在登录成功或者失败之后结束动画。

创建按钮状态枚举

Normal正常情况下的状态

Pressed 按下时的状态

remember 记录状态的值

//按钮添加动画
enum class ButtonState{
    Normal,Pressed
}
//记录状态值
val buttonState = remember {
        mutableStateOf(ButtonState.Normal)
}

定义transition

定义一个transition,以及后面通过该元素设置颜色、大小等参数。

val transition = updateTransition(targetState = buttonState, label = "ButtonTransition")

设置按钮颜色、大小以及shape

    val buttonBackgroundColor: Color by transition.animateColor(
        transitionSpec = { tween(duration)}
    ) { buttonState ->
        when(buttonState.value){
            ButtonState.Normal -> Color(68,84,246)
            ButtonState.Pressed -> Color(68,84,246)
        }
    }

    val buttonWidth: Dp by transition.animateDp(transitionSpec = {
        tween(duration)}
    ) {buttonState ->
        when(buttonState.value){
            ButtonState.Normal -> 300.dp
            ButtonState.Pressed -> 60.dp
        }
    }

    val buttonShape: Dp by transition.animateDp(transitionSpec = {
        tween(duration)}
    ) {buttonState ->
        when(buttonState.value){
            ButtonState.Normal -> 4.dp
            ButtonState.Pressed -> 100.dp
        }
    }

使用Button并配置样式

属性列表:

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun Button(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    elevation: ButtonElevation? = ButtonDefaults.elevation(),
    shape: Shape = MaterialTheme.shapes.small,
    border: BorderStroke? = null,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    content: @Composable RowScope.() -> Unit
)
  • onClick: () -> Unit 点击事件回调
  • enabled: Boolean = true 是否可用,是否可以点击,这里可以加上判断,当用户名和密码都不为空的时候可以使用enabled = !userName.isNullOrBlank() && !password.isNullOrBlank()
  • colors: ButtonColors = ButtonDefaults.buttonColors() 设置背景颜色以及点击时候的背景颜色等
  • content: @Composable RowScope.() -> Unit compose函数,实现逻辑
Button(modifier = Modifier
        .width(buttonWidth)
        .height(50.dp),shape = RoundedCornerShape(buttonShape),
        onClick = {
        //todo
	    },colors = ButtonDefaults.buttonColors(
	        backgroundColor = buttonBackgroundColor,
	        disabledBackgroundColor = Color(68,84,246).copy(0.5f)
	    ),enabled = !userName.isNullOrBlank() && !password.isNullOrBlank()) {
	        if (buttonState.value == ButtonState.Normal){
	            Text(text = "登录")
	        }else{
	            CircularProgressIndicator(
	                color = Color.White,
	                strokeWidth = 2.dp,
	                modifier = Modifier.size(24.dp)
	            )
	        }
	    }

点击逻辑,将按钮状态设置成Pressed

buttonState.value = ButtonState.Pressed

并请求登录接口

loginViewModel.toLogin(userName,password,{
	//回调  状态重置
      buttonState.value = ButtonState.Normal
      navHostController.navigateUp()
})

通过CircularProgressIndicator实现动画

if (buttonState.value == ButtonState.Normal){
            Text(text = "登录")
}else{
            CircularProgressIndicator(
                color = Color.White,
                strokeWidth = 2.dp,
                modifier = Modifier.size(24.dp)
            )
}

按钮全部代码

按钮封装的代码:

//按钮添加动画
enum class ButtonState{
    Normal,Pressed
}

@Composable
fun MyButton(userName:String,password:String,loginViewModel:LoginViewModel,navHostController: NavHostController){
    val buttonState = remember {
        mutableStateOf(ButtonState.Normal)
    }

    val transition = updateTransition(targetState = buttonState, label = "ButtonTransition")

    val duration = 600

    val buttonBackgroundColor: Color by transition.animateColor(
        transitionSpec = { tween(duration)}
    ) { buttonState ->
        when(buttonState.value){
            ButtonState.Normal -> Color(68,84,246)
            ButtonState.Pressed -> Color(68,84,246)
        }
    }

    val buttonWidth: Dp by transition.animateDp(transitionSpec = {
        tween(duration)}
    ) {buttonState ->
        when(buttonState.value){
            ButtonState.Normal -> 300.dp
            ButtonState.Pressed -> 60.dp
        }
    }

    val buttonShape: Dp by transition.animateDp(transitionSpec = {
        tween(duration)}
    ) {buttonState ->
        when(buttonState.value){
            ButtonState.Normal -> 4.dp
            ButtonState.Pressed -> 100.dp
        }
    }

    Button(modifier = Modifier
        .width(buttonWidth)
        .height(50.dp),shape = RoundedCornerShape(buttonShape),onClick = {
        buttonState.value = ButtonState.Pressed
        loginViewModel.toLogin(userName,password,{
            buttonState.value = ButtonState.Normal
            navHostController.navigateUp()
        })
    },colors = ButtonDefaults.buttonColors(
        backgroundColor = buttonBackgroundColor,
        disabledBackgroundColor = Color(68,84,246).copy(0.5f)
    ),enabled = !userName.isNullOrBlank() && !password.isNullOrBlank()) {
        if (buttonState.value == ButtonState.Normal){
            Text(text = "登录")
        }else{
            CircularProgressIndicator(
                color = Color.White,
                strokeWidth = 2.dp,
                modifier = Modifier.size(24.dp)
            )
        }
    }
}

调用:

MyButton(userName.value,password.value,loginViewModel,navHostController)

到这里呢整个登录页面所有的元素都构建好了,剩下的就是viewmodel实现登录请求以及结果回调了。

LoginViewModel

class LoginViewModel : ViewModel() {

    private val _loginInfo = MutableLiveData<Any>()
    val loginInfo = _loginInfo

    fun toLogin(username:String,password:String,callback:()->Unit){
        NetWork.service.login(username,password).enqueue(object : Callback<BaseResult<Any>>{
            override fun onResponse(call: Call<BaseResult<Any>>,response: Response<BaseResult<Any>>) {
                response.body()?.let {
                    _loginInfo.value = it
                }
                callback.invoke()
            }

            override fun onFailure(call: Call<BaseResult<Any>>, t: Throwable) {
                callback.invoke()
            }
        })
    }

}

登录页面的绘制以及实现就完成了,因为不能放置视频,登录按钮点击时的动画也没有弄成GIF,这里就不放效果图了,代码很简单,效果跑起来就能看到。

源码地址

gitee源码地址:戳我~

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇

)">
下一篇>>