Allow resizing of custom clock widgets
This commit is contained in:
parent
64002c9f38
commit
38c048ec03
@ -163,7 +163,7 @@ fun ClockWidget(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.then(if (fillScreenHeight) Modifier.weight(1f) else Modifier)
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth().padding(horizontal = 24.dp),
|
||||
contentAlignment = when (alignment) {
|
||||
ClockWidgetAlignment.Center -> Alignment.Center
|
||||
ClockWidgetAlignment.Top -> Alignment.TopCenter
|
||||
@ -408,6 +408,7 @@ fun ConfigureClockWidgetSheet(
|
||||
styles = availableStyles,
|
||||
compact = compact!!,
|
||||
colors = color!!,
|
||||
themeColors = useAccentColor,
|
||||
selected = style,
|
||||
onSelect = {
|
||||
viewModel.setClockStyle(it)
|
||||
|
||||
@ -5,34 +5,49 @@ import android.app.ActivityOptions
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ArrowDropDown
|
||||
import androidx.compose.material.icons.rounded.Check
|
||||
import androidx.compose.material.icons.rounded.CheckCircle
|
||||
import androidx.compose.material.icons.rounded.ChevronLeft
|
||||
import androidx.compose.material.icons.rounded.ChevronRight
|
||||
import androidx.compose.material.icons.rounded.CropFree
|
||||
import androidx.compose.material.icons.rounded.Done
|
||||
import androidx.compose.material.icons.rounded.PhotoSizeSelectSmall
|
||||
import androidx.compose.material.icons.rounded.RadioButtonUnchecked
|
||||
import androidx.compose.material.icons.rounded.RestartAlt
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material.icons.rounded.SwapHoriz
|
||||
import androidx.compose.material.icons.rounded.Tune
|
||||
import androidx.compose.material.icons.rounded.Widgets
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@ -48,16 +63,26 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
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.unit.coerceAtMost
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.isUnspecified
|
||||
import androidx.compose.ui.zIndex
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
import de.mm20.launcher2.preferences.ClockWidgetColors
|
||||
import de.mm20.launcher2.preferences.ClockWidgetStyle
|
||||
import de.mm20.launcher2.ui.BuildConfig
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.base.LocalAppWidgetHost
|
||||
import de.mm20.launcher2.ui.component.DragResizeHandle
|
||||
import de.mm20.launcher2.ui.ktx.toDp
|
||||
import de.mm20.launcher2.ui.launcher.sheets.WidgetPickerSheet
|
||||
import de.mm20.launcher2.ui.launcher.widgets.external.AppWidgetHost
|
||||
import de.mm20.launcher2.ui.locals.LocalDarkTheme
|
||||
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
|
||||
import de.mm20.launcher2.widgets.AppWidget
|
||||
@ -68,24 +93,41 @@ fun WatchFaceSelector(
|
||||
styles: List<ClockWidgetStyle>,
|
||||
compact: Boolean,
|
||||
colors: ClockWidgetColors,
|
||||
themeColors: Boolean,
|
||||
selected: ClockWidgetStyle?,
|
||||
onSelect: (ClockWidgetStyle) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
var showWidgetPicker by rememberSaveable { mutableStateOf(false) }
|
||||
var resizeCustomWidget by remember { mutableStateOf(false) }
|
||||
|
||||
val lightBackground =
|
||||
colors == ClockWidgetColors.Dark || colors == ClockWidgetColors.Auto && LocalPreferDarkContentOverWallpaper.current
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp, horizontal = 32.dp),
|
||||
color = if (colors == ClockWidgetColors.Dark || colors == ClockWidgetColors.Auto && LocalPreferDarkContentOverWallpaper.current) {
|
||||
.padding(vertical = 16.dp, horizontal = 0.dp),
|
||||
color = if (lightBackground) {
|
||||
if (LocalDarkTheme.current) MaterialTheme.colorScheme.inverseSurface else MaterialTheme.colorScheme.surfaceContainer
|
||||
} else {
|
||||
if (LocalDarkTheme.current) MaterialTheme.colorScheme.surfaceContainer else MaterialTheme.colorScheme.inverseSurface
|
||||
},
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
) {
|
||||
AnimatedContent(resizeCustomWidget) { resize ->
|
||||
if (resize && selected is ClockWidgetStyle.Custom) {
|
||||
ResizeCustomWidget(
|
||||
style = selected,
|
||||
compact = compact,
|
||||
themeColors = themeColors,
|
||||
lightBackground = lightBackground,
|
||||
onChange = { onSelect(it) },
|
||||
onExit = { resizeCustomWidget = false }
|
||||
)
|
||||
return@AnimatedContent
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier,
|
||||
) {
|
||||
@ -106,7 +148,7 @@ fun WatchFaceSelector(
|
||||
|
||||
Box {
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
selected is ClockWidgetStyle.Digital1 || selected is ClockWidgetStyle.Custom,
|
||||
selected is ClockWidgetStyle.Digital1 || (selected is ClockWidgetStyle.Custom && selected.widgetId != null),
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.zIndex(1f),
|
||||
@ -127,9 +169,11 @@ fun WatchFaceSelector(
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.clock_variant_outlined)) },
|
||||
leadingIcon = {
|
||||
if (selected.outlined) {
|
||||
Icon(Icons.Rounded.Check, null)
|
||||
}
|
||||
Icon(
|
||||
if (selected.outlined) Icons.Rounded.CheckCircle
|
||||
else Icons.Rounded.RadioButtonUnchecked,
|
||||
null
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onSelect(selected.copy(outlined = !selected.outlined))
|
||||
@ -140,13 +184,20 @@ fun WatchFaceSelector(
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.widget_pick_widget)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Rounded.Widgets, null)
|
||||
Icon(Icons.Rounded.SwapHoriz, null)
|
||||
},
|
||||
onClick = {
|
||||
showWidgetPicker = true
|
||||
showStyleSettings = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(Icons.Rounded.PhotoSizeSelectSmall, null)
|
||||
},
|
||||
text = { Text(stringResource(R.string.widget_config_appwidget_resize)) },
|
||||
onClick = { resizeCustomWidget = true }
|
||||
)
|
||||
val widget = remember(selected.widgetId) {
|
||||
val id = selected.widgetId ?: return@remember null
|
||||
AppWidgetManager.getInstance(context)
|
||||
@ -181,12 +232,30 @@ fun WatchFaceSelector(
|
||||
}
|
||||
)
|
||||
}
|
||||
if (BuildConfig.DEBUG) {
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(Icons.Rounded.RestartAlt, null)
|
||||
},
|
||||
text = { Text("Reset") },
|
||||
onClick = {
|
||||
val widgetId = selected.widgetId
|
||||
if (widgetId != null) {
|
||||
appWidgetHost.deleteAppWidgetId(widgetId)
|
||||
}
|
||||
onSelect(
|
||||
ClockWidgetStyle.Custom()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val darkColors = colors == ClockWidgetColors.Auto && LocalPreferDarkContentOverWallpaper.current || colors == ClockWidgetColors.Dark
|
||||
val darkColors =
|
||||
colors == ClockWidgetColors.Auto && LocalPreferDarkContentOverWallpaper.current || colors == ClockWidgetColors.Dark
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides if (darkColors) {
|
||||
@ -201,13 +270,47 @@ fun WatchFaceSelector(
|
||||
state = pagerState,
|
||||
verticalAlignment = Alignment.Top,
|
||||
) { pageIndex ->
|
||||
val currentPageStyle = styles[pageIndex]
|
||||
if (currentPageStyle is ClockWidgetStyle.Custom && currentPageStyle.widgetId == null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 24.dp, bottom = 8.dp),
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
) {
|
||||
val currentPageStyle = styles[pageIndex]
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
showWidgetPicker = true
|
||||
},
|
||||
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.padding(end = ButtonDefaults.IconSpacing)
|
||||
.size(ButtonDefaults.IconSize),
|
||||
imageVector = Icons.Rounded.Widgets,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(stringResource(R.string.widget_pick_widget))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 24.dp, bottom = 8.dp)
|
||||
.pointerInput(Unit) {
|
||||
awaitEachGesture {
|
||||
val event =
|
||||
awaitFirstDown(pass = PointerEventPass.Initial)
|
||||
event.consume()
|
||||
}
|
||||
},
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
) {
|
||||
|
||||
if (currentPageStyle.javaClass == selected?.javaClass) {
|
||||
Clock(selected, compact, darkColors)
|
||||
} else {
|
||||
@ -215,6 +318,7 @@ fun WatchFaceSelector(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -301,6 +405,7 @@ fun WatchFaceSelector(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showWidgetPicker && selected is ClockWidgetStyle.Custom) {
|
||||
val previousWidgetId = selected.widgetId
|
||||
@ -311,7 +416,14 @@ fun WatchFaceSelector(
|
||||
if (previousWidgetId != null) {
|
||||
appWidgetHost.deleteAppWidgetId(previousWidgetId)
|
||||
}
|
||||
onSelect(selected.copy(widgetId = (it as AppWidget).config.widgetId))
|
||||
it as AppWidget
|
||||
onSelect(
|
||||
selected.copy(
|
||||
widgetId = it.config.widgetId,
|
||||
width = it.config.width,
|
||||
height = it.config.height,
|
||||
)
|
||||
)
|
||||
},
|
||||
onDismiss = {
|
||||
showWidgetPicker = false
|
||||
@ -333,3 +445,145 @@ fun getClockStyleName(context: Context, style: ClockWidgetStyle): String {
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ResizeCustomWidget(
|
||||
style: ClockWidgetStyle.Custom,
|
||||
compact: Boolean,
|
||||
themeColors: Boolean,
|
||||
lightBackground: Boolean,
|
||||
onChange: (ClockWidgetStyle.Custom) -> Unit,
|
||||
onExit: () -> Unit = {},
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val widgetId = style.widgetId
|
||||
|
||||
val widgetInfo = remember(widgetId) {
|
||||
widgetId?.let {
|
||||
AppWidgetManager.getInstance(context)
|
||||
.getAppWidgetInfo(it)
|
||||
}
|
||||
}
|
||||
if (widgetId != null && widgetInfo != null) {
|
||||
val minWidth = when {
|
||||
compact -> 64.dp
|
||||
widgetInfo.minResizeWidth in 1..widgetInfo.minWidth -> {
|
||||
widgetInfo.minResizeWidth.toDp()
|
||||
}
|
||||
|
||||
else -> {
|
||||
widgetInfo.minWidth.toDp()
|
||||
}
|
||||
}
|
||||
|
||||
val minHeight = when {
|
||||
compact -> 16.dp
|
||||
widgetInfo.minResizeHeight in 1..widgetInfo.minHeight -> {
|
||||
widgetInfo.minResizeHeight.toDp()
|
||||
}
|
||||
|
||||
else -> {
|
||||
widgetInfo.minHeight.toDp()
|
||||
}
|
||||
}
|
||||
|
||||
val maxWidth = when {
|
||||
compact -> 200.dp
|
||||
isAtLeastApiLevel(31) && widgetInfo.maxResizeWidth > 0 -> {
|
||||
widgetInfo.maxResizeWidth.toDp()
|
||||
}
|
||||
|
||||
else -> Dp.Unspecified
|
||||
}
|
||||
|
||||
val maxHeight = when {
|
||||
compact -> 64.dp
|
||||
isAtLeastApiLevel(31) && widgetInfo.maxResizeHeight > 0 -> {
|
||||
widgetInfo.maxResizeHeight.toDp()
|
||||
}
|
||||
|
||||
else -> Dp.Unspecified
|
||||
}
|
||||
|
||||
|
||||
var resizeWidth by remember(style.widgetId) {
|
||||
mutableStateOf(
|
||||
style.width?.dp ?: Dp.Unspecified
|
||||
)
|
||||
}
|
||||
var resizeHeight by remember(style.widgetId) { mutableStateOf(style.height.dp) }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = 16.dp, bottom = 64.dp),
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
) {
|
||||
AppWidgetHost(
|
||||
widgetInfo = widgetInfo,
|
||||
widgetId = widgetId,
|
||||
modifier = Modifier
|
||||
.then(
|
||||
when {
|
||||
compact && resizeWidth.isUnspecified -> Modifier.widthIn(max = 200.dp)
|
||||
compact && !resizeWidth.isUnspecified -> Modifier.width(
|
||||
resizeWidth.coerceAtMost(
|
||||
200.dp
|
||||
)
|
||||
)
|
||||
|
||||
!compact && !resizeWidth.isUnspecified -> Modifier.width(resizeWidth)
|
||||
else -> Modifier.fillMaxWidth()
|
||||
}
|
||||
)
|
||||
.then(
|
||||
when {
|
||||
compact -> Modifier.height(resizeHeight.coerceAtMost(64.dp))
|
||||
else -> Modifier.height(resizeHeight)
|
||||
}
|
||||
)
|
||||
.pointerInput(Unit) {
|
||||
awaitEachGesture {
|
||||
val event = awaitFirstDown(pass = PointerEventPass.Initial)
|
||||
event.consume()
|
||||
}
|
||||
},
|
||||
borderless = compact,
|
||||
useThemeColors = themeColors,
|
||||
onLightBackground = lightBackground,
|
||||
)
|
||||
|
||||
DragResizeHandle(
|
||||
alignment = Alignment.TopCenter,
|
||||
width = resizeWidth.coerceAtMost(maxWidth),
|
||||
height = resizeHeight.coerceAtMost(maxHeight),
|
||||
minWidth = minWidth,
|
||||
minHeight = minHeight,
|
||||
maxWidth = maxWidth,
|
||||
maxHeight = if (compact) 64.dp else Dp.Unspecified,
|
||||
onResize = { w, h ->
|
||||
resizeWidth = w
|
||||
resizeHeight = h
|
||||
},
|
||||
onResizeStopped = {
|
||||
onChange(
|
||||
style.copy(
|
||||
width = resizeWidth.value.toInt(),
|
||||
height = resizeHeight.value.toInt()
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
FilledIconButton(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(8.dp)
|
||||
.offset(y = 64.dp),
|
||||
onClick = onExit
|
||||
) {
|
||||
Icon(Icons.Rounded.Done, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,10 @@
|
||||
package de.mm20.launcher2.ui.launcher.widgets.clock.clocks
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -21,22 +24,36 @@ fun CustomClock(
|
||||
) {
|
||||
val widgetId = style.widgetId
|
||||
|
||||
if (widgetId == null) {
|
||||
Text("Hmmm…")
|
||||
} else {
|
||||
if (widgetId != null) {
|
||||
val context = LocalContext.current
|
||||
val widgetInfo = remember(widgetId) {
|
||||
AppWidgetManager.getInstance(context)
|
||||
.getAppWidgetInfo(widgetId)
|
||||
}
|
||||
if (widgetInfo != null) {
|
||||
val width = style.width
|
||||
val height = style.height
|
||||
AppWidgetHost(
|
||||
widgetInfo = widgetInfo,
|
||||
widgetId = widgetId,
|
||||
useThemeColors = useThemeColor,
|
||||
onLightBackground = darkColors,
|
||||
borderless = compact,
|
||||
modifier = Modifier.widthIn(max = 250.dp).height(if (compact) 64.dp else 200.dp)
|
||||
modifier = Modifier
|
||||
.then(
|
||||
when {
|
||||
compact && width == null -> Modifier.widthIn(max = 200.dp)
|
||||
compact && width != null -> Modifier.width(width.coerceAtMost(200).dp)
|
||||
!compact && width != null -> Modifier.width(width.dp)
|
||||
else -> Modifier.fillMaxWidth()
|
||||
}
|
||||
)
|
||||
.then(
|
||||
when {
|
||||
compact -> Modifier.height(height.coerceAtMost(64).dp)
|
||||
else -> Modifier.height(height.dp)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -843,6 +843,7 @@
|
||||
<string name="widget_config_appwidget_background">Background card</string>
|
||||
<string name="widget_config_appwidget_configure">Configure widget</string>
|
||||
<string name="widget_config_appwidget_resize_hint">Drag to resize</string>
|
||||
<string name="widget_config_appwidget_resize">Resize</string>
|
||||
<string name="widget_config_weather_integration_settings">Weather integration settings</string>
|
||||
<string name="widget_config_calendar_no_calendars">No calendars found</string>
|
||||
<string name="widget_pick_widget">Pick widget</string>
|
||||
|
||||
@ -239,7 +239,11 @@ sealed interface ClockWidgetStyle {
|
||||
|
||||
@Serializable
|
||||
@SerialName("custom")
|
||||
data class Custom(val widgetId: Int? = null) : ClockWidgetStyle
|
||||
data class Custom(
|
||||
val widgetId: Int? = null,
|
||||
val width: Int? = null,
|
||||
val height: Int = 200,
|
||||
) : ClockWidgetStyle
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user