阅读KotlinCompose从入门到实战读书笔记一

1.Compose基本概念

相较于之前基于view体系的UI,Compose是Android全新的声明式UI,那么声明式和命令式的区别是什么?
基于View体现的UI就是典型的命令式UI,我们需要一步步的告诉程序怎么取执行,我们需要获取控件,然后给控件设置对应的属性,而控件会持有相应的属性值,相当于控件是持有状态的。但是声明式UI是我们告诉程序我想要怎么样,剩下的由程序处理。

2.设置项目支持Compose

build.gradle的相关配置

android{
    ...
    //compose是基于单一语言(Kotlin)的
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion '1.2.0'
    }
    packagingOptions {
        resources {
            excludes += '/META-INF/{AL2.0,LGPL2.1}'
        }
    }
}
dependencies{
    ...
    //ComponentActivity
    implementation 'androidx.activity:activity-compose:1.4.0'
    //compose的基础库
    implementation "androidx.compose.ui:ui:$compose_ui_version"
    //用于compose的预览
    implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
    //Material的compose
    implementation 'androidx.compose.material:material:1.2.0'
    //compose版本的constraintlayout
    implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"
    //一个图片加载框架
    implementation "io.coil-kt:coil-compose:2.2.2"
    //compose版本的livedata
    implementation "androidx.compose.runtime:runtime-livedata:$compose_ui_version"
}

如果是通过AS来直接生成一个Compose项目的话,上面部分配置和依赖都会已经添加好,不过我们也可以自行配置嘛,Compose是支持和原生项目混编的,支持在Compose中使用原生View体系,也支持在原生的View体系中使用Compose。
来看一下通过AS直接创建一个空的Compose项目的MainActivity

//继承的是ComponentActivity
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //设置了个沉浸,非必须
        WindowCompat.setDecorFitsSystemWindows(window, false)
        val controller = WindowCompat.getInsetsController(window, window.decorView)
        controller.isAppearanceLightStatusBars = true
        //设置内容
        setContent {
            //在ui/theme/下Theme.kt中配置的主体
            StComposeTheme {
                //具体的控件后面再说,这里可以理解为全屏,背景色为StComposeTheme中配置的background的color
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
    /**
    声明的一个Compose函数,@Preview可以预览当前Compose的样式,不过需要注意有参数的话记得给出默认值
    */
    @Preview(showBackground = true)
    @Composable
    fun Greeting(str:String = "123"){
        //就一个文本控件
        Text(text = str)
    }
}

Compose函数预览效果

不过现在Greeting函数非常简单,下面我们来构建一个复杂的页面:
先看预览的效果图:

@Composable
fun Greeting(name: String) {
    Box(
        modifier = Modifier
            .fillMaxSize()
            //compose中只有padding的概念,根据调用的顺序可以实现原来的外边距和内边距的效果
            //这里设置的就是外边距
            .padding(8.dp)
            //设置边框 宽2dp 黄色 圆角为2dp
            .border(2.dp, Color.Yellow, shape = RoundedCornerShape(2.dp))
            .background(color = Color.Red)
            .padding(8.dp)
    ) {
        Spacer(
            //注意这里的Modifier,因为它是在BoxScope的中,所以它可以设置的方法会多出两个分别是align和matchParenSize,这是在Box中的Modifier中不具备的
            modifier = Modifier
                .matchParentSize()
                .background(color = Color.Green)
        )
    }
    //定义个列表
    Column {
        //文本控件
        Text(text = "Hello $name!")
        //因为Compose中没有Margin的概念,不过可以通过Spacer来设置两个控件之间的间距
        Spacer(modifier = Modifier.height(10.dp))
        //图片控件
        Image(
            //使用本地图片资源的方式
            painter = painterResource(id = R.mipmap.ic_hotel_share_img),
            //描述信息,可以为空
            contentDescription = null,
            //设置图片的宽高以及裁剪样式为圆形
            modifier = Modifier
                .size(width = 200.dp, height = 200.dp)
                .clip(CircleShape)
        )
        Spacer(modifier = Modifier.height(10.dp))
        Box(
            modifier = Modifier
                .size(50.dp)
                .background(color = Color.Red)
        ) {
            Text(text = "纯色", Modifier.align(Alignment.Center))
        }
        Spacer(modifier = Modifier.height(10.dp))
        Box(
            modifier = Modifier
                .size(50.dp)
                .background(
                    //背景的渐变效果
                    brush = verticalGradientBrush1
                )
        ) {
            Text(text = "渐变色", Modifier.align(Alignment.Center))
        }
        Spacer(modifier = Modifier.height(10.dp))
        Box(
            modifier = Modifier
                .size(100.dp)
                //设置偏移量
                //.offset(50.dp, 50.dp)
                //offset的重载方法
                .offset { IntOffset(50.dp.roundToPx(), 50.dp.roundToPx()) }
                .background(color = Color.Yellow)
        )
    }
}

//两种创建渐变色的方式
//方式一给定颜色列表,颜色均匀分布
val verticalGradientBrush1 = Brush.verticalGradient(
    colors = listOf(Color.Red, Color.Yellow, Color.White)
)
//给定偏移量和颜色
val verticalGradientBrush2 = Brush.verticalGradient(
    //0-0.1显示红色
    0.1f to Color.Red,
    //0.1-0.3显示红到绿色的渐变
    0.3f to Color.Green,
    //0.3-0.9显示绿色到蓝色的渐变
    0.9f to Color.Blue
)

3.关于Scopse的举例说明

在Compose所提供的控件都是有一个content参数,这就是每个的作用域

@Composable
inline fun Box(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    propagateMinConstraints: Boolean = false,
    content: @Composable BoxScope.() -> Unit
) {
    val measurePolicy = rememberBoxMeasurePolicy(contentAlignment, propagateMinConstraints)
    Layout(
        content = { BoxScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier
    )
}

在之前的View体系中,例如在xml布局中我们可以给一个子view设置android:layout_toRight属性,哪怕它的外层布局是Linerlayout布局,这个属性并不会生效。
而在Compose中通过Scope限制对应属性能够生效的范围,下面是BoxScope的代码,通过@LayoutScopeMarker元注解,除了是Modifier的公共属性外,那么在特定Scope中定义的方法,只能在对应的作用域来调用。就像下面的BoxScope,它内部定义有两个方法align和matchParentSize,那么这两个方法只能在BoxScope作用域内调用。

@LayoutScopeMarker
@Immutable
interface BoxScope {
    /**
     * Pull the content element to a specific [Alignment] within the [Box]. This alignment will
     * have priority over the [Box]'s `alignment` parameter.
     */
    @Stable
    fun Modifier.align(alignment: Alignment): Modifier

    /**
     * Size the element to match the size of the [Box] after all other content elements have
     * been measured.
     *
     * The element using this modifier does not take part in defining the size of the [Box].
     * Instead, it matches the size of the [Box] after all other children (not using
     * matchParentSize() modifier) have been measured to obtain the [Box]'s size.
     * In contrast, a general-purpose [Modifier.fillMaxSize] modifier, which makes an element
     * occupy all available space, will take part in defining the size of the [Box]. Consequently,
     * using it for an element inside a [Box] will make the [Box] itself always fill the
     * available space.
     */
    @Stable
    fun Modifier.matchParentSize(): Modifier
}