├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── zj │ │ └── composemoon │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── zj │ │ │ └── composemoon │ │ │ ├── MainActivity.kt │ │ │ ├── ui │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Shape.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── widget │ │ │ ├── Moon.kt │ │ │ ├── Poetry.kt │ │ │ └── StarrySky.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── zj │ └── composemoon │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | /local.properties 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 效果图 2 | ![](https://raw.githubusercontents.com/shenzhen2017/resource/main/2021/september/p4.gif) 3 | > 人有悲欢离合,月有阴晴圆缺,此事古难全。 4 | > 但愿人长久,千里共婵娟。 5 | > 恰逢中秋佳节,我们今天就使用`Compose`来实现一下月相变化动画吧~ 6 | > 感兴趣的同学可以点个`Star` : [Compose 实现月亮阴晴圆缺动画](https://github.com/shenzhen2017/ComposeMoon) 7 | 8 | ## 主要思路 9 | ### 满天繁星 10 | 为了实现月相动画,我们首先需要一个背景,因此我们需要一个好看的星空,最好还有闪烁的效果 11 | 为为实现星空背景,我们需要做以下几件事 12 | 1. 绘制背景 13 | 2. 生成几十个星星,在背景上随机分布 14 | 3. 通过`scale`与`alpha`动画,实现每个星星的闪烁效果 15 | 16 | 我们一起来看下代码 17 | ```kotlin 18 | @Composable 19 | fun Stars(starNum: Int) { 20 | BoxWithConstraints(modifier = Modifier.fillMaxSize()) { 21 | val list = remember { mutableStateListOf() } 22 | LaunchedEffect(true) { 23 | for (i in 0..starNum) { 24 | delay(100L) 25 | //添加星星,它们的位置在屏幕上随机 26 | list.add(Star(maxWidth.value * density, maxHeight.value * density)) 27 | } 28 | } 29 | list.forEach { 30 | Star(it) 31 | } 32 | } 33 | } 34 | 35 | @Composable 36 | fun Star(star: Star) { 37 | var progress: Float by remember { mutableStateOf(0f) } 38 | val infiniteTransition = rememberInfiniteTransition() 39 | .... 40 | star.updateStar(progress) // 通过动画更新progress,从而更新star的属性值 41 | Canvas(modifier = Modifier.wrapContentSize()) { 42 | scale(star.scale, Offset(star.x, star.y)) { // 缩放动画 43 | drawCircle( 44 | star.starColor, 45 | star.radius, 46 | center = Offset(star.x, star.y), 47 | alpha = star.alpha // alpha动画 48 | ) 49 | } 50 | } 51 | } 52 | ``` 53 | 54 | ### 月相变化 55 | 月相,天文学术语。(`phase of the moon`)是天文学中对于地球上看到的月球被太阳照明部分的称呼。随着月亮每天在星空中自东向西移动一大段距离,它的形状也在不断地变化着,这就是月亮位相变化,叫做月相。 56 | 它的变化过程如下图所示 57 | 58 | ![](https://raw.githubusercontents.com/shenzhen2017/resource/main/2021/september/p5.jpeg) 59 | 60 | 每个阶段都有各自的名字,如下图所示: 61 | 62 | ![](https://raw.githubusercontents.com/shenzhen2017/resource/main/2021/september/p6.jpeg) 63 | 64 | 可以看出,月相变化过程还是有些复杂的,那我们怎么实现这个效果呢? 65 | 66 | #### 思路分析 67 | 为了实现月相变化,首先我们需要画一个圆,代表月亮,最终的满月其实就是这样,比较简单 68 | 69 | 有了满月,如何在它的基础上,画出其它的月相呢?我们可以通过图像混合模式来实现 70 | 71 | 图像混合模式定义的是,当两个图像合成时,图像最终的展示方式。在`Androd`中,有相应的`API`接口来支持图像混合模式,即`Xfermode`. 72 | 73 | 图像混合模式主要有以下16种,以下这张图片从一定程度上形象地说明了图像混合的作用,两个图形一圆一方通过一定的计算产生不同的组合效果,具体如下 74 | 75 | ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2ccb418c0dc8431fb449cb5ddd117bd5~tplv-k3u1fbpfcp-watermark.awebp) 76 | 77 | 我们为了实现月相动画,主要需要使用以下两种混合模式 78 | - `DST_OUT`:只在源图像和目标图像不相交的地方绘制【目标图像】,在相交的地方根据源图像的`alpha`进行过滤,源图像完全不透明则完全过滤,完全透明则不过滤 79 | - `DST_OVER`:将目标图像放在源图像上方 80 | 81 | 我们已经了解了图形混合模式,那么需要在满月上画什么才能实现其它效果呢? 82 | 我们可以通过在满月上放一个半圆`+`一个椭圆来实现 83 | ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f872b4e4c2e6417bbd46c8547deb007a~tplv-k3u1fbpfcp-watermark.awebp) 84 | 1. 如上所示,椭圆上水平的线叫长轴,竖直的线叫短轴 85 | 2. 短轴不变,长轴半径从0到满月半径发生变化,再加上一个半圆,就可以实现不同的月相 86 | 3. 比如为了画上蛾眉月,可以通过左半边画半圆,再加上一个椭圆,两都都使用`DST_OVER`混合模式来实现,就实现了它们两的并集,然后覆盖在下层满月上,就实现了上蛾眉月 87 | 4. 为了画渐盈凸月,则同样就左半边以`DST_OVER`画半圆,再以`DST_OUT`画椭圆,就只剩下半圆与椭圆不相交的部分,再与下层的满月混合,就实现了渐盈凸月 88 | 89 | 这样说可能还是比较抽象,感兴趣的同学可下载源码详细了解下 90 | 91 | #### 源码实现 92 | ```kotlin 93 | //月亮动画控件 94 | @Composable 95 | fun Moon(modifier: Modifier) { 96 | var progress: Float by remember { mutableStateOf(0f) } 97 | BoxWithConstraints(modifier = modifier) { 98 | Canvas( 99 | modifier = Modifier 100 | .size(canvasSize) 101 | .align(Alignment.TopCenter) 102 | ) { 103 | drawMoonCircle(this, progress) 104 | drawIntoCanvas { 105 | it.withSaveLayer(Rect(0f, 0f, size.width, size.height), paint = Paint()) { 106 | if (progress != 1f) { 107 | //必须先画半圆,再画椭圆 108 | drawMoonArc(this, it, paint, progress) 109 | drawMoonOval(this, it, paint, progress) 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } 116 | 117 | // 1.首先画一个满月 118 | private fun drawMoonCircle(scope: DrawScope, progress: Float) { 119 | //.... 120 | drawCircle(Color(0xfff9dc60)) 121 | } 122 | 123 | // 2. 画半圆 124 | private fun drawMoonArc(scope: DrawScope, canvas: Canvas, paint: Paint, progress: Float) { 125 | val sweepAngle = when { //从新月到满月在一边画半圆,从满月回到新月则在另一边画半圆 126 | progress <= 0.5f -> 180f 127 | progress <= 1f -> 180f 128 | progress <= 1.5f -> -180f 129 | else -> -180f 130 | } 131 | paint.blendMode = BlendMode.DstOver //半圆的混合模式始终是DstOver 132 | scope.run { 133 | canvas.drawArc(Rect(0f, 0f, size.width, size.height), 90f, sweepAngle, false, paint) 134 | } 135 | } 136 | 137 | // 2. 画椭圆 138 | private fun drawMoonOval(scope: DrawScope, canvas: Canvas, paint: Paint, progress: Float) { 139 | val blendMode = when { //椭圆的混合模式会发生变化,这里需要注意下 140 | progress <= 0.5f -> BlendMode.DstOver 141 | progress <= 1f -> BlendMode.DstOut 142 | progress <= 1.5f -> BlendMode.DstOut 143 | else -> BlendMode.DstOver 144 | } 145 | paint.blendMode = blendMode 146 | scope.run { 147 | canvas.drawOval( 148 | Rect(offset = topLeft, size = Size(horizontalAxis, verticalAxis)), //椭圆的长轴会随着动画变化 149 | paint = paint 150 | ) 151 | } 152 | } 153 | ``` 154 | 如上所示: 155 | 1. 主要就是3个步骤,画满月,再画半圆,再画椭圆 156 | 2. 半圆的混合模式始终是`DstOver`,而椭圆的混合模式会发生变化,它们的颜色都是黑色。 157 | 3. 可以看到半圆与椭圆新建了一个`Layer`,混合模式的变化,表示的就是最后剩下的是它们的并集,还是`Dst`不相交的部分,最后覆盖到满月上,所以必须先画半圆 158 | 4. 随着动画的变化,椭圆的长轴会发生变化,这样就可以实现不同的月相 159 | 160 | ### 诗歌打字机效果 161 | 上面其实已经做得差不多了,我们最后再添加一些诗歌,并为它们添加打字机效果 162 | ```kotlin 163 | @Composable 164 | fun PoetryColumn( 165 | list: List, 166 | offsetX: Float = 0f, 167 | offsetY: Float = 0f 168 | ) { 169 | val targetList = remember { mutableStateListOf() } 170 | LaunchedEffect(list) { 171 | targetList.clear() 172 | list.forEach { 173 | delay(500) //通过在LaunchedEffect中delay实现动画效果 174 | targetList.add(it) 175 | } 176 | } 177 | //将 Jetpack Compose 环境的 Paint 对象转换为原生的 Paint 对象 178 | val textPaint = Paint().asFrameworkPaint().apply { 179 | //... 180 | } 181 | Canvas(modifier = Modifier.wrapContentSize()) { 182 | drawIntoCanvas { 183 | for (i in targetList.indices) { 184 | it.nativeCanvas.drawText(list[i].toString(), x, y, textPaint) 185 | y += delta // 更新文字y轴位置 186 | } 187 | } 188 | } 189 | } 190 | ``` 191 | 如上所示,代码比较简单 192 | 1. 通过在`LaunchedEffect`中调用挂起函数,来实现动画效果 193 | 2. 为了实现竖直方向的文字,我们需要使用`Paint`来绘制`Text`,而不能使用`Text`组件 194 | 3. `Compose`目前还不支持直接绘制`Text`,所以我们需要调用`asFrameworkPaint`将其转化为原生的`Paint` 195 | 196 | ## 总结 197 | 通过以上步骤,我们就通过`Compose`实现了月相阴晴圆缺+星空闪耀+诗歌打字机的动画效果 198 | 开发起来跟`Android`自定义绘制其实并没有多大差别,代码量因为`Compose`强大的`API`与声明式特点可能还有所减少 199 | 在我看来,`Compose`已经相当成熟了,而且将是`Android UI`的未来~ 200 | 201 | 开源不易,如果项目对你有所帮助,欢迎点赞,`Star`,收藏~ 202 | ### 参考资料 203 | [蹭中秋热度来了~Android 自定义View——月有阴晴圆缺](https://juejin.cn/post/7006142194230755341) 204 | 205 | [「寒草的中秋献礼🥮,实现30s前端创意动画」陪你看日落和月升|与你赏星空和诗歌](https://juejin.cn/post/7005355142413287438) 206 | 207 | ### 项目地址 208 | [Compose 实现月亮阴晴圆缺动画](https://github.com/shenzhen2017/ComposeMoon) 209 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdk 31 8 | 9 | defaultConfig { 10 | applicationId "com.zj.composemoon" 11 | minSdk 21 12 | targetSdk 31 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | vectorDrawables { 18 | useSupportLibrary true 19 | } 20 | } 21 | 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | compileOptions { 29 | sourceCompatibility JavaVersion.VERSION_1_8 30 | targetCompatibility JavaVersion.VERSION_1_8 31 | } 32 | kotlinOptions { 33 | jvmTarget = '1.8' 34 | useIR = true 35 | } 36 | buildFeatures { 37 | compose true 38 | } 39 | composeOptions { 40 | kotlinCompilerExtensionVersion compose_version 41 | kotlinCompilerVersion '1.5.21' 42 | } 43 | packagingOptions { 44 | resources { 45 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 46 | } 47 | } 48 | } 49 | 50 | dependencies { 51 | implementation 'androidx.core:core-ktx:1.6.0' 52 | implementation 'androidx.appcompat:appcompat:1.3.1' 53 | implementation 'com.google.android.material:material:1.4.0' 54 | implementation "androidx.compose.ui:ui:$compose_version" 55 | implementation "androidx.compose.material:material:$compose_version" 56 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 57 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' 58 | implementation 'androidx.activity:activity-compose:1.3.1' 59 | implementation "com.google.accompanist:accompanist-systemuicontroller:0.17.0" 60 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/zj/composemoon/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.zj.composemoon 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.zj.composemoon", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/zj/composemoon/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.zj.composemoon 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.material.MaterialTheme 8 | import androidx.compose.material.Surface 9 | import androidx.compose.material.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.tooling.preview.Preview 15 | import androidx.compose.ui.unit.dp 16 | import androidx.core.view.WindowCompat 17 | import com.google.accompanist.systemuicontroller.rememberSystemUiController 18 | import com.zj.composemoon.ui.theme.ComposeMoonTheme 19 | import com.zj.composemoon.widget.Moon 20 | import com.zj.composemoon.widget.Poetry 21 | import com.zj.composemoon.widget.StarrySky 22 | 23 | class MainActivity : ComponentActivity() { 24 | override fun onCreate(savedInstanceState: Bundle?) { 25 | super.onCreate(savedInstanceState) 26 | WindowCompat.setDecorFitsSystemWindows(window, false) 27 | setContent { 28 | ComposeMoonTheme { 29 | rememberSystemUiController().setStatusBarColor(Color.Transparent, darkIcons = true) 30 | Surface(color = MaterialTheme.colors.background) { 31 | Greeting("Android") 32 | } 33 | } 34 | } 35 | } 36 | } 37 | 38 | @Composable 39 | fun Greeting(name: String) { 40 | Box(modifier = Modifier.fillMaxSize()) { 41 | StarrySky(Modifier.fillMaxSize(), 50) 42 | Moon( 43 | modifier = Modifier 44 | .size(250.dp) 45 | .align(Alignment.Center) 46 | .offset(x = -(200.dp)) 47 | ) 48 | Poetry( 49 | modifier = Modifier 50 | .padding(0.dp, 100.dp, 80.dp, 0.dp) 51 | .wrapContentSize() 52 | .align(Alignment.TopEnd) 53 | ) 54 | } 55 | } 56 | 57 | @Preview(showBackground = true) 58 | @Composable 59 | fun DefaultPreview() { 60 | ComposeMoonTheme { 61 | Greeting("Android") 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zj/composemoon/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.zj.composemoon.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple200 = Color(0xFFBB86FC) 6 | val Purple500 = Color(0xFF6200EE) 7 | val Purple700 = Color(0xFF3700B3) 8 | val Teal200 = Color(0xFF03DAC5) -------------------------------------------------------------------------------- /app/src/main/java/com/zj/composemoon/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package com.zj.composemoon.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/zj/composemoon/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.zj.composemoon.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | 9 | private val DarkColorPalette = darkColors( 10 | primary = Purple200, 11 | primaryVariant = Purple700, 12 | secondary = Teal200 13 | ) 14 | 15 | private val LightColorPalette = lightColors( 16 | primary = Purple500, 17 | primaryVariant = Purple700, 18 | secondary = Teal200 19 | 20 | /* Other default colors to override 21 | background = Color.White, 22 | surface = Color.White, 23 | onPrimary = Color.White, 24 | onSecondary = Color.Black, 25 | onBackground = Color.Black, 26 | onSurface = Color.Black, 27 | */ 28 | ) 29 | 30 | @Composable 31 | fun ComposeMoonTheme( 32 | darkTheme: Boolean = isSystemInDarkTheme(), 33 | content: @Composable() () -> Unit 34 | ) { 35 | val colors = if (darkTheme) { 36 | DarkColorPalette 37 | } else { 38 | LightColorPalette 39 | } 40 | 41 | MaterialTheme( 42 | colors = colors, 43 | typography = Typography, 44 | shapes = Shapes, 45 | content = content 46 | ) 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zj/composemoon/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.zj.composemoon.ui.theme 2 | 3 | import androidx.compose.material.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp 15 | ) 16 | /* Other default text styles to override 17 | button = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.W500, 20 | fontSize = 14.sp 21 | ), 22 | caption = TextStyle( 23 | fontFamily = FontFamily.Default, 24 | fontWeight = FontWeight.Normal, 25 | fontSize = 12.sp 26 | ) 27 | */ 28 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/zj/composemoon/widget/Moon.kt: -------------------------------------------------------------------------------- 1 | package com.zj.composemoon.widget 2 | 3 | import android.graphics.BlurMaskFilter 4 | import androidx.compose.animation.core.* 5 | import androidx.compose.foundation.Canvas 6 | import androidx.compose.foundation.layout.BoxWithConstraints 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.material.MaterialTheme 9 | import androidx.compose.material.Text 10 | import androidx.compose.runtime.* 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.geometry.Offset 14 | import androidx.compose.ui.geometry.Rect 15 | import androidx.compose.ui.geometry.Size 16 | import androidx.compose.ui.graphics.* 17 | import androidx.compose.ui.graphics.drawscope.DrawScope 18 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 19 | import androidx.compose.ui.unit.dp 20 | 21 | @Composable 22 | fun Moon(modifier: Modifier) { 23 | var progress: Float by remember { mutableStateOf(0f) } 24 | 25 | val infiniteTransition = rememberInfiniteTransition() 26 | val duration = 10000 27 | val paint = Paint().apply { 28 | color = Color.Black 29 | isAntiAlias = true 30 | } 31 | progress = infiniteTransition.animateFloat( 32 | initialValue = 0f, 33 | targetValue = 2f, 34 | animationSpec = infiniteRepeatable( 35 | keyframes { 36 | durationMillis = duration 37 | delayMillis = 500 38 | 0.5f.at((duration * 0.25).toInt()) 39 | 0.5f.at((duration * 0.3).toInt()) 40 | 1f.at((duration * 0.4).toInt()) 41 | 1f.at((duration * 0.6).toInt()) 42 | 1.5f.at((duration * 0.75).toInt()) 43 | 1.5f.at((duration * 0.8).toInt()) 44 | 2.0f.at(duration) 45 | }, 46 | repeatMode = RepeatMode.Restart 47 | ), 48 | ).value 49 | BoxWithConstraints(modifier = modifier) { 50 | val canvasSize = minOf(maxWidth, maxHeight) - 40.dp 51 | Canvas( 52 | modifier = Modifier 53 | .size(canvasSize) 54 | .align(Alignment.TopCenter) 55 | ) { 56 | drawMoonCircle(this, progress) 57 | drawIntoCanvas { 58 | it.withSaveLayer(Rect(0f, 0f, size.width, size.height), paint = Paint()) { 59 | if (progress != 1f) { 60 | drawMoonArc(this, it, paint, progress) 61 | drawMoonOval(this, it, paint, progress) 62 | } 63 | } 64 | } 65 | } 66 | Text( 67 | text = getPhaseText(progress), 68 | color = Color(0xfff9dc60), 69 | modifier = Modifier.align(Alignment.BottomCenter), 70 | style = MaterialTheme.typography.h5 71 | ) 72 | } 73 | } 74 | 75 | private fun getPhaseText(progress: Float): String { 76 | return when { 77 | progress <= 0f -> "新月" 78 | progress < 0.5f -> "上蛾眉月" 79 | progress == 0.5f -> "上弦月" 80 | progress < 1.0f -> "渐盈凸月" 81 | progress == 1.0f -> "满月" 82 | progress < 1.45f -> "渐亏凸月" 83 | progress in 1.45f..1.55f -> "下弦月" 84 | progress < 2.0f -> "下蛾眉月" 85 | else -> "晦" 86 | } 87 | } 88 | 89 | private fun drawMoonCircle(scope: DrawScope, progress: Float) { 90 | scope.run { 91 | if (progress == 1f) { 92 | drawIntoCanvas { 93 | val blurRadius = 30f 94 | val moonPaint = Paint().asFrameworkPaint().apply { 95 | maskFilter = BlurMaskFilter(blurRadius, BlurMaskFilter.Blur.SOLID) 96 | color = android.graphics.Color.parseColor("#f9dc60") 97 | isAntiAlias = true 98 | } 99 | it.nativeCanvas.drawCircle(center.x, center.y, size.minDimension / 2.0f, moonPaint) 100 | } 101 | } else { 102 | drawCircle(Color(0xfff9dc60)) 103 | } 104 | } 105 | } 106 | 107 | private fun drawMoonOval(scope: DrawScope, canvas: Canvas, paint: Paint, progress: Float) { 108 | val blendMode = when { 109 | progress <= 0.5f -> BlendMode.DstOver 110 | progress <= 1f -> BlendMode.DstOut 111 | progress <= 1.5f -> BlendMode.DstOut 112 | else -> BlendMode.DstOver 113 | } 114 | paint.blendMode = blendMode 115 | scope.run { 116 | val moonRadius = size.minDimension / 2.0f 117 | val ovalHRadius = getOvalHRadius(moonRadius, progress) 118 | val topLeft = Offset(center.x - ovalHRadius, center.y - moonRadius) 119 | val horizontalAxis = ovalHRadius * 2 120 | val verticalAxis = moonRadius * 2 121 | canvas.drawOval( 122 | Rect(offset = topLeft, size = Size(horizontalAxis, verticalAxis)), 123 | paint = paint 124 | ) 125 | } 126 | } 127 | 128 | private fun drawMoonArc(scope: DrawScope, canvas: Canvas, paint: Paint, progress: Float) { 129 | val sweepAngle = when { 130 | progress <= 0.5f -> 180f 131 | progress <= 1f -> 180f 132 | progress <= 1.5f -> -180f 133 | else -> -180f 134 | } 135 | paint.blendMode = BlendMode.DstOver 136 | scope.run { 137 | canvas.drawArc(Rect(0f, 0f, size.width, size.height), 90f, sweepAngle, false, paint) 138 | } 139 | } 140 | 141 | //获取椭圆横轴半径 142 | private fun getOvalHRadius(radius: Float, progress: Float): Float { 143 | return when { 144 | progress <= 0.5f -> { 145 | radius * (0.5f - progress) * 2 146 | } 147 | progress <= 1 -> { 148 | radius * (progress - 0.5f) * 2 149 | } 150 | progress <= 1.5 -> { 151 | radius * (1.5f - progress) * 2 152 | } 153 | else -> { 154 | radius * (progress - 1.5f) * 2f 155 | } 156 | } 157 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zj/composemoon/widget/Poetry.kt: -------------------------------------------------------------------------------- 1 | package com.zj.composemoon.widget 2 | 3 | import android.graphics.Color 4 | import android.graphics.Typeface 5 | import androidx.compose.foundation.Canvas 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.wrapContentSize 8 | import androidx.compose.runtime.* 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Paint 11 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 12 | import androidx.compose.ui.graphics.nativeCanvas 13 | import kotlinx.coroutines.delay 14 | 15 | @Composable 16 | fun Poetry(modifier: Modifier) { 17 | Box(modifier = modifier) { 18 | val targetList = remember { mutableStateListOf>() } 19 | LaunchedEffect(true) { 20 | delay(1000) 21 | val text = "人有悲欢离合月有阴晴圆缺" 22 | val list1 = text.substring(0, 6).toList() 23 | targetList.add(list1) 24 | delay(3000) 25 | val list2 = text.substring(6, 12).toList() 26 | targetList.add(list2) 27 | } 28 | var xOffset = 0f 29 | var yOffset = 0f 30 | for (i in targetList.indices) { 31 | PoetryColumn(list = targetList[i], offsetX = xOffset, offsetY = yOffset) 32 | xOffset += 120f 33 | yOffset += 80f 34 | } 35 | } 36 | } 37 | 38 | @Composable 39 | fun PoetryColumn( 40 | list: List, 41 | offsetX: Float = 0f, 42 | offsetY: Float = 0f 43 | ) { 44 | val targetList = remember { mutableStateListOf() } 45 | LaunchedEffect(list) { 46 | targetList.clear() 47 | list.forEach { 48 | delay(500) 49 | targetList.add(it) 50 | } 51 | } 52 | //将 Jetpack Compose 环境的 Paint 对象转换为原生的 Paint 对象 53 | val textPaint = Paint().asFrameworkPaint().apply { 54 | isAntiAlias = true 55 | isDither = true 56 | typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD) 57 | textAlign = android.graphics.Paint.Align.CENTER 58 | } 59 | textPaint.color = Color.parseColor("#f9dc60") 60 | textPaint.textSize = 70f 61 | Canvas(modifier = Modifier.wrapContentSize()) { 62 | drawIntoCanvas { 63 | val x = 0 - offsetX 64 | var y = offsetY 65 | val delta = 100f 66 | for (i in targetList.indices) { 67 | it.nativeCanvas.drawText(list[i].toString(), x, y, textPaint) 68 | y += delta 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zj/composemoon/widget/StarrySky.kt: -------------------------------------------------------------------------------- 1 | package com.zj.composemoon.widget 2 | 3 | import androidx.compose.animation.core.* 4 | import androidx.compose.foundation.Canvas 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.BoxWithConstraints 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.wrapContentSize 9 | import androidx.compose.runtime.* 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.geometry.Offset 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 14 | import androidx.compose.ui.graphics.drawscope.scale 15 | import androidx.compose.ui.platform.LocalDensity 16 | import kotlinx.coroutines.delay 17 | 18 | @Composable 19 | fun StarrySky(modifier: Modifier, starNum: Int) { 20 | Box(modifier = modifier) { 21 | Canvas(modifier = Modifier.fillMaxSize()) { 22 | drawIntoCanvas { 23 | drawRect(Color.Black) 24 | } 25 | } 26 | Stars(starNum = starNum) 27 | } 28 | } 29 | 30 | @Composable 31 | fun Stars(starNum: Int) { 32 | BoxWithConstraints(modifier = Modifier.fillMaxSize()) { 33 | val density = LocalDensity.current.density 34 | val list = remember { mutableStateListOf() } 35 | LaunchedEffect(true) { 36 | for (i in 0..starNum) { 37 | delay(100L) 38 | list.add(Star(maxWidth.value * density, maxHeight.value * density)) 39 | } 40 | } 41 | list.forEach { 42 | Star(it) 43 | } 44 | } 45 | } 46 | 47 | @Composable 48 | fun Star(star: Star) { 49 | var progress: Float by remember { mutableStateOf(0f) } 50 | val infiniteTransition = rememberInfiniteTransition() 51 | progress = infiniteTransition.animateFloat( 52 | initialValue = 1f, 53 | targetValue = 1f, 54 | animationSpec = infiniteRepeatable( 55 | keyframes { 56 | durationMillis = 2000 57 | 0.25f.at(500) 58 | 1f.at(1000) 59 | 2f.at(1500) 60 | }, 61 | repeatMode = RepeatMode.Restart 62 | ), 63 | ).value 64 | star.updateStar(progress) 65 | Canvas(modifier = Modifier.wrapContentSize()) { 66 | scale(star.scale, Offset(star.x, star.y)) { 67 | drawCircle( 68 | star.starColor, 69 | star.radius, 70 | center = Offset(star.x, star.y), 71 | alpha = star.alpha 72 | ) 73 | } 74 | } 75 | } 76 | 77 | class Star( 78 | var maxWidth: Float, 79 | var maxHeight: Float 80 | ) { 81 | var x: Float = 0f 82 | private set 83 | var y: Float = 0f 84 | private set 85 | var radius: Float = 3f 86 | private set 87 | var starColor: Color = Color(0xfff9dc60) 88 | private set 89 | var alpha = 1f 90 | private set 91 | var scale = 1f 92 | private set 93 | 94 | init { 95 | randomInitStar() 96 | } 97 | 98 | fun updateStar(progress: Float) { 99 | alpha = if (progress > 1) 1f else progress 100 | scale = progress 101 | } 102 | 103 | private fun randomInitStar() { 104 | x = (0..maxWidth.toInt()).random().toFloat() 105 | y = (0..maxHeight.toInt()).random().toFloat() 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RicardoJiang/ComposeMoon/8a5f559d939e520e992bee48eebf61dfc4611be9/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RicardoJiang/ComposeMoon/8a5f559d939e520e992bee48eebf61dfc4611be9/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RicardoJiang/ComposeMoon/8a5f559d939e520e992bee48eebf61dfc4611be9/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RicardoJiang/ComposeMoon/8a5f559d939e520e992bee48eebf61dfc4611be9/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RicardoJiang/ComposeMoon/8a5f559d939e520e992bee48eebf61dfc4611be9/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RicardoJiang/ComposeMoon/8a5f559d939e520e992bee48eebf61dfc4611be9/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RicardoJiang/ComposeMoon/8a5f559d939e520e992bee48eebf61dfc4611be9/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RicardoJiang/ComposeMoon/8a5f559d939e520e992bee48eebf61dfc4611be9/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RicardoJiang/ComposeMoon/8a5f559d939e520e992bee48eebf61dfc4611be9/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RicardoJiang/ComposeMoon/8a5f559d939e520e992bee48eebf61dfc4611be9/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ComposeMoon 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 21 | 22 |