Icon pack improvements

* Simplify UI for icon system
* Add compat system for monochrome icons below T
* Add option to tint backgrounds according to accent color
* Fix icon shadow setting

Todo: implement home screen themed icons compat setting, alongside look into calendar behavior
This commit is contained in:
SuperDragonXD
2024-09-02 21:13:38 +08:00
parent f561aaccdc
commit 8f89197b61
6 changed files with 292 additions and 139 deletions

View File

@@ -10,7 +10,6 @@ import android.graphics.drawable.InsetDrawable
import android.os.Build
import android.os.Process
import android.os.UserHandle
import app.lawnchair.util.getThemedIconPacksInstalled
import com.android.launcher3.icons.ClockDrawableWrapper
import com.android.launcher3.icons.ThemedIconDrawable
import com.android.launcher3.util.MainThreadInitializedObject
@@ -47,16 +46,14 @@ class IconPackProvider(private val context: Context) {
iconPack.loadBlocking()
val packageManager = context.packageManager
val drawable = iconPack.getIcon(iconEntry, iconDpi) ?: return null
val themedIconPacks = packageManager.getThemedIconPacksInstalled(context)
val isThemedIconsEnabled =
context.isThemedIconsEnabled() && (iconEntry.packPackageName in themedIconPacks)
val shouldTintBackgrounds = context.shouldTintIconPackBackgrounds()
val clockMetadata =
if (user == Process.myUserHandle()) iconPack.getClock(iconEntry) else null
try {
if (clockMetadata != null) {
val clockDrawable: ClockDrawableWrapper =
ClockDrawableWrapper.forMeta(Build.VERSION.SDK_INT, clockMetadata) {
if (isThemedIconsEnabled) {
if (shouldTintBackgrounds) {
wrapThemedData(
packageManager,
iconEntry,
@@ -66,22 +63,20 @@ class IconPackProvider(private val context: Context) {
drawable
}
}
if (clockDrawable != null) {
return if (isThemedIconsEnabled && context.shouldTransparentBGIcons()) {
clockDrawable.foreground
} else {
CustomAdaptiveIconDrawable(
clockDrawable.background,
clockDrawable.foreground,
)
}
return if (shouldTintBackgrounds && context.shouldTransparentBGIcons()) {
clockDrawable.foreground
} else {
CustomAdaptiveIconDrawable(
clockDrawable.background,
clockDrawable.foreground,
)
}
}
} catch (t: Throwable) {
// Ignore
}
if (isThemedIconsEnabled) {
if (shouldTintBackgrounds) {
return wrapThemedData(packageManager, iconEntry, drawable)
}
return drawable
@@ -93,22 +88,27 @@ class IconPackProvider(private val context: Context) {
drawable: Drawable,
): Drawable? {
val themedColors: IntArray = ThemedIconDrawable.getThemedColors(context)
val res = packageManager.getResourcesForApplication(iconEntry.packPackageName)
try {
val res = packageManager.getResourcesForApplication(iconEntry.packPackageName)
@SuppressLint("DiscouragedApi")
val resId = res.getIdentifier(iconEntry.name, "drawable", iconEntry.packPackageName)
val bg: Drawable = ColorDrawable(themedColors[0])
val td = ThemedIconDrawable.ThemeData(res, iconEntry.packPackageName, resId)
return if (drawable is AdaptiveIconDrawable) {
if (context.shouldTransparentBGIcons() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && drawable.monochrome != null) {
drawable.monochrome?.apply { setTint(themedColors[1]) }
@SuppressLint("DiscouragedApi")
val resId = res.getIdentifier(iconEntry.name, "drawable", iconEntry.packPackageName)
val bg: Drawable = ColorDrawable(themedColors[0])
val td = ThemedIconDrawable.ThemeData(res, iconEntry.packPackageName, resId)
return if (drawable is AdaptiveIconDrawable) {
if (context.shouldTransparentBGIcons() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && drawable.monochrome != null) {
drawable.monochrome?.apply { setTint(themedColors[1]) }
} else {
val foregroundDr = drawable.foreground.apply { setTint(themedColors[1]) }
CustomAdaptiveIconDrawable(bg, foregroundDr)
}
} else {
val foregroundDr = drawable.foreground.apply { setTint(themedColors[1]) }
CustomAdaptiveIconDrawable(bg, foregroundDr)
val iconFromPack = InsetDrawable(drawable, .3f).apply { setTint(themedColors[1]) }
td.wrapDrawable(CustomAdaptiveIconDrawable(bg, iconFromPack), 0)
}
} else {
val iconFromPack = InsetDrawable(drawable, .3f).apply { setTint(themedColors[1]) }
td.wrapDrawable(CustomAdaptiveIconDrawable(bg, iconFromPack), 0)
} catch (_: Exception) {
return drawable
}
}

View File

@@ -19,7 +19,6 @@ import android.content.res.Resources
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Handler
import android.os.UserHandle
import android.os.UserManager
@@ -34,6 +33,7 @@ import app.lawnchair.util.getPackageVersionCode
import app.lawnchair.util.isPackageInstalled
import com.android.launcher3.BuildConfig
import com.android.launcher3.R
import com.android.launcher3.Utilities
import com.android.launcher3.icons.IconProvider
import com.android.launcher3.icons.ThemedIconDrawable
import com.android.launcher3.util.ComponentKey
@@ -49,25 +49,31 @@ class LawnchairIconProvider @JvmOverloads constructor(
private val prefs = PreferenceManager.getInstance(context)
private val iconPackPref = prefs.iconPackPackage
private val themedIconPackPref = prefs.themedIconPackPackage
private val drawerThemedIcons get() = prefs.drawerThemedIcons
private val iconPackProvider = IconPackProvider.INSTANCE.get(context)
private val overrideRepo = IconOverrideRepository.INSTANCE.get(context)
private val iconPack get() = iconPackProvider.getIconPack(iconPackPref.get())?.apply { loadBlocking() }
private val themedIconPack get() = iconPackProvider.getIconPack(themedIconPackPref.get())?.apply { loadBlocking() }
private var isOlderLawnIconsInstalled = context.packageManager.getPackageVersionCode(LAWNICONS_PACKAGE_NAME) in 1..3
private val iconPack
get() = iconPackProvider.getIconPack(iconPackPref.get())?.apply { loadBlocking() }
private val themedIconPack
get() = iconPackProvider.getIconPack(themedIconPackPref.get())?.apply { loadBlocking() }
private var isOlderLawniconsInstalled = context.packageManager.getPackageVersionCode(LAWNICONS_PACKAGE_NAME) in 1..3
private var iconPackVersion = 0L
private var themeMapName: String = ""
private var _themeMap: Map<ComponentName, ThemedIconDrawable.ThemeData>? = null
val themeMap: Map<ComponentName, ThemedIconDrawable.ThemeData>
get() {
if (drawerThemedIcons.get() && !(isOlderLawnIconsInstalled)) {
if (!context.isThemedIconsEnabled()) {
_themeMap = DISABLED_MAP
}
if (_themeMap == null) {
_themeMap = createThemedIconMap()
}
if (isOlderLawnIconsInstalled && themedIconPackPref.get() == LAWNICONS_PACKAGE_NAME) {
if (isOlderLawniconsInstalled) {
themeMapName = themedIconPackPref.get()
_themeMap = createThemedIconMap()
}
@@ -84,7 +90,7 @@ class LawnchairIconProvider @JvmOverloads constructor(
}
override fun setIconThemeSupported(isSupported: Boolean) {
_themeMap = if (isSupported && isOlderLawnIconsInstalled) null else DISABLED_MAP
_themeMap = if (isSupported && isOlderLawniconsInstalled) null else DISABLED_MAP
}
private fun resolveIconEntry(componentName: ComponentName, user: UserHandle): IconEntry? {
@@ -151,23 +157,41 @@ class LawnchairIconProvider @JvmOverloads constructor(
val icon = resolvedEntry?.let { iconPackProvider.getDrawable(it, iconDpi, user) }
val td = themeData
if (icon != null) return if (td != null) td.wrapDrawable(icon, iconType) else icon
// use default icon from system
var defaultIcon =
super.getIconWithOverrides(packageName, component, user, iconDpi, fallback)
if (context.isThemedIconsEnabled() && defaultIcon is AdaptiveIconDrawable &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && defaultIcon.monochrome != null
) {
defaultIcon = defaultIcon.monochrome
return if (td != null) {
td.wrapDrawable(defaultIcon, iconType)
} else {
val themedColors = ThemedIconDrawable.getThemedColors(context)
if (context.shouldTransparentBGIcons()) {
return defaultIcon.apply { setTint(themedColors[1]) }
if (context.shouldTintIconPackBackgrounds() && defaultIcon is AdaptiveIconDrawable) {
if (Utilities.ATLEAST_T && defaultIcon.monochrome != null) {
defaultIcon = defaultIcon.monochrome
return if (td != null) {
td.wrapDrawable(defaultIcon, iconType)
} else {
val themedColors = ThemedIconDrawable.getThemedColors(context)
if (context.shouldTransparentBGIcons()) {
return defaultIcon.apply { setTint(themedColors[1]) }
}
CustomAdaptiveIconDrawable(
ColorDrawable(themedColors[0]),
defaultIcon.apply { setTint(themedColors[1]) },
)
}
} else {
val iconCompat = ThemedIconCompat.getThemedIcon(context, componentName) ?: return defaultIcon
return if (td != null) {
td.wrapDrawable(iconCompat, iconType)
} else {
val themedColors = ThemedIconDrawable.getThemedColors(context)
if (context.shouldTransparentBGIcons()) {
return iconCompat.apply { setTint(themedColors[1]) }
}
CustomAdaptiveIconDrawable(
ColorDrawable(themedColors[0]),
iconCompat.apply { setTint(themedColors[1]) },
)
}
CustomAdaptiveIconDrawable(
ColorDrawable(themedColors[0]),
defaultIcon.apply { setTint(themedColors[1]) },
)
}
}
return defaultIcon
@@ -368,17 +392,13 @@ class LawnchairIconProvider @JvmOverloads constructor(
}
}
updateMapFromResources(
resources = context.resources,
packageName = context.packageName,
)
if (context.packageManager.isPackageInstalled(packageName = themeMapName)) {
iconPackVersion = context.packageManager.getPackageVersionCode(themeMapName)
updateMapFromResources(
resources = context.packageManager.getResourcesForApplication(themeMapName),
packageName = themeMapName,
)
if (isOlderLawnIconsInstalled) {
if (isOlderLawniconsInstalled) {
updateMapWithDynamicIcons(context, map)
}
}

View File

@@ -0,0 +1,97 @@
package app.lawnchair.icons
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.content.res.Resources
import android.content.res.XmlResourceParser
import android.graphics.drawable.Drawable
import android.util.Log
import java.io.IOException
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
object ThemedIconCompat {
const val TAG = "ThemedIconCompat"
fun getThemedIcon(
context: Context,
componentName: ComponentName,
): Drawable? {
val activityInfo = resolveActivityInfo(context, componentName) ?: return null
val drawable = getMonochromeIconResource(
context,
activityInfo,
) ?: return null
return drawable
}
private fun resolveActivityInfo(context: Context, componentName: ComponentName): ActivityInfo? {
return try {
context.packageManager.getActivityInfo(
componentName,
0,
)
} catch (e: PackageManager.NameNotFoundException) {
// Handle the case where the activity is not found
null
}
}
@SuppressLint("UseCompatLoadingForDrawables")
private fun getMonochromeIconResource(context: Context, activityInfo: ActivityInfo): Drawable? {
val iconResource = activityInfo.applicationInfo.icon
val resources = try {
context.packageManager.getResourcesForApplication(activityInfo.packageName)
} catch (e: PackageManager.NameNotFoundException) {
Log.e(TAG, e.toString())
return null
}
var xmlParser: XmlResourceParser? = null
try {
xmlParser = resources.getXml(iconResource)
if (!xmlParser.skipToNextTag()) return null
if (xmlParser.name != "adaptive-icon") {
return null
}
while (xmlParser.skipToNextTag()) {
if (xmlParser.name == "monochrome") {
val drawable = xmlParser.getAttributeResourceValue(
"http://schemas.android.com/apk/res/android",
"drawable",
0,
)
if (drawable == 0) return null
return resources.getDrawable(drawable, null)
}
}
} catch (e: Resources.NotFoundException) {
Log.e(TAG, e.toString())
return null
} catch (e: IOException) {
Log.e(TAG, e.toString())
return null
} catch (e: XmlPullParserException) {
Log.e(TAG, e.toString())
return null
} finally {
xmlParser?.close()
}
return null
}
}
fun XmlPullParser.skipToNextTag(): Boolean {
while (next() != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG) return true
}
return false
}

View File

@@ -94,6 +94,8 @@ class PreferenceManager private constructor(private val context: Context) : Base
val themedIcons = BoolPref("themed_icons", true, recreate)
val drawerThemedIcons = BoolPref("drawer_themed_icons", false, recreate)
val tintIconPackBackgrounds = BoolPref("tint_icon_pack_backgrounds", false, recreate)
val hotseatQsbCornerRadius = FloatPref("pref_hotseatQsbCornerRadius", 1F, recreate)
val hotseatQsbAlpha = IntPref("pref_searchHotseatTranparency", 100, recreate)
val hotseatQsbStrokeWidth = FloatPref("pref_searchStrokeWidth", 0F, recreate)

View File

@@ -19,20 +19,26 @@ package app.lawnchair.ui.preferences.destinations
import android.content.res.Configuration
import android.graphics.drawable.Drawable
import androidx.annotation.StringRes
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@@ -41,9 +47,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -60,8 +68,9 @@ import app.lawnchair.ui.preferences.components.DummyLauncherLayout
import app.lawnchair.ui.preferences.components.WallpaperPreview
import app.lawnchair.ui.preferences.components.controls.ListPreference
import app.lawnchair.ui.preferences.components.controls.ListPreferenceEntry
import app.lawnchair.ui.preferences.components.controls.SwitchPreference
import app.lawnchair.ui.preferences.components.invariantDeviceProfile
import app.lawnchair.ui.preferences.components.layout.ExpandAndShrink
import app.lawnchair.ui.preferences.components.layout.Chip
import app.lawnchair.ui.preferences.components.layout.NestedScrollStretch
import app.lawnchair.ui.preferences.components.layout.PreferenceGroup
import app.lawnchair.ui.preferences.components.layout.PreferenceLayout
@@ -71,6 +80,7 @@ import app.lawnchair.util.isPackageInstalled
import com.android.launcher3.R
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.launch
data class IconPackInfo(
val name: String,
@@ -106,6 +116,8 @@ fun IconPackPreferences(
modifier: Modifier = Modifier,
) {
val prefs = preferenceManager()
val context = LocalContext.current
val iconPackAdapter = prefs.iconPackPackage.getAdapter()
val themedIconPackAdapter = prefs.themedIconPackPackage.getAdapter()
val themedIconsAdapter = prefs.themedIcons.getAdapter()
@@ -144,74 +156,98 @@ fun IconPackPreferences(
}
}
Column {
ExpandAndShrink(visible = !drawerThemedIconsEnabled) {
PreferenceGroup(
heading = stringResource(id = R.string.icon_pack),
) {
IconPackGrid(
adapter = iconPackAdapter,
themedIconsAdapter.state.value,
false,
)
}
}
ExpandAndShrink(visible = themedIconsAdapter.state.value && !drawerThemedIconsEnabled) {
PreferenceGroup(
heading = stringResource(id = R.string.themed_icon_pack),
) {
IconPackGrid(
adapter = themedIconPackAdapter,
drawerThemedIconsEnabled,
true,
)
}
}
ExpandAndShrink(visible = drawerThemedIconsEnabled) {
PreferenceGroup(
heading = stringResource(id = R.string.themed_icon_pack),
) {
IconPackGrid(
adapter = iconPackAdapter,
drawerThemedIconsEnabled,
true,
)
}
}
PreferenceGroup {
val themedIconsAvailable = LocalContext.current.packageManager
.getThemedIconPacksInstalled(LocalContext.current)
.any { LocalContext.current.packageManager.isPackageInstalled(it) } ||
LocalContext.current.packageManager
.isPackageInstalled(Constants.LAWNICONS_PACKAGE_NAME)
ListPreference(
enabled = themedIconsAvailable,
label = stringResource(id = R.string.themed_icon_title),
entries = ThemedIconsState.entries.map {
ListPreferenceEntry(
value = it,
label = { stringResource(id = it.labelResourceId) },
)
}.toPersistentList(),
value = ThemedIconsState.getForSettings(
themedIcons = themedIconsAdapter.state.value,
drawerThemedIcons = drawerThemedIconsEnabled,
),
onValueChange = {
themedIconsAdapter.onChange(newValue = it.themedIcons)
drawerThemedIconsAdapter.onChange(newValue = it.drawerThemedIcons)
iconPackAdapter.onChange(newValue = iconPackAdapter.state.value)
if (it.themedIcons && !it.drawerThemedIcons) {
themedIconPackAdapter.onChange(newValue = themedIconPackAdapter.state.value)
} else {
themedIconPackAdapter.onChange(newValue = "")
}
},
description = if (themedIconsAvailable.not()) {
stringResource(id = R.string.lawnicons_not_installed_description)
} else {
null
},
val pagerState = rememberPagerState(
initialPage = 0,
pageCount = { 2 },
)
val scope = rememberCoroutineScope()
val scrollToPage =
{ page: Int -> scope.launch { pagerState.animateScrollToPage(page) } }
Row(
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
modifier = Modifier.padding(horizontal = 16.dp),
) {
Chip(
label = stringResource(id = R.string.icon_pack),
onClick = { scrollToPage(0) },
currentOffset = pagerState.currentPage + pagerState.currentPageOffsetFraction,
page = 0,
)
Chip(
label = stringResource(id = R.string.themed_icon_pack),
onClick = { scrollToPage(1) },
currentOffset = pagerState.currentPage + pagerState.currentPageOffsetFraction,
page = 1,
)
}
Spacer(Modifier.height(16.dp))
HorizontalPager(
state = pagerState,
verticalAlignment = Alignment.Top,
modifier = Modifier.animateContentSize(),
) { page ->
when (page) {
0 -> {
PreferenceGroup {
IconPackGrid(
adapter = iconPackAdapter,
false,
)
SwitchPreference(
adapter = prefs.tintIconPackBackgrounds.getAdapter(),
label = "Tint with accent color",
)
}
}
1 -> {
val packageManager = context.packageManager
PreferenceGroup {
val themedIconsAvailable = packageManager
.getThemedIconPacksInstalled(LocalContext.current)
.any { packageManager.isPackageInstalled(it) } ||
packageManager
.isPackageInstalled(Constants.LAWNICONS_PACKAGE_NAME)
if (themedIconsAvailable && themedIconsAdapter.state.value) {
IconPackGrid(
adapter = themedIconPackAdapter,
true,
)
}
ListPreference(
enabled = themedIconsAvailable,
label = stringResource(id = R.string.themed_icon_title),
entries = ThemedIconsState.entries.map {
ListPreferenceEntry(
value = it,
label = { stringResource(id = it.labelResourceId) },
)
}.toPersistentList(),
value = ThemedIconsState.getForSettings(
themedIcons = themedIconsAdapter.state.value,
drawerThemedIcons = drawerThemedIconsEnabled,
),
onValueChange = {
themedIconsAdapter.onChange(newValue = it.themedIcons)
drawerThemedIconsAdapter.onChange(newValue = it.drawerThemedIcons)
iconPackAdapter.onChange(newValue = iconPackAdapter.state.value)
themedIconPackAdapter.onChange(newValue = themedIconPackAdapter.state.value)
},
description = if (themedIconsAvailable.not()) {
stringResource(id = R.string.lawnicons_not_installed_description)
} else {
null
},
)
}
}
}
}
}
}
@@ -220,25 +256,21 @@ fun IconPackPreferences(
@Composable
fun IconPackGrid(
adapter: PreferenceAdapter<String>,
drawerThemedIcons: Boolean,
isThemedIconPack: Boolean,
modifier: Modifier = Modifier,
) {
val iconPacks by LocalPreferenceInteractor.current.iconPacks.collectAsStateWithLifecycle()
val themedIconPacks by LocalPreferenceInteractor.current.themedIconPacks.collectAsStateWithLifecycle()
val preferenceInteractor = LocalPreferenceInteractor.current
val iconPacks by preferenceInteractor.iconPacks.collectAsStateWithLifecycle()
val themedIconPacks by preferenceInteractor.themedIconPacks.collectAsStateWithLifecycle()
val lazyListState = rememberLazyListState()
val padding = 12.dp
var iconPacksLocal = iconPacks
val themedIconPacksName = themedIconPacks.map { it.name }
if (isThemedIconPack) {
iconPacksLocal = if (drawerThemedIcons) {
themedIconPacks
} else {
themedIconPacks.filter { it.packageName != "" }
}
} else if (drawerThemedIcons) {
iconPacksLocal = iconPacks.filter { it.packageName == "" || !themedIconPacksName.contains(it.name) }
val iconPacksLocal = if (isThemedIconPack) {
themedIconPacks.filter { it.packageName != "" }
} else {
iconPacks
}
val selectedPack = adapter.state.value
@@ -260,7 +292,9 @@ fun IconPackGrid(
state = lazyListState,
horizontalArrangement = Arrangement.spacedBy(space = padding),
contentPadding = PaddingValues(horizontal = padding),
modifier = Modifier.padding(bottom = 6.dp, top = 6.dp).fillMaxWidth(),
modifier = Modifier
.padding(bottom = 6.dp, top = 6.dp)
.fillMaxWidth(),
) {
itemsIndexed(iconPacksLocal, { _, item -> item.packageName }) { index, item ->
IconPackItem(
@@ -307,7 +341,7 @@ fun IconPackItem(
Surface(
onClick = onClick,
shape = MaterialTheme.shapes.large,
tonalElevation = if (selected) 2.dp else 0.dp,
color = if (selected) MaterialTheme.colorScheme.surfaceContainerHighest else Color.Transparent,
modifier = modifier,
) {
Column(