斷更一時(shí)爽益兄,一直斷更一直爽~ 哈哈哈,就當(dāng)給自己放了個(gè)長(zhǎng)假吧箭券。最近的行情太糟了净捅,身邊有同學(xué)已經(jīng)被畢業(yè),兩個(gè)多月終于降薪找到下家··· 這里呼吁大家一定要存好六個(gè)月沒(méi)有工作還能正常生活的銀子辩块,以備不時(shí)之需蛔六!希望疫情能早日平息,經(jīng)濟(jì)可以快速恢復(fù)吧~
自己也沒(méi)想到這個(gè)系列可以到第六篇废亭,斷更確實(shí)很久了国章,居然還收到了小伙伴的催更,感謝你們的不離不棄豆村。閑話少說(shuō)液兽,我們這次要介紹的是 Compose 主題,那么 Compose 主題 Theme 到底有什么掌动?用 Compose 實(shí)現(xiàn)換膚簡(jiǎn)單嗎四啰?一起來(lái)看看吧宁玫!
Jetpack Compose 的主題 Theme 就是一套 UI 風(fēng)格,其中包括字體拟逮、字號(hào)撬统、色值等等,類比于 Android View 體系中的 Theme.MaterialComponents.DayNight.DarkActionBar
等等的主題樣式敦迄。與 View 體系最大的不同在于,它完全拋棄了 xml 文件的設(shè)置凭迹,所有樣式都是通過(guò)代碼設(shè)置的罚屋,主題樣式大體可以分為 色值、文案樣式嗅绸、形狀樣式 三大類脾猛。先來(lái)看看主題中的色值。
1. Color 色值
許多組件不僅支持設(shè)置它自己的背景色鱼鸠,還可以設(shè)置它包含的其他可組合項(xiàng)的默認(rèn)色值猛拴,使用 contentColorFor
方法就可以實(shí)現(xiàn)。例如下面 code 1:
// code 1
Surface (color = Color.Yellow,contentColor = Color.Red) {
Text(text = "July 2021",style = typography.body2)
}
你會(huì)發(fā)現(xiàn)蚀狰,Surface
的背景色為黃色愉昆,而 Text
中文案為 紅色,如果將 Text
換為 Icon
麻蹋,那么 Icon
的色調(diào)也會(huì)變?yōu)榧t色跛溉,感興趣的同學(xué)可以試試。
類似 Surface
的還有 TopAppBar
可組合項(xiàng)扮授,下面是它們的實(shí)現(xiàn)源碼:
// code 2
Surface(
color: Color = MaterialTheme.colors.surface,
contentColor: Color = contentColorFor(color),
...
TopAppBar(
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor),
...
Compose 官方推薦使用 Surface
來(lái)給任何可組合項(xiàng)設(shè)置顏色芳室,因?yàn)樗鼤?huì)設(shè)置適當(dāng)?shù)膬?nèi)容顏色 CompositionLocal
值,看 code 2 中 Surface
的 color
屬性就默認(rèn)設(shè)置了 MaterialTheme.colors.surface
色值刹勃。不推薦直接調(diào)用 Modifier.background
設(shè)置顏色堪侯,因?yàn)樗](méi)有設(shè)置任何的默認(rèn)色值。在實(shí)際開發(fā)中荔仁,其實(shí)咱也沒(méi)咋用到 MaterialTheme
伍宦,所以這里還是看個(gè)人吧~
// code 3
-Row(Modifier.background(MaterialTheme.colors.primary)) { // 不推薦
+Surface(color = MaterialTheme.colors.primary) { // 推薦
+ Row(
...
在可組合項(xiàng)中,一些 UI 的參數(shù)是有默認(rèn)值的咕晋,比如 Alpha 透明度雹拄、ContentColor 內(nèi)容色等。我們可以使用CompositionLocalProvider
類去自定義這些屬性的默認(rèn)值掌呜。比如:
// code 4
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
Text(text = "Hello, 修之竹~")
}
對(duì)比沒(méi)有加 CompositionLocalProvider
的情況滓玖,會(huì)發(fā)現(xiàn)文案顏色更淺。這是因?yàn)橹式叮J(rèn)情況下 Text
文案的 alpha
值為 ContentAlpha.high
势篡,這里設(shè)置為 ContentAlpha.disabled
翩肌,還有一個(gè) ContentAlpha.medium
,alpha
值的大小排序?yàn)椋?code>high > medium > disabled禁悠。具體的值可以查看源碼念祭,它還分了高對(duì)比度和低對(duì)比度兩種情況。
Compose 在暗夜模式支持方面也做的不錯(cuò)碍侦。比如粱坤,是否在淺色模式中運(yùn)行的判斷很簡(jiǎn)單:
// code 5
val isLightTheme = MaterialTheme.colors.isLight
此外,如果在實(shí)際中就是使用的 MaterialTheme
中的色值來(lái)設(shè)置瓷产,那么需要注意的是站玄,Compose 默認(rèn)的可組合項(xiàng)中常見(jiàn)的情況是在淺色模式中將容器設(shè)為 primary
色值,在暗夜模式中將其設(shè)為 surface
色值濒旦,許多組件默認(rèn)都是使用這種模式株旷,例如TopAppBar
(應(yīng)用欄) 和 BottomNavigation
(底部導(dǎo)航欄)。
2. 文案樣式
文案樣式也可以復(fù)用 MaterialTheme
中已有的字體樣式尔邓,當(dāng)然也可以先將已有的樣式 copy 一份晾剖,然后修改其中的某些屬性。比如可以修改字間距:
// code 6
Text(
text = "Hello, 修之竹~",
// style = MaterialTheme.typography.body1 // 復(fù)用 MaterialTheme 中的字體樣式
style = MaterialTheme.typography.body1.copy( // copy 已有樣式并修改字間距屬性的值
letterSpacing = 5.sp
),
fontSize = 20.sp // 在Text中設(shè)置 fontSize 可重寫覆蓋 MaterialTheme.typography.body1 TextStyle 中的字體大小
)
2.1 AnnotatedString 類來(lái)設(shè)置多種樣式
AnnotatedString
用來(lái)代替 SpannableString
最好不過(guò)了梯嗽,因?yàn)樗娴谋?SpannableString
好用多了齿尽!再也不用擔(dān)心使用 SpannableString
引發(fā)的數(shù)組越界問(wèn)題了。代碼及效果如下慷荔,當(dāng)然還可以實(shí)現(xiàn)許多其他的文案樣式雕什,感興趣的同學(xué)可以自行查閱 SpanStyle
的官方文檔。
// code 7
val annotatedString = buildAnnotatedString {
withStyle(SpanStyle(color = Color.Red, fontWeight = FontWeight.Bold)) {
append("Kotlin ")
}
append("是世上 ")
withStyle(SpanStyle(fontSize = 24.sp)) {
append("最好的語(yǔ)言")
}
}
Text(text = annotatedString)
SpanStyle
是設(shè)置文案的樣式的显晶,作用于字符單位贷岸;而如果要針對(duì)文案的行高、對(duì)齊方式等進(jìn)行設(shè)置磷雇,則需要使用ParagraphStyle
偿警,顧名思義它是針對(duì)段落樣式的。
3. 形狀樣式
MaterialTheme
主題中也有 Shape
形狀屬性唯笙,在許多的官方 Composable 組件中都有這個(gè) Shape
屬性螟蒸,比如 Button
組件的 Shape
屬性默認(rèn)值就是 MaterialTheme.shapes.small
。
// code 8
fun Button(
···
shape: Shape = MaterialTheme.shapes.small,
···
) {
}
Shapes.kt
提供了 small
崩掘、medium
七嫌、large
3 種不同的屬性值,其實(shí)都是 RoundedCornerShape
的具體實(shí)現(xiàn)苞慢,只不過(guò)圓角的大小不太一樣罷了诵原,具體數(shù)值可查看源碼。
如果需要在自定義 Composable 組件中使用 Shape
,有兩種方法:一是使用擁有 Shape
屬性的官方 Composable 組件绍赛;二是使用 Modifier
中可設(shè)置 shape
的方法去接收自定義 Composable 組件傳進(jìn)來(lái)的 Shape
參數(shù)值蔓纠。先來(lái)看看第一種方法褒搔,如 code 9 所示绊汹。
// code 9
@Composable
fun RoundedCornerImage(painter: Painter, cornerSize: Int) {
Surface(
shape = RoundedCornerShape(cornerSize.dp)
) {
Image(
painter = painter,
contentDescription = "圓角圖片"
)
}
}
這是個(gè)可以設(shè)置圖片圓角大小的自定義 Composable 組件,因?yàn)樾枰玫?Shape
設(shè)置圓角昼窗,所以使用了 Surface
這個(gè)組件的 Shape 屬性來(lái)具體實(shí)現(xiàn)蚯妇。
第二種方法就是借助 Modifier
的方法敷燎,比如 Modifier.clip(shape: Shape)
、Modifier.background(color: Color, shape: Shape = RectangleShape)
箩言、Modifier.border(width: Dp, brush: Brush, shape: Shape)
等等懈叹。比較簡(jiǎn)單,感興趣的同學(xué)可以試試分扎。
4. 切換主題
上面說(shuō)了這么多,其實(shí)都是針對(duì)單個(gè)主題說(shuō)的胧洒,在實(shí)際應(yīng)用中畏吓,我們可以做個(gè)切換主題的小功能,如下圖 2 所示:
其中包含了色值卫漫、字體菲饼、形狀的切換,用到的思路和原理都是一樣的列赎,所以這里就只拿主題色值的切換來(lái)說(shuō)明宏悦。想要實(shí)現(xiàn)這一功能,首先需要明白的是包吝,點(diǎn)擊事件之后切換主題的回調(diào)該怎么做饼煞?
總不能給所有設(shè)置色值的地方都設(shè)置一個(gè)監(jiān)聽(tīng)器吧?那樣做想想都覺(jué)得“酸爽”诗越。其實(shí)砖瞧,在 Compose 中,我們可以將當(dāng)前主題用一個(gè) MutableState
對(duì)象來(lái)保存嚷狞,然后將主題中的色值集合與這個(gè)狀態(tài)相關(guān)聯(lián)块促,當(dāng)用戶切換主題改變了這個(gè) MutableState
值之后,與之關(guān)聯(lián)的色值集合就會(huì)收到回調(diào)進(jìn)行切換床未,同時(shí)通知 Compose 進(jìn)行重組竭翠,這樣就使用新的色值集合進(jìn)行渲染了。
關(guān)于 MutableState
狀態(tài)的相關(guān)知識(shí)薇搁,可以查閱我的另一篇文章:Jetpack-Compose 學(xué)習(xí)筆記(五)—— State 狀態(tài)是個(gè)啥斋扰?又是新概念?
OK,整體的思路有了褥实,咱們?cè)僭敿?xì)看看具體是如何實(shí)現(xiàn)的呀狼。按照之前的分析,我們需要在每次渲染頁(yè)面的時(shí)候讀取當(dāng)前主題的值损离,所以哥艇,首先得先獲取當(dāng)前的主題值。我這里是使用 MMKV
存儲(chǔ)當(dāng)前主題值僻澎,主題值是 String
類型貌踏,如下 code 10 所示:
// code 10
//獲取選中的主題 id
val chosenThemeId = remember {
mutableStateOf(
MMKV.defaultMMKV().getString(MMKVConstant.ChosenThemeCode, ThemeKinds.DEFAULT.name)
?: ThemeKinds.DEFAULT.name
)
}
enum class ThemeKinds {
DEFAULT, //默認(rèn)主題
RED, //紅色主題
YELLOW, //黃色主題
BLUE //藍(lán)色主題
}
然后自定義主題,在這里需要規(guī)定主題用到的色值窟勃、文案樣式祖乳、形狀樣式等。在每次切換主題后秉氧,在這里還需要根據(jù)傳入的當(dāng)前主題值眷昆,設(shè)置相應(yīng)的色值組等等。詳細(xì)如下代碼:
// code 11
@Composable
fun CustomTheme(
chosenThemeId: MutableState<String>,
content: @Composable () -> Unit
) {
//自定義主題色值
val colors = when (chosenThemeId.value) {
ThemeKinds.DEFAULT.name -> {
LightColors
}
ThemeKinds.RED.name -> {
RedThemeColors
}
ThemeKinds.YELLOW.name -> {
YellowThemeColors
}
ThemeKinds.BLUE.name -> {
BlueThemeColors
}
else -> {
DarkColors
}
}
MaterialTheme(
colors = colors,
typography = typography,
shapes = shapes
) {
content()
}
}
//紅色主題色值
private val RedThemeColors = lightColors(
primary = Color(0xFFFF4040),
background = Color(0x66FF4040)
)
//黃色主題色值
private val YellowThemeColors = lightColors(
primary = Color(0xFFDAA520),
background = Color(0x66FFD700)
)
//藍(lán)色主題色值
private val BlueThemeColors = lightColors(
primary = Color(0xFF436EEE),
background = Color(0x6600FFFF)
)
private val DarkColors = darkColors(
primary = Color.White,
primaryVariant = Red700,
onPrimary = Color.Black,
secondary = Red300,
onSecondary = Color.Black,
error = Red200
)
private val LightColors = lightColors(
primary = Color.Black,
primaryVariant = Red900,
onPrimary = Color.White,
secondary = Red700,
secondaryVariant = Red900,
onSecondary = Color.White,
error = Red800,
)
可以看到汁咏,在我們自定義的主題 CustomTheme
最后亚斋,還是使用的 MaterialTheme
,只不過(guò)將官方的 MaterialTheme
中 colors
設(shè)置成了我們自己的 colors
攘滩,同理帅刊,我們還可以設(shè)置文案 typography
和 形狀 shapes
等參數(shù)。
其實(shí)漂问,所謂的色值組就是一個(gè) Colors
對(duì)象赖瞒,Compose 中默認(rèn)就有 lightColors
和 darkColors
兩種 Colors
對(duì)象,分別用于暗夜模式和白天模式的主題色值的設(shè)置蚤假,我們這里統(tǒng)一是以白天模式的 lightColors
對(duì)象為基準(zhǔn)來(lái)進(jìn)行其他主題色值的設(shè)置栏饮,作為例子這里就重寫了 primary
和 background
兩個(gè)屬性,分別用來(lái)設(shè)置文案色值和背景色的色值勤哗。
定義好自定義主題中的各個(gè)色值組后抡爹,別忘了最后還是要設(shè)置到 MaterialTheme
中的 colors
屬性中,然后我們才可以通過(guò)調(diào)用 MaterialTheme colors
來(lái)使用自定義主題中的各個(gè)色值芒划。下面的代碼就是使用樣例:
// code 12
CustomTheme(chosenThemeId) {
Surface(color = MaterialTheme.colors.background) {
···
}
}
所以冬竟,如果我們要新增一組色值,我們只需要在 CustomTheme
中新增一組主題色值就可以了民逼,不用去改動(dòng)設(shè)置色值的代碼泵殴,改動(dòng)代碼量較少。
再來(lái)看看切換主題的點(diǎn)擊觸發(fā)事件拼苍,顯然是在這幾個(gè)小方塊里笑诅,而且每個(gè)方塊代表一種主題调缨,具體的代碼如下:
// code 13
@Composable
fun ThemeColorCube(themeItem: ThemeItem, chosenThemeId: MutableState<String>, onClick: () -> Unit) {
Surface(
shape = RoundedCornerShape(10.dp),
elevation = 5.dp,
color = themeItem.mainColor,
modifier = Modifier
.size(85.dp)
.padding(10.dp)
.clickable {
onClick()
}
) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
if (themeItem.id.name == chosenThemeId.value) {
Image(
modifier = Modifier.size(20.dp),
painter = painterResource(id = R.drawable.ic_checkbox_selected_gray),
contentScale = ContentScale.FillBounds,
contentDescription = "被選中標(biāo)記圖"
)
} else {
Text(
text = themeItem.name,
textAlign = TextAlign.Center,
style = TextStyle(color = MaterialTheme.colors.primary)
)
}
}
}
}
data class ThemeItem(
val id: ThemeKinds, //主題 id
val name: String, //主題 name
val mainColor: Color, //主色
)
點(diǎn)擊事件的回調(diào)在主頁(yè)面 LazyRow
列表的方法中:
// code 14
LazyRow() {
items(themeList) { item: ThemeItem ->
ThemeColorCube(themeItem = item, chosenThemeId) {
//點(diǎn)擊色塊選擇其中的一種顏色
MMKV.defaultMMKV().putString(MMKVConstant.ChosenThemeCode, item.id.name)
chosenThemeId.value = item.id.name
}
}
}
可以看到,點(diǎn)擊之后吆你,需要將選中的主題 id
存儲(chǔ)在本地弦叶,以便下次打開 App 可以獲取到選中的主題并設(shè)置相應(yīng)的主題色值組,更為重要的是更新 MutableState
對(duì)象妇多,即通過(guò) CustomTheme
傳進(jìn)來(lái)的 chosenThemeId
的值伤哺。由于 MutableState
的特性,所有引用它的地方者祖,都會(huì)觸發(fā)重組立莉,從而會(huì)使得 CustomTheme
重組,重組會(huì)根據(jù)到更新后的 chosenThemeId
的值來(lái)設(shè)置色值組七问,那么 MaterialTheme.colors
的色值組就切換為新選中主題的色值組了蜓耻。
另外文案字體和大小,以及圖片的圓角大小械巡,都是類似的原理刹淌,不再贅述,文末見(jiàn)源碼獲取方法讥耗。
5. 彩蛋 —— 切換主題進(jìn)階版
這就完了么芦鳍?作為主題切換功能來(lái)講,已經(jīng)實(shí)現(xiàn)完了葛账,但,剛剛的切換過(guò)程是不是感覺(jué)比較生硬皮仁?有沒(méi)有更加絲滑的做法籍琳?答案當(dāng)然是有的。
如圖3 所示贷祈,每次切換時(shí)趋急,背景色和字體大小、圓角大小都是漸變的势誊,切換過(guò)程絲滑呜达,過(guò)渡自然。
要想實(shí)現(xiàn)絲滑的效果粟耻,先得認(rèn)識(shí)一位新的朋友:animateXxxAsState查近。
5.1 animateXxxAsState
看前綴就知道是為動(dòng)畫而生的,Xxx 是因?yàn)樗性S多重載的參數(shù)方法挤忙,比如 Color霜威、Dp、Float 等册烈,我們這里色值的漸變就是用到的 animateColorAsState
方法戈泼。同樣地,文案字體大小的動(dòng)畫以及圓角的動(dòng)畫,分別使用的是 animateFloatAsState
和 animateDpAsState
方法大猛。
這一類方法非常好用扭倾,官方文檔上是這么介紹 animateColorAsState
方法的:
Fire-and-forget animation function for Color.
只需要觸發(fā)調(diào)用它即可,不用管其他的事情挽绩。這里只對(duì) animateColorAsState
方法進(jìn)行舉例說(shuō)明膛壹,其他方法以此類推。先來(lái)看看它的聲明:
// code 15
@Composable
fun animateColorAsState(
targetValue: Color,
animationSpec: AnimationSpec<Color> = colorDefaultSpring,
finishedListener: ((Color) -> Unit)? = null
): State<Color>
第一個(gè)參數(shù)就是設(shè)置色值漸變的終值琼牧,一旦設(shè)置的終值改變恢筝,漸變的動(dòng)畫就會(huì)自動(dòng)觸發(fā)。當(dāng)動(dòng)畫還未結(jié)束終值又有變化時(shí)巨坊,則動(dòng)畫會(huì)調(diào)整動(dòng)畫路徑到新的終值撬槽。
第二個(gè)參數(shù)可以設(shè)置動(dòng)畫的執(zhí)行規(guī)范,實(shí)現(xiàn)了 AnimationSpec
接口的有 1)FloatSpringSpec
趾撵;2)FloatTweenSpec
侄柔;3)InfiniteRepeatableSpec
;4)KeyframesSpec
占调;5)RepeatableSpec
暂题;6)SnapSpec
;7)SpringSpec
究珊;8)TweenSpec
. 這些都是針對(duì)動(dòng)畫進(jìn)行的設(shè)置薪者,例如動(dòng)畫時(shí)間,以及動(dòng)畫速度的變化剿涮,類似于插值器言津。
第三個(gè)參數(shù)就很好理解了,即動(dòng)畫完成后的回調(diào)方法取试。
返回值是一個(gè) State
狀態(tài)對(duì)象悬槽,所以它可以不斷地去更新值,直至動(dòng)畫完成瞬浓。
需要注意的是初婆,只要?jiǎng)赢嬎饔玫目山M合項(xiàng)沒(méi)有從 Compose 組件樹上被移除,那么這個(gè)動(dòng)畫方法不會(huì)被取消或被停止猿棉。
5.2 Color 漸變實(shí)現(xiàn)
從上一節(jié)可以得知磅叛,animateColorAsState
方法返回的是個(gè) State
狀態(tài),我們需要這個(gè)返回值去重組更新調(diào)用了該色值的 Composable 組件萨赁,所以宪躯,每種需要漸變的色值都需要聲明一個(gè) State
狀態(tài)對(duì)象,我這里統(tǒng)一都放在 ViewModel
中管理了:
// code 16
class MainViewModel : ViewModel() {
var primaryColor: Color by mutableStateOf(Color(0xFF000000)) // 用于文案色值漸變
var backgroundColor: Color by mutableStateOf(Color(0xFFFFFFFF)) // 用于背景色漸變
···
val chosenThemeId = mutableStateOf(
MMKV.defaultMMKV().getString(MMKVConstant.ChosenThemeCode, ThemeKinds.DEFAULT.name)
?: ThemeKinds.DEFAULT.name
)
}
當(dāng)切換主題后位迂,主題 id 存儲(chǔ)的 MutableState
觸發(fā)重組访雪,然后根據(jù)新的主題 id 獲取到新的色值組详瑞,這時(shí) animateColorAsState
中的 targetValue
就發(fā)生了變化,觸發(fā)漸變動(dòng)畫臣缀,從而不斷更新 ViewModel
中的 primaryColor
State 值坝橡,進(jìn)而重組所有引用了 primaryColor
值的可組合項(xiàng),這時(shí)漸變效果出現(xiàn)精置。下面是 CustomTheme
部分代碼:
// code 17
val targetColors: AppColors
if (isSystemInDarkTheme()) {
//如果是深色模式计寇,則只能是深色模式的色值組,無(wú)法切換
targetColors = DarkColors
} else {
targetColors = when (mainViewModel.chosenThemeId.value) {
ThemeKinds.RED.name -> {
RedThemeColors
}
ThemeKinds.YELLOW.name -> {
YellowThemeColors
}
ThemeKinds.BLUE.name -> {
BlueThemeColors
}
else -> {
DefaultColors
}
}
}
//漸變實(shí)現(xiàn)
mainViewModel.primaryColor = animateColorAsState(targetColors.primary, TweenSpec(500)).value
mainViewModel.backgroundColor = animateColorAsState(targetColors.background, TweenSpec(500)).value
這里設(shè)置的漸變時(shí)長(zhǎng)為 500ms脂倦,并且為了方便管理番宁,將所有色值放在 AppColors
類中進(jìn)行管理,各個(gè)不同的主題有著各自不同的 AppColors
類對(duì)象赖阻,如下所示:
// code 18
@Stable
data class AppColors (
val primary: Color,
val background: Color
)
//紅色主題色值
private val RedThemeColors = AppColors(
primary = Color(0xFFFF4040),
background = Color(0x66FF4040)
)
//黃色主題色值
private val YellowThemeColors = AppColors(
primary = Color(0xFFDAA520),
background = Color(0x66FFD700)
)
至于圓角大小以及文字大小的漸變蝶押,都是一樣的實(shí)現(xiàn)方法,就是需要在 ViewModel
中定義需要的 MutableState
狀態(tài)對(duì)象火欧,然后使用相應(yīng)的 animateXxxAsState
進(jìn)行漸變動(dòng)畫的實(shí)現(xiàn)即可棋电。
碎碎念:其實(shí) Compose 官方教程中的 Theme 主題內(nèi)容不多,且比較簡(jiǎn)單苇侵,所以就想借著主題切換的功能來(lái)鞏固和運(yùn)用這一知識(shí)點(diǎn)赶盔,希望大家能夠?qū)W有所得~ 如有問(wèn)題歡迎留言探討~
如需文中源碼,請(qǐng)?jiān)诠娞?hào)回復(fù):Compose換膚
贊人玫瑰榆浓,手留余香于未!歡迎點(diǎn)贊、轉(zhuǎn)發(fā)~ 轉(zhuǎn)發(fā)請(qǐng)注明出處~
更多內(nèi)容陡鹃,歡迎關(guān)注公眾號(hào): 修之竹