Refactor widgets

This commit is contained in:
MM20
2022-03-29 18:14:07 +02:00
parent 2e43b871c5
commit 956af82f79
21 changed files with 804 additions and 846 deletions
@@ -173,7 +173,7 @@ class LauncherScaffoldView @JvmOverloads constructor(
}
widgetsViewModel.isEditMode.observe(context) {
OneShotLayoutTransition.run(binding.scrollContainer)
//OneShotLayoutTransition.run(binding.scrollContainer)
if (it) {
binding.scrollView.setOnTouchListener(null)
binding.searchBar.visibility = View.INVISIBLE
@@ -0,0 +1,164 @@
package de.mm20.launcher2.ui.launcher.widgets
import android.app.Activity
import android.appwidget.AppWidgetHost
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.gestures.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.LauncherCard
import de.mm20.launcher2.ui.launcher.widgets.calendar.CalendarWidget
import de.mm20.launcher2.ui.launcher.widgets.external.ExternalWidget
import de.mm20.launcher2.ui.launcher.widgets.music.MusicWidget
import de.mm20.launcher2.ui.launcher.widgets.weather.WeatherWidget
import de.mm20.launcher2.widgets.*
import java.lang.Integer.max
import kotlin.math.roundToInt
@Composable
fun WidgetItem(
widget: Widget,
appWidgetHost: AppWidgetHost,
modifier: Modifier = Modifier,
editMode: Boolean = false,
onWidgetResize: (newHeight: Int) -> Unit = {},
onWidgetRemove: () -> Unit = {},
draggableState: DraggableState = rememberDraggableState {},
onDragStopped: () -> Unit = {}
) {
val context = LocalContext.current
var resizeMode by remember(editMode) { mutableStateOf(false) }
var isDragged by remember { mutableStateOf(false) }
val elevation by animateDpAsState(if (isDragged) 8.dp else 2.dp)
LauncherCard(
modifier = modifier.zIndex(if (isDragged) 1f else 0f),
elevation = elevation
) {
Column {
AnimatedVisibility(editMode) {
Row(
modifier = Modifier.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Rounded.DragIndicator,
contentDescription = null,
modifier = Modifier.draggable(
state = draggableState,
orientation = Orientation.Vertical,
startDragImmediately = true,
onDragStarted = {
isDragged = true
},
onDragStopped = {
isDragged = false
onDragStopped()
}
)
)
Text(
text = remember(widget) { widget.loadLabel(context) },
style = MaterialTheme.typography.titleMedium,
modifier = Modifier
.weight(1f)
.padding(horizontal = 8.dp),
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
if (widget is ExternalWidget) {
IconButton(onClick = { resizeMode = !resizeMode }) {
Icon(
imageVector = Icons.Rounded.Edit,
contentDescription = stringResource(R.string.widget_action_adjust_height)
)
}
}
if (widget.isConfigurable) {
IconButton({
widget.configure(context as Activity, appWidgetHost)
}) {
Icon(
imageVector = Icons.Rounded.Settings,
contentDescription = stringResource(R.string.settings)
)
}
}
IconButton(onClick = { onWidgetRemove() }) {
Icon(
imageVector = Icons.Rounded.Delete,
contentDescription = stringResource(R.string.widget_action_remove)
)
}
}
}
AnimatedVisibility(!editMode || resizeMode) {
when (widget) {
is WeatherWidget -> {
WeatherWidget()
}
is MusicWidget -> {
MusicWidget()
}
is CalendarWidget -> {
CalendarWidget()
}
is ExternalWidget -> {
var height by remember(widget) { mutableStateOf(widget.height) }
Column {
ExternalWidget(
appWidgetHost = appWidgetHost,
widgetId = widget.widgetId,
modifier = Modifier.fillMaxWidth(),
height = height,
)
if (resizeMode) {
val density = LocalDensity.current
val drgStt = rememberDraggableState {
height = max(
height + (it / density.density).roundToInt(),
widget.widgetProviderInfo.minResizeHeight
)
}
Icon(
imageVector = Icons.Rounded.DragHandle,
contentDescription = null,
modifier = Modifier
.padding(top = 12.dp)
.requiredHeight(24.dp)
.fillMaxWidth()
.draggable(
state = drgStt,
orientation = Orientation.Vertical,
startDragImmediately = true,
onDragStopped = {
onWidgetResize(height)
}
)
)
}
}
}
}
}
}
}
}
@@ -1,11 +1,17 @@
package de.mm20.launcher2.ui.launcher.widgets
import android.appwidget.AppWidgetManager
import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
import de.mm20.launcher2.widgets.ExternalWidget
import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetRepository
import de.mm20.launcher2.widgets.WidgetType
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -20,11 +26,47 @@ class WidgetsVM : ViewModel(), KoinComponent {
isEditMode.value = editMode
}
fun saveWidgets(widgets: List<Widget>) {
fun addWidget(widget: Widget) {
widgetRepository.addWidget(widget, widgets.value?.size ?: 0)
}
fun addAppWidget(context: Context, widgetId: Int) {
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return
val appWidget = AppWidgetManager.getInstance(context)
.getAppWidgetInfo(widgetId) ?: return
val widget = ExternalWidget(
widgetProviderInfo = appWidget,
height = appWidget.minHeight,
widgetId = widgetId,
)
addWidget(widget)
}
fun removeWidget(widget: Widget) {
widgetRepository.removeWidget(widget)
}
fun setWidgetHeight(widget: Widget, newHeight: Int) {
widgetRepository.setWidgetHeight(widget, newHeight)
}
fun getAvailableBuiltInWidgets(): List<Widget> {
return widgetRepository.getInternalWidgets().filter {
widgets.value?.contains(it)?.not() ?: false
}
}
fun moveUp(index: Int) {
val widgets = widgets.value?.toMutableList() ?: return
val widget = widgets.removeAt(index)
widgets.add(index - 1, widget)
widgetRepository.saveWidgets(widgets)
}
fun getInternalWidgets(): List<Widget> {
return widgetRepository.getInternalWidgets()
fun moveDown(index: Int) {
val widgets = widgets.value?.toMutableList() ?: return
val widget = widgets.removeAt(index)
widgets.add(index + 1, widget)
widgetRepository.saveWidgets(widgets)
}
}
@@ -1,7 +1,5 @@
package de.mm20.launcher2.ui.launcher.widgets
import android.animation.LayoutTransition
import android.annotation.SuppressLint
import android.app.Activity
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetManager
@@ -9,259 +7,317 @@ import android.content.Context
import android.content.Intent
import android.util.AttributeSet
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.FrameLayout
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.iterator
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItems
import com.balsikandar.crashreporter.CrashReporter
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.transition.ChangingLayoutTransition
import de.mm20.launcher2.transition.OneShotLayoutTransition
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
import androidx.compose.animation.graphics.vector.AnimatedImageVector
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
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.layout.onPlaced
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.MutableLiveData
import de.mm20.launcher2.ui.ClockWidget
import de.mm20.launcher2.ui.MdcLauncherTheme
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.databinding.ViewWidgetsBinding
import de.mm20.launcher2.ui.base.ProvideSettings
import de.mm20.launcher2.ui.ktx.toDp
import de.mm20.launcher2.ui.launcher.widgets.picker.PickAppWidgetActivity
import de.mm20.launcher2.ui.legacy.component.WidgetView
import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetType
import kotlinx.coroutines.awaitCancellation
import de.mm20.launcher2.widgets.ExternalWidget
import kotlinx.coroutines.launch
import java.util.*
import kotlin.math.roundToInt
@OptIn(ExperimentalAnimationGraphicsApi::class)
class WidgetsView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {
private val binding = ViewWidgetsBinding.inflate(LayoutInflater.from(context), this)
) : FrameLayout(context, attrs) {
private val widgetHost: AppWidgetHost = AppWidgetHost(context.applicationContext, 44203)
private val viewModel: WidgetsVM by (context as AppCompatActivity).viewModels()
private lateinit var widgets: MutableList<Widget>
private val pickWidgetLauncher: ActivityResultLauncher<Intent>
private val clockWidgetHeight = MutableLiveData(0)
init {
context as AppCompatActivity
pickWidgetLauncher = context.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
val data = it.data ?: return@registerForActivityResult
val widgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
val widgetId = data.getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID
)
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return@registerForActivityResult
if (it.resultCode == Activity.RESULT_OK) {
bindAppWidget(widgetId)
viewModel.addAppWidget(context, widgetId)
}
}
viewModel.widgets.observe(context) {
if (it != null && !::widgets.isInitialized) {
widgets = it.toMutableList()
initWidgets()
}
}
viewModel.isEditMode.observe(context) {
if (it) {
OneShotLayoutTransition.run(binding.widgetList)
binding.clockWidget.visibility = View.GONE
val composeView = ComposeView(context)
composeView.setContent {
MdcLauncherTheme {
ProvideSettings {
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(8.dp)
) {
val editMode by viewModel.isEditMode.observeAsState(false)
val clockHeight by clockWidgetHeight.observeAsState(0)
val scope = rememberCoroutineScope()
var showAddDialog by remember { mutableStateOf(false) }
for (v in binding.widgetList.iterator()) {
if (v is WidgetView) {
v.editMode = true
v.onResizeModeChange = {
OneShotLayoutTransition.run(binding.widgetList)
OneShotLayoutTransition.run(this)
AnimatedVisibility(!editMode) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(clockHeight.toDp()),
contentAlignment = Alignment.BottomCenter
) {
ClockWidget(
modifier = Modifier.fillMaxWidth()
)
}
}
val widgets by viewModel.widgets.observeAsState(emptyList())
Column {
val swapThresholds = remember(widgets) {
Array(widgets.size) { floatArrayOf(0f, 0f) }
}
for ((i, widget) in widgets.withIndex()) {
key(if (widget is ExternalWidget) widget.widgetId else widget) {
var dragOffsetAfterSwap = remember<Float?> { null }
val offsetY = remember(widgets) { Animatable(dragOffsetAfterSwap ?: 0f) }
LaunchedEffect(widgets) {
dragOffsetAfterSwap = null
}
WidgetItem(
widget = widget,
appWidgetHost = widgetHost,
editMode = editMode,
onWidgetRemove = {
if (widget is ExternalWidget) {
widgetHost.deleteAppWidgetId(widget.widgetId)
}
viewModel.removeWidget(widget)
},
onWidgetResize = {
viewModel.setWidgetHeight(widget, it)
},
modifier = Modifier
.fillMaxWidth()
.onPlaced {
swapThresholds[i][0] = it.positionInParent().y
swapThresholds[i][1] = it.positionInParent().y + it.size.height
}
.padding(top = 8.dp)
.offset {
IntOffset(0, offsetY.value.toInt())
},
draggableState = rememberDraggableState {
scope.launch {
val newOffset = offsetY.value + it
offsetY.snapTo(newOffset)
if (i > 0 && newOffset < (swapThresholds[i - 1][0] - swapThresholds[i - 1][1])) {
if (dragOffsetAfterSwap == null) {
dragOffsetAfterSwap = swapThresholds[i - 1][1] - swapThresholds[i - 1][0] + newOffset
viewModel.moveUp(i)
}
}
if (i < widgets.lastIndex && newOffset > (swapThresholds[i + 1][1] - swapThresholds[i + 1][0])) {
if (dragOffsetAfterSwap == null) {
dragOffsetAfterSwap = swapThresholds[i + 1][0] - swapThresholds[i + 1][1] + newOffset
viewModel.moveDown(i)
}
}
}
},
onDragStopped = {
scope.launch {
offsetY.animateTo(0f)
}
}
)
}
}
}
val icon =
AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_edit_add)
ExtendedFloatingActionButton(
modifier = Modifier
.padding(16.dp)
.align(Alignment.CenterHorizontally),
icon = {
Icon(
painter = rememberAnimatedVectorPainter(
animatedImageVector = icon,
atEnd = !editMode
), contentDescription = null
)
},
text = {
Text(
stringResource(
if (editMode) R.string.widget_add_widget
else R.string.menu_edit_widgets
)
)
}, onClick = {
if (!editMode) {
viewModel.setEditMode(true)
} else {
if (viewModel.getAvailableBuiltInWidgets().isEmpty()) {
pickWidgetLauncher.launch(
Intent(
context,
PickAppWidgetActivity::class.java
)
)
} else {
showAddDialog = true
}
}
})
if (showAddDialog) {
val availableBuiltInWidgets =
remember { viewModel.getAvailableBuiltInWidgets() }
Dialog(onDismissRequest = { showAddDialog = false }) {
Surface(
tonalElevation = 16.dp,
shadowElevation = 16.dp,
shape = RoundedCornerShape(16.dp),
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = stringResource(R.string.widget_add_widget),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(
start = 24.dp,
end = 24.dp,
top = 24.dp,
bottom = 8.dp
)
)
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.padding(
top = 16.dp
)
) {
items(availableBuiltInWidgets) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
viewModel.addWidget(it)
showAddDialog = false
}
.padding(
horizontal = 24.dp,
vertical = 16.dp
)
) {
Text(
text = it.loadLabel(context),
style = MaterialTheme.typography.bodyLarge
)
}
}
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
.clickable {
pickWidgetLauncher.launch(
Intent(
context,
PickAppWidgetActivity::class.java
)
)
}
.padding(
horizontal = 24.dp,
vertical = 16.dp
),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = stringResource(R.string.widget_add_external),
style = MaterialTheme.typography.titleMedium
)
}
}
}
TextButton(
onClick = { showAddDialog = false },
modifier = Modifier
.align(Alignment.End)
.padding(bottom = 16.dp, end = 24.dp)
) {
Text(
stringResource(android.R.string.cancel)
)
}
}
}
}
}
}
}
binding.fabEditWidget.apply {
setIconResource(R.drawable.ic_add)
setText(R.string.widget_add_widget)
setOnClickListener {
addWidget()
}
}
} else {
if (::widgets.isInitialized) {
viewModel.saveWidgets(widgets)
OneShotLayoutTransition.run(binding.widgetList)
}
binding.clockWidget.visibility = View.VISIBLE
for (v in binding.widgetList.iterator()) {
if (v is WidgetView) {
v.editMode = false
}
}
binding.fabEditWidget.apply {
setIconResource(R.drawable.ic_edit)
setText(R.string.menu_edit_widgets)
setOnClickListener {
viewModel.setEditMode(true)
}
}
}
}
context.lifecycleScope.launch {
context.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
widgetHost.startListening()
try {
awaitCancellation()
} finally {
// TODO: find out why there is a NPE thrown sometimes
try {
widgetHost.stopListening()
} catch (e: NullPointerException) {
CrashReporter.logException(e)
}
}
}
}
binding.fabEditWidget.setOnClickListener {
viewModel.setEditMode(true)
}
addView(composeView)
}
fun setClockWidgetHeight(height: Int) {
val params = binding.clockWidget.layoutParams
params.height = height
binding.clockWidget.layoutParams = params
}
private fun initWidgets() {
binding.widgetList.removeAllViews()
val params = LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
params.topMargin = (8 * dp).roundToInt()
for (w in widgets) {
val view = WidgetView(context)
view.layoutParams = params
if (view.setWidget(w, widgetHost)) {
binding.widgetList.addDragView(view, view.getDragHandle())
view.onRemove = {
OneShotLayoutTransition.run(binding.widgetList)
binding.widgetList.removeDragView(view)
removeWidget(view.widget)
}
view.onResizeModeChange = {
OneShotLayoutTransition.run(binding.widgetList)
}
}
}
binding.widgetList.setOnViewSwapListener { _, firstPosition, _, secondPosition ->
Collections.swap(widgets, firstPosition, secondPosition)
}
}
@SuppressLint("CheckResult")
private fun addWidget() {
val usedWidgets = widgets.filter { it.type == WidgetType.INTERNAL }.map { it.data }
val internalWidgets =
viewModel.getInternalWidgets().filter { !usedWidgets.contains(it.data) }
if (internalWidgets.isNotEmpty()) {
MaterialDialog(context).show {
title(R.string.widget_add_widget)
listItems(items = internalWidgets.map { it.label }) { dialog, index, _ ->
val widget = internalWidgets[index]
val view = WidgetView(this@WidgetsView.context)
val params = LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
params.topMargin = (8 * dp).roundToInt()
view.layoutParams = params
if (view.setWidget(widget, widgetHost)) {
view.editMode = true
binding.widgetList.addDragView(view, view.getDragHandle())
view.onRemove = {
OneShotLayoutTransition.run(binding.widgetList)
OneShotLayoutTransition.run(this@WidgetsView)
binding.widgetList.removeDragView(view)
removeWidget(view.widget)
}
view.onResizeModeChange = {
OneShotLayoutTransition.run(binding.widgetList)
OneShotLayoutTransition.run(this@WidgetsView)
}
widgets.add(widget)
}
dialog.dismiss()
}
@Suppress("DEPRECATION") // I don't care that neutral buttons are discouraged.
neutralButton(R.string.widget_add_external) {
pickAppWidget()
it.dismiss()
}
}
} else {
pickAppWidget()
}
}
private fun removeWidget(widget: Widget?) {
widget ?: return
widgets.remove(widget)
val id = widget.data.toIntOrNull() ?: return
widgetHost.deleteAppWidgetId(id)
}
private fun pickAppWidget() {
val pickIntent = Intent(context, PickAppWidgetActivity::class.java)
pickWidgetLauncher.launch(pickIntent)
}
private fun bindAppWidget(widgetId: Int) {
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return
val appWidget = AppWidgetManager.getInstance(context)
.getAppWidgetInfo(widgetId) ?: return
val widget = Widget(
type = WidgetType.THIRD_PARTY,
data = widgetId.toString(),
height = appWidget.minHeight
)
val params = LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
params.topMargin = (8 * dp).roundToInt()
val view = WidgetView(context)
view.layoutParams = params
if (view.setWidget(widget, widgetHost)) {
view.editMode = true
binding.widgetList.addDragView(view, view.getDragHandle())
view.onRemove = {
OneShotLayoutTransition.run(binding.widgetList)
OneShotLayoutTransition.run(this)
binding.widgetList.removeDragView(view)
removeWidget(view.widget)
}
view.onResizeModeChange = {
OneShotLayoutTransition.run(binding.widgetList)
OneShotLayoutTransition.run(this)
}
widgets.add(widget)
}
clockWidgetHeight.value = height
}
}
@@ -0,0 +1,58 @@
package de.mm20.launcher2.ui.launcher.widgets.external
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetManager
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.ListView
import android.widget.ScrollView
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.doOnNextLayout
import androidx.core.view.iterator
import de.mm20.launcher2.ui.ktx.toPixels
@Composable
fun ExternalWidget(
appWidgetHost: AppWidgetHost,
widgetId: Int,
height: Int,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
val widgetInfo = remember(widgetId) {
AppWidgetManager.getInstance(context).getAppWidgetInfo(widgetId)
}
val viewHeightPx = height.dp.toPixels()
key(widgetId) {
AndroidView(
modifier = modifier.fillMaxWidth().height(height.dp),
factory = {
val view = appWidgetHost.createView(it.applicationContext, widgetId, widgetInfo)
enableNestedScroll(view)
return@AndroidView view
},
update = {
it.updateAppWidgetSize(null, 0, 0, it.width, height)
}
)
}
}
private fun enableNestedScroll(view: View) {
if (view is ViewGroup) {
for (child in view.iterator()) {
enableNestedScroll(child)
}
}
if (view is ListView || view is ScrollView) view.isNestedScrollingEnabled = true
}
@@ -1,120 +0,0 @@
package de.mm20.launcher2.ui.legacy.component
import android.appwidget.AppWidgetHost
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import androidx.appcompat.widget.TooltipCompat
import androidx.core.view.get
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.transition.OneShotLayoutTransition
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.databinding.ViewWidgetBinding
import de.mm20.launcher2.ui.legacy.view.LauncherCardView
import de.mm20.launcher2.ui.legacy.widget.*
import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetType
class WidgetView : LauncherCardView {
var onRemove: (() -> Unit)? = null
var widget: Widget? = null
var widgetView: LauncherWidget? = null
var editMode = false
set(value) {
OneShotLayoutTransition.run(this)
if (value) {
binding.widgetControlPanel.visibility = View.VISIBLE
val widget = binding.widgetWrapper[2]
widget.visibility = View.GONE
binding.widgetName.visibility = View.VISIBLE
visibility = View.VISIBLE
} else {
resizeMode = false
binding.widgetControlPanel.visibility = View.GONE
val widget = binding.widgetWrapper[2] as LauncherWidget
widget.visibility = View.VISIBLE
binding.widgetName.visibility = View.GONE
}
field = value
}
private var resizeMode = false
set(value) {
if (value == field) return
onResizeModeChange?.invoke(value)
OneShotLayoutTransition.run(this)
OneShotLayoutTransition.run(binding.widgetWrapper)
if (value) {
binding.widgetResizeDragHandle.visibility = View.VISIBLE
val widget = binding.widgetWrapper[2]
widget.visibility = View.VISIBLE
binding.widgetName.visibility = View.GONE
} else {
binding.widgetResizeDragHandle.visibility = View.GONE
if (editMode) {
val widget = binding.widgetWrapper[2]
widget.visibility = View.GONE
binding.widgetName.visibility = View.VISIBLE
}
}
field = value
}
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
private val binding = ViewWidgetBinding.inflate(LayoutInflater.from(context), this)
init {
binding.widgetActionResize.setOnClickListener {
resizeMode = !resizeMode
}
binding.widgetActionRemove.setOnClickListener {
onRemove?.invoke()
}
TooltipCompat.setTooltipText(binding.widgetActionResize, context.getString(R.string.widget_action_adjust_height))
TooltipCompat.setTooltipText(binding.widgetActionRemove, context.getString(R.string.widget_action_remove))
}
var onResizeModeChange: ((Boolean) -> Unit)? = null
fun setWidget(widget: Widget, widgetHost: AppWidgetHost): Boolean {
if (widget.type == WidgetType.INTERNAL) {
widgetView = when (widget.data) {
CalendarWidget.ID -> CalendarWidget(context)
WeatherWidget.ID -> WeatherWidget(context)
MusicWidget.ID -> MusicWidget(context)
else -> return false
}
binding.widgetActionResize.visibility = View.GONE
binding.widgetResizeDragHandle.resizeView = widgetView
binding.widgetWrapper.addView(widgetView, 2)
binding.widgetName.text = widgetView?.name
} else {
widgetView = ExternalWidget(context, widget, widgetHost)
binding.widgetResizeDragHandle.resizeView = widgetView
binding.widgetResizeDragHandle.onResize = {
widget.height = (it / dp).toInt()
}
binding.widgetWrapper.addView(widgetView, 2)
binding.widgetName.text = widgetView?.name
binding.widgetActionResize.visibility = View.VISIBLE
}
this.widget = widget
return true
}
fun getDragHandle(): View {
return binding.widgetDragHandle
}
}
@@ -1,43 +0,0 @@
package de.mm20.launcher2.ui.legacy.view
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.MotionEvent.INVALID_POINTER_ID
import android.view.View
import android.widget.ImageView
class WidgetResizeDragView : ImageView {
var resizeView: View? = null
private var lastY = 0f
var onResize: ((Int) -> Unit)? = null
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
parent.requestDisallowInterceptTouchEvent(true)
val y = event.y
lastY = y
}
MotionEvent.ACTION_MOVE -> {
val y = event.y
val dY = y - lastY
val view = resizeView ?: return false
val params = view.layoutParams
val newHeight = (view.height + dY).toInt()
params.height = newHeight
onResize?.invoke(newHeight)
view.layoutParams = params
}
}
return true
}
}
@@ -1,59 +0,0 @@
package de.mm20.launcher2.ui.legacy.widget
import android.content.Context
import android.util.AttributeSet
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.LocalAbsoluteTonalElevation
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.ui.MdcLauncherTheme
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.base.ProvideSettings
import de.mm20.launcher2.ui.launcher.widgets.calendar.CalendarWidget
class CalendarWidget : LauncherWidget {
override val canResize: Boolean
get() = false
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
context,
attrs,
defStyleRes
)
init {
val composeView = ComposeView(context)
composeView.setContent {
MdcLauncherTheme {
ProvideSettings {
// TODO: Temporary solution until parent widget card is rewritten in Compose
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colorScheme.onSurface,
LocalAbsoluteTonalElevation provides 1.dp
) {
Column {
CalendarWidget()
}
}
}
}
}
addView(composeView)
}
override val name: String
get() = resources.getString(R.string.widget_name_calendar)
companion object {
const val ID = "calendar"
}
}
@@ -1,43 +0,0 @@
package de.mm20.launcher2.ui.legacy.widget
import android.animation.LayoutTransition
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.compose.ui.platform.ComposeView
import de.mm20.launcher2.ui.ClockWidget
import de.mm20.launcher2.ui.MdcLauncherTheme
class ClockWidget : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
context,
attrs,
defStyleRes
)
val view = ComposeView(context)
init {
clipToPadding = false
clipChildren = false
layoutTransition = LayoutTransition()
val composeView = ComposeView(context)
addView(composeView)
composeView.layoutParams =
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
composeView.setContent {
MdcLauncherTheme {
ClockWidget()
}
}
}
}
@@ -1,75 +0,0 @@
package de.mm20.launcher2.ui.legacy.widget
import android.annotation.SuppressLint
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetHostView
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.Context
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.ListView
import android.widget.ScrollView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.doOnLayout
import androidx.core.view.doOnNextLayout
import androidx.core.view.get
import androidx.core.view.iterator
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.widgets.Widget
@SuppressLint("ViewConstructor")
class ExternalWidget(
context: Context,
val widget: Widget,
host: AppWidgetHost
) : LauncherWidget(context) {
val widgetInfo: AppWidgetProviderInfo?
val widgetView: View
init {
val id = widget.data.toInt()
widgetInfo = AppWidgetManager.getInstance(context.applicationContext).getAppWidgetInfo(id)
widgetView = host.createView(context.applicationContext, id, widgetInfo)
?: View(context)
if (widgetView is AppWidgetHostView && widgetView.childCount > 0) {
enableNestedScroll(widgetView[0])
}
val h = widget.height * dp
val params = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, h.toInt())
val p = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
layoutParams = params
widgetView.layoutParams = p
addView(widgetView)
}
override fun setLayoutParams(params: ViewGroup.LayoutParams?) {
super.setLayoutParams(params)
params ?: return
doOnNextLayout {
val width = if (params.width > 0) params.width else it.width
val height = if (params.height > 0) params.height else widgetInfo?.minHeight ?: it.height
if (widgetView is AppWidgetHostView) {
widgetView.updateAppWidgetSize(Bundle(), 0, 0, width, height)
}
}
}
private fun enableNestedScroll(view: View) {
if (view is ViewGroup) {
for (child in view.iterator()) {
enableNestedScroll(child)
}
}
if (view is ListView || view is ScrollView) view.isNestedScrollingEnabled = true
}
override val canResize: Boolean
get() = true
override val name: String
get() = widgetInfo?.loadLabel(context.packageManager) ?: ""
}
@@ -1,17 +0,0 @@
package de.mm20.launcher2.ui.legacy.widget
import android.animation.LayoutTransition
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
abstract class LauncherWidget : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
abstract val canResize: Boolean
abstract val name: String
}
@@ -1,47 +0,0 @@
package de.mm20.launcher2.ui.legacy.widget
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.ComposeView
import de.mm20.launcher2.ui.MdcLauncherTheme
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.launcher.widgets.music.MusicWidget
class MusicWidget : LauncherWidget {
override val canResize: Boolean
get() = false
override val name: String
get() = context.getString(R.string.widget_name_music)
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
init {
val composeView = ComposeView(context)
composeView.id = FrameLayout.generateViewId()
composeView.setContent {
MdcLauncherTheme {
// TODO: Temporary solution until parent widget card is rewritten in Compose
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
Column {
MusicWidget()
}
}
}
}
addView(composeView)
}
companion object {
const val ID = "music"
}
}
@@ -1,50 +0,0 @@
package de.mm20.launcher2.ui.legacy.widget
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.ComposeView
import de.mm20.launcher2.ui.MdcLauncherTheme
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.launcher.widgets.weather.WeatherWidget
class WeatherWidget : LauncherWidget {
override val canResize: Boolean
get() = false
override val name: String
get() = resources.getString(R.string.widget_name_weather)
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
init {
val composeView = ComposeView(context)
composeView.id = FrameLayout.generateViewId()
composeView.setContent {
MdcLauncherTheme {
// TODO: Temporary solution until parent widget card is rewritten in Compose
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
Column {
WeatherWidget()
}
}
}
}
addView(composeView)
}
companion object {
const val ID = "weather"
}
}
@@ -37,8 +37,7 @@
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical"
android:layout_margin="8dp" />
android:orientation="vertical" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
-80
View File
@@ -1,80 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/widgetWrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:id="@+id/widgetControlPanel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:visibility="gone">
<ImageView
android:id="@+id/widgetDragHandle"
style="?iconStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:focusable="true"
android:foreground="?selectableItemBackgroundBorderless"
android:padding="12dp"
android:src="@drawable/ic_drag_handle"/>
<ImageView
android:id="@+id/widgetActionResize"
style="?iconStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="end"
android:layout_marginStart="4dp"
android:layout_marginEnd="56dp"
android:clickable="true"
android:focusable="true"
android:foreground="?selectableItemBackgroundBorderless"
android:padding="12dp"
android:src="@drawable/ic_widget_resize"/>
<ImageView
android:id="@+id/widgetActionRemove"
style="?iconStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="end"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:clickable="true"
android:focusable="true"
android:foreground="?selectableItemBackgroundBorderless"
android:padding="12dp"
android:src="@drawable/ic_delete"/>
</FrameLayout>
<TextView
android:id="@+id/widgetName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?textAppearanceTitleLarge"
android:visibility="gone"/>
<de.mm20.launcher2.ui.legacy.view.WidgetResizeDragView
android:id="@+id/widgetResizeDragHandle"
style="?iconStyle"
android:layout_width="match_parent"
android:layout_height="24dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:src="@drawable/ic_resize_drag_handle"
android:visibility="gone"/>
</LinearLayout>
</merge>
-34
View File
@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:layout_height="wrap_content"
tools:layout_width="match_parent">
<de.mm20.launcher2.ui.legacy.widget.ClockWidget
android:id="@+id/clockWidget"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.jmedeisis.draglinearlayout.DragLinearLayout
android:id="@+id/widgetList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical">
<!-- Widgets will be added here -->
</com.jmedeisis.draglinearlayout.DragLinearLayout>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/fabEditWidget"
app:icon="@drawable/ic_edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:text="@string/menu_edit_widgets" />
</merge>