widgets: fix responsiveness issues

Fix some problems with the Pre-12 widget responsiveness backport, most
notably the wrong layout being chosen on more dense launcher grids.
Also change the default widget size from 2x2 to 3x2 so that it's more
readable.
This commit is contained in:
OxygenCobalt 2021-08-06 11:45:13 -06:00
parent 1b5822eae0
commit e81d4b6d17
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
11 changed files with 193 additions and 243 deletions

View file

@ -0,0 +1,116 @@
package org.oxycblt.auxio.widgets
import android.content.Context
import android.widget.RemoteViews
import androidx.annotation.LayoutRes
import org.oxycblt.auxio.R
import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.playback.system.PlaybackService
import org.oxycblt.auxio.ui.newBroadcastIntent
import org.oxycblt.auxio.ui.newMainIntent
private fun createBaseView(
context: Context,
@LayoutRes layout: Int,
state: WidgetState
): RemoteViews {
val views = RemoteViews(context.packageName, layout)
views.setOnClickPendingIntent(
android.R.id.background,
context.newMainIntent()
)
views.setOnClickPendingIntent(
R.id.widget_skip_prev,
context.newBroadcastIntent(
PlaybackService.ACTION_SKIP_PREV
)
)
views.setOnClickPendingIntent(
R.id.widget_play_pause,
context.newBroadcastIntent(
PlaybackService.ACTION_PLAY_PAUSE
)
)
views.setOnClickPendingIntent(
R.id.widget_skip_next,
context.newBroadcastIntent(
PlaybackService.ACTION_SKIP_NEXT
)
)
views.setTextViewText(R.id.widget_song, state.song.name)
views.setTextViewText(R.id.widget_artist, state.song.album.artist.name)
views.setImageViewResource(
R.id.widget_play_pause,
if (state.isPlaying) {
R.drawable.ic_pause
} else {
R.drawable.ic_play
}
)
if (state.albumArt != null) {
views.setImageViewBitmap(R.id.widget_cover, state.albumArt)
views.setCharSequence(
R.id.widget_cover, "setContentDescription",
context.getString(R.string.desc_album_cover, state.song.album.name)
)
} else {
views.setImageViewResource(R.id.widget_cover, R.drawable.ic_song)
views.setCharSequence(
R.id.widget_cover,
"setContentDescription",
context.getString(R.string.desc_no_cover)
)
}
return views
}
fun createSmallWidget(context: Context, state: WidgetState): RemoteViews {
return createBaseView(context, R.layout.widget_small, state)
}
fun createFullWidget(context: Context, state: WidgetState): RemoteViews {
val views = createBaseView(context, R.layout.widget_full, state)
// The main way the large widget differs from the other widgets is the addition of extra
// controls. However, since the context we use to load attributes is from the main process,
// attempting to dynamically color anything will result in an error. More duplicate
// resources it is. This is getting really tiring.
views.setOnClickPendingIntent(
R.id.widget_loop,
context.newBroadcastIntent(
PlaybackService.ACTION_LOOP
)
)
views.setOnClickPendingIntent(
R.id.widget_shuffle,
context.newBroadcastIntent(
PlaybackService.ACTION_SHUFFLE
)
)
val shuffleRes = if (state.isShuffled)
R.drawable.ic_shuffle_tinted
else
R.drawable.ic_shuffle
val loopRes = when (state.loopMode) {
LoopMode.NONE -> R.drawable.ic_loop
LoopMode.ALL -> R.drawable.ic_loop_all_tinted
LoopMode.TRACK -> R.drawable.ic_loop_one_tinted
}
views.setImageViewResource(R.id.widget_shuffle, shuffleRes)
views.setImageViewResource(R.id.widget_loop, loopRes)
return views
}

View file

