Skip to content

Commit 8c62c82

Browse files
committed
feat: Composable AlterableAdaptiveIcon
Signed-off-by: Hu Shenghao <dede.hu@qq.com>
1 parent 60a2afe commit 8c62c82

File tree

11 files changed

+377
-265
lines changed

11 files changed

+377
-265
lines changed

app/src/main/java/com/dede/android_eggs/views/main/compose/DrawableImage.kt

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,24 @@ package com.dede.android_eggs.views.main.compose
33
import android.graphics.drawable.Drawable
44
import androidx.annotation.DrawableRes
55
import androidx.compose.foundation.Image
6-
import androidx.compose.foundation.layout.Column
7-
import androidx.compose.foundation.layout.size
86
import androidx.compose.runtime.Composable
97
import androidx.compose.runtime.remember
108
import androidx.compose.ui.Alignment
119
import androidx.compose.ui.Modifier
1210
import androidx.compose.ui.layout.ContentScale
1311
import androidx.compose.ui.platform.LocalContext
14-
import androidx.compose.ui.res.stringArrayResource
1512
import androidx.compose.ui.tooling.preview.Preview
16-
import androidx.compose.ui.unit.dp
1713
import com.dede.android_eggs.R
18-
import com.dede.android_eggs.ui.drawables.AlterableAdaptiveIconDrawable
1914
import com.dede.basic.requireDrawable
2015
import com.google.accompanist.drawablepainter.rememberDrawablePainter
2116

2217
@Composable
23-
@Preview(showBackground = true)
18+
@Preview
2419
private fun PreviewImage() {
25-
val context = LocalContext.current
26-
Column(horizontalAlignment = Alignment.CenterHorizontally) {
27-
DrawableImage(res = R.mipmap.ic_launcher_round, contentDescription = null)
28-
29-
val maskPath = stringArrayResource(id = com.dede.android_eggs.settings.R.array.icon_shape_override_paths).last()
30-
val drawable = remember(context.theme, maskPath) {
31-
AlterableAdaptiveIconDrawable(context, R.mipmap.ic_launcher, maskPath)
32-
}
33-
DrawableImage(
34-
drawable = drawable, contentDescription = null,
35-
modifier = Modifier.size(56.dp)
36-
)
37-
}
20+
DrawableImage(
21+
res = R.mipmap.ic_launcher_round,
22+
contentDescription = null
23+
)
3824
}
3925

4026
@Composable

app/src/main/java/com/dede/android_eggs/views/settings/compose/prefs/IconShapePref.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ import androidx.compose.ui.unit.Constraints
2828
import androidx.compose.ui.unit.dp
2929
import androidx.core.os.bundleOf
3030
import com.dede.android_eggs.R
31+
import com.dede.android_eggs.alterable_adaptive_icon.PathShape
3132
import com.dede.android_eggs.ui.composes.icons.rounded.Shapes
3233
import com.dede.android_eggs.util.LocalEvent
33-
import com.dede.android_eggs.util.compose.PathShape
3434
import com.dede.android_eggs.views.settings.compose.basic.ExpandOptionsPref
3535
import com.dede.android_eggs.views.settings.compose.basic.SettingPrefUtil
3636
import com.dede.android_eggs.views.settings.compose.basic.rememberPrefIntState

