Android compose学习笔记

如标题所言,就是一篇学习笔记而已,没有看的必要,只是写给自己看的,内容是慢慢更新的。
因为白天要上班,有时还会加班。而我自己也经常写一些个人项目,还会花时间玩游戏,而且现在所在的公司也不会用compose去开发,所以就慢慢学,慢慢记。

Build

必须在build文件的android下面设置下面的属性

buildFeatures {
    compose true
}

compose的依赖,这些是android studio自动生成的,每个的作用不清楚

implementation 'androidx.activity:activity-compose:1.5.1'
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'

在构建时,可能会由于kotlin版本的问题而构建失败,按照提示修改为合适的版本即可

Activity

compose必须继承哪个Activity还不清楚,android studio生成的代码继承的是androidx.activity.ComponentActivity。
这个Activity与其他的ComponentActivity有所不同,代码已经是不一样的。而且setContent这个方法,就是这个ComponentActivity的扩展方法。
建议直接用这个Activity,下面是使用原生Activity的情况,出现了很多问题。最终结果是失败的,如果不想看这个过程,可以跳过这部分。

compose还提供了一个ComposeView。
先看看ComposeView的注释
Composes the given composable into the given activity. The content will become the root view of the given activity.
This is roughly equivalent to calling ComponentActivity.setContentView with a ComposeView i.e.:

setContentView(
    ComposeView(this).apply {
        setContent {
            MyComposableContent()
        }
    }
)

如果在Activity直接使用这个View,会抛出:“IllegalStateException: ViewTreeLifecycleOwner not found from androidx.compose.ui.platform.ComposeView”。
经过测试,如果只是new,而没有调用setContentView,就没有这个异常,说明是setContentView调用了View的某段代码出现的。
阅读源码后发现,问题是出在ComposeView的onAttachedToWindow方法上,在这个方法里面,最终会调用WindowRecomposer.android.kt:349这行代码检查是否设置了Lifecycle。
这行代码所在的方法有一个Lifecycle参数,这里应该是空的,所以是调用ViewTreeLifecycleOwner.kt的View.findViewTreeLifecycleOwner获取的。
最终获取到的值也是空的,所以就抛出异常。在这个kt文件也可以看到,它还提供了set方法,所以可以调用set方法设置Lifecycle。但设置之后,出现了新的问题。
这次没有报Lifecycle的异常了,但有了新的异常:“IllegalStateException: Composed into the View which doesn’t propagateViewTreeSavedStateRegistryOwner!”。
阅读代码之后,发现是少了SavedStateRegistryOwner。这个是调用了AndroidComposeView.android.kt的代码,这个文件里面,同样有一个set方法。
同样的套路设置,不过这个不能像Lifecycle那样直接设置。我的做法是:实现SavedStateRegistryOwner,此时,需要在savedStateRegistry返回一个SavedStateRegistry。
本来想要直接new的,但发现构造方法是internal。然后我在源码中发现,可以这样获取SavedStateRegistryController.create(this).savedStateRegistry。
经过一番折腾后,项目总算不抛异常了。但是,当调用ComposeView的setContent方法添加View之后。会发现,View并没有显示在屏幕上。
所以还要看看怎么样才能显示出来,这个我就不折腾了,这个难度比找异常大多了。
所以可以发现,除非有什么定制需求,或者自己对这块已经了然于心,否则没事不要用Activity去玩compose。
看一下最终代码:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val composeView = ComposeView(this)
    composeView.setViewTreeLifecycleOwner(this)
    composeView.setViewTreeSavedStateRegistryOwner(this)
    composeView.setContent {
        Text(text = "text")
    }
    setContentView(composeView, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT))
}
override val lifecycle: Lifecycle = LifecycleRegistry(this)
override val savedStateRegistry: SavedStateRegistry = SavedStateRegistryController.create(this).savedStateRegistry

而如果是继承ComponentActivity,就会发现很简单

