Migrate icon shape preference

This commit is contained in:
MM20 2022-01-27 22:22:10 +01:00
parent 1370901f46
commit 75529b8f42
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
50 changed files with 467 additions and 163 deletions

View File

@ -8,7 +8,6 @@ import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.lifecycleScope
import androidx.preference.ListPreference
import androidx.preference.Preference
@ -88,7 +87,6 @@ class PreferencesAppearanceFragment : PreferenceFragmentCompat() {
}
val shapePreference = findPreference<Preference>("icon_shape")!!
shapePreference.summary = getShapeName()
shapePreference.setOnPreferenceClickListener {
val launcherIcon = LauncherIcon(
foreground = requireContext().getDrawable(R.mipmap.ic_launcher_foreground)!!,
@ -110,23 +108,6 @@ class PreferencesAppearanceFragment : PreferenceFragmentCompat() {
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT)
val dialog = MaterialDialog(requireContext())
shapes.forEachIndexed { i, shape ->
val view = View.inflate(requireContext(), R.layout.preference_icon_shape_row, null)
view.findViewById<LauncherIconView>(R.id.icon).also { iconView ->
iconView.icon = launcherIcon
iconView.shape = shape.first
}
view.findViewById<TextView>(R.id.label).also { labelView ->
labelView.setText(shape.second)
}
view.layoutParams = layoutParams
iconShapeList.addView(view)
view.setOnClickListener {
LauncherPreferences.instance.iconShape = shape.first
shapePreference.summary = getShapeName()
dialog.dismiss()
}
}
dialog.customView(view = iconShapeList, scrollable = true)
.title(R.string.preference_icon_shape)
@ -143,19 +124,6 @@ class PreferencesAppearanceFragment : PreferenceFragmentCompat() {
}
}
private fun getShapeName(): String {
return requireContext().getString(when (LauncherIconView.getDefaultShape(requireContext())) {
IconShape.TRIANGLE -> R.string.preference_icon_shape_triangle
IconShape.HEXAGON -> R.string.preference_icon_shape_hexagon
IconShape.ROUNDED_SQUARE -> R.string.preference_icon_shape_rounded_square
IconShape.SQUIRCLE -> R.string.preference_icon_shape_squircle
IconShape.SQUARE -> R.string.preference_icon_shape_square
IconShape.PENTAGON -> R.string.preference_icon_shape_pentagon
IconShape.PLATFORM_DEFAULT -> R.string.preference_icon_shape_platform
else -> R.string.preference_icon_shape_circle
})
}
override fun onResume() {
super.onResume()

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -106,5 +106,12 @@ fun createFactorySettings(context: Context): Settings {
.setSearchBarStyle(Settings.SearchBarSettings.SearchBarStyle.Transparent)
.build()
)
.setIcons(
Settings.IconSettings.newBuilder()
.setLegacyIconBg(Settings.IconSettings.LegacyIconBackground.Dynamic)
.setShape(Settings.IconSettings.IconShape.PlatformDefault)
.setThemedIcons(false)
.setIconPack("")
)
.build()
}

View File

@ -133,4 +133,28 @@ message Settings {
}
SearchBarSettings search_bar = 20;
message IconSettings {
enum IconShape {
PlatformDefault = 0;
Circle = 1;
Square = 2;
RoundedSquare = 3;
Triangle = 4;
Squircle = 5;
Hexagon = 6;
Pentagon = 7;
EasterEgg = 8;
}
IconShape shape = 1;
bool themed_icons = 2;
string icon_pack = 3;
enum LegacyIconBackground {
Dynamic = 0;
None = 1;
White = 2;
}
LegacyIconBackground legacyIconBg = 4;
}
IconSettings icons = 21;
}

View File