@ -17,9 +17,6 @@ import org.oxycblt.auxio.logD
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.ui.isLandscape
import org.oxycblt.auxio.ui.newMainIntent
import org.oxycblt.auxio.widgets.forms.FullWidgetForm
import org.oxycblt.auxio.widgets.forms.SmallWidgetForm
import org.oxycblt.auxio.widgets.forms.WidgetForm
/**
* Auxio's one and only appwidget. This widget follows a more unorthodox approach, effectively
@ -28,7 +25,7 @@ import org.oxycblt.auxio.widgets.forms.WidgetForm
* - For widgets Wx2 or higher, show an expanded view with album art and basic controls
* - For widgets 4x2 or higher, show a complete view with all playback controls
*
* For more specific details about these sub-widgets, see [WidgetForm].
* For more specific details about these sub-widgets, see Forms.kt.
*/
class WidgetProvider : AppWidgetProvider() {
override fun onUpdate(
@ -40,6 +37,21 @@ class WidgetProvider : AppWidgetProvider() {
requestUpdate(context)
}
override fun onAppWidgetOptionsChanged(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
newOptions: Bundle?
) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
// We can't resize the widget until we can generate the views, so request an update
// from PlaybackService.
requestUpdate(context)
}
}
/*
* Update the widget based on the playback state.
*/
@ -65,59 +77,14 @@ class WidgetProvider : AppWidgetProvider() {
// Map each widget form to the rough dimensions where it would look at least okay.
val views = mapOf(
SizeF(110f, 110f) to SmallWidgetForm().createViews(context, state),
SizeF(272f, 110f) to FullWidgetForm().createViews(context, state)
SizeF(110f, 110f) to createSmallWidget(context, state),
SizeF(250f, 110f) to createFullWidget(context, state)
)
appWidgetManager.applyViewsCompat(context, views)
}
}
/*
* Revert this widget to its default view
*/
fun reset(context: Context) {
logD("Resetting widget")
applyDefaultViews(context, AppWidgetManager.getInstance(context))
}
@SuppressLint("RemoteViewLayout")
private fun applyDefaultViews(context: Context, manager: AppWidgetManager) {
val views = RemoteViews(context.packageName, R.layout.widget_default)
views.setOnClickPendingIntent(
android.R.id.background,
context.newMainIntent()
)
manager.updateAppWidget(ComponentName(context, this::class.java), views)
}
override fun onAppWidgetOptionsChanged(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
newOptions: Bundle?
) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
// We can't resize the widget until we can generate the views, so request an update
// from PlaybackService.
requestUpdate(context)
}
}
private fun requestUpdate(context: Context) {
logD("Sending update intent to PlaybackService")
val intent = Intent(ACTION_WIDGET_UPDATE)
.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
context.sendBroadcast(intent)
}
private fun AppWidgetManager.applyViewsCompat(
context: Context,
views: Map<SizeF, RemoteViews>
@ -134,10 +101,6 @@ class WidgetProvider : AppWidgetProvider() {
// problems [most notably UI jittering when resizing]. Its better than just using
// one layout though. It may be improved once Android 12's source is released.
// Theres a non-zero likelihood that this code ends up being copy-pasted all over
// by other apps that are trying to refresh their widgets for Android 12. So, if
// you're doing that than uh...hi.
// Each widget has independent dimensions, so we iterate through them all
// and do this for each.
val ids = getAppWidgetIds(ComponentName(context, WidgetProvider::class.java))
@ -159,13 +122,20 @@ class WidgetProvider : AppWidgetProvider() {
height = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT)
}
logD("Assuming widget dimens are ${width}x$height")
// Widget dimens pre-12 are weird. Basically, they correspond to columns
// but with 2 columns worth of DiP added for some insane reason. Take
// the dimens, normalize them into cells, and then turn them back into dimens.
// This is super lossy and may result in wonky layouts, but it *seems* to work.
width = normalizeDimen(width)
height = normalizeDimen(height)
logD("Assuming true widget dimens are ${width}x$height")
// Find layouts that fit into the widget
val candidates = mutableListOf<SizeF>()
for (size in views.keys) {
if (size.width < width && size.height < height) {
if (size.width <= width && size.height <= height) {
candidates.add(size)
}
}
@ -195,6 +165,46 @@ class WidgetProvider : AppWidgetProvider() {
}
}
private fun normalizeDimen(dimen: Int): Int {
var cells = 0
while (70 * cells - 30 < dimen) {
cells++
}
return 70 * (cells - 2) - 30
}
/*
* Revert this widget to its default view
*/
fun reset(context: Context) {
logD("Resetting widget")
applyDefaultViews(context, AppWidgetManager.getInstance(context))
}
@SuppressLint("RemoteViewLayout")
private fun applyDefaultViews(context: Context, manager: AppWidgetManager) {
val views = RemoteViews(context.packageName, R.layout.widget_default)
views.setOnClickPendingIntent(
android.R.id.background,
context.newMainIntent()
)
manager.updateAppWidget(ComponentName(context, this::class.java), views)
}
private fun requestUpdate(context: Context) {
logD("Sending update intent to PlaybackService")
val intent = Intent(ACTION_WIDGET_UPDATE)
.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
context.sendBroadcast(intent)
}
companion object {
const val ACTION_WIDGET_UPDATE = BuildConfig.APPLICATION_ID + ".action.WIDGET_UPDATE"
}

View file

@ -1,98 +0,0 @@
package org.oxycblt.auxio.widgets.forms
import android.content.Context
import android.widget.RemoteViews
import org.oxycblt.auxio.R
import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.playback.system.PlaybackService
import org.oxycblt.auxio.ui.newBroadcastIntent
import org.oxycblt.auxio.widgets.WidgetState
class FullWidgetForm : WidgetForm(R.layout.widget_full) {
override fun createViews(context: Context, state: WidgetState): RemoteViews {
val views = super.createViews(context, state)
views.setOnClickPendingIntent(
R.id.widget_loop,
context.newBroadcastIntent(
PlaybackService.ACTION_LOOP
)
)
views.setOnClickPendingIntent(
R.id.widget_skip_prev,
context.newBroadcastIntent(
PlaybackService.ACTION_SKIP_PREV
)
)
views.setOnClickPendingIntent(
R.id.widget_play_pause,
context.newBroadcastIntent(
PlaybackService.ACTION_PLAY_PAUSE
)
)
views.setOnClickPendingIntent(
R.id.widget_skip_next,
context.newBroadcastIntent(
PlaybackService.ACTION_SKIP_NEXT
)
)
views.setOnClickPendingIntent(
R.id.widget_shuffle,
context.newBroadcastIntent(
PlaybackService.ACTION_SHUFFLE
)
)
views.setTextViewText(R.id.widget_song, state.song.name)
views.setTextViewText(R.id.widget_artist, state.song.album.artist.name)
views.setImageViewResource(
R.id.widget_play_pause,
if (state.isPlaying) {
R.drawable.ic_pause
} else {
R.drawable.ic_play
}
)
if (state.albumArt != null) {
views.setImageViewBitmap(R.id.widget_cover, state.albumArt)
views.setCharSequence(
R.id.widget_cover, "setContentDescription",
context.getString(R.string.desc_album_cover, state.song.album.name)
)
} else {
views.setImageViewResource(R.id.widget_cover, R.drawable.ic_song)
views.setCharSequence(
R.id.widget_cover,
"setContentDescription",
context.getString(R.string.desc_no_cover)
)
}
// The main way the large widget differs from the other widgets is the addition of extra
// controls. However, since the context we use to load attributes is from the main process,
// attempting to dynamically color anything will result in an error. More duplicate
// resources it is, then. This is getting really tiring.
val shuffleRes = if (state.isShuffled)
R.drawable.ic_shuffle_tinted
else
R.drawable.ic_shuffle
val loopRes = when (state.loopMode) {
LoopMode.NONE -> R.drawable.ic_loop
LoopMode.ALL -> R.drawable.ic_loop_all_tinted
LoopMode.TRACK -> R.drawable.ic_loop_one_tinted
}
views.setImageViewResource(R.id.widget_shuffle, shuffleRes)
views.setImageViewResource(R.id.widget_loop, loopRes)
return views
}
}

View file

@ -1,64 +0,0 @@
package org.oxycblt.auxio.widgets.forms
import android.content.Context
import android.widget.RemoteViews
import org.oxycblt.auxio.R
import org.oxycblt.auxio.playback.system.PlaybackService
import org.oxycblt.auxio.ui.newBroadcastIntent
import org.oxycblt.auxio.widgets.WidgetState
class SmallWidgetForm : WidgetForm(R.layout.widget_small) {
override fun createViews(context: Context, state: WidgetState): RemoteViews {
val views = super.createViews(context, state)
views.setOnClickPendingIntent(
R.id.widget_skip_prev,
context.newBroadcastIntent(
PlaybackService.ACTION_SKIP_PREV
)
)
views.setOnClickPendingIntent(
R.id.widget_play_pause,
context.newBroadcastIntent(
PlaybackService.ACTION_PLAY_PAUSE
)
)
views.setOnClickPendingIntent(
R.id.widget_skip_next,
context.newBroadcastIntent(
PlaybackService.ACTION_SKIP_NEXT
)
)
views.setTextViewText(R.id.widget_song, state.song.name)
views.setTextViewText(R.id.widget_artist, state.song.album.artist.name)
views.setImageViewResource(
R.id.widget_play_pause,
if (state.isPlaying) {
R.drawable.ic_pause
} else {
R.drawable.ic_play
}
)
if (state.albumArt != null) {
views.setImageViewBitmap(R.id.widget_cover, state.albumArt)
views.setCharSequence(
R.id.widget_cover, "setContentDescription",
context.getString(R.string.desc_album_cover, state.song.album.name)
)
} else {
views.setImageViewResource(R.id.widget_cover, R.drawable.ic_song)
views.setCharSequence(
R.id.widget_cover,
"setContentDescription",
context.getString(R.string.desc_no_cover)
)
}
return views
}
}

View file

@ -1,20 +0,0 @@
package org.oxycblt.auxio.widgets.forms
import android.content.Context
import android.widget.RemoteViews
import androidx.annotation.LayoutRes
import org.oxycblt.auxio.ui.newMainIntent
import org.oxycblt.auxio.widgets.WidgetState
abstract class WidgetForm(@LayoutRes private val layout: Int) {
open fun createViews(context: Context, state: WidgetState): RemoteViews {
val views = RemoteViews(context.packageName, layout)
views.setOnClickPendingIntent(
android.R.id.background,
context.newMainIntent()
)
return views
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -12,7 +12,7 @@
android:contentDescription="@string/desc_no_cover"
android:src="@drawable/ic_song" />
<LinearLayout style="@style/Widget.Component.AppWidget.Panel.Base">
<LinearLayout style="@style/Widget.Component.AppWidget.Panel">
<TextView
android:id="@+id/widget_song"

View file

@ -12,7 +12,7 @@
android:contentDescription="@string/desc_no_cover"
android:src="@drawable/ic_song" />
<LinearLayout style="@style/Widget.Component.AppWidget.Panel.Base">
<LinearLayout style="@style/Widget.Component.AppWidget.Panel">
<TextView
android:id="@+id/widget_song"

View file

@ -44,11 +44,15 @@
<style name="Widget.Component.AppWidget.ImageView" parent="">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">0dp</item>
<item name="android:layout_weight">1</item>
<item name="android:background">?attr/colorSurface</item>
<item name="android:scaleType">centerCrop</item>
</style>
<style name="Widget.Component.AppWidget.Button.Base" parent="Widget.AppCompat.Button.Borderless">
<item name="android:layout_height">@dimen/height_widget_button</item>
<item name="android:layout_width">0dp</item>
<item name="android:layout_weight">1</item>
<item name="android:scaleType">fitCenter</item>
<item name="android:padding">@dimen/spacing_micro</item>
</style>

View file

@ -6,7 +6,7 @@
android:minResizeHeight="110dp"
android:previewLayout="@layout/widget_small"
android:resizeMode="horizontal|vertical"
android:targetCellWidth="2"
android:targetCellWidth="3"
android:targetCellHeight="2"
android:updatePeriodMillis="0"
android:widgetCategory="home_screen" />

View file

@ -1,8 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget_small"
android:minWidth="110dp"
android:minWidth="180dp"
android:minHeight="110dp"
android:minResizeWidth="110dp"
android:minResizeHeight="110dp"
android:previewImage="@drawable/ui_widget_preview"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="0"