要求
studio版本不能太低,要求是 Android Studio Arctic Fox
然后最简单的就是创建一个新的工程,模板选择Empty Compose Activity
如果是旧的工程,需要添加的依赖如下所示 参考:developer.android.com/jetpack/com…
dependencies {
implementation("androidx.compose.ui:ui:1.0.5")
// Tooling support (Previews, etc.)
implementation("androidx.compose.ui:ui-tooling:1.0.5")
// Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.)
implementation("androidx.compose.foundation:foundation:1.0.5")
// Material Design
implementation("androidx.compose.material:material:1.0.5")
// Material design icons
implementation("androidx.compose.material:material-icons-core:1.0.5")
implementation("androidx.compose.material:material-icons-extended:1.0.5")
// Integration with observables
implementation("androidx.compose.runtime:runtime-livedata:1.0.5")
implementation("androidx.compose.runtime:runtime-rxjava2:1.0.5")
// UI Tests
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.0.5")
}
详细的需求可以看这里,developer.android.com/jetpack/com… 其实最简单的就是新建个工程了。。。
注意这个库
implementation 'androidx.activity:activity-compose:1.3.0-alpha06'
demo里的setContent用到这个库了。。。
简单记录下gradle里的需求
android {
defaultConfig {
...
minSdkVersion 21
}
buildFeatures {
// Enables Jetpack Compose for this module
compose true
}
...
// Set both the Java and Kotlin compilers to target Java 8.
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
composeOptions {
kotlinCompilerExtensionVersion '1.0.5'
}
}
这里只研究纯component的用法,不研究那种混合用法了 继承ComponentActivity 然后setContent里就可以写要显示的控件了
class ComponentLearnActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContent {
// MessageCard(name = "card...card")
// }
}
预览有下边两种
方法不带参数的话,直接添加@composable和@preview就行了。 如果方法带参数,参数有默认值,那么直接@preview也可以了 如果参数没有默认值,如果想预览,必须添加@previewparameter的注解来给定默认参数,否则无法预览,如下
@Preview(showBackground = true)
@Composable
fun MessageCard(name: String="xxxxxxx") {
// Text(text = "Hello $name!")
LableText(lable = "11111111",lable2 = "world")
}
@Preview(showBackground = false)
@Composable
fun LableText(@PreviewParameter(provider = Provider::class) lable:String,lable2:String="222222"){
Text(text = "$lable&&&$lable2")
}
class Provider(override val values: Sequence<String> = sequenceOf("hello")) :PreviewParameterProvider<String>{}
变量改变自动刷新的问题
我们需要一个变量,带状态的变量,通过by remember 返回一个mutableStateOf 如下点击column txtColor颜色变化以后,Text的颜色自动就变了。
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
var txtColor by remember { mutableStateOf(Color.Black) }
Column(Modifier.clickable {
txtColor = if (txtColor == Color.Black) Color.Red else Color.Black
}) {
Text(text = "Hello $name!", color = txtColor,)
}
这个remember在页面旋转或者配置改变的时候,还是会销毁的,那么咋办? 还有一个可用的rememberSaveable
var expand by rememberSaveable {
mutableStateOf(false)
}
编程思想
可组合函数可以按任何顺序执行
动态变量
如下,当select变化的时候,card的圆角会动态的从15变成0
val radius by animateDpAsState(targetValue =if(selected) 15.dp else 0.dp )
Card (shape = RoundedCornerShape(topStart = radius)){
当然了还有一些类似的animateIntAsState,animateSizeAsState... 简单看下源码
fun animateDpAsState(
targetValue: Dp,
animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
finishedListener: ((Dp) -> Unit)? = null
): State<Dp> {
return animateValueAsState(
targetValue,
Dp.VectorConverter,
animationSpec,
finishedListener = finishedListener
)
}
可以看到还有另外两个默认参数
animationSpec:可以自定义动画效果
finishedListener:动画结束的回调 看下第二个参数
fun <T> spring(
dampingRatio: Float = Spring.DampingRatioNoBouncy,
stiffness: Float = Spring.StiffnessMedium,
visibilityThreshold: T? = null
): SpringSpec<T> =
SpringSpec(dampingRatio, stiffness, visibilityThreshold)
可以看下Spring定义的几个默认值,无回弹,回弹幅度小,中,大。 以及阻尼系数
自定义布局
简单概括下流程 就是返回一个Layout,我们用这个
@Composable inline fun Layout(
content: @Composable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
)
measurePolicy是个接口, 如下
Layout(content,modifier,measurePolicy = object :MeasurePolicy{
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
}
})
可以简化写成如下的
Layout(content,modifier,measurePolicy = MeasurePolicy { measurables, constraints -> })
其实我们就是要返回一个MeasureResult
可能用到的方法
val placeable=measurable.measure(constraints) //获取测量后的信息,可以拿到child的宽高等信息
最后返回一个
layout(width ,height){//整个容器的宽高
val rowX=IntArray(rows){0}
placeables.forEachIndexed { index, placeable ->
placeable.placeRelative(x,y)//循环放置每个child
}
}
wrap效果
Row(modifier = modifier.height(IntrinsicSize.Min))
//上边的row限制,下边这个divier就不会铺满屏幕了。
Divider(color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp))
key可组合项
@Composable
fun MoviesScreen(movies: List<Movie>) {
Column {
for (movie in movies) {
key(movie.id) { // Unique ID for this movie
MovieOverview(movie)
}
}
}
}
@Composable
fun MoviesScreen(movies: List<Movie>) {
LazyColumn {
items(movies, key = { movie -> movie.id }) { movie ->
MovieOverview(movie)
}
}
}
如果您希望可组合项的尺寸固定不变,而不考虑传入的约束条件,请使用 requiredSize 修饰符:
固有特性测量
Row(modifier = modifier.height(IntrinsicSize.Min))
tabrow
源码
@Composable
fun TabRow(
selectedTabIndex: Int,
modifier: Modifier = Modifier,
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor),
indicator: @Composable (tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
TabRowDefaults.Indicator(
Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
)
},
divider: @Composable () -> Unit = @Composable {
TabRowDefaults.Divider()
},
tabs: @Composable () -> Unit
)
参数说明:
selectedTabIndex :选中的tab索引
backgroundColor:tab的背景颜色
contentColor:内容颜色,也是默认indicator的颜色,
indicator:指示器,就是选中的tab上的图标,如图上的红圈
divider:最下边那条黄线,用来区分tab和tab下边的内容的
indicator和divider都有默认值,我们可以自定义修改
TabRow(
selectedTabIndex = selectIndex,
indicator = { tabPositions ->
Box (Modifier
.tabIndicatorOffset(tabPositions[selectIndex])
.fillMaxSize()
.padding(4.dp)
.border(BorderStroke(2.dp, Color.Red), CircleShape)
)
},
divider = {
androidx.compose.material.Divider(modifier = Modifier, thickness = 5.dp, color = Color.Yellow)
}
) {
listOf("first", "sec", "third").forEachIndexed { index, s ->
Tab(selected = selectIndex == index,
onClick = { selectIndex = index },
modifier = Modifier
.padding(4.dp)
.clip(CircleShape)) {
Text(text = s, modifier = Modifier.padding(8.dp, 16.dp))
}
}
}
顺道复习下
val CircleShape = RoundedCornerShape(50)
RoundedCornerShape 参数可以是dp,float的像素,或者int类型表示百分比(0到100) 当然也可以单独设置4个角
简单说明下上边的contentColor为啥会影响默认的indicator线条的颜色
TabRow点进去可以看到如下的代码
里边设置了LocalContentColor的值为contentColor
CompositionLocalProvider(
LocalContentColor provides contentColor,
LocalAbsoluteElevation provides absoluteElevation
) {
Box(){
content()}
然后我们看下默认的TabRowDefaults.Indicator的代码,可以看到background用的就是LocalContentColor.current
@Composable
fun Indicator(
modifier: Modifier = Modifier,
height: Dp = IndicatorHeight,
color: Color = LocalContentColor.current
) {
Box(
modifier
.fillMaxWidth()
.height(height)
.background(color = color)
)
}
BackdropScaffold
就是android view里的bottomsheet那个玩意,抽屉效果那种
fun BackdropScaffold(
appBar: @Composable () -> Unit,
backLayerContent: @Composable () -> Unit,
frontLayerContent: @Composable () -> Unit,
modifier: Modifier = Modifier,
scaffoldState: BackdropScaffoldState = rememberBackdropScaffoldState(Concealed),
gesturesEnabled: Boolean = true,
peekHeight: Dp = BackdropScaffoldDefaults.PeekHeight,
headerHeight: Dp = BackdropScaffoldDefaults.HeaderHeight,
persistentAppBar: Boolean = true,
stickyFrontLayer: Boolean = true,
backLayerBackgroundColor: Color = MaterialTheme.colors.primary,
backLayerContentColor: Color = contentColorFor(backLayerBackgroundColor),
frontLayerShape: Shape = BackdropScaffoldDefaults.frontLayerShape,
frontLayerElevation: Dp = BackdropScaffoldDefaults.FrontLayerElevation,
frontLayerBackgroundColor: Color = MaterialTheme.colors.surface,
frontLayerContentColor: Color = contentColorFor(frontLayerBackgroundColor),
frontLayerScrimColor: Color = BackdropScaffoldDefaults.frontLayerScrimColor,
snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }
)
appbar:这个算底层内容,显示在底层顶部
backLayerContent:底层的内容,显示在appbar下边的
frontLayerContent:这个是最上层显示用的
scaffoldState:底层显示与否,枚举类,这里设置的是默认值,可以默认显示底层或者默认不显示底层,后边可以滑动改变状态
peekHeight:这个是底层内容最小显示的高度,防止上层滑动把底层全部盖住
headerHeight:这个是上层内容显示的最小高度,你要是弄个0,生成可能会完全划出屏幕看不见了。
snackbarHost:就是之前android里的那个底部提示框,这里可以看到已经设置了默认值了,如果你对颜色形状啥的不喜欢可以自定义
比如,如下自定义snackbarhost
{
SnackbarHost(hostState = it){
Snackbar(snackbarData = it, backgroundColor = Color.Blue, contentColor = contentColorFor(backgroundColor = Color.Blue))
}
}
使用如下,我们只需要点击某个item的时候修改下showContent的内容就行了
val state=rememberBackdropScaffoldState(BackdropValue.Revealed)
var showContent by remember {
mutableStateOf("")
}
LaunchedEffect(showContent ){
launch {
if(showContent.isEmpty())return@launch
state.snackbarHostState.showSnackbar(showContent)
}
}
canvas
这个东西,如果modifier你不设置高度,那么它是不占高度的,画出来的东西在底层, 比如
column{
canvas// 画个圆
text//你会发现text改在canvad那个圆上边
}
textfiled
文本输入框 下边是个sample,里边添加了自动显示输入法,并且按下done键的时候取消焦点以便隐藏输入法
val (text, setText) = remember { mutableStateOf("") }
val keyboardController = LocalSoftwareKeyboardController.current//这个可以调用show,hidden显示隐藏输入法,此处没有用到。
val focusRequest= remember {
FocusRequester()
}
val focusManager= LocalFocusManager.current//焦点管理器
OutlinedTextField(
value = text,
onValueChange = setText,
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent),
maxLines = 1,
singleLine=true,
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = {
focusManager.clearFocus()//清除焦点,输入法自动隐藏
}),
modifier = Modifier
.focusRequester(focusRequest)
.fillMaxWidth()
.height(58.dp)
.constrainAs(edit) {
top.linkTo(con.bottom, 10.dp)
},
label = { Text(text = "lable") },
placeholder = { Text(text = "place holder..",color= Color.Blue) },
leadingIcon = {
Icon(painter = painterResource(id = R.drawable.ic_baseline_clear_24),
contentDescription = "leading..", tint = Color.Red)
},
trailingIcon = { Text(text = "clear words",color= Color.Red, modifier = Modifier.clickable { setText.invoke("") }) },
)
LaunchedEffect(key1 = focusRequest, block = {
focusRequest.requestFocus()
})
下边单独把默认获取焦点的方法拉出来
val focusRequest= remember {
FocusRequester() //第一步
}
val focusManager= LocalFocusManager.current//焦点管理器,用来清除focus,以便隐藏输入法
Modifier
.focusRequester(focusRequest)//第二步
LaunchedEffect(key1 = focusRequest, block = {
focusRequest.requestFocus()//第三步,这个请求得放在launch里,
})
还有注意labe,placeholder那几个参数的类型都是代码块,记得用花括号括起来。
option简单说明
class KeyboardOptions constructor(
val capitalization: KeyboardCapitalization = KeyboardCapitalization.None,
val autoCorrect: Boolean = true,
val keyboardType: KeyboardType = KeyboardType.Text,//还可以选择number选择数字输入键盘
val imeAction: ImeAction = ImeAction.Default
)
还有个参数忘了写了,这个就是用来对输入的文本内容进行转换的。字面意思 视觉上的转换,也就是文本框里实际显示的内容
visualTransformation= VisualTransformation { text-> TransformedText(text.toUpperCase(), OffsetMapping.Identity) },
text的类型是AnnotatedString,这个类里扩展了不少已经写好的方法,比如上边的就是字母大写 还有如下的,首字母大写,首字母小写,全部小写
fun AnnotatedString.toUpperCase
fun AnnotatedString.toLowerCase
fun AnnotatedString.capitalize
fun AnnotatedString.decapitalize
比如密码输入框,我们需要把文本内容显示成星号,如下
visualTransformation = VisualTransformation { text ->
TransformedText(buildAnnotatedString {
this.append("*".repeat(text.length))
}, OffsetMapping.Identity)
},
还有电话号码,如果你想加个横杠啥的,都在这里处理,text是原本输入的内容,自己处理后返回一个另外的text用来显示即可
看些这个类的源码,可以发现它还有其他一些属性可以设置的,感觉都是一些样式,可以单独设置部分文字的样式啥的,没做研究。
class AnnotatedString internal constructor(
val text: String,
val spanStyles: List<Range<SpanStyle>> = emptyList(),
val paragraphStyles: List<Range<ParagraphStyle>> = emptyList(),
internal val annotations: List<Range<out Any>> = emptyList()
)
一些简单的动画效果
也不是动画,就是状态变化有个过程,比如宽度从0到100,有个渐变的过程
- Animatable animateTo 这个需要放在一个launch里,我们用LaunchEffect即可,监听click变化触发
var we = remember {
Animatable(1f)
}
var click by remember {
mutableStateOf(0)
}
LaunchedEffect(key1 = click, block = {
if(click>0)
we.animateTo(if(we.value==1f) 2f else 1f, animationSpec = tween(1111))
})
Modifier.clickable {
click++
}
- transition
val transition = rememberInfiniteTransition()
val alpha = transition.animateFloat(initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(keyframes {
durationMillis = 1000
0.7f at 500
}, RepeatMode.Reverse))
- animatexxxAsState 这个有好几个,可以是dp,float,int,color,还可以是某种type
如下当变量select变化的时候,我们的radius也会动态变化了
val radius by animateDpAsState(targetValue =if(selected) 15.dp else 0.dp )
异步执行
下边这个协成可以异步执行一些task,而且他本身和组件是绑定的,组件重组的时候它也会取消
val scope= rememberCoroutineScope()
下边这个是个compose,所以组件重组的时候会执行,key发生变化的时候也会重新执行block。
这个多用来监听key的变化而重复执行block块
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
key1: Any?,
block: suspend CoroutineScope.() -> Unit
)
nav 添加可选参数
- 可选参数必须使用查询参数语法 (
"?argName={argName}") 来添加 - 可选参数必须具有
defaultValue集或nullability = true(将默认值隐式设置为null)
composable(
"profile?userId={userId}",
arguments = listOf(navArgument("userId") { defaultValue = "me" })
) { backStackEntry ->
Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->
navController.navigate("profile?userId=$friendUserId")
}
}
deeplink的使用demo
从外部打开app的对应页面
<activity
android:name=".RallyActivity"
android:windowSoftInputMode="adjustResize"
android:label="@string/app_name"
android:exported="true">//这个为true,其他app才能调用打开我们的app
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="rally" android:host="accounts" />//这个设置scheme和host
</intent-filter>
</activity>
- navhost的代码
NavHost(...){
composable("a"){}
//...
composable(
route = "accounts/{name}",
arguments = listOf(//arguments参数非必须
navArgument("name") {
type = NavType.StringType
}
),
deepLinks = listOf(
navDeepLink {
uriPattern = "rally://accounts/{name}"
}
),
) { entry ->
val accountName = entry.arguments?.getString("name")
}
}
命令行执行adb命令就可以测试了,可以正常打开上边的composable了
adb shell am start -d "rally://accounts/Checking" -a android.intent.action.VIEW
代码跳转
val intent= Intent(Intent.ACTION_VIEW)
intent.data="rally://accounts/test2222".toUri()
context.startActivity(intent)
LaunchedEffect 和 DisposableEffect
两者的区别,后者多了一个onDispose{}的东西,这个东西是在Effect组件销毁的时候才会执行,首次进去是不会执行的,只有当比如key发生了变化要重组了,那么onDispose里的代码会执行,或者其他原因【比如页面切换,退出页面等】引起的组件被销毁的时候才会执行
var count by remember {
mutableStateOf(0)
}
LaunchedEffect(key1 = count, block = {
println("launched effect============1")
})
DisposableEffect(key1 = count, effect ={
println("disposable effect=============1")
onDispose {
println("on dispose============1")
}
} )
Button(onClick = { count++ }) {
Text(text = "click$count")
}
还有一个SideEffect 这个了就是每次重组都会执行,这里的重组感觉就是它自己同级别的发生变化。
SideEffect {
println("side effect==========")
}
带drawer的组件 Scaffold ,ModalDrawer
scaffold可以用到的compose比较多,topbar,bottombar,floatactionbutton,snackbar等
fun Scaffold(
modifier: Modifier = Modifier,
scaffoldState: ScaffoldState = rememberScaffoldState(),
topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {},
snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
isFloatingActionButtonDocked: Boolean = false,
drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
drawerGesturesEnabled: Boolean = true,
drawerShape: Shape = MaterialTheme.shapes.large,
drawerElevation: Dp = DrawerDefaults.Elevation,
drawerBackgroundColor: Color = MaterialTheme.colors.surface,
drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
drawerScrimColor: Color = DrawerDefaults.scrimColor,
backgroundColor: Color = MaterialTheme.colors.background,
contentColor: Color = contentColorFor(backgroundColor),
content: @Composable (PaddingValues) -> Unit
)
modalDrawer 里的compose就两部分,drawer和content
fun ModalDrawer(
drawerContent: @Composable ColumnScope.() -> Unit,
modifier: Modifier = Modifier,
drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
gesturesEnabled: Boolean = true,
drawerShape: Shape = MaterialTheme.shapes.large,
drawerElevation: Dp = DrawerDefaults.Elevation,
drawerBackgroundColor: Color = MaterialTheme.colors.surface,
drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
scrimColor: Color = DrawerDefaults.scrimColor,
content: @Composable () -> Unit
)
Icon , Image
fun Icon(
painter: Painter,
contentDescription: String?,
modifier: Modifier = Modifier,
tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
){
Box(
modifier.toolingGraphicsLayer().defaultSizeFor(painter)
.paint(
painter,
colorFilter = colorFilter,
contentScale = ContentScale.Fit
)
.then(semantics)
)
}
两者都是通过Modifier的paint方法来绘制的,Image还多了3个参数可以设定
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
@Composable
fun Image(
painter: Painter,
contentDescription: String?,
modifier: Modifier = Modifier,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null
) {
// Explicitly use a simple Layout implementation here as Spacer squashes any non fixed
// constraint with zero
Layout(
{},
modifier.then(semantics).clipToBounds().paint(
painter,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter
)
) { _, constraints ->
layout(constraints.minWidth, constraints.minHeight) {}
}
}
IconToggleButton
An IconButton with two states, for icons that can be toggled 'on' and 'off', such as a bookmark icon, or a navigation icon that opens a drawer
var favorite by remember {
mutableStateOf(false)
}
IconToggleButton(checked = favorite, onCheckedChange ={
favorite=!favorite
} ) {
Icon(imageVector = if (favorite) Icons.Filled.AlarmOn else Icons.Filled.AlarmOff,
contentDescription = "")
}
IconButton
默认的ripple范围是48*48dp,里边icon的大小是24*24dp
一般用在topbar上的action按钮,或者类似的地方,当然你可以通过modifier修改button的大小,,不过你会发现那个ripple的大小就是48dp的范围,看起来就很奇怪了。
fun IconButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable () -> Unit
)
解构声明
在 Koltin 中可以把一个对象赋值给多个变量,这种操作叫做解构声明(Destructuring declaration)
operator 运算符重载,就是对已有的方法进行重载【赋予新的含义】
MutableState这个经常要用到
interface MutableState<T> : State<T> {
override var value: T
operator fun component1(): T
operator fun component2(): (T) -> Unit
}
常用的
var show by remember {
mutableStateOf(false)
}
然后看demo发现了这个,哎,咋还可以有两个变量在括号里
val (a, b) = rememberSaveable {
mutableStateOf(section)
}
我以为括号里随便加了,弄3个发现就报错了,然后查了下,发现是一种解构声明的东西。
如上MutableState里,component1() ,component2() ,其实括号里里(a,b) 对应的就是这个两个方法
顺道看下源码里这两个重载方法的实现
override operator fun component1(): T = value
override operator fun component2(): (T) -> Unit = { value = it }
component后边可以跟任意数字,你就可以重载多个方法了
常见的component的用法 data class
data class City(val name: String, val country: String)
反编译以后就能看到
@NotNull
public final String component1() {
return this.name;
}
@NotNull
public final String component2() {
return this.country;
}