@ -40,7 +40,6 @@ import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.*
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.notifications.NotificationRepository
import de.mm20.launcher2.notifications.NotificationService
import de.mm20.launcher2.search.data.AppInstallation
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.LauncherApp
@ -79,8 +78,8 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
setOnLongClickListener(null)
findViewById<TextView>(R.id.appName).text = application.label
val iconView = findViewById<LauncherIconView>(R.id.icon).apply {
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(application)
shape = LauncherIconView.currentShape
}
val notificationView = findViewById<ChipGroup>(R.id.notifications)
@ -107,6 +106,11 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
updateNotifications(notificationView, it)
}
}
launch {
LauncherIconView.getDefaultShape().collectLatest {
iconView.shape = it
}
}
}
}
findViewById<SwipeCardView>(R.id.appCard).also {

View File

@ -9,7 +9,6 @@ import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
@ -46,7 +45,6 @@ class BasicGridRepresentation : Representation, KoinComponent {
.alpha(1f)
.start()*/
findViewById<LauncherIconView>(R.id.icon).apply {
shape = LauncherIconView.getDefaultShape(context)
setOnClickListener {
if (!ActivityStarter.start(
context,
@ -58,6 +56,7 @@ class BasicGridRepresentation : Representation, KoinComponent {
}
}
icon = iconRepository.getIconIfCached(searchable)
shape = LauncherIconView.currentShape
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
@ -72,6 +71,11 @@ class BasicGridRepresentation : Representation, KoinComponent {
badge = it
}
}
launch {
LauncherIconView.getDefaultShape().collectLatest {
shape = it
}
}
}
}
setOnLongClickListener {

View File

@ -20,7 +20,6 @@ import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.ktx.setStartCompoundDrawable
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.Contact
@ -54,8 +53,8 @@ class ContactDetailRepresentation : Representation, KoinComponent {
scene.setEnterAction {
with(rootView) {
findViewById<LauncherIconView>(R.id.icon).apply {
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(contact)
shape = LauncherIconView.currentShape
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
@ -69,6 +68,11 @@ class ContactDetailRepresentation : Representation, KoinComponent {
badge = it
}
}
launch {
LauncherIconView.getDefaultShape().collectLatest {
shape = it
}
}
}
}
}

View File

@ -10,7 +10,6 @@ import de.mm20.launcher2.ui.R
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.search.data.Contact
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
@ -38,8 +37,8 @@ class ContactListRepresentation : Representation, KoinComponent {
scene.setEnterAction {
with(rootView) {
findViewById<LauncherIconView>(R.id.icon).apply {
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(contact)
shape = LauncherIconView.currentShape
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
@ -53,6 +52,11 @@ class ContactListRepresentation : Representation, KoinComponent {
badge = it
}
}
launch {
LauncherIconView.getDefaultShape().collectLatest {
shape = it
}
}
}
}
}

View File

@ -48,8 +48,8 @@ class FileDetailRepresentation : Representation, KoinComponent {
findViewById<TextView>(R.id.fileLabel).text = file.label
findViewById<TextView>(R.id.fileInfo).text = getInfo(context, file)
findViewById<LauncherIconView>(R.id.icon).apply {
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(file)
shape = LauncherIconView.currentShape
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
@ -63,6 +63,11 @@ class FileDetailRepresentation : Representation, KoinComponent {
badge = it
}
}
launch {
LauncherIconView.getDefaultShape().collectLatest {
shape = it
}
}
}
}
}

View File

@ -44,8 +44,8 @@ class FileListRepresentation : Representation, KoinComponent {
findViewById<TextView>(R.id.fileLabel).text = file.label
findViewById<TextView>(R.id.fileInfo).text = file.getFileType(context)
findViewById<LauncherIconView>(R.id.icon).apply {
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(file)
shape = LauncherIconView.currentShape
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
@ -59,6 +59,11 @@ class FileListRepresentation : Representation, KoinComponent {
badge = it
}
}
launch {
LauncherIconView.getDefaultShape().collectLatest {
shape = it
}
}
}
}
}

View File

@ -47,8 +47,8 @@ class AppShortcutDetailRepresentation : Representation, KoinComponent {
setOnLongClickListener(null)
findViewById<TextView>(R.id.appName).text = appShortcut.label
findViewById<LauncherIconView>(R.id.icon).apply {
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(appShortcut)
shape = LauncherIconView.currentShape
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
@ -62,6 +62,11 @@ class AppShortcutDetailRepresentation : Representation, KoinComponent {
badge = it
}
}
launch {
LauncherIconView.getDefaultShape().collectLatest {
shape = it
}
}
}
}
}

View File

@ -11,7 +11,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.Scene
import coil.load
import coil.size.Scale
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleOwner
@ -74,14 +73,21 @@ class WebsiteDetailRepresentation : Representation, KoinComponent {
label.transitionName = null
websiteFavIcon.transitionName = "icon"
websiteFavIcon.apply {
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(website)
shape = LauncherIconView.currentShape
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
iconRepository.getIcon(website, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
launch {
iconRepository.getIcon(website, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
}
}
launch {
LauncherIconView.getDefaultShape().collectLatest {
shape = it
}
}
}
}
}