app/src/main/java/com/dede/android_eggs/views/timeline/TimelineListDialog.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ import androidx.constraintlayout.compose.Dimension
5050
import androidx.hilt.navigation.compose.hiltViewModel
5151
import androidx.lifecycle.ViewModel
5252
import com.dede.android_eggs.R
53+
import com.dede.android_eggs.alterable_adaptive_icon.PathShape
5354
import com.dede.android_eggs.navigation.EasterEggsDestination
54-
import com.dede.android_eggs.util.compose.PathShape
5555
import com.dede.android_eggs.views.main.compose.EasterEggLogo
5656
import com.dede.android_eggs.views.main.util.AndroidLogoMatcher
5757
import com.dede.android_eggs.views.settings.compose.prefs.IconShapePrefUtil
@@ -218,9 +218,9 @@ private fun TimelineItem(
218218
top.linkTo(parent.top, 16.dp)
219219
centerHorizontallyTo(line)
220220
}
221-
val isAdaptiveIcon = remember { context.isAdaptiveIconDrawable(logoRes) }
221+
val isAdaptiveIcon = context.isAdaptiveIconDrawable(logoRes)
222222
if (!isAdaptiveIcon) {
223-
val iconShape = remember { IconShapePrefUtil.getMaskPath(context) }
223+
val iconShape = IconShapePrefUtil.getMaskPath(context)
224224
imageModifier = Modifier
225225
.background(
226226
colorScheme.secondaryContainer,

core/alterable-adaptive-icon/build.gradle.kts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
plugins {
2-
id("easter.eggs.library")
2+
id("easter.eggs.compose.library")
33
}
44

55
android {
@@ -9,4 +9,10 @@ android {
99
dependencies {
1010
implementation(project(":core:settings"))
1111
implementation(libs.androidx.core)
12+
implementation(platform(libs.androidx.compose.bom))
13+
implementation(libs.androidx.compose.runtime)
14+
implementation(libs.androidx.compose.ui)
15+
implementation(libs.google.accompanist.drawablepainter)
16+
implementation(libs.androidx.compose.ui.tooling.preview)
17+
debugImplementation(libs.androidx.compose.ui.tooling)
1218
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package com.dede.android_eggs.alterable_adaptive_icon
2+
3+
import android.content.Context
4+
import android.graphics.drawable.AdaptiveIconDrawable
5+
import android.graphics.drawable.Drawable
6+
import android.os.Build
7+
import androidx.annotation.DrawableRes
8+
import androidx.compose.foundation.Image
9+
import androidx.compose.foundation.background
10+
import androidx.compose.foundation.layout.Arrangement
11+
import androidx.compose.foundation.layout.Box
12+
import androidx.compose.foundation.layout.Column
13+
import androidx.compose.foundation.layout.aspectRatio
14+
import androidx.compose.foundation.layout.fillMaxSize
15+
import androidx.compose.foundation.layout.size
16+
import androidx.compose.foundation.layout.wrapContentSize
17+
import androidx.compose.runtime.Composable
18+
import androidx.compose.runtime.remember
19+
import androidx.compose.ui.Alignment
20+
import androidx.compose.ui.Modifier
21+
import androidx.compose.ui.draw.clip
22+
import androidx.compose.ui.draw.drawWithContent
23+
import androidx.compose.ui.graphics.Color
24+
import androidx.compose.ui.graphics.Matrix
25+
import androidx.compose.ui.graphics.Path
26+
import androidx.compose.ui.graphics.asComposePath
27+
import androidx.compose.ui.graphics.drawscope.withTransform
28+
import androidx.compose.ui.layout.layout
29+
import androidx.compose.ui.platform.LocalContext
30+
import androidx.compose.ui.tooling.preview.Preview
31+
import androidx.compose.ui.unit.Constraints
32+
import androidx.compose.ui.unit.dp
33+
import androidx.compose.ui.util.fastRoundToInt
34+
import com.dede.android_eggs.util.PathInflater
35+
import com.dede.android_eggs.views.settings.compose.prefs.IconShapePrefUtil
36+
import com.dede.basic.DefType
37+
import com.dede.basic.getIdentifier
38+
import com.dede.basic.requireDrawable
39+
import com.google.accompanist.drawablepainter.rememberDrawablePainter
40+
41+
42+
private const val ADAPTIVE_ICON_INSET_FACTOR = 1 / 4f
43+
private const val DEFAULT_CHILD_SCALE = 1 + 2 * ADAPTIVE_ICON_INSET_FACTOR
44+
45+
private const val BACKGROUND_ID = 0
46+
private const val FOREGROUND_ID = 1
47+
48+
private class ChildDrawable(
49+
val drawable: Drawable,
50+
val id: Int
51+
)
52+
53+
private fun Array<ChildDrawable>.getBackground(): Drawable {
54+
return checkNotNull(this.find { it.id == BACKGROUND_ID }).drawable
55+
}
56+
57+
private fun Array<ChildDrawable>.getForeground(): Drawable? {
58+
return this.find { it.id == FOREGROUND_ID }?.drawable
59+
}
60+
61+
private fun buildChildDrawableArray(context: Context, @DrawableRes res: Int): Array<ChildDrawable> {
62+
val drawable = context.requireDrawable(res)
63+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && drawable is AdaptiveIconDrawable) {
64+
arrayOf(
65+
ChildDrawable(drawable.background, BACKGROUND_ID),
66+
ChildDrawable(drawable.foreground, FOREGROUND_ID),
67+
)
68+
} else {
69+
arrayOf(
70+
ChildDrawable(drawable, BACKGROUND_ID),
71+
)
72+
}
73+
}
74+
75+
@Preview(showBackground = true)
76+
@Composable
77+
private fun PreviewAlterableAdaptiveIcon() {
78+
Column(
79+
modifier = Modifier
80+
.background(Color.White)
81+
.fillMaxSize(),
82+
horizontalAlignment = Alignment.CenterHorizontally,
83+
verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically),
84+
) {
85+
val context = LocalContext.current
86+
val id = context.getIdentifier("ic_launcher", DefType.MIPMAP)
87+
val maskPathStr = IconShapePrefUtil.getMaskPath(context)
88+
val composePath = PathInflater.inflate(maskPathStr).asComposePath()
89+
AlterableAdaptiveIcon(
90+
modifier = Modifier.size(100.dp),
91+
res = id,
92+
maskPath = composePath
93+
)
94+
95+
Image(
96+
painter = rememberDrawablePainter(context.requireDrawable(id)),
97+
contentDescription = null,
98+
modifier = Modifier.size(100.dp),
99+
)
100+
}
101+
}
102+
103+
private fun Modifier.adaptiveIconChild(scale: Float = DEFAULT_CHILD_SCALE) = this then Modifier
104+
.layout { measurable, constraints ->
105+
val width: Int = if (constraints.hasBoundedWidth) {
106+
(constraints.maxWidth * scale).fastRoundToInt()
107+
} else {
108+
constraints.maxWidth
109+
}
110+
val height: Int = if (constraints.hasBoundedHeight) {
111+
(constraints.maxHeight * scale).fastRoundToInt()
112+
} else {
113+
constraints.maxHeight
114+
}
115+
116+
val placeable = measurable.measure(Constraints(width, width, height, height))
117+
layout(width, height) {
118+
placeable.placeRelative(0, 0)
119+
}
120+
}
121+
122+
@Composable
123+
fun AlterableAdaptiveIcon(
124+
modifier: Modifier = Modifier,
125+
maskPath: Path,
126+
@DrawableRes res: Int,
127+
foregroundMatrix: Matrix = Matrix(),
128+
) {
129+
val context = LocalContext.current
130+
val childDrawables = remember(res, context.theme) {
131+
buildChildDrawableArray(context, res)
132+
}
133+
val clipShape = remember(maskPath) {
134+
PathShape(maskPath)
135+
}
136+
val isAdaptiveIcon = childDrawables.size >= 2
137+
Box(
138+
modifier = Modifier
139+
.then(modifier)
140+
.clip(clipShape)
141+
.aspectRatio(1f),
142+
contentAlignment = Alignment.Center,
143+
) {
144+
val background: Drawable = childDrawables.getBackground()
145+
if (!isAdaptiveIcon) {
146+
Image(
147+
painter = rememberDrawablePainter(background),
148+
modifier = Modifier.wrapContentSize(),
149+
contentDescription = null,
150+
)
151+
return@Box
152+
}
153+
154+
Image(
155+
painter = rememberDrawablePainter(background),
156+
modifier = Modifier.adaptiveIconChild(),
157+
contentDescription = null,
158+
)
159+
160+
val foreground: Drawable? = childDrawables.getForeground()
161+
if (foreground != null) {
162+
Image(
163+
painter = rememberDrawablePainter(foreground),
164+
modifier = Modifier
165+
.drawWithContent {
166+
withTransform({
167+
transform(foregroundMatrix)
168+
}) {
169+
this@drawWithContent.drawContent()
170+
}
171+
}
172+
.adaptiveIconChild(),
173+
contentDescription = null,
174+
)
175+
}
176+
}
177+
}

app/src/main/java/com/dede/android_eggs/util/compose/PathShape.kt renamed to core/alterable-adaptive-icon/src/main/java/com/dede/android_eggs/alterable_adaptive_icon/PathShape.kt

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.dede.android_eggs.util.compose
1+
package com.dede.android_eggs.alterable_adaptive_icon
22

33
import androidx.compose.ui.geometry.Size
44
import androidx.compose.ui.graphics.Matrix
@@ -9,10 +9,21 @@ import androidx.compose.ui.graphics.asComposePath
99
import androidx.compose.ui.unit.Density
1010
import androidx.compose.ui.unit.LayoutDirection
1111
import com.dede.android_eggs.util.PathInflater
12+
import android.graphics.Path as AndroidPath
1213

13-
class PathShape(pathData: String, private val pathSize: Float = 100f) : Shape {
14+
internal const val DEFAULT_PATH_SIZE = 100f
15+
16+
class PathShape(
17+
private val path: Path,
18+
private val pathSize: Float = DEFAULT_PATH_SIZE
19+
) : Shape {
20+
21+
constructor(androidPath: AndroidPath, pathSize: Float = DEFAULT_PATH_SIZE) :
22+
this(androidPath.asComposePath(), pathSize)
23+
24+
constructor(pathData: String, pathSize: Float = DEFAULT_PATH_SIZE) :
25+
this(PathInflater.inflate(pathData), pathSize)
1426

15-
private val shapePath = PathInflater.inflate(pathData).asComposePath()
1627
private val matrix = Matrix()
1728

1829
override fun createOutline(
@@ -21,12 +32,12 @@ class PathShape(pathData: String, private val pathSize: Float = 100f) : Shape {
2132
density: Density
2233
): Outline {
2334
val path = Path().apply {
24-
addPath(shapePath)
35+
addPath(path)
2536
matrix.reset()
2637
matrix.scale(size.width / pathSize, size.height / pathSize)
2738
transform(matrix)
2839
close()
2940
}
3041
return Outline.Generic(path)
3142
}
32-
}
43+
}

0 commit comments

Comments
 (0)