Refactor icons

This commit is contained in:
MM20
2022-06-18 18:51:41 +02:00
parent 08e736f5d1
commit 6de89f3f79
54 changed files with 894 additions and 1489 deletions
@@ -1,14 +1,11 @@
package de.mm20.launcher2.ui.component
import android.graphics.*
import android.graphics.Matrix
import android.graphics.Path
import android.graphics.RectF
import android.graphics.drawable.AdaptiveIconDrawable
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.combinedClickable
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@@ -25,15 +22,26 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.drawWithColorFilter
import de.mm20.launcher2.preferences.Settings.IconSettings.IconShape
import de.mm20.launcher2.ui.base.LocalTime
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import palettes.TonalPalette
import java.time.Instant
import java.time.ZoneId
import kotlin.math.pow
import kotlin.math.roundToInt
@@ -48,6 +56,25 @@ fun ShapedLauncherIcon(
onLongClick: (() -> Unit)? = null,
shape: Shape = LocalIconShape.current
) {
val time = LocalTime.current
var currentIcon by remember(icon) {
mutableStateOf(
when (icon) {
is DynamicLauncherIcon -> null
is StaticLauncherIcon -> icon
null -> null
}
)
}
if (icon is DynamicLauncherIcon) {
LaunchedEffect(time) {
currentIcon = icon.getIcon(time)
}
}
Box(
modifier = modifier
.size(size)
@@ -56,7 +83,7 @@ fun ShapedLauncherIcon(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
clip = true
clip = currentIcon?.backgroundLayer !is TransparentLayer
this.shape = shape
}
.combinedClickable(
@@ -68,45 +95,19 @@ fun ShapedLauncherIcon(
),
contentAlignment = Alignment.Center
) {
if (icon != null) {
val fgScale = icon.foregroundScale
val bgScale = icon.backgroundScale
val themedFgColor = MaterialTheme.colorScheme.onPrimaryContainer
val themedBgColor = MaterialTheme.colorScheme.primaryContainer
val fg = remember(icon, icon.isThemeable, themedFgColor) {
icon.foreground.also {
if (icon.isThemeable) it.setTint(themedFgColor.toArgb())
}
}
val bg = remember(icon, icon.isThemeable, themedBgColor) {
icon.background?.also {
if (icon.isThemeable) it.setTint(themedBgColor.toArgb())
}
}
Canvas(modifier = Modifier.fillMaxSize()) {
drawIntoCanvas {
val paddingFg = (size * (1 - fgScale) * 0.5f).toPx()
val paddingBg = (size * (1 - bgScale) * 0.5f).toPx()
bg?.setBounds(
paddingBg.toInt(),
paddingBg.toInt(),
(this.size.width - paddingBg).toInt(),
(this.size.height - paddingBg).toInt()
)
bg?.draw(it.nativeCanvas)
fg.setBounds(
paddingFg.toInt(),
paddingFg.toInt(),
(this.size.width - paddingFg).toInt(),
(this.size.height - paddingFg).toInt()
)
fg.draw(it.nativeCanvas)
}
}
currentIcon?.let {
IconLayer(
it.backgroundLayer,
size,
colorTone = if (!LocalDarkTheme.current) 30 else 90,
MaterialTheme.colorScheme.primaryContainer
)
IconLayer(
it.foregroundLayer,
size,
colorTone = if (!LocalDarkTheme.current) 90 else 10,
MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
if (badge != null) {
@@ -138,12 +139,18 @@ fun ShapedLauncherIcon(
val number = badge.number
if (badgeIconRes != null) {
Image(
modifier = Modifier.fillMaxSize().padding(size / 48),
modifier = Modifier
.fillMaxSize()
.padding(size / 48),
painter = painterResource(badgeIconRes),
contentDescription = null
)
} else if (badgeIcon != null) {
Canvas(modifier = Modifier.fillMaxSize().padding(size / 48)) {
Canvas(
modifier = Modifier
.fillMaxSize()
.padding(size / 48)
) {
badgeIcon.setBounds(
0,
0,
@@ -171,6 +178,172 @@ fun ShapedLauncherIcon(
}
}
@Composable
private fun IconLayer(
layer: LauncherIconLayer,
size: Dp,
colorTone: Int,
defaultTintColor: Color
) {
when (layer) {
is ClockLayer -> {
ClockLayer(layer.sublayers, scale = layer.scale, tintColor = null)
}
is TintedClockLayer -> {
ClockLayer(
layer.sublayers,
scale = layer.scale,
tintColor = if (layer.color == 0) defaultTintColor
else Color(getTone(layer.color, colorTone))
)
}
is ColorLayer -> {
Box(
modifier = Modifier
.fillMaxSize()
.background(
if (layer.color == 0) {
defaultTintColor
} else {
Color(getTone(layer.color, colorTone))
}
)
)
}
is StaticIconLayer -> {
Canvas(modifier = Modifier.fillMaxSize()) {
withTransform({
this.scale(layer.scale)
}) {
drawIntoCanvas {
layer.icon.bounds = this.size.toRect().toAndroidRect()
layer.icon.draw(it.nativeCanvas)
}
}
}
}
is TextLayer -> {
Text(
text = layer.text,
style = MaterialTheme.typography.headlineSmall.copy(
fontSize = 20.sp * (size / 48.dp)
),
color = if (layer.color == 0) {
defaultTintColor
} else {
Color(getTone(layer.color, colorTone))
},
)
}
is TintedIconLayer -> {
val color =
if (layer.color == 0) defaultTintColor.toArgb()
else getTone(layer.color, colorTone)
Canvas(modifier = Modifier.fillMaxSize()) {
withTransform({
this.scale(layer.scale)
}) {
drawIntoCanvas {
layer.icon.bounds = this.size.toRect().toAndroidRect()
layer.icon.drawWithColorFilter(
it.nativeCanvas,
PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
)
}
}
}
}
is TransparentLayer -> {}
}
}
private fun getTone(argb: Int, tone: Int): Int {
return TonalPalette
.fromInt(argb)
.tone(tone)
}
@Composable
private fun ClockLayer(
sublayers: List<ClockSublayer>,
scale: Float,
tintColor: Color?,
) {
val time = remember {
Instant.ofEpochMilli(System.currentTimeMillis()).atZone(ZoneId.systemDefault())
}
val transition = rememberInfiniteTransition()
val minute by transition.animateFloat(
initialValue = 0f,
targetValue = 60f,
animationSpec = InfiniteRepeatableSpec(
animation = tween(durationMillis = 60 * 60 * 1000, easing = LinearEasing),
initialStartOffset = StartOffset(
offsetMillis = time.minute * 60 * 1000 + time.second * 1000,
offsetType = StartOffsetType.FastForward
)
)
)
val hour by transition.animateFloat(
initialValue = 0f,
targetValue = 12f,
animationSpec = InfiniteRepeatableSpec(
animation = tween(durationMillis = 12 * 60 * 60 * 1000, easing = LinearEasing),
initialStartOffset = StartOffset(
offsetMillis = (time.hour % 12) * 60 * 60 * 1000 + time.minute * 60 * 1000 + time.second * 1000,
offsetType = StartOffsetType.FastForward
)
)
)
val second by transition.animateFloat(
initialValue = 0f,
targetValue = 60f,
animationSpec = InfiniteRepeatableSpec(
animation = tween(durationMillis = 60000, easing = LinearEasing),
initialStartOffset = StartOffset(
offsetMillis = time.second * 1000,
offsetType = StartOffsetType.FastForward
)
)
)
Canvas(modifier = Modifier.fillMaxSize()) {
val colorFilter = tintColor?.let {
PorterDuffColorFilter(tintColor.toArgb(), PorterDuff.Mode.SRC_IN)
}
withTransform({
this.scale(scale)
}) {
for (sublayer in sublayers) {
withTransform({
when (sublayer.role) {
ClockSublayerRole.Hour -> {
rotate(hour / 12f * 360f)
}
ClockSublayerRole.Minute -> {
rotate(minute / 60f * 360f)
}
ClockSublayerRole.Second -> {
rotate(second / 60f * 360f)
}
ClockSublayerRole.Static -> {}
}
}) {
drawIntoCanvas {
sublayer.drawable.bounds = this.size.toRect().toAndroidRect()
sublayer.drawable.drawWithColorFilter(it.nativeCanvas, colorFilter)
}
}
}
}
}
}
val LocalIconShape = compositionLocalOf<Shape> { CircleShape }
fun getShape(iconShape: IconShape): Shape {
@@ -1,145 +0,0 @@
package de.mm20.launcher2.ui.icons
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.colorResource
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.*
data class PlaceholderIcon(
val color: Color,
val icon: ImageVector
)
@Composable
fun Searchable.getPlaceholderIcon(): PlaceholderIcon {
return when (this) {
is Application -> getPlaceholderIcon()
is File -> getPlaceholderIcon()
else -> PlaceholderIcon(
Color.LightGray,
Icons.Rounded.Circle
)
}
}
@Composable
fun Application.getPlaceholderIcon(): PlaceholderIcon {
return PlaceholderIcon(
colorResource(id = R.color.android_green),
Icons.Rounded.Android
)
}
@Composable
fun File.getPlaceholderIcon(): PlaceholderIcon {
return when {
isDirectory -> PlaceholderIcon(
colorResource(id = R.color.lightblue),
Icons.Rounded.Folder
)
mimeType.startsWith("image/") -> PlaceholderIcon(
colorResource(id = R.color.teal),
Icons.Rounded.Image
)
mimeType.startsWith("audio/") -> PlaceholderIcon(
colorResource(id = R.color.orange),
Icons.Rounded.Audiotrack
)
mimeType.startsWith("video/") -> PlaceholderIcon(
colorResource(id = R.color.purple),
Icons.Rounded.Movie
)
/*
else -> when (mimeType) {
"application/vnd.google-apps.drawing" -> R.drawable.ic_file_picture to R.color.teal
}*/
else -> when (mimeType) {
"application/pdf" -> PlaceholderIcon(
colorResource(id = R.color.red),
Icons.Rounded.Pdf
)
"application/zip",
"application/x-gtar",
"application/x-tar",
"application/java-archive",
"application/x-7z-compressed",
"application/x-compressed-tar",
"application/x-zip-compressed",
"application/x-gzip",
"application/x-bzip2" -> PlaceholderIcon(
colorResource(id = R.color.brown),
Icons.Rounded.Archive
)
"application/vnd.oasis.opendocument.text",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/msword",
"text/plain",
"application/x-iwork-pages-sffpages",
"application/vnd.apple.pages",
"application/vnd.google-apps.document" -> PlaceholderIcon(
colorResource(id = R.color.blue),
Icons.Rounded.Notes
)
"application/vnd.oasis.opendocument.spreadsheet",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-excel",
"application/x-iwork-numbers-sffnumbers",
"application/vnd.apple.numbers",
"application/vnd.google-apps.spreadsheet" -> PlaceholderIcon(
colorResource(id = R.color.lightgreen),
Icons.Rounded.BorderAll
)
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.ms-powerpoint",
"application/x-iwork-keynote-sffkey",
"application/vnd.apple.keynote",
"application/vnd.google-apps.presentation" -> PlaceholderIcon(
colorResource(id = R.color.amber),
Icons.Rounded.Slideshow
)
"application/vnd.android.package-archive" -> PlaceholderIcon(
colorResource(id = R.color.android_green),
Icons.Rounded.Android
)
"text/x-asm",
"text/x-c",
"text/x-java-source",
"text/x-script.phyton",
"text/x-pascal",
"text/x-script.perl",
"text/javascript",
"application/json" -> PlaceholderIcon(
colorResource(id = R.color.pink),
Icons.Rounded.Code
)
"text/xml",
"text/html" -> PlaceholderIcon(
colorResource(id = R.color.deeporange),
Icons.Rounded.Code
)
"application/vnd.google-apps.form" -> PlaceholderIcon(
colorResource(id = R.color.deeppurple),
Icons.Rounded.ViewList
)
"application/epub+zip" -> PlaceholderIcon(
colorResource(id = R.color.blue),
Icons.Rounded.Book
)
else -> PlaceholderIcon(
colorResource(id = R.color.bluegrey),
Icons.Rounded.InsertDriveFile
)
}
}
}
@@ -31,7 +31,6 @@ import com.afollestad.materialdialogs.callbacks.onDismiss
import com.afollestad.materialdialogs.customview.customView
import com.android.launcher3.GestureNavContract
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.icons.DynamicIconController
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.base.BaseActivity
@@ -45,7 +44,6 @@ import de.mm20.launcher2.ui.launcher.transitions.LocalHomeTransitionManager
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
import de.mm20.launcher2.ui.locals.LocalWindowSize
import de.mm20.launcher2.ui.theme.LauncherTheme
import org.koin.android.ext.android.inject
class LauncherActivity : BaseActivity() {
@@ -174,10 +172,6 @@ class LauncherActivity : BaseActivity() {
editFavoritesDialog = null
}
}
val dynamicIconController: DynamicIconController by inject()
lifecycle.addObserver(dynamicIconController)
}
override fun onAttachedToWindow() {
@@ -1,31 +1,10 @@
package de.mm20.launcher2.ui.legacy.view
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.*
import android.graphics.drawable.AdaptiveIconDrawable
import android.util.AttributeSet
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import androidx.core.content.ContextCompat
import com.bartoszlipinski.viewpropertyobjectanimator.ViewPropertyObjectAnimator
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.toRectF
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.IconSettings.IconShape
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.helper.BitmapHolder
import kotlinx.coroutines.flow.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import java.lang.Math.pow
import kotlin.math.abs
import kotlin.math.hypot
import kotlin.math.roundToInt
class LauncherIconView : View, KoinComponent {
constructor(context: Context) : super(context)
@@ -36,499 +15,10 @@ class LauncherIconView : View, KoinComponent {
defStyleRes
)
var shape: IconShape
set(value) {
if (value == IconShape.PlatformDefault) {
platformShape = getSystemShape()
transformMatrix = Matrix()
platformShapeBounds = RectF()
field = value
} else {
platformShape = null
transformMatrix = null
platformShapeBounds = null
field = value
}
}
private var platformShape: Path? = null
private var transformMatrix: Matrix? = null
private var platformShapeBounds: RectF? = null
private fun getSystemShape(): Path {
return AdaptiveIconDrawable(null, null).iconMask
}
var icon: LauncherIcon? = null
set(value) {
field = value
foregroundScale = value?.foregroundScale ?: 1f
backgroundScale = value?.backgroundScale ?: 1f
value?.registerCallback(iconObserver)
invalidate()
}
private val iconObserver: (LauncherIcon) -> Unit = {
foregroundScale = it.foregroundScale
backgroundScale = it.backgroundScale
// Implicit invalidate
}
var foregroundScale = 1f
set(value) {
field = value
postInvalidate()
}
var backgroundScale = 1f
set(value) {
field = value
postInvalidate()
}
init {
shape = IconShape.Circle
setLayerType(LAYER_TYPE_SOFTWARE, null)
}
override fun setElevation(elevation: Float) {
super.setElevation(elevation)
shadowPaint = updateShadowPaint()
badgeShadowPaint = updateBadgeShadowPaing()
}
override fun setTranslationZ(translationZ: Float) {
super.setTranslationZ(translationZ)
shadowPaint = updateShadowPaint()
badgeShadowPaint = updateBadgeShadowPaing()
}
private var shadowPaint: Paint = updateShadowPaint()
private var badgeShadowPaint = updateBadgeShadowPaing()
private fun updateShadowPaint(): Paint {
return Paint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OVER)
color = Color.TRANSPARENT
isAntiAlias = true
setShadowLayer(0.5f * z, 0f, 0.5f * z, 0x40000000)
}
}
private fun updateBadgeShadowPaing(): Paint {
return Paint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)
color = Color.TRANSPARENT
isAntiAlias = true
setShadowLayer(0.5f * z, 0f, 0.5f * z, 0x40000000)
}
}
private val drawRect = Rect()
private val bmpDrawRect = Rect()
private val maskPaint = Paint().apply {
style = Paint.Style.FILL
color = 0xFF000000.toInt()
isAntiAlias = true
}
private val bitmapPaint = Paint().apply {
color = 0xFF000000.toInt()
xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
isAntiAlias = true
isFilterBitmap = true
}
private val badgeTextPaint = Paint().apply {
color = ContextCompat.getColor(context, R.color.badge_text)
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
private val badgeProgressPaint = Paint().apply {
color = 0x30000000
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
private var path: Path = Path()
private val badgeRect = RectF()
private val textBounds = Rect()
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val fg = icon?.foreground ?: return
val bg = icon?.background
canvas.getClipBounds(drawRect)
drawRect.left += paddingLeft
drawRect.top += paddingTop
drawRect.right -= paddingRight
drawRect.bottom -= paddingBottom
val (bmp, c) = BitmapHolder.getBitmapAndCanvas((drawRect.width() * 1.8).toInt())
c.getClipBounds(bmpDrawRect)
fg.bounds = bmpDrawRect
if (bg != null) {
bg.bounds = bmpDrawRect
when (shape) {
IconShape.PlatformDefault -> {
path.rewind()
val matrix = transformMatrix!!
val bounds = platformShapeBounds!!
val shape = platformShape!!
shape.computeBounds(bounds, true)
matrix.setRectToRect(
bounds,
badgeRect.also { drawRect.toRectF(it) },
Matrix.ScaleToFit.CENTER
)
path.rewind()
shape.transform(matrix, path)
canvas.drawPath(path, maskPaint)
}
IconShape.Circle -> {
canvas.drawOval(
drawRect.left.toFloat(),
drawRect.top.toFloat(),
drawRect.right.toFloat(),
drawRect.bottom.toFloat(),
maskPaint
)
}
IconShape.Square -> {
canvas.drawRect(drawRect, maskPaint)
}
IconShape.RoundedSquare -> {
canvas.drawRoundRect(
drawRect.left.toFloat(),
drawRect.top.toFloat(),
drawRect.right.toFloat(),
drawRect.bottom.toFloat(),
width * 0.125f,
height * 0.125f,
maskPaint
)
}
IconShape.Triangle -> {
path.rewind()
var cx = drawRect.left.toFloat()
var cy = drawRect.top + drawRect.height().toFloat() * 0.86f
val r = drawRect.width()
path.moveTo(cx, cy)
path.arcTo(cx - r, cy - r, cx + r, cy + r, 300f, 60f, true)
canvas.drawArc(cx - r, cy - r, cx + r, cy + r, 300f, 60f, true, maskPaint)
cx = drawRect.right.toFloat()
cy = drawRect.top + drawRect.height().toFloat() * 0.86f
path.lineTo(cx, cy)
path.arcTo(cx - r, cy - r, cx + r, cy + r, 180f, 60f, true)
canvas.drawArc(cx - r, cy - r, cx + r, cy + r, 180f, 60f, true, maskPaint)
cx = drawRect.left + drawRect.width() * 0.5f
cy = drawRect.top.toFloat()
path.lineTo(cx, cy)
path.close()
path.arcTo(cx - r, cy - r, cx + r, cy + r, 60f, 60f, true)
canvas.drawArc(cx - r, cy - r, cx + r, cy + r, 60f, 60f, true, maskPaint)
}
IconShape.Squircle -> {
path.rewind()
val radius = drawRect.width() / 2
val radiusToPow = pow(radius.toDouble(), 3.0)
path.moveTo(-radius.toFloat(), 0f)
for (x in -radius..radius)
path.lineTo(
x.toFloat(),
Math.cbrt(radiusToPow - Math.abs(x * x * x)).toFloat()
)
for (x in radius downTo -radius)
path.lineTo(
x.toFloat(),
(-Math.cbrt(radiusToPow - Math.abs(x * x * x))).toFloat()
)
path.close()
canvas.save()
canvas.translate(width / 2f, height / 2f)
canvas.drawPath(path, maskPaint)
canvas.restore()
}
IconShape.Hexagon -> {
path.rewind()
path.moveTo(
drawRect.left + drawRect.width() * 0.25f,
drawRect.top + drawRect.height() * 0.933f
)
path.lineTo(
drawRect.left + drawRect.width() * 0.75f,
drawRect.top + drawRect.height() * 0.933f
)
path.lineTo(
drawRect.left + drawRect.width() * 1.0f,
drawRect.top + drawRect.height() * 0.5f
)
path.lineTo(
drawRect.left + drawRect.width() * 0.75f,
drawRect.top + drawRect.height() * 0.067f
)
path.lineTo(
drawRect.left + drawRect.width() * 0.25f,
drawRect.top + drawRect.height() * 0.067f
)
path.lineTo(drawRect.left.toFloat(), drawRect.top + drawRect.height() * 0.5f)
path.close()
canvas.drawPath(path, maskPaint)
}
IconShape.EasterEgg -> {
path.rewind()
path.moveTo(
0.49999999f * drawRect.width() + drawRect.left,
1f * drawRect.height() + drawRect.top
)
path.lineTo(
0.42749999f * drawRect.width() + drawRect.left,
0.9339999999999999f * drawRect.height() + drawRect.top
)
path.cubicTo(
0.16999998f * drawRect.width() + drawRect.left,
0.7005004f * drawRect.height() + drawRect.top,
0f + drawRect.left,
0.5460004f * drawRect.height() + drawRect.top,
0f + drawRect.left,
0.3575003f * drawRect.height() + drawRect.top
)
path.cubicTo(
0f + drawRect.left,
0.2030004f * drawRect.height() + drawRect.top,
0.12100002f * drawRect.width() + drawRect.left,
0.0825004f * drawRect.height() + drawRect.top,
0.275f * drawRect.width() + drawRect.left,
0.0825004f * drawRect.height() + drawRect.top
)
path.cubicTo(
0.362f * drawRect.width() + drawRect.left,
0.0825004f * drawRect.height() + drawRect.top,
0.4455f * drawRect.width() + drawRect.left,
0.123f * drawRect.height() + drawRect.top,
0.5f * drawRect.width() + drawRect.left,
0.1865003f * drawRect.height() + drawRect.top
)
path.cubicTo(
0.55449999f * drawRect.width() + drawRect.left,
0.123f * drawRect.height() + drawRect.top,
0.638f * drawRect.width() + drawRect.left,
0.0825f * drawRect.height() + drawRect.top,
0.725f * drawRect.width() + drawRect.left,
0.0825f * drawRect.height() + drawRect.top
)
path.cubicTo(
0.87900006f * drawRect.width() + drawRect.left,
0.0825004f * drawRect.height() + drawRect.top,
1f * drawRect.width() + drawRect.left,
0.2030004f * drawRect.height() + drawRect.top,
1f * drawRect.width() + drawRect.left,
0.3575003f * drawRect.height() + drawRect.top
)
path.cubicTo(
1f * drawRect.width() + drawRect.left,
0.5460004f * drawRect.height() + drawRect.top,
0.82999999f * drawRect.width() + drawRect.left,
0.7005004f * drawRect.height() + drawRect.top,
0.57250001f * drawRect.width() + drawRect.left,
0.9340004f * drawRect.height() + drawRect.top
)
path.close()
canvas.drawPath(path, maskPaint)
}
IconShape.Pentagon -> {
path.rewind()
path.moveTo(
0.49997027f * drawRect.width() + drawRect.left,
0.0060308f * drawRect.height() + drawRect.top
)
path.lineTo(
0.99994053f * drawRect.width() + drawRect.left,
0.36928048f * drawRect.height() + drawRect.top
)
path.lineTo(
0.80896887f * drawRect.width() + drawRect.left,
0.95703078f * drawRect.height() + drawRect.top
)
path.lineTo(
0.19097162f * drawRect.width() + drawRect.left,
0.95703076f * drawRect.height() + drawRect.top
)
path.lineTo(
drawRect.left.toFloat(),
0.36928045f * drawRect.height() + drawRect.top
)
path.close()
canvas.drawPath(path, maskPaint)
}
}
c.save()
c.scale(
backgroundScale,
backgroundScale,
bmpDrawRect.centerX().toFloat(),
bmpDrawRect.centerY().toFloat()
)
bg.draw(c)
c.restore()
}
c.save()
c.scale(
foregroundScale,
foregroundScale,
bmpDrawRect.centerX().toFloat(),
bmpDrawRect.centerY().toFloat()
)
fg.draw(c)
c.restore()
if (bg != null) {
canvas.drawBitmap(bmp, bmpDrawRect, drawRect, bitmapPaint)
} else {
canvas.drawBitmap(bmp, bmpDrawRect, drawRect, maskPaint)
}
if (bg != null) {
when (shape) {
IconShape.Circle -> {
canvas.drawOval(
drawRect.left.toFloat(),
drawRect.top.toFloat(),
drawRect.right.toFloat(),
drawRect.bottom.toFloat(),
shadowPaint
)
}
IconShape.Square -> {
canvas.drawRect(drawRect, shadowPaint)
}
IconShape.RoundedSquare -> {
canvas.drawRoundRect(
drawRect.left.toFloat(),
drawRect.top.toFloat(),
drawRect.right.toFloat(),
drawRect.bottom.toFloat(),
width * 0.125f,
height * 0.125f,
shadowPaint
)
}
IconShape.Triangle, IconShape.Hexagon, IconShape.EasterEgg, IconShape.Pentagon, IconShape.PlatformDefault -> {
canvas.drawPath(path, shadowPaint)
}
IconShape.Squircle -> {
canvas.save()
canvas.translate(width / 2f, height / 2f)
canvas.drawPath(path, shadowPaint)
canvas.restore()
}
}
}
}
private var longClicked = false
private val longClickRunnable = Runnable {
longClicked = true
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
performLongClick()
}
private var downX = 0f
private var downY = 0f
override fun onTouchEvent(ev: MotionEvent): Boolean {
if (!hasOnClickListeners()) return false
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
animateTouchDown()
downX = ev.rawX
downY = ev.rawY
longClicked = false
handler?.postDelayed(
longClickRunnable,
ViewConfiguration.getLongPressTimeout().toLong()
)
return true
}
MotionEvent.ACTION_MOVE -> {
if (abs(hypot(downX - ev.rawX, downY - ev.rawY)) > width * 0.25f) {
handler?.removeCallbacks(longClickRunnable)
animateTouchUp()
return false
}
}
MotionEvent.ACTION_UP -> {
animateTouchUp()
if (ev.x > 0 && ev.x < width && ev.y > 0 && ev.y < height && !longClicked) {
performClick()
}
handler?.removeCallbacks(longClickRunnable)
return false
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_OUTSIDE -> {
animateTouchUp()
handler?.removeCallbacks(longClickRunnable)
return false
}
}
return true
}
private fun animateTouchUp() {
AnimatorSet().also {
it.playTogether(
ViewPropertyObjectAnimator.animate(this).translationZ(0f).get(),
ObjectAnimator.ofFloat(this, "foregroundScale", icon?.foregroundScale ?: 1f),
ObjectAnimator.ofFloat(this, "backgroundScale", icon?.backgroundScale ?: 1f)
)
it.duration = 300
it.start()
}
}
private fun animateTouchDown() {
AnimatorSet().also {
it.playTogether(
ViewPropertyObjectAnimator.animate(this).translationZ(2 * dp).get(),
ObjectAnimator.ofFloat(
this, "foregroundScale", (icon?.foregroundScale
?: 1f) * 0.8f
),
ObjectAnimator.ofFloat(
this, "backgroundScale", (icon?.backgroundScale
?: 1f) * 1.2f
)
)
it.duration = 250
it.start()
}
}
companion object: KoinComponent {
var currentShape: IconShape = IconShape.PlatformDefault
fun getDefaultShape(): Flow<IconShape> = channelFlow {
send(currentShape)
val dataStore: LauncherDataStore = get()
dataStore.data.map { it.icons.shape }.distinctUntilChanged().collectLatest { shape ->
dataStore.data.map { it.easterEgg }.distinctUntilChanged().collectLatest { ee ->
if (ee) {
currentShape = IconShape.EasterEgg
send(IconShape.EasterEgg)
} else {
currentShape = shape
send(shape)
}
}
}
}
}
}
@@ -1,17 +1,12 @@
package de.mm20.launcher2.ui.settings.appearance
import android.graphics.drawable.ColorDrawable
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
@@ -24,6 +19,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.core.content.ContextCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import com.airbnb.lottie.LottieProperty
import com.airbnb.lottie.compose.*
@@ -31,7 +27,9 @@ import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.HorizontalPagerIndicator
import com.google.accompanist.pager.rememberPagerState
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticIconLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.preferences.Settings.*
import de.mm20.launcher2.preferences.Settings.AppearanceSettings.ColorScheme
import de.mm20.launcher2.preferences.Settings.AppearanceSettings.Theme
@@ -58,8 +56,8 @@ fun AppearanceSettingsScreen() {
title = stringResource(id = R.string.preference_layout),
summary = stringResource(id = R.string.preference_layout_summary),
value = layout, onValueChanged = {
viewModel.setLayout(it)
})
viewModel.setLayout(it)
})
val theme by viewModel.theme.observeAsState()
ListPreference(
title = stringResource(id = R.string.preference_theme),
@@ -147,6 +145,15 @@ fun AppearanceSettingsScreen() {
viewModel.setIconShape(it)
}
)
val adaptifyLegacyIcons by viewModel.adaptifyLegacyIcons.observeAsState()
SwitchPreference(
title = stringResource(R.string.preference_enforce_icon_shape),
summary = stringResource(R.string.preference_enforce_icon_shape_summary),
value = adaptifyLegacyIcons == true,
onValueChanged = {
viewModel.setAdaptifyLegacyIcons(it)
}
)
val themedIcons by viewModel.themedIcons.observeAsState()
SwitchPreference(
title = stringResource(R.string.preference_themed_icons),
@@ -176,17 +183,6 @@ fun AppearanceSettingsScreen() {
if (it != null) viewModel.setIconPack(it)
}
)
val legacyIconBackground by viewModel.legacyIconBackground.observeAsState()
LegacyIconBackgroundPreference(
title = stringResource(R.string.preference_legacy_icon_bg),
summary = stringResource(R.string.preference_legacy_icon_bg_summary),
value = legacyIconBackground,
onValueChanged = {
viewModel.setLegacyIconBackground(it)
},
iconShape = iconShape
)
}
PreferenceCategory(stringResource(R.string.preference_category_searchbar)) {
val searchBarStyle by viewModel.searchBarStyle.observeAsState()
@@ -370,13 +366,15 @@ fun IconShapePreference(
) {
ShapedLauncherIcon(
size = 48.dp,
icon = LauncherIcon(
foreground = AppCompatResources.getDrawable(
LocalContext.current,
R.mipmap.ic_launcher_foreground
)!!,
foregroundScale = 1.5f,
background = ColorDrawable(LocalContext.current.getColor(R.color.ic_launcher_background))
icon = StaticLauncherIcon(
foregroundLayer = StaticIconLayer(
icon = ContextCompat.getDrawable(
LocalContext.current,
R.mipmap.ic_launcher_foreground
)!!,
scale = 1.5f,
),
ColorLayer(LocalContext.current.getColor(R.color.ic_launcher_background))
),
onClick = {
onValueChanged(it)
@@ -399,82 +397,6 @@ fun IconShapePreference(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LegacyIconBackgroundPreference(
title: String,
summary: String? = null,
value: IconSettings.LegacyIconBackground?,
onValueChanged: (IconSettings.LegacyIconBackground) -> Unit,
iconShape: IconSettings.IconShape
) {
var showDialog by remember { mutableStateOf(false) }
Preference(title = title, summary = summary, onClick = { showDialog = true })
if (showDialog && value != null) {
val colors = remember {
IconSettings.LegacyIconBackground.values()
.filter { it != IconSettings.LegacyIconBackground.UNRECOGNIZED }
}
Dialog(onDismissRequest = { showDialog = false }) {
Surface(
tonalElevation = 16.dp,
shadowElevation = 16.dp,
shape = MaterialTheme.shapes.extraLarge,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(
start = 24.dp, end = 24.dp, top = 16.dp, bottom = 8.dp
)
)
LazyVerticalGrid(
columns = GridCells.Adaptive(96.dp),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp, start = 16.dp, end = 16.dp)
) {
items(colors) {
Column(
modifier = Modifier
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
ShapedLauncherIcon(
size = 48.dp,
icon = LauncherIcon(
foreground = AppCompatResources.getDrawable(
LocalContext.current,
R.mipmap.ic_launcher_foreground
)!!,
background = null,
autoGenerateBackgroundMode = when (it) {
IconSettings.LegacyIconBackground.Dynamic -> LauncherIcon.BACKGROUND_DYNAMIC
IconSettings.LegacyIconBackground.None -> LauncherIcon.BACKGROUND_NONE
else -> LauncherIcon.BACKGROUND_WHITE
}
),
onClick = {
onValueChanged(it)
showDialog = false
}
)
}
}
}
}
}
}
}
}
@OptIn(ExperimentalPagerApi::class)
@Composable
fun LayoutPreference(
@@ -9,7 +9,6 @@ import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.Settings.AppearanceSettings.ColorScheme
import de.mm20.launcher2.preferences.Settings.AppearanceSettings.Theme
import de.mm20.launcher2.preferences.Settings.IconSettings.LegacyIconBackground
import de.mm20.launcher2.preferences.Settings.SearchBarSettings
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@@ -108,14 +107,14 @@ class AppearanceSettingsScreenVM : ViewModel(), KoinComponent {
}
}
val legacyIconBackground = dataStore.data.map { it.icons.legacyIconBg }.asLiveData()
fun setLegacyIconBackground(legacyIconBackground: LegacyIconBackground) {
val adaptifyLegacyIcons = dataStore.data.map { it.icons.adaptify }.asLiveData()
fun setAdaptifyLegacyIcons(adaptify: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setIcons(
it.icons.toBuilder()
.setLegacyIconBg(legacyIconBackground)
.setAdaptify(adaptify)
)
.build()
}