View File

@ -77,8 +77,8 @@ class WebsiteListRepresentation : Representation, KoinComponent {
label.transitionName = null
websiteFavIcon.transitionName = "icon"
websiteFavIcon.apply {
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(website)
shape = LauncherIconView.currentShape
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
@ -92,6 +92,11 @@ class WebsiteListRepresentation : Representation, KoinComponent {
badge = it
}
}
launch {
LauncherIconView.getDefaultShape().collectLatest {
shape = it
}
}
}
}
}

View File

@ -7,37 +7,25 @@ import android.graphics.*
import android.graphics.drawable.AdaptiveIconDrawable
import android.os.Build
import android.util.AttributeSet
import android.util.Log
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.repeatOnLifecycle
import com.bartoszlipinski.viewpropertyobjectanimator.ViewPropertyObjectAnimator
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.toRectF
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.preferences.IconShape
import de.mm20.launcher2.preferences.LauncherPreferences
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.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.component.get
import java.lang.Math.pow
import java.lang.Runnable
import kotlin.math.abs
import kotlin.math.hypot
import kotlin.math.roundToInt
@ -45,17 +33,19 @@ import kotlin.math.roundToInt
class LauncherIconView : View, KoinComponent {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
context,
attrs,
defStyleRes
)
var shape: IconShape
set(value) {
if (value == IconShape.PLATFORM_DEFAULT) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
platformShape = getSystemShape()
transformMatrix = Matrix()
platformShapeBounds = RectF()
field = value
} else field = IconShape.CIRCLE
if (value == IconShape.PlatformDefault) {
platformShape = getSystemShape()
transformMatrix = Matrix()
platformShapeBounds = RectF()
field = value
} else {
platformShape = null
transformMatrix = null
@ -108,7 +98,7 @@ class LauncherIconView : View, KoinComponent {
}
init {
shape = IconShape.CIRCLE
shape = IconShape.Circle
setLayerType(LAYER_TYPE_SOFTWARE, null)
}
@ -200,34 +190,45 @@ class LauncherIconView : View, KoinComponent {
if (bg != null) {
bg.bounds = bmpDrawRect
when (shape) {
IconShape.PLATFORM_DEFAULT -> {
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)
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.ROUNDED_SQUARE -> {
canvas.drawRoundRect(drawRect.left.toFloat(),
drawRect.top.toFloat(),
drawRect.right.toFloat(),
drawRect.bottom.toFloat(),
width * 0.125f,
height * 0.125f,
maskPaint
IconShape.Circle -> {
canvas.drawOval(
drawRect.left.toFloat(),
drawRect.top.toFloat(),
drawRect.right.toFloat(),
drawRect.bottom.toFloat(),
maskPaint
)
}
IconShape.TRIANGLE -> {
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
@ -248,63 +249,157 @@ class LauncherIconView : View, KoinComponent {
canvas.drawArc(cx - r, cy - r, cx + r, cy + r, 60f, 60f, true, maskPaint)
}
IconShape.SQUIRCLE -> {
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())
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.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 -> {
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.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.HEART -> {
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.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 -> {
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.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())
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())
c.scale(
foregroundScale,
foregroundScale,
bmpDrawRect.centerX().toFloat(),
bmpDrawRect.centerY().toFloat()
)
fg.draw(c)
c.restore()
if (bg != null) {
@ -314,26 +409,33 @@ class LauncherIconView : View, KoinComponent {
}
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.ROUNDED_SQUARE -> {
canvas.drawRoundRect(drawRect.left.toFloat(),
drawRect.top.toFloat(),
drawRect.right.toFloat(),
drawRect.bottom.toFloat(),
width * 0.125f,
height * 0.125f,
shadowPaint
IconShape.Circle -> {
canvas.drawOval(
drawRect.left.toFloat(),
drawRect.top.toFloat(),
drawRect.right.toFloat(),
drawRect.bottom.toFloat(),
shadowPaint
)
}
IconShape.TRIANGLE, IconShape.HEXAGON, IconShape.HEART, IconShape.PENTAGON, IconShape.PLATFORM_DEFAULT -> {
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 -> {
IconShape.Squircle -> {
canvas.save()
canvas.translate(width / 2f, height / 2f)
canvas.drawPath(path, shadowPaint)
@ -361,12 +463,18 @@ class LauncherIconView : View, KoinComponent {
canvas.drawArc(badgeRect, 270f, it * 360, true, badgeProgressPaint)
}
badgeIcon?.let {
it.setBounds((drawRect.right - badgeSize * 0.9f).toInt(),
(drawRect.bottom - badgeSize * 0.9f).toInt(),
(drawRect.right - badgeSize * 0.1f).toInt(),
(drawRect.bottom - badgeSize * 0.1f).toInt()
it.setBounds(
(drawRect.right - badgeSize * 0.9f).toInt(),
(drawRect.bottom - badgeSize * 0.9f).toInt(),
(drawRect.right - badgeSize * 0.1f).toInt(),
(drawRect.bottom - badgeSize * 0.1f).toInt()
)
it.setBounds(
badgeRect.left.roundToInt(),
badgeRect.top.roundToInt(),
badgeRect.right.roundToInt(),
badgeRect.bottom.roundToInt()
)
it.setBounds(badgeRect.left.roundToInt(), badgeRect.top.roundToInt(), badgeRect.right.roundToInt(), badgeRect.bottom.roundToInt())
it.draw(canvas)
return
}
@ -375,7 +483,12 @@ class LauncherIconView : View, KoinComponent {
val textSize = (1f - 0.1f - text.length * 0.1f) * badgeSize
badgeTextPaint.textSize = textSize
badgeTextPaint.getTextBounds(text, 0, text.length, textBounds)
canvas.drawText(it.toString(), badgeRect.centerX(), badgeRect.centerY() - textBounds.exactCenterY(), badgeTextPaint)
canvas.drawText(
it.toString(),
badgeRect.centerX(),
badgeRect.centerY() - textBounds.exactCenterY(),
badgeTextPaint
)
}
}
@ -398,7 +511,10 @@ class LauncherIconView : View, KoinComponent {
downX = ev.rawX
downY = ev.rawY
longClicked = false
handler?.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout().toLong())
handler?.postDelayed(
longClickRunnable,
ViewConfiguration.getLongPressTimeout().toLong()
)
return true
}
MotionEvent.ACTION_MOVE -> {
@ -428,9 +544,9 @@ class LauncherIconView : View, KoinComponent {
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)
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()
@ -440,11 +556,15 @@ class LauncherIconView : View, KoinComponent {
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)
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()
@ -452,12 +572,16 @@ class LauncherIconView : View, KoinComponent {
}
companion object {
companion object: KoinComponent {
fun getDefaultShape(context: Context): IconShape {
if (LauncherPreferences.instance.easterEggEnabled) return IconShape.HEART
return LauncherPreferences.instance.iconShape.let {
return@let if (it != IconShape.HEART) it else IconShape.PLATFORM_DEFAULT
var currentShape: IconShape = IconShape.PlatformDefault
fun getDefaultShape(): Flow<IconShape> = channelFlow {
send(currentShape)
val dataStore: LauncherDataStore = get()
dataStore.data.map { it.icons.shape }.distinctUntilChanged().collectLatest {
currentShape = it
send(it)
}
}
}

View File

@ -1,31 +1,45 @@
package de.mm20.launcher2.ui.settings.appearance
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.foundation.lazy.GridCells
import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.viewmodel.compose.viewModel
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.ktx.dp
import de.mm20.launcher2.preferences.IconShape
import de.mm20.launcher2.preferences.Settings.AppearanceSettings.ColorScheme
import de.mm20.launcher2.preferences.Settings.AppearanceSettings.Theme
import de.mm20.launcher2.preferences.Settings.IconSettings
import de.mm20.launcher2.preferences.Settings.SearchBarSettings
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.*
import de.mm20.launcher2.ui.launcher.search.SearchBar
import de.mm20.launcher2.ui.launcher.search.SearchBarLevel
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
@ -96,6 +110,17 @@ fun AppearanceSettingsScreen() {
}
)
}
PreferenceCategory(stringResource(R.string.preference_category_icons)) {
val iconShape by viewModel.iconShape.observeAsState()
IconShapePreference(
title = stringResource(R.string.preference_icon_shape),
summary = getShapeName(iconShape),
value = iconShape,
onValueChanged = {
viewModel.setIconShape(it)
}
)
}
PreferenceCategory(stringResource(R.string.preference_category_searchbar)) {
val searchBarStyle by viewModel.searchBarStyle.observeAsState()
SearchBarStylePreference(
@ -123,14 +148,15 @@ fun SearchBarStylePreference(
Preference(title = title, summary = summary, onClick = { showDialog = true })
if (showDialog && value != null) {
val styles = remember {
SearchBarSettings.SearchBarStyle.values().filter { it != SearchBarSettings.SearchBarStyle.UNRECOGNIZED }
SearchBarSettings.SearchBarStyle.values()
.filter { it != SearchBarSettings.SearchBarStyle.UNRECOGNIZED }
}
val pagerState = rememberPagerState(styles.indexOf(value))
var level by remember { mutableStateOf(SearchBarLevel.Resting) }
var previewSearchValue by remember { mutableStateOf("") }
LaunchedEffect(null) {
while(isActive) {
while (isActive) {
delay(2000)
level = SearchBarLevel.Active
delay(1000)
@ -181,11 +207,108 @@ fun SearchBarStylePreference(
.padding(bottom = 16.dp)
.background(MaterialTheme.colorScheme.secondary)
) {
SearchBar(level = level, style = styles[it], websearches = emptyList(), value = previewSearchValue, onValueChange = {})
SearchBar(
level = level,
style = styles[it],
websearches = emptyList(),
value = previewSearchValue,
onValueChange = {})
}
HorizontalPagerIndicator(pagerState = pagerState)
}
}
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun IconShapePreference(
title: String,
summary: String? = null,
value: IconSettings.IconShape?,
onValueChanged: (IconSettings.IconShape) -> Unit
) {
var showDialog by remember { mutableStateOf(false) }
Preference(title = title, summary = summary, onClick = { showDialog = true })
if (showDialog && value != null) {
val shapes = remember {
IconSettings.IconShape.values()
.filter { it != IconSettings.IconShape.UNRECOGNIZED && it != IconSettings.IconShape.EasterEgg }
}
Dialog(onDismissRequest = { showDialog = false }) {
Surface(
tonalElevation = 16.dp,
shadowElevation = 16.dp,
shape = RoundedCornerShape(16.dp),
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(
cells = GridCells.Adaptive(96.dp),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp, start = 16.dp, end = 16.dp)
) {
items(shapes) {
Column(modifier = Modifier
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally) {
AndroidView(factory = { context ->
LauncherIconView(context).apply {
shape = it
icon = LauncherIcon(
foreground = AppCompatResources.getDrawable(context, R.mipmap.ic_launcher_foreground)!!,
background = ColorDrawable(context.getColor(R.color.ic_launcher_background))
)
setOnClickListener { _ ->
onValueChanged(it)
showDialog = false
}
layoutParams = ViewGroup.LayoutParams(
(48 * context.dp).toInt(),
(48 * context.dp).toInt(),
)
}
})
Text(
getShapeName(it) ?: "",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.labelMedium,
modifier = Modifier.padding(top = 4.dp))
}
}
}
}
}
}
}
}
@Composable
private fun getShapeName(shape: IconSettings.IconShape?): String? {
return stringResource(when (shape) {
IconSettings.IconShape.Triangle -> R.string.preference_icon_shape_triangle
IconSettings.IconShape.Hexagon -> R.string.preference_icon_shape_hexagon
IconSettings.IconShape.RoundedSquare -> R.string.preference_icon_shape_rounded_square
IconSettings.IconShape.Squircle -> R.string.preference_icon_shape_squircle
IconSettings.IconShape.Square -> R.string.preference_icon_shape_square
IconSettings.IconShape.Pentagon -> R.string.preference_icon_shape_pentagon
IconSettings.IconShape.PlatformDefault -> R.string.preference_icon_shape_platform
IconSettings.IconShape.Circle -> R.string.preference_icon_shape_circle
else -> return null
})
}

View File

@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
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.SearchBarSettings
@ -70,8 +71,23 @@ class AppearanceSettingsScreenVM : ViewModel(), KoinComponent {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setSearchBar(it.searchBar.toBuilder()
.setSearchBarStyle(searchBarStyle)
.setSearchBar(
it.searchBar.toBuilder()
.setSearchBarStyle(searchBarStyle)
)
.build()
}
}
}
val iconShape = dataStore.data.map { it.icons.shape }.asLiveData()
fun setIconShape(iconShape: Settings.IconSettings.IconShape) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setIcons(
it.icons.toBuilder()
.setShape(iconShape)
)
.build()
}