Add horizontal appwidget resizing
This commit is contained in:
parent
7056c5963a
commit
1a45ae9003
@ -0,0 +1,183 @@
|
||||
package de.mm20.launcher2.ui.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.draggable
|
||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
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.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.layout.requiredWidth
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.UnfoldMore
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.coerceIn
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.isUnspecified
|
||||
|
||||
enum class ResizeAxis {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
Both,
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DragResizeHandle(
|
||||
resizeAxis: ResizeAxis = ResizeAxis.Both,
|
||||
alignment: Alignment = Alignment.TopStart,
|
||||
minWidth: Dp = 0.dp,
|
||||
minHeight: Dp = 0.dp,
|
||||
maxWidth: Dp = Dp.Infinity,
|
||||
maxHeight: Dp = Dp.Infinity,
|
||||
snapToMeasuredWidth: Boolean = false,
|
||||
snapToMeasuredHeight: Boolean = false,
|
||||
width: Dp = Dp.Unspecified,
|
||||
height: Dp = Dp.Unspecified,
|
||||
onResize: (width: Dp, height: Dp) -> Unit,
|
||||
onResizeStopped: () -> Unit = { },
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
val measuredWidth = this.maxWidth
|
||||
val measuredHeight = this.maxHeight
|
||||
|
||||
val density = LocalDensity.current
|
||||
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.then(if (width.isUnspecified) Modifier.fillMaxWidth() else Modifier.requiredWidth(width))
|
||||
.then(if (height.isUnspecified) Modifier.fillMaxHeight() else Modifier.requiredHeight(height))
|
||||
.align(alignment)
|
||||
.border(1.dp, color = MaterialTheme.colorScheme.primary, MaterialTheme.shapes.small)
|
||||
) {
|
||||
if (resizeAxis == ResizeAxis.Both || resizeAxis == ResizeAxis.Horizontal) {
|
||||
val horizontalDragState = rememberDraggableState {
|
||||
val currentWidth = if (width.isUnspecified) measuredWidth else width
|
||||
val dragDelta =
|
||||
when (alignment) {
|
||||
Alignment.Center, Alignment.TopCenter, Alignment.BottomCenter -> it * 2
|
||||
else -> it
|
||||
}
|
||||
|
||||
val newWidth = (currentWidth + with(density) { dragDelta.toDp() }).coerceIn(
|
||||
minWidth,
|
||||
maxWidth
|
||||
)
|
||||
|
||||
if (snapToMeasuredWidth &&
|
||||
maxWidth >= measuredWidth &&
|
||||
newWidth > measuredWidth - 16.dp &&
|
||||
dragDelta > 0
|
||||
) {
|
||||
if (!width.isUnspecified) {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
onResize(Dp.Unspecified, height)
|
||||
}
|
||||
} else {
|
||||
onResize(newWidth, height)
|
||||
}
|
||||
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.draggable(
|
||||
state = horizontalDragState,
|
||||
orientation = Orientation.Horizontal,
|
||||
onDragStopped = {
|
||||
onResizeStopped()
|
||||
},
|
||||
startDragImmediately = true,
|
||||
)
|
||||
.requiredSize(128.dp)
|
||||
.offset(x = 64.dp)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.primary, CircleShape)
|
||||
.padding(8.dp)
|
||||
.rotate(90f)
|
||||
.align(Alignment.Center),
|
||||
imageVector = Icons.Rounded.UnfoldMore,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (resizeAxis == ResizeAxis.Both || resizeAxis == ResizeAxis.Vertical) {
|
||||
val verticalDragState = rememberDraggableState {
|
||||
val currentHeight = if (height.isUnspecified) measuredHeight else height
|
||||
val dragDelta =
|
||||
when (alignment) {
|
||||
Alignment.Center, Alignment.CenterStart, Alignment.CenterEnd -> it * 2
|
||||
else -> it
|
||||
}
|
||||
val newHeight = (currentHeight + with(density) { dragDelta.toDp() }).coerceIn(
|
||||
minHeight,
|
||||
maxHeight
|
||||
)
|
||||
|
||||
if (snapToMeasuredHeight &&
|
||||
maxHeight >= measuredHeight &&
|
||||
newHeight > measuredHeight - 16.dp &&
|
||||
dragDelta > 0
|
||||
) {
|
||||
if (!height.isUnspecified) {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
onResize(width, Dp.Unspecified)
|
||||
}
|
||||
} else {
|
||||
onResize(width, newHeight)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.draggable(
|
||||
state = verticalDragState,
|
||||
orientation = Orientation.Vertical,
|
||||
onDragStopped = {
|
||||
onResizeStopped()
|
||||
},
|
||||
startDragImmediately = true,
|
||||
)
|
||||
.requiredSize(128.dp)
|
||||
.offset(y = 64.dp)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.primary, CircleShape)
|
||||
.padding(8.dp)
|
||||
.align(Alignment.Center),
|
||||
imageVector = Icons.Rounded.UnfoldMore,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,8 @@ import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.gestures.draggable
|
||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@ -26,6 +28,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.layout.requiredWidth
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
@ -65,13 +68,17 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.isUnspecified
|
||||
import androidx.core.net.toUri
|
||||
import de.mm20.launcher2.calendar.CalendarRepository
|
||||
import de.mm20.launcher2.calendar.UserCalendar
|
||||
@ -82,6 +89,7 @@ import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.base.LocalAppWidgetHost
|
||||
import de.mm20.launcher2.ui.component.BottomSheetDialog
|
||||
import de.mm20.launcher2.ui.component.DragResizeHandle
|
||||
import de.mm20.launcher2.ui.component.LargeMessage
|
||||
import de.mm20.launcher2.ui.component.MissingPermissionBanner
|
||||
import de.mm20.launcher2.ui.component.preferences.CheckboxPreference
|
||||
@ -338,111 +346,58 @@ fun ColumnScope.ConfigureAppWidget(
|
||||
return
|
||||
}
|
||||
|
||||
var dragDelta by remember { mutableStateOf(0) }
|
||||
Column {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.padding(bottom = 64.dp)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant, MaterialTheme.shapes.medium)
|
||||
) {
|
||||
var resizeWidth by remember {
|
||||
mutableStateOf(widget.config.width?.dp ?: Dp.Unspecified)
|
||||
}
|
||||
|
||||
var resizeHeight by remember {
|
||||
mutableStateOf(widget.config.height.dp)
|
||||
}
|
||||
|
||||
AppWidgetHost(
|
||||
widgetInfo = widgetInfo,
|
||||
widgetId = widget.config.widgetId,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
height = widget.config.height + dragDelta,
|
||||
modifier = Modifier
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.then(if (resizeWidth.isUnspecified) Modifier.fillMaxWidth() else Modifier.requiredWidth(resizeWidth))
|
||||
.height(resizeHeight)
|
||||
.align(Alignment.TopCenter)
|
||||
.pointerInput(Unit) {
|
||||
awaitEachGesture {
|
||||
val event = awaitFirstDown(pass = PointerEventPass.Initial)
|
||||
event.consume()
|
||||
}
|
||||
},
|
||||
borderless = widget.config.borderless,
|
||||
useThemeColors = widget.config.themeColors,
|
||||
onLightBackground = (!LocalDarkTheme.current && widget.config.background) || LocalPreferDarkContentOverWallpaper.current
|
||||
)
|
||||
}
|
||||
val density = LocalDensity.current
|
||||
val draggableState = rememberDraggableState {
|
||||
dragDelta = (dragDelta + it / density.density).roundToInt()
|
||||
.coerceIn(
|
||||
-widget.config.height + 1,
|
||||
500 - widget.config.height
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp, bottom = 16.dp)
|
||||
.padding(horizontal = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val tooltipState = rememberTooltipState()
|
||||
TooltipBox(
|
||||
state = tooltipState,
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(stringResource(R.string.widget_config_appwidget_resize_hint))
|
||||
}
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||
.height(36.dp)
|
||||
.width(48.dp)
|
||||
.draggable(
|
||||
state = draggableState,
|
||||
orientation = Orientation.Vertical,
|
||||
onDragStopped = {
|
||||
onWidgetUpdated(
|
||||
widget.copy(
|
||||
config = widget.config.copy(
|
||||
height = widget.config.height + dragDelta
|
||||
)
|
||||
)
|
||||
)
|
||||
dragDelta = 0
|
||||
}
|
||||
)
|
||||
.clickable {
|
||||
scope.launch {
|
||||
tooltipState.show()
|
||||
}
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.UnfoldMore,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
var textFieldValue by remember(widget.config.height) { mutableStateOf(widget.config.height.toString()) }
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(bottom = 8.dp),
|
||||
value = (widget.config.height + dragDelta).toString(),
|
||||
onValueChange = {
|
||||
val intValue = it.toIntOrNull()
|
||||
if (intValue == null) textFieldValue = ""
|
||||
else if (intValue in 1..1000) {
|
||||
onWidgetUpdated(
|
||||
widget.copy(
|
||||
config = widget.config.copy(
|
||||
height = intValue
|
||||
)
|
||||
DragResizeHandle(
|
||||
alignment = Alignment.TopCenter,
|
||||
height = resizeHeight,
|
||||
width = resizeWidth,
|
||||
snapToMeasuredWidth = true,
|
||||
onResize = { w, h ->
|
||||
resizeWidth = w
|
||||
resizeHeight = h
|
||||
},
|
||||
onResizeStopped = {
|
||||
onWidgetUpdated(
|
||||
widget.copy(
|
||||
config = widget.config.copy(
|
||||
height = resizeHeight.value.roundToInt(),
|
||||
width = resizeWidth.takeIf { it != Dp.Unspecified }?.value?.roundToInt()
|
||||
)
|
||||
)
|
||||
textFieldValue = intValue.toString()
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(R.string.widget_config_appwidget_height)) },
|
||||
suffix = { Text("dp") },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number,
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package de.mm20.launcher2.ui.launcher.widgets.clock.clocks
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -32,11 +33,10 @@ fun CustomClock(
|
||||
AppWidgetHost(
|
||||
widgetInfo = widgetInfo,
|
||||
widgetId = widgetId,
|
||||
height = if (compact) 64 else 200,
|
||||
useThemeColors = useThemeColor,
|
||||
onLightBackground = darkColors,
|
||||
borderless = compact,
|
||||
modifier = Modifier.widthIn(max = 250.dp)
|
||||
modifier = Modifier.widthIn(max = 250.dp).height(if (compact) 64.dp else 200.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,11 @@ package de.mm20.launcher2.ui.launcher.widgets.external
|
||||
|
||||
import android.appwidget.AppWidgetHost
|
||||
import android.appwidget.AppWidgetManager
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredWidth
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Warning
|
||||
import androidx.compose.material3.Button
|
||||
@ -15,10 +18,12 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.isUnspecified
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.Banner
|
||||
import de.mm20.launcher2.ui.ktx.toPixels
|
||||
@ -90,14 +95,22 @@ fun AppWidget(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
AppWidgetHost(
|
||||
widgetId = widget.config.widgetId,
|
||||
widgetInfo = widgetInfo,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
height = widget.config.height,
|
||||
borderless = widget.config.borderless,
|
||||
useThemeColors = widget.config.themeColors,
|
||||
onLightBackground = lightBackground,
|
||||
)
|
||||
val width = widget.config.width
|
||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
AppWidgetHost(
|
||||
widgetId = widget.config.widgetId,
|
||||
widgetInfo = widgetInfo,
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (width == null) Modifier.fillMaxWidth() else Modifier.requiredWidth(
|
||||
width.dp
|
||||
)
|
||||
)
|
||||
.height(widget.config.height.dp),
|
||||
borderless = widget.config.borderless,
|
||||
useThemeColors = widget.config.themeColors,
|
||||
onLightBackground = lightBackground,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,8 @@
|
||||
package de.mm20.launcher2.ui.launcher.widgets.external
|
||||
|
||||
import android.appwidget.AppWidgetHost
|
||||
import android.appwidget.AppWidgetProviderInfo
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.util.SparseIntArray
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@ -12,6 +10,7 @@ import android.widget.ListView
|
||||
import android.widget.ScrollView
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@ -33,7 +32,6 @@ import kotlin.math.roundToInt
|
||||
fun AppWidgetHost(
|
||||
widgetInfo: AppWidgetProviderInfo,
|
||||
widgetId: Int,
|
||||
height: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
borderless: Boolean = false,
|
||||
useThemeColors: Boolean = false,
|
||||
@ -44,12 +42,15 @@ fun AppWidgetHost(
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val appWidgetHost = LocalAppWidgetHost.current
|
||||
|
||||
BoxWithConstraints {
|
||||
BoxWithConstraints(
|
||||
modifier = modifier,
|
||||
) {
|
||||
val maxWidth = maxWidth
|
||||
val maxHeight = maxHeight
|
||||
key(widgetId) {
|
||||
AndroidView(
|
||||
modifier = modifier
|
||||
.height(height.dp),
|
||||
.fillMaxSize(),
|
||||
factory = {
|
||||
val view = appWidgetHost.createView(it.applicationContext, widgetId, widgetInfo)
|
||||
enableNestedScroll(view)
|
||||
@ -71,9 +72,9 @@ fun AppWidgetHost(
|
||||
it.updateAppWidgetSize(
|
||||
null,
|
||||
maxWidth.value.roundToInt(),
|
||||
height,
|
||||
maxHeight.value.roundToInt(),
|
||||
maxWidth.value.roundToInt(),
|
||||
height,
|
||||
maxHeight.value.roundToInt(),
|
||||
)
|
||||
it.setPadding(padding)
|
||||
// Workaround to force update of the widget view
|
||||
|
||||
@ -15,6 +15,7 @@ import java.util.UUID
|
||||
data class AppWidgetConfig(
|
||||
val widgetId: Int,
|
||||
val height: Int,
|
||||
val width: Int? = null,
|
||||
val borderless: Boolean = false,
|
||||
val background: Boolean = true,
|
||||
val themeColors: Boolean = true,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user