setContentView(ComposeView(this).apply {
    setContent {
        Text(text = "text")
    }
})

不过都继承ComponentActivity了,那就没有太大的必要使用这个了。而且,在setContent的代码里面,还加了很多处理compose的代码。

然后有一点需要注意,compose的ComponentActivity和非compose的ComponentActivity是不一样的。
虽然两者的包名一样,但实现的类是不完全一样的。
如果没有使用compose,那ComponentActivity是这样的

public class ComponentActivity extends androidx.core.app.ComponentActivity implements
        LifecycleOwner,
        ViewModelStoreOwner,
        SavedStateRegistryOwner,
        OnBackPressedDispatcherOwner{
    // ...
}

而如果引入compose的ComponentActivity依赖则是

public class ComponentActivity extends androidx.core.app.ComponentActivity implements
        ContextAware,
        LifecycleOwner,
        ViewModelStoreOwner,
        HasDefaultViewModelProviderFactory,
        SavedStateRegistryOwner,
        OnBackPressedDispatcherOwner,
        ActivityResultRegistryOwner,
        ActivityResultCaller,
        OnConfigurationChangedProvider,
        OnTrimMemoryProvider,
        OnNewIntentProvider,
        OnMultiWindowModeChangedProvider,
        OnPictureInPictureModeChangedProvider,
        MenuHost{
    // ...
}

实现的类明显多了。

使用

在setContent里面,可以调用带有@Composable注解的方法来构建UI。
google自带的compose方法都是大写开头,猜测是为了让开发者能够快速看出是compose方法。
可以在compose方法上面使用@Preview注解,用来预览界面。但使用该注解的compose方法必须没有参数,否则没办法预览。
code:

// 这样就能够显示 hello world
setContent {
    Text(text = "hello world")
}

// 如果希望可以预览,可以这样
setContent {
    HelloWorldPreview()
}

@Preview
@Composable
fun HelloWorldPreview() {
    Text(text = "hello world")
}

如果想要预览,可以在Activity的右上角点击"Split",如果compose没有及时更新,可以在预览界面的右上角找到Build & Refresh。
一个元素想要预览,必须在没有参数的方法里面编写界面,如果一个方法有参数,是没办法预览的,官方文档就是这样写的。

布局

把布局放在控件前面是因为常用布局不多,而且还需要通过布局引出Modifier,Modifier也是compose重要的组成部分。
Modifier无论是在布局,还是控件上,都有重要的作用。
默认的布局

setContent{
    Message()    
}

@Preview
@Composable
fun Message() {
    Text("Text1")
    Text("Text2")
}

这段代码会在内容视图中创建两个文本元素。不过,由于您未提供有关如何排列这两个文本元素的信息,因此它们会相互重叠,使文本无法阅读。
Column

setContent {
    Column {
        Message()
    }
}

Column函数可以垂直排列控件。Column方法有4个参数,分别是:Modifier、Arrangement.Vertical、Alignment.Horizontal和content。
Modifier功能强大,下面再说。content就是显示控件,不用说。 Arrangement.Vertical和Alignment.Horizontal和content从名字就可以看出,是指定子控件的位置。
比如给Arrangement.Vertical设置一个Arrangement.Bottom,所有子控件就会在底部。给Alignment.Horizontal设置一个Alignment.End,所有子控件就会在屏幕右边。
两者的默认值分别是Top和Start,所以不指定该参数时,子元素就显示在左上角。
在设置这两个参数时,还需要设置Modifier,可以将Column大小设置为最大:Modifier.fillMaxSize(),这样才能看到实际效果。如果不设置,相当于宽高都为WRAP_CONTENT。
Row

setContent {
    Row {
        Message()
    }
}

Row函数可以水平排列元素。Row的参数和Column差不多,只是第2个和第3个参数变成了Arrangement.Horizontal和Alignment.Vertical。
用法上,和Column差不多。默认值和Column一样,也是Top和Start。
Box

setContent {
    Box {
        Message()
    }
}

这个有点像RelativeLayout,如果配合Modifier使用,好像功能会变得很强大。我在官方文档看到很多示例代码,但不知道那些代码的作用,后面再看看吧。
Box第2个参数和第3个参数分别为:Alignment和propagateMinConstraints: Boolean。两个的默认值分别为:Alignment.TopStart和false。
Alignment和Column和Row有点不一样,但也没有太大区别。该对象可以指定CenterEnd、BottomStart等参数,看名称也知道是什么意思,不用解释。
propagateMinConstraints:Whether the incoming min constraints should be passed to content.
TODO,测试了一下,设置为true之后,即使size为最大,还是没办法显示在屏幕底部。这个参数应该会影响大小,但不敢肤浅地就这样下定论,所以会好好查一下这个参数的真正作用。

不同测量单位

在将Modifier之前,先搞定这个,这个不止能用在Modifier,文本大小也需要用到。
在layout文件里面,通常想要指定大小为dp时,只需在数字后面写上dp即可。而java/kotlin方面,则需要获取屏幕宽度换算,或者使用TypedValue.applyDimension计算。
而在compose里面,这两种方式都用不了。需要使用Dp对象,Sp需要使用TextUnit对象。

// dp
Dp(100f)
100f.dp
// sp
100f.sp
// em
100f.em

sp和em用的都是TextUtil对象,没办法直接new,只能用扩展变量。
上面声明时都是用Float类型,实际上,除了第一个使用new的方式,像100f.dp这种,还有Int和Double两种声明方式。
Dp为plus、minus、unaryMinus、div和times编写了operator,所以可以直接使用+、-、*和/这些符号操作dp。
对于plus和minus来说,需要传入一个Dp对象。times可以Int和Float。div可以Int、Float和Dp对象。unaryMinus则是在数字前面加上"-“。
Dp里面还有Dp.Hairline、Dp.Infinity和Dp.Unspecified可以使用。
sp和em用的是TextUtil,所以看看TextUtil。TextUtil有unaryMinus、times和div。 times和div都有Int、Float和Double3个参数。
无论是Dp还是TextUtil,都实现了Comparable,所以可以还能使用compareTo或者”<>"来比较两个的值大小。
DpSize
除了上面提到的dp和sp,compose还提供了DpSize,DpSize可以用在Modifier里面。
DpSize有两个参数,分别是width和height,都是Dp类型。可以通过dpSize.width和dpSize.height分别获取宽高。
DpSize也提供了plus、minus、times和div用来操作DpSize,提供了copy用于深拷贝。除此之外,还提供了component1和component2。所以可以这样获取宽高。

val (width, height) = DpSize(100.dp, 100.dp)

DpOffset
这个目前不知道有什么用,但被我看到了,就提出来。DpOffset两个参数分别为x和y。operator有plus和minus,也有copy方法,其他方法就没有了。
DpRect
有4个初始参数,分别是left、top、right、bottom。DpRect还提供了一个有两个参数的构造方法,为origin: DpOffset, size: DpSize。
DpRect只有参数,没有其他方法。DpRect是一个data class,所以可以这样获取left、top、right、bottom。

val (left, top, right, bottom) = DpRect()

DpRect有3个扩展字段,width、height、size,size返回的是一个DpSize对象。

Modifier

在xml编写View时,可以给一个View指定background、size、padding等参数,到了compose,这一切都交给了Modifier,所以我才在上面提到,这个的功能很强大。
首先,需要知道一件事,Modifier可以链式调用。比如:Modifier.fillMaxSize().background(Color.Black)。指定了maxSize和background。
知道了Modifier可以这样使用之后,就会发现Modifier是很强大的,可以调用Modifier不同的方法,组合出不同的效果。

Modifier.size(100.dp, 100.dp)

控件

自定义View

你可能感兴趣的:(android,笔记)