widgets: responsive layout
This commit is contained in:
parent
fdb34edf13
commit
b743d0de47
4 changed files with 119 additions and 46 deletions
|
@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
- switching to PiP when changing device orientation on Android >=13
|
- switching to PiP when changing device orientation on Android >=13
|
||||||
- handling wallpaper intent without URI
|
- handling wallpaper intent without URI
|
||||||
|
- sizing widgets with some launchers on Android >=12
|
||||||
|
|
||||||
## <a id="v1.11.3"></a>[v1.11.3] - 2024-06-17
|
## <a id="v1.11.3"></a>[v1.11.3] - 2024-06-17
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.util.SizeF
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import app.loup.streams_channel.StreamsChannel
|
import app.loup.streams_channel.StreamsChannel
|
||||||
import deckers.thibault.aves.channel.AvesByteSendingMethodCodec
|
import deckers.thibault.aves.channel.AvesByteSendingMethodCodec
|
||||||
|
@ -42,10 +43,10 @@ class HomeWidgetProvider : AppWidgetProvider() {
|
||||||
|
|
||||||
defaultScope.launch {
|
defaultScope.launch {
|
||||||
val backgroundProps = getProps(context, widgetId, widgetInfo, drawEntryImage = false)
|
val backgroundProps = getProps(context, widgetId, widgetInfo, drawEntryImage = false)
|
||||||
updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, backgroundProps)
|
updateWidgetImage(context, appWidgetManager, widgetId, backgroundProps)
|
||||||
|
|
||||||
val imageProps = getProps(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = false)
|
val imageProps = getProps(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = false)
|
||||||
updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageProps)
|
updateWidgetImage(context, appWidgetManager, widgetId, imageProps)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,20 +62,32 @@ class HomeWidgetProvider : AppWidgetProvider() {
|
||||||
imageByteFetchJob = defaultScope.launch {
|
imageByteFetchJob = defaultScope.launch {
|
||||||
delay(500)
|
delay(500)
|
||||||
val imageProps = getProps(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = true)
|
val imageProps = getProps(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = true)
|
||||||
updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageProps)
|
updateWidgetImage(context, appWidgetManager, widgetId, imageProps)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDevicePixelRatio(): Float = Resources.getSystem().displayMetrics.density
|
private fun getDevicePixelRatio(): Float = Resources.getSystem().displayMetrics.density
|
||||||
|
|
||||||
private fun getWidgetSizePx(context: Context, widgetInfo: Bundle): Pair<Int, Int> {
|
private fun getWidgetSizesDip(context: Context, widgetInfo: Bundle): List<FieldMap> {
|
||||||
val devicePixelRatio = getDevicePixelRatio()
|
var sizes: List<SizeF>? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
val isPortrait = context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
|
widgetInfo.getParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, SizeF::class.java)
|
||||||
val widthKey = if (isPortrait) AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH else AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
val heightKey = if (isPortrait) AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT else AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT
|
@Suppress("DEPRECATION")
|
||||||
val widthPx = (widgetInfo.getInt(widthKey) * devicePixelRatio).roundToInt()
|
widgetInfo.getParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES)
|
||||||
val heightPx = (widgetInfo.getInt(heightKey) * devicePixelRatio).roundToInt()
|
} else {
|
||||||
return Pair(widthPx, heightPx)
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sizes.isNullOrEmpty()) {
|
||||||
|
val isPortrait = context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||||
|
val widthKey = if (isPortrait) AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH else AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH
|
||||||
|
val heightKey = if (isPortrait) AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT else AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT
|
||||||
|
val widthDip = widgetInfo.getInt(widthKey)
|
||||||
|
val heightDip = widgetInfo.getInt(heightKey)
|
||||||
|
sizes = listOf(SizeF(widthDip.toFloat(), heightDip.toFloat()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return sizes.map { size -> hashMapOf("widthDip" to size.width, "heightDip" to size.height) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getProps(
|
private suspend fun getProps(
|
||||||
|
@ -84,8 +97,11 @@ class HomeWidgetProvider : AppWidgetProvider() {
|
||||||
drawEntryImage: Boolean,
|
drawEntryImage: Boolean,
|
||||||
reuseEntry: Boolean = false,
|
reuseEntry: Boolean = false,
|
||||||
): FieldMap? {
|
): FieldMap? {
|
||||||
val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo)
|
val sizesDip = getWidgetSizesDip(context, widgetInfo)
|
||||||
if (widthPx == 0 || heightPx == 0) return null
|
if (sizesDip.isEmpty()) return null
|
||||||
|
|
||||||
|
val sizeDip = sizesDip.first()
|
||||||
|
if (sizeDip["widthDip"] == 0 || sizeDip["heightDip"] == 0) return null
|
||||||
|
|
||||||
val isNightModeOn = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
val isNightModeOn = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
||||||
|
|
||||||
|
@ -98,8 +114,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
|
||||||
FlutterUtils.runOnUiThread {
|
FlutterUtils.runOnUiThread {
|
||||||
channel.invokeMethod("drawWidget", hashMapOf(
|
channel.invokeMethod("drawWidget", hashMapOf(
|
||||||
"widgetId" to widgetId,
|
"widgetId" to widgetId,
|
||||||
"widthPx" to widthPx,
|
"sizesDip" to sizesDip,
|
||||||
"heightPx" to heightPx,
|
|
||||||
"devicePixelRatio" to getDevicePixelRatio(),
|
"devicePixelRatio" to getDevicePixelRatio(),
|
||||||
"drawEntryImage" to drawEntryImage,
|
"drawEntryImage" to drawEntryImage,
|
||||||
"reuseEntry" to reuseEntry,
|
"reuseEntry" to reuseEntry,
|
||||||
|
@ -127,7 +142,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
|
||||||
@Suppress("unchecked_cast")
|
@Suppress("unchecked_cast")
|
||||||
return props as FieldMap?
|
return props as FieldMap?
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(LOG_TAG, "failed to draw widget for widgetId=$widgetId widthPx=$widthPx heightPx=$heightPx", e)
|
Log.e(LOG_TAG, "failed to draw widget for widgetId=$widgetId sizesPx=$sizesDip", e)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -136,36 +151,83 @@ class HomeWidgetProvider : AppWidgetProvider() {
|
||||||
context: Context,
|
context: Context,
|
||||||
appWidgetManager: AppWidgetManager,
|
appWidgetManager: AppWidgetManager,
|
||||||
widgetId: Int,
|
widgetId: Int,
|
||||||
widgetInfo: Bundle,
|
|
||||||
props: FieldMap?,
|
props: FieldMap?,
|
||||||
) {
|
) {
|
||||||
props ?: return
|
props ?: return
|
||||||
|
|
||||||
val bytes = props["bytes"] as ByteArray?
|
val bytesBySizeDip = (props["bytesBySizeDip"] as List<*>?)?.mapNotNull {
|
||||||
|
if (it is Map<*, *>) {
|
||||||
|
val widthDip = (it["widthDip"] as Number?)?.toFloat()
|
||||||
|
val heightDip = (it["heightDip"] as Number?)?.toFloat()
|
||||||
|
val bytes = it["bytes"] as ByteArray?
|
||||||
|
if (widthDip != null && heightDip != null && bytes != null) {
|
||||||
|
Pair(SizeF(widthDip, heightDip), bytes)
|
||||||
|
} else null
|
||||||
|
} else null
|
||||||
|
}
|
||||||
val updateOnTap = props["updateOnTap"] as Boolean?
|
val updateOnTap = props["updateOnTap"] as Boolean?
|
||||||
if (bytes == null || updateOnTap == null) {
|
if (bytesBySizeDip == null || updateOnTap == null) {
|
||||||
Log.e(LOG_TAG, "missing arguments")
|
Log.e(LOG_TAG, "missing arguments")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo)
|
if (bytesBySizeDip.isEmpty()) {
|
||||||
if (widthPx == 0 || heightPx == 0) return
|
Log.e(LOG_TAG, "empty image list")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val bitmaps = ArrayList<Bitmap>()
|
||||||
|
|
||||||
|
fun createRemoteViewsForSize(
|
||||||
|
context: Context,
|
||||||
|
widgetId: Int,
|
||||||
|
sizeDip: SizeF,
|
||||||
|
bytes: ByteArray,
|
||||||
|
updateOnTap: Boolean,
|
||||||
|
): RemoteViews? {
|
||||||
|
val devicePixelRatio = getDevicePixelRatio()
|
||||||
|
val widthPx = (sizeDip.width * devicePixelRatio).roundToInt()
|
||||||
|
val heightPx = (sizeDip.height * devicePixelRatio).roundToInt()
|
||||||
|
|
||||||
|
try {
|
||||||
|
val bitmap = Bitmap.createBitmap(widthPx, heightPx, Bitmap.Config.ARGB_8888).also {
|
||||||
|
bitmaps.add(it)
|
||||||
|
it.copyPixelsFromBuffer(ByteBuffer.wrap(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
val pendingIntent = if (updateOnTap) buildUpdateIntent(context, widgetId) else buildOpenAppIntent(context, widgetId)
|
||||||
|
|
||||||
|
return RemoteViews(context.packageName, R.layout.app_widget).apply {
|
||||||
|
setImageViewBitmap(R.id.widget_img, bitmap)
|
||||||
|
setOnClickPendingIntent(R.id.widget_img, pendingIntent)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(LOG_TAG, "failed to draw widget", e)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val bitmap = Bitmap.createBitmap(widthPx, heightPx, Bitmap.Config.ARGB_8888)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(bytes))
|
// multiple rendering for all possible sizes
|
||||||
|
val views = RemoteViews(
|
||||||
val pendingIntent = if (updateOnTap) buildUpdateIntent(context, widgetId) else buildOpenAppIntent(context, widgetId)
|
bytesBySizeDip.associateBy(
|
||||||
|
{ (sizeDip, _) -> sizeDip },
|
||||||
val views = RemoteViews(context.packageName, R.layout.app_widget).apply {
|
{ (sizeDip, bytes) -> createRemoteViewsForSize(context, widgetId, sizeDip, bytes, updateOnTap) },
|
||||||
setImageViewBitmap(R.id.widget_img, bitmap)
|
).filterValues { it != null }.mapValues { (_, view) -> view!! }
|
||||||
setOnClickPendingIntent(R.id.widget_img, pendingIntent)
|
)
|
||||||
|
appWidgetManager.updateAppWidget(widgetId, views)
|
||||||
|
} else {
|
||||||
|
// single rendering
|
||||||
|
val (sizeDip, bytes) = bytesBySizeDip.first()
|
||||||
|
val views = createRemoteViewsForSize(context, widgetId, sizeDip, bytes, updateOnTap)
|
||||||
|
appWidgetManager.updateAppWidget(widgetId, views)
|
||||||
}
|
}
|
||||||
|
|
||||||
appWidgetManager.updateAppWidget(widgetId, views)
|
|
||||||
bitmap.recycle()
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(LOG_TAG, "failed to draw widget", e)
|
Log.e(LOG_TAG, "failed to draw widget", e)
|
||||||
|
} finally {
|
||||||
|
bitmaps.forEach { it.recycle() }
|
||||||
|
bitmaps.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,8 +38,9 @@ void widgetMainCommon(AppFlavor flavor) async {
|
||||||
|
|
||||||
Future<Map<String, dynamic>> _drawWidget(dynamic args) async {
|
Future<Map<String, dynamic>> _drawWidget(dynamic args) async {
|
||||||
final widgetId = args['widgetId'] as int;
|
final widgetId = args['widgetId'] as int;
|
||||||
final widthPx = args['widthPx'] as int;
|
final sizesDip = (args['sizesDip'] as List).cast<Map>().map((kv) {
|
||||||
final heightPx = args['heightPx'] as int;
|
return Size(kv['widthDip'] as double, kv['heightDip'] as double);
|
||||||
|
}).toList();
|
||||||
final cornerRadiusPx = args['cornerRadiusPx'] as double?;
|
final cornerRadiusPx = args['cornerRadiusPx'] as double?;
|
||||||
final devicePixelRatio = args['devicePixelRatio'] as double;
|
final devicePixelRatio = args['devicePixelRatio'] as double;
|
||||||
final drawEntryImage = args['drawEntryImage'] as bool;
|
final drawEntryImage = args['drawEntryImage'] as bool;
|
||||||
|
@ -54,15 +55,22 @@ Future<Map<String, dynamic>> _drawWidget(dynamic args) async {
|
||||||
entry: entry,
|
entry: entry,
|
||||||
devicePixelRatio: devicePixelRatio,
|
devicePixelRatio: devicePixelRatio,
|
||||||
);
|
);
|
||||||
final bytes = await painter.drawWidget(
|
final bytesBySizeDip = <Map<String, dynamic>>[];
|
||||||
widthPx: widthPx,
|
await Future.forEach(sizesDip, (sizeDip) async {
|
||||||
heightPx: heightPx,
|
final bytes = await painter.drawWidget(
|
||||||
cornerRadiusPx: cornerRadiusPx,
|
sizeDip: sizeDip,
|
||||||
outline: outline,
|
cornerRadiusPx: cornerRadiusPx,
|
||||||
shape: settings.getWidgetShape(widgetId),
|
outline: outline,
|
||||||
);
|
shape: settings.getWidgetShape(widgetId),
|
||||||
|
);
|
||||||
|
bytesBySizeDip.add({
|
||||||
|
'widthDip': sizeDip.width,
|
||||||
|
'heightDip': sizeDip.height,
|
||||||
|
'bytes': bytes,
|
||||||
|
});
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
'bytes': bytes,
|
'bytesBySizeDip': bytesBySizeDip,
|
||||||
'updateOnTap': settings.getWidgetOpenPage(widgetId) == WidgetOpenPage.updateWidget,
|
'updateOnTap': settings.getWidgetOpenPage(widgetId) == WidgetOpenPage.updateWidget,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,14 +27,16 @@ class HomeWidgetPainter {
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<Uint8List> drawWidget({
|
Future<Uint8List> drawWidget({
|
||||||
required int widthPx,
|
required Size sizeDip,
|
||||||
required int heightPx,
|
|
||||||
required double? cornerRadiusPx,
|
required double? cornerRadiusPx,
|
||||||
required Color? outline,
|
required Color? outline,
|
||||||
required WidgetShape shape,
|
required WidgetShape shape,
|
||||||
ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba,
|
ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba,
|
||||||
}) async {
|
}) async {
|
||||||
final widgetSizePx = Size(widthPx.toDouble(), heightPx.toDouble());
|
final widthPx = sizeDip.width * devicePixelRatio;
|
||||||
|
final heightPx = sizeDip.height * devicePixelRatio;
|
||||||
|
final widgetSizePx = Size(widthPx, heightPx);
|
||||||
|
debugPrint('draw widget for $sizeDip dp ($widgetSizePx px), entry=$entry');
|
||||||
final ui.Image? entryImage;
|
final ui.Image? entryImage;
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
final extent = shape.extentPx(widgetSizePx, entry!) / devicePixelRatio;
|
final extent = shape.extentPx(widgetSizePx, entry!) / devicePixelRatio;
|
||||||
|
@ -57,7 +59,7 @@ class HomeWidgetPainter {
|
||||||
if (outline != null) {
|
if (outline != null) {
|
||||||
drawOutline(canvas, path, devicePixelRatio, outline);
|
drawOutline(canvas, path, devicePixelRatio, outline);
|
||||||
}
|
}
|
||||||
final widgetImage = await recorder.endRecording().toImage(widthPx, heightPx);
|
final widgetImage = await recorder.endRecording().toImage(widthPx.round(), heightPx.round());
|
||||||
final byteData = await widgetImage.toByteData(format: format);
|
final byteData = await widgetImage.toByteData(format: format);
|
||||||
return byteData?.buffer.asUint8List() ?? Uint8List(0);
|
return byteData?.buffer.asUint8List() ?? Uint8List(0);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue