小伙伴复制代码到开发工具之后:ctrl+alt+l ,快速对齐,然后要导包的,导入相关包即可。其中登录过程,我都省了,用提示代替。
一、Android View体系下的登录
1.仿写登录运行效果:
我们想要布局出这样一个效果,各位小伙伴,可以先行试试布局哦。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@mipmap/login">
<androidx.appcompat.widget.AppCompatImageView android:id="@+id/ivBreak" android:layout_width="30dp" android:layout_height="30dp" android:layout_marginStart="30dp" android:layout_marginTop="40dp" android:src="@mipmap/ic_break" />
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" android:layout_marginStart="60dp" android:layout_marginTop="200dp" android:layout_marginEnd="60dp" android:background="@drawable/shape_qianse" android:gravity="center|left" android:paddingStart="10dp" android:paddingTop="10dp" android:paddingBottom="10dp">
<androidx.appcompat.widget.AppCompatImageView android:layout_width="30dp" android:layout_height="30dp" android:src="@mipmap/icon_user" />
<androidx.appcompat.widget.AppCompatEditText android:id="@+id/edInputUserName" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="10dp" android:layout_weight="1" android:background="@null" android:hint="@string/inputUserNameToast" android:textSize="18sp" android:textStyle="bold" />
</LinearLayout>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" android:layout_marginStart="60dp" android:layout_marginTop="270dp" android:layout_marginEnd="60dp" android:background="@drawable/shape_qianse" android:gravity="center|left" android:paddingStart="10dp" android:paddingTop="10dp" android:paddingBottom="10dp">
<androidx.appcompat.widget.AppCompatImageView android:layout_width="30dp" android:layout_height="30dp" android:src="@mipmap/icon_psw" />
<androidx.appcompat.widget.AppCompatEditText android:id="@+id/edInputUserPsw" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="10dp" android:layout_weight="1" android:background="@null" android:hint="@string/inputUserPswToast" android:inputType="textPassword" android:textSize="18sp" android:textStyle="bold" />
</LinearLayout>
<TextView android:id="@+id/tvLogin" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_marginStart="60dp" android:layout_marginTop="350dp" android:layout_marginEnd="60dp" android:background="@drawable/shape_qianse" android:gravity="center" android:paddingTop="10dp" android:paddingBottom="10dp" android:text="@string/login" android:textSize="18sp" android:textStyle="bold" />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/tvLogin" android:layout_alignStart="@id/tvLogin" android:layout_alignEnd="@id/tvLogin" android:gravity="center" android:paddingTop="20dp" android:text="@string/loginType" android:textColor="@color/white" android:textSize="16sp" android:textStyle="bold" />
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/tvLogin" android:layout_marginTop="60dp" android:gravity="center">
<androidx.appcompat.widget.AppCompatImageView android:id="@+id/ivWxIcon" android:layout_width="45dp" android:layout_height="45dp" android:src="@mipmap/icon_wx" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.237" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.213" />
<TextView android:id="@+id/tvWxLogin" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:paddingTop="8dp" android:text="@string/loginWx" android:textColor="@color/white" android:textSize="18sp" android:textStyle="bold" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@+id/ivWxIcon" app:layout_constraintStart_toStartOf="@+id/ivWxIcon" app:layout_constraintTop_toBottomOf="@+id/ivWxIcon" />
<androidx.appcompat.widget.AppCompatImageView android:id="@+id/ivQQIcon" android:layout_width="40dp" android:layout_height="40dp" android:src="@mipmap/icon_qq" app:layout_constraintBottom_toBottomOf="@+id/ivWxIcon" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.62" app:layout_constraintStart_toEndOf="@+id/ivWxIcon" app:layout_constraintTop_toTopOf="@+id/ivWxIcon" app:layout_constraintVertical_bias="0.213" />
<TextView android:id="@+id/tvQQLogin" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:paddingTop="10dp" android:text="@string/loginQQ" android:textColor="@color/white" android:textSize="18sp" android:textStyle="bold" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@+id/ivQQIcon" app:layout_constraintStart_toStartOf="@+id/ivQQIcon" app:layout_constraintTop_toBottomOf="@+id/ivQQIcon" />
</androidx.constraintlayout.widget.ConstraintLayout>
<CheckBox android:id="@+id/cbAgree" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:layout_marginBottom="30dp" android:checked="false" android:text="请阅读并同意《隐私协议》和《用户协议》" android:textColor="@color/white" android:textSize="16sp" android:textStyle="bold" />
</RelativeLayout> |
由于考虑尽可能使用讲解过的容器和控件,这种布局是可行的,这肯定不是最优解,小伙伴们可以自己布局哦;当然我们在实际开发中,应考虑减少层级嵌套,这样可以减少代码行数,还可以减少渲染ui时间,在体验感觉上更好。
接下来我们看看Activity里的代码。
class LoginActivity : BaseActivity<ActivityLoginBinding>() {
override fun initData(savedInstanceState: Bundle?) { SpannableUtils {//设置 CheckBox 文字部分,用户协议、隐私协议变红,且可以点击 val intent = Intent(this, WebViewActivity::class.java) //在intent里传递数据 if (it == "《隐私协议》") intent.putExtra("title", "隐私协议") else intent.putExtra("title", "用户协议") startActivity(intent) }.create("请阅读并同意《隐私协议》和《用户协议》") .setSpanOnclick("《隐私协议》") .setSpanOnclick("《用户协议》") .build(binding.cbAgree)
setOnClickListener(binding.tvQQLogin, binding.ivQQIcon, binding.tvWxLogin, binding.ivWxIcon, binding.tvLogin) binding.ivBreak.setOnClickListener { finish() } }
override fun myClick(v: View) { when (v.id) { binding.tvQQLogin.id, binding.ivQQIcon.id -> { if (isAgree()) { "执行qq登录".showToast() startActivity(Intent(this, MainActivity::class.java)) } }
binding.tvWxLogin.id, binding.ivWxIcon.id -> { if (isAgree()) { "执行微信登录".showToast() startActivity(Intent(this, MainActivity::class.java)) } }
binding.tvLogin.id -> { if (isInputData() && isAgree()) checkUser() } } }
private fun isAgree(): Boolean { val checked = binding.cbAgree.isChecked if (!checked) "请先阅读并同意隐私协议和用户协议".showToast() return checked }
private fun isInputData(): Boolean { val nullOrEmpty = binding.edInputUserName.text.isNullOrEmpty() val nullOrEmpty1 = binding.edInputUserPsw.text.isNullOrEmpty() return if (nullOrEmpty || nullOrEmpty1) { "请输入账户或者密码".showToast() false } else true }
private fun checkUser() { if (binding.edInputUserName.text.toString() == "张三" && binding.edInputUserPsw.text.toString() == "123456") { "登录成功".showToast() startActivity(Intent(this, MainActivity::class.java)) } else { "输入账号或密码错误".showToast() } } } |
注意观看 setOnClickListener()、myClick(),这是我写的设置点击监听封装。主要是对BaseActicity做了处理,我们去看看,都做了什么处理。
第一步:继承点击事件的接口:
第二写方法
1是设置监听的点击按键的控件,为不确定数量的View,针对每一做监听。
2.是点击时候的回调
3.是可以重写的方法
在重写方法中,更具id找到对应的点击控件,然后做不同的处理。
这里去新的界面使用 startActivity(Intent(this, MainActivity::class.java));传递一个Intent的意图,意图是传递了一个上下文和需要启动的界面class。他会帮助我们启动新的界面。我们还可以调用 finish(),关闭当前界面。
SpannableUtils,则是本人封装的一个类,直接用即可
//dataCall 是选择其中设置的一段内容时候的回调,可根据回到做不同事件分发处理 class SpannableUtils(private val dataCall: ((text: String) -> Unit)?=null) {
private var spannableString: SpannableString? = null
fun create(text: String): SpannableUtils { spannableString = SpannableString(text) return this }
//chooseText 在 SpannableString 一定要存在,否则会抛出异常;spannedType可以设置选择的颜色,默认是红色 fun setSpanOnclick( chooseText: String, spannedType: Int = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, color: Int = Color.RED, isUnderlineText: Boolean = false, ): SpannableUtils { spannableString?.apply { try { this.setSpan( ComplexClickTextUtils(chooseText, color, isUnderlineText,dataCall), this.indexOf(chooseText), this.indexOf(chooseText) + chooseText.length, spannedType) } catch (e: Exception) { e.printStackTrace() e.message?.showLog() } } return this }
fun setSpan( chooseText: String, spannedType: Int = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, color: Int = Color.RED, ): SpannableUtils { spannableString?.apply { try { val colorSpan = ForegroundColorSpan(color) this.setSpan(colorSpan, this.indexOf(chooseText), this.indexOf(chooseText) + chooseText.length, spannedType) } catch (e: Exception) { e.printStackTrace() e.message?.showLog() } } return this }
// 设置好的text文本内容与控件绑定 fun build(bindView: TextView) = spannableString?.apply { bindView.text = this bindView.movementMethod = LinkMovementMethod.getInstance() } } |
ComplexClickTextUtils直接使用:
class ComplexClickTextUtils( private val title: String, private val color: Int, private val isUnderlineText: Boolean = false, private val dataCallPost: ((text: String) -> Unit)?=null ) : ClickableSpan() { override fun updateDrawState(ds: TextPaint) { super.updateDrawState(ds) //设置文本的颜色 ds.color = color //超链接形式的下划线,false 表示不显示下划线,true表示显示下划线 ds.isUnderlineText = isUnderlineText ds.isUnderlineText = false }
override fun onClick(widget: View) { dataCallPost?.invoke(title) } } |
我们看看WebViewActivity;这个界面就是加载应用协议和隐私协议的。
布局代码:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/black" android:orientation="vertical">
<RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="40dp">
<androidx.appcompat.widget.AppCompatImageView android:id="@+id/ivBreak" android:layout_width="30dp" android:layout_height="30dp" android:layout_marginStart="30dp" android:src="@mipmap/ic_break" />
<TextView android:id="@+id/tvTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:text="标题中心" android:textColor="@color/white" android:textSize="20sp" />
</RelativeLayout>
<WebView android:id="@+id/wvWebView" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" />
</LinearLayout> |
再看看WebViewActivity界面代码
class WebViewActivity : BaseActivity<ActivityWebViewBinding>() {
private val url = "https://www.baidu.com/" //隐私和用户协议都用百度的链接
override fun initData(savedInstanceState: Bundle?) {
//设置标题
binding.tvTitle.text=intent.getStringExtra("title")
binding.ivBreak.setOnClickListener {
if (binding.wvWebView.canGoBack()){
binding.wvWebView.goBack() //先判断是否可以回退
}else{ //否则关闭当前界面
finish()
}
}
// binding.wvWebView.settings 还可以支持设置,这里就简单写一下,后续使用在讲解
binding.wvWebView.settings.javaScriptEnabled=true //开启支持java角本
binding.wvWebView.loadUrl(url)
}
override fun onDestroy() {
super.onDestroy()
binding.wvWebView.destroy() //在界面被销毁的时候,释放掉加载的资源
}
二,我们看看Compose 怎么写的。
看先看运行效果图:
我们再看看布局代码:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
//根据返回的标志进行事件处理
fun LoginLayout(callBack: (isAgree : Boolean,name : String,pws : String,event :Int) -> Unit) {
var inputUserName by remember { mutableStateOf("") }
var inputUserPsw by remember { mutableStateOf("") }
var isAgree by remember { mutableStateOf(false) }
Box(
Modifier
.fillMaxSize()
.background(Color.White)) { //层叠布局
//先加载图片背景 //因为在compose中,
// 容器背景无法使用图片,解决方法使用当前布局在最底层加载图片即可
Image(
painterResource(id = R.mipmap.login),
contentDescription = null,
contentScale = ContentScale.FillHeight,
modifier = Modifier.fillMaxSize())
Image(
painterResource(id = R.mipmap.ic_break),
contentDescription = null,
modifier = Modifier
.padding(start = 30.dp, top = 40.dp)
.size(30.dp)
.clickable { callBack.invoke(isAgree,"", "", 0) }
.align(Alignment.TopStart))
Row(
Modifier
.padding(top = 200.dp)
.align(Alignment.TopCenter),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center) {
Image(painterResource(id = R.mipmap.icon_user),
contentDescription = null,
modifier = Modifier
.padding(end = 10.dp)
.size(30.dp))
TextField(value = inputUserName, onValueChange = {
inputUserName = it
},placeholder = {
Text(text = "请输入账户", fontSize = 17.sp)
}, modifier = Modifier.height(55.dp))
}
Row(
Modifier
.padding(top = 270.dp)
.align(Alignment.TopCenter),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center) {
Image(painterResource(id = R.mipmap.icon_psw),
contentDescription = null,
modifier = Modifier
.padding(end = 10.dp)
.size(30.dp))
TextField(value = inputUserPsw, onValueChange = {
inputUserPsw = it
}, placeholder = {
Text(text = "请输入密码", fontSize = 17.sp)
}, modifier = Modifier.height(55.dp))
}
Button(onClick = { callBack.invoke(isAgree,inputUserName,inputUserPsw,1) },
modifier = Modifier
.padding(top = 360.dp)
.align(Alignment.TopCenter)
.width(240.dp)) {
Text(text = "登录")
}
Text(text = "其他登录方式", fontSize = 16.sp,color = Color.White,
modifier = Modifier
.padding(top = 420.dp)
.align(Alignment.TopCenter))
Row(modifier = Modifier
.padding(top = 470.dp)
.fillMaxWidth()
.align(Alignment.TopCenter)) {
Column(
Modifier
.padding(start = 80.dp)
.clickable {
callBack.invoke(isAgree,inputUserName, inputUserPsw, 2)
},horizontalAlignment = Alignment.CenterHorizontally) {
Image(painter = painterResource(id = R.mipmap.icon_wx),
contentDescription = null,Modifier.size(45.dp))
Text(text = "微信登录", color = Color.White,
modifier = Modifier.padding(top = 10.dp))
}
Column(
Modifier
.padding(start = 100.dp)
.clickable {
callBack.invoke(isAgree,inputUserName, inputUserPsw, 3)
}, horizontalAlignment = Alignment.CenterHorizontally) {
Image(painter = painterResource(id = R.mipmap.icon_qq),
contentDescription = null,Modifier.size(45.dp))
Text(text = "QQ登录", color = Color.White,
modifier = Modifier.padding(top = 10.dp))
}
}
Row(modifier = Modifier.fillMaxWidth()
.align(Alignment.BottomCenter).padding(bottom = 20.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked =isAgree , onCheckedChange ={
isAgree=it
})
Text(text = "请阅读并同意", color = Color.White, fontSize = 15.sp)
Text(text = "《隐私协议》", color = Color.Red, modifier = Modifier.clickable {
callBack.invoke(isAgree,"", "", 4)
}, fontSize = 15.sp)
Text(text = "和",color = Color.White, fontSize = 15.sp)
Text(text = "《用户协议》",color = Color.Red,modifier = Modifier.clickable {
callBack.invoke(isAgree,"", "", 5)
}, fontSize = 15.sp)
}
}
}
@Composable
@Preview
fun mPreview() {
LoginLayout { isAgree,user,pws ,wvent->
}
}
接下来看看你,LoginActivity代码。
package com.example.compose.uiactivity
import android.content.Intent import androidx.compose.runtime.Composable import com.example.compose.base.BaseActivity import com.example.compose.showToast import com.example.compose.uilayout.LoginLayout
class LoginActivity : BaseActivity() {
@Composable 这里是加载布局的 override fun InitView() { LoginLayout { isAgree, user, pws, event -> when (event) { 0 -> finish() 1 -> { if (isAgree) { if (user.isNullOrEmpty() || pws.isNullOrEmpty()) { "请输入账户或者密码".showToast() } else { if (user == "张三" && pws == "123456") "登录成功".showToast() else "密码错误".showToast() } } else "请先同意隐私和用户协议".showToast() }
2 -> { if (isAgree) "微信登录成功".showToast() else "请先同意隐私和用户协议".showToast() }
3 -> { if (isAgree) "QQ登录成功".showToast() else "请先同意隐私和用户协议".showToast() }
4 -> { val intent = Intent(this, WebViewActivity::class.java) intent.putExtra("title", "隐私协议") startActivity(intent) }
5 -> { val intent = Intent(this, WebViewActivity::class.java) intent.putExtra("title", "用户协议") startActivity(intent) } } } }
override fun initData() {
}
} |
看看BaseActivity,很简单的代码
abstract class BaseActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) actionBar?.hide() //一个三方库,布局升到状态栏 implementation ("com.gyf.immersionbar:immersionbar:3.0.0") ImmersionBar.with(this) .transparentStatusBar() .transparentNavigationBar() .fullScreen(true) .statusBarDarkFont(true) .init() WindowCompat.getInsetsController(window, this.window.decorView).let { it.hide(WindowInsetsCompat.Type.statusBars())//隐藏状态栏 it.hide(WindowInsetsCompat.Type.navigationBars()) //隐藏导航栏 } setContent { ComposeTheme { InitView() // } }
}
//Composable 的特性 首字母大写 @Composable abstract fun InitView()
abstract fun initData() } |
我们再看看WebView的代码
class WebViewActivity : BaseActivity() {
private val urlLL = "https://www.baidu.com/" //隐私和用户协议都用百度的链接 private lateinit var webView : WebView @SuppressLint("SetJavaScriptEnabled") @Composable override fun InitView() { Column { Row(horizontalArrangement=Arrangement.Center, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
Image(painter = painterResource(id = R.mipmap.ic_break), contentDescription = null, Modifier.padding(start = 20.dp, top = 40.dp).size(30.dp).clickable { finish() })
Text(intent.getStringExtra("title").toString(),Modifier.padding(top = 40.dp).fillMaxWidth(), textAlign = TextAlign.Center, fontSize = 18.sp) }
AndroidView(factory = { WebView(it).apply { webView=this webView.settings.javaScriptEnabled=true webView.loadUrl(urlLL) } }, update = {
}, modifier = Modifier.fillMaxSize()) } }
override fun initData() {
}
override fun onDestroy() { super.onDestroy() webView.destroy() } } |
三、鸿蒙开发中的布局
看看布局代码:
@Entry
//这是装饰器的入口
@Component
//这代表是一个Component类型的布局
//定义一个名 LayoutLayout的组合布局
struct LoginLayout {
@State userName: string = ""
@State psw: string = ""
@State isAgree: boolean = false
build() { //构建布局 弹性布局,即使平均分配剩下的控件
Stack() { // 层叠布局 默认居中
Image($r('app.media.login'))
.width('100%')
.height('100%')
.objectFit(ImageFit.Fill) //拉满全屏幕
Image($r('app.media.ic_break')).width(35).height(35).onClick(() => {
console.log("点击了退出按键")
}).margin({ left: 20, top: 20 })
Row() {
Image($r('app.media.icon_user')).width(35).height(35).margin({ right: 20 })
TextInput({ text: this.userName, placeholder: "请输入账户" })
.fontSize(18)
.fontColor(Color.White)
.onChange((it: string) => {
this.userName = it //输入账户
})
.width(150)
.height(50)
}.width('100%').margin({ left: 60, top: 120 })
Row() {
Image($r('app.media.icon_psw')).width(35).height(35).margin({ right: 20 })
TextInput({ text: this.psw, placeholder: "请输入密码" })
.fontSize(18)
.fontColor(Color.White)
.onChange((it: string) => {
this.psw = it //输入密码
})
.width(150)
.height(50)
}.width('100%').margin({ left: 60, top: 180 })
Button("登录")
.onClick(() => {
if (this.userName.length <= 0 || this.psw.length <= 0) {
console.log("请输入账户或者密码");
} else {
if (this.isAgree) {
console.log("登录成功");
} else {
console.log("请同一隐私协议和用户协议");
}
}
})
.width(220)
.height(40)
.fontSize(30)
.margin({ left: 60, top: 240 })
Text('其他登录方式').fontColor(Color.White)
.fontSize(18).margin({ left: 110, top: 290 })
Row() {
Flex() {
Column() {
Image($r('app.media.icon_wx')).width(35).height(35)
Text('微信登录').fontColor(Color.White)
.fontSize(18).onClick(() => {
if (this.isAgree) {
console.log("微信登录");
} else {
console.log("请同一隐私协议和用户协议");
}
})
}.width('50%')
Column() {
Image($r('app.media.icon_qq')).width(35).height(35)
Text('QQ登录').fontColor(Color.White)
.fontSize(18).onClick(() => {
if (this.isAgree) {
console.log("QQ登录");
} else {
console.log("请同一隐私协议和用户协议");
}
})
}.width('50%')
}.width('100%')
}.width('100%').margin({ top: 360 })
Checkbox({ name: 'checkbox1', group: 'checkboxGroup' })
.select(this.isAgree)
.onChange((isCheck: boolean) => {
this.isAgree=isCheck
})
.unselectedColor(Color.Black)
.selectedColor(Color.Red)
.margin({ top: 450, left: 20 })
Row() {
Text("请阅读并同意").fontColor(Color.White)
Text("《隐私协议》").fontColor(Color.Red).onClick(() => {
console.log("点击了隐私协议");
})
Text("和").fontColor(Color.White)
Text("《用户协议》").fontColor(Color.Red).onClick(() => {
console.log("点击了用户协议");
})
}.margin({ top: 450, left: 50 })
}.height('100%') //设置高度度铺满全屏
.width('100%') //设置宽度铺满全屏
.backgroundColor(Color.Black)
.align(Alignment.TopStart) //此处设置开始布局位置
}
}
最后我们看看运行效果:
我们下一章:将Android 中的RecyclerView,讲完它,我们就开始尝试做小项目,加深对控件的记忆和熟悉度,大家一起共同学习,在开发小项目时候,我不一定Android View体系和Compose 、鸿蒙开发一起发,大概率一个小项目分三次发分为上中下三篇开发对应AndroidView 体系,Android Compose 、鸿蒙。