widgets: improve widgets

Enhance widgets in a couple of ways:
1. Make the cover art expand to fit it's aspect ratio with padding
instead of the entire widget. This does the cover art justice and
is more visually appealing in general.
2. Add two new widget forms: Terminal, which applies to only the
smallest widgets, and Minimal, which applies to short and wide
widgets
This commit is contained in:
OxygenCobalt 2021-10-21 19:17:55 -06:00
parent 9030de7774
commit 36228d0536
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
13 changed files with 346 additions and 49 deletions

View file

@ -27,6 +27,8 @@ import androidx.databinding.BindingAdapter
import coil.Coil
import coil.fetch.Fetcher
import coil.request.ImageRequest
import coil.size.OriginalSize
import coil.transform.RoundedCornersTransformation
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
@ -110,7 +112,12 @@ inline fun <reified T : BaseModel> ImageView.load(
* failed/shouldn't occur.
* **This not meant for UIs, instead use the Binding Adapters.**
*/
fun loadBitmap(context: Context, song: Song, onDone: (Bitmap?) -> Unit) {
fun loadBitmap(
context: Context,
song: Song,
cornerRadius: Float = 0f,
onDone: (Bitmap?) -> Unit
) {
val settingsManager = SettingsManager.getInstance()
if (!settingsManager.showCovers) {
@ -122,6 +129,8 @@ fun loadBitmap(context: Context, song: Song, onDone: (Bitmap?) -> Unit) {
ImageRequest.Builder(context)
.data(song.album)
.fetcher(AlbumArtFetcher(context))
.size(OriginalSize)
.transformations(RoundedCornersTransformation(cornerRadius))
.target(
onError = { onDone(null) },
onSuccess = { onDone(it.toBitmap()) }

View file

@ -56,9 +56,6 @@ import org.oxycblt.auxio.util.logE
/**
* The main "Launching Point" fragment of Auxio, allowing navigation to the detail
* views for each respective fragment.
* FIXME: Edge-to-edge is borked still, unsure how to really fix this aside from making some
* magic layout like Material Files, but even then it might not work since the scrolling
* views are not laid side-by-side to the layout itself.
* @author OxygenCobalt
*/
class HomeFragment : Fragment() {

View file

@ -90,8 +90,6 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
// themselves when being dragged. Too bad google's implementation of this doesn't even
// work! To emulate it on my own, I check if this child is in a drag state and then animate
// an elevation change.
// TODO: Some other enhancements I could make maybe
// - Maybe stopping dragged items from extending beyond their specific part of the queue?
val holder = viewHolder as QueueAdapter.QueueSongViewHolder

View file

@ -194,6 +194,7 @@ fun View.applyEdge(onApply: (Rect) -> Unit) {
* and removing the padding if it isnt available, which works okayish. I think Material Files has
* a better implementation of the same fix however, so once I'm able to hack that layout into
* Auxio things should be better.
* TODO: Get rid of this get rid of this get rid of this
*/
fun RecyclerView.applyEdgeRespectingBar(
playbackModel: PlaybackViewModel,

View file

@ -56,7 +56,7 @@ private fun RemoteViews.applyMeta(context: Context, state: WidgetState) {
}
}
fun RemoteViews.applyControls(context: Context, state: WidgetState) {
private fun RemoteViews.applyControls(context: Context, state: WidgetState) {
setOnClickPendingIntent(
R.id.widget_skip_prev,
context.newBroadcastIntent(
@ -92,6 +92,20 @@ fun createDefaultWidget(context: Context): RemoteViews {
return createViews(context, R.layout.widget_default)
}
fun createTerminalWidget(context: Context, state: WidgetState): RemoteViews {
val views = createViews(context, R.layout.widget_terminal)
views.applyMeta(context, state)
views.applyControls(context, state)
return views
}
fun createMinimalWidget(context: Context, state: WidgetState): RemoteViews {
val views = createViews(context, R.layout.widget_minimal)
views.applyMeta(context, state)
views.applyControls(context, state)
return views
}
fun createSmallWidget(context: Context, state: WidgetState): RemoteViews {
val views = createViews(context, R.layout.widget_small)
views.applyMeta(context, state)

View file

@ -24,23 +24,32 @@ import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.util.SizeF
import android.widget.RemoteViews
import androidx.core.graphics.drawable.toBitmap
import coil.Coil
import coil.request.ImageRequest
import coil.size.OriginalSize
import coil.transform.RoundedCornersTransformation
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.coil.loadBitmap
import org.oxycblt.auxio.coil.AlbumArtFetcher
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.util.isLandscape
import org.oxycblt.auxio.util.logD
/**
* Auxio's one and only appwidget. This widget follows a more unorthodox approach, effectively
* packing what could be considered multiple widgets into a single responsive widget. More
* specifically:
* packing what could be considered multiple widgets into a single responsive widget. All types
* are listed below:
*
* - 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
* - Full: Large widgets will show cover art and all controls
* - Small: Tall and thin widgets will show cover art and three controls
* - Minimal: Wide and short widgets will show cover art and all controls in a compact manner
* - Text: Small widgets will only show text and three controls
*
* There are some minor problems with this implementation [notably UI jittering when the widget
* picks a new layout below Android 12], but this is tolerable. It may be improved in the future.
@ -60,7 +69,7 @@ class WidgetProvider : AppWidgetProvider() {
return
}
loadBitmap(context, song) { bitmap ->
loadWidgetBitmap(context, song) { bitmap ->
val state = WidgetState(
song,
bitmap,
@ -71,14 +80,39 @@ class WidgetProvider : AppWidgetProvider() {
// Map each widget form to the cells where it would look at least okay.
val views = mapOf(
SizeF(180f, 110f) to createSmallWidget(context, state),
SizeF(250f, 110f) to createFullWidget(context, state)
SizeF(180f, 110f) to createTerminalWidget(context, state),
SizeF(250f, 110f) to createMinimalWidget(context, state),
SizeF(180f, 270f) to createSmallWidget(context, state),
SizeF(250f, 270f) to createFullWidget(context, state)
)
appWidgetManager.applyViewsCompat(context, views)
}
}
private fun loadWidgetBitmap(context: Context, song: Song, onDone: (Bitmap?) -> Unit) {
val cornerRadius = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
context.resources.getDimensionPixelSize(
android.R.dimen.system_app_widget_inner_radius
).toFloat()
} else {
0f
}
Coil.imageLoader(context).enqueue(
ImageRequest.Builder(context)
.data(song.album)
.fetcher(AlbumArtFetcher(context))
.size(OriginalSize)
.transformations(RoundedCornersTransformation(cornerRadius))
.target(
onError = { onDone(null) },
onSuccess = { onDone(it.toBitmap()) }
)
.build()
)
}
/*
* Revert this widget to its default view
*/
@ -110,9 +144,12 @@ class WidgetProvider : AppWidgetProvider() {
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)
} else {
logD(newOptions?.getParcelableArrayList<SizeF>(AppWidgetManager.OPTION_APPWIDGET_SIZES) ?: "nothing")
}
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid
android:color="#00000000" />
<size
android:width="1000px"
android:height="1000px"/>
</shape>

View file

@ -1,22 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@android:id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="?attr/colorSurface"
android:theme="@style/Theme.Widget">
android:theme="@style/Theme.Widget"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.widget.ImageView
<!--
We can't use ConstraintLayout on widgets due to RemoteView limitations, but
for this widget to work, we need to get the cover to preserve it's aspect
ratio. So, we use a fixed-size 1000x1000 drawable and then align the cover view
to that so that the bounds will scale properly.
This is easily one of the worst layout hacks I've done, but it works.
-->
<ImageView
android:id="@+id/widget_aspect_ratio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:layout_marginTop="@dimen/spacing_medium"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_above="@+id/widget_panel"
android:src="@drawable/ui_widget_aspect_ratio"
android:visibility="invisible"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/widget_cover"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"
android:contentDescription="@string/desc_no_cover"
android:scaleType="centerCrop"
android:src="@drawable/ic_song" />
android:layout_alignStart="@id/widget_aspect_ratio"
android:layout_alignTop="@id/widget_aspect_ratio"
android:layout_alignEnd="@id/widget_aspect_ratio"
android:layout_alignBottom="@id/widget_aspect_ratio"
android:src="@drawable/ic_song"
tools:ignore="ContentDescription" />
<android.widget.LinearLayout style="@style/Widget.Auxio.AppWidget.Panel">
<android.widget.LinearLayout
android:id="@+id/widget_panel"
style="@style/Widget.Auxio.AppWidget.Panel"
android:layout_alignParentBottom="true"
android:layout_gravity="center">
<android.widget.TextView
android:id="@+id/widget_song"
@ -35,8 +66,7 @@
<android.widget.LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_medium"
android:orientation="horizontal">
android:layout_marginTop="@dimen/spacing_medium">
<android.widget.ImageButton
android:id="@+id/widget_loop"
@ -87,4 +117,4 @@
</android.widget.LinearLayout>
</LinearLayout>
</RelativeLayout>

View file

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_vertical"
android:background="?attr/colorSurface"
android:theme="@style/Theme.Widget">
<android.widget.LinearLayout style="@style/Widget.Auxio.AppWidget.Panel">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/widget_cover"
style="@style/Widget.Auxio.Image.Normal"
android:contentDescription="@string/desc_no_cover"
android:scaleType="centerCrop"
android:layout_marginEnd="@dimen/spacing_medium"
android:src="@drawable/ic_song" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:orientation="vertical">
<android.widget.TextView
android:id="@+id/widget_song"
style="@style/Widget.Auxio.TextView.Primary.AppWidget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/def_widget_song" />
<android.widget.TextView
android:id="@+id/widget_artist"
style="@style/Widget.Auxio.TextView.Secondary.AppWidget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/def_widget_artist" />
</LinearLayout>
</LinearLayout>
<android.widget.LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_medium"
android:orientation="horizontal">
<android.widget.ImageButton
android:id="@+id/widget_loop"
style="@style/Widget.Auxio.Button.AppWidget"
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/desc_change_loop"
android:src="@drawable/ic_loop" />
<android.widget.ImageButton
android:id="@+id/widget_skip_prev"
android:layout_weight="1"
style="@style/Widget.Auxio.Button.AppWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/desc_skip_prev"
android:src="@drawable/ic_skip_prev" />
<android.widget.ImageButton
android:id="@+id/widget_play_pause"
android:layout_weight="1"
style="@style/Widget.Auxio.Button.AppWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/desc_play_pause"
android:src="@drawable/sel_playing_state" />
<android.widget.ImageButton
android:id="@+id/widget_skip_next"
android:layout_weight="1"
style="@style/Widget.Auxio.Button.AppWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/desc_skip_next"
android:src="@drawable/ic_skip_next" />
<android.widget.ImageButton
android:id="@+id/widget_shuffle"
style="@style/Widget.Auxio.Button.AppWidget"
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/desc_shuffle"
android:src="@drawable/ic_shuffle" />
</android.widget.LinearLayout>
</android.widget.LinearLayout>
</LinearLayout>

View file

@ -1,22 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@android:id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="?attr/colorSurface"
android:theme="@style/Theme.Widget">
android:theme="@style/Theme.Widget"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.widget.ImageView
<!--
We can't use ConstraintLayout on widgets due to RemoteView limitations, but
for this widget form to work, we need to get the cover to preserve it's aspect
ratio. So, we use a fixed-size 1000x1000 drawable and then align the cover view
to that so that the bounds will scale properly.
This is easily one of the worst layout hacks I've done, but it seems to work.
-->
<ImageView
android:id="@+id/widget_aspect_ratio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:layout_marginTop="@dimen/spacing_medium"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_above="@+id/widget_panel"
android:src="@drawable/ui_widget_aspect_ratio"
android:visibility="invisible"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/widget_cover"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"
android:contentDescription="@string/desc_no_cover"
android:scaleType="centerCrop"
android:src="@drawable/ic_song" />
android:layout_alignStart="@id/widget_aspect_ratio"
android:layout_alignTop="@id/widget_aspect_ratio"
android:layout_alignEnd="@id/widget_aspect_ratio"
android:layout_alignBottom="@id/widget_aspect_ratio"
android:src="@drawable/ic_song"
tools:ignore="ContentDescription" />
<android.widget.LinearLayout style="@style/Widget.Auxio.AppWidget.Panel">
<android.widget.LinearLayout
android:id="@+id/widget_panel"
style="@style/Widget.Auxio.AppWidget.Panel"
android:layout_alignParentBottom="true"
android:layout_gravity="center">
<android.widget.TextView
android:id="@+id/widget_song"
@ -35,37 +66,39 @@
<android.widget.LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_medium">
android:layout_marginTop="@dimen/spacing_medium"
android:orientation="horizontal">
<android.widget.ImageButton
android:id="@+id/widget_skip_prev"
style="@style/Widget.Auxio.Button.AppWidget"
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:contentDescription="@string/desc_skip_prev"
android:src="@drawable/ic_skip_prev" />
<android.widget.ImageButton
android:id="@+id/widget_play_pause"
style="@style/Widget.Auxio.Button.AppWidget"
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:contentDescription="@string/desc_play_pause"
android:src="@drawable/ic_play" />
<android.widget.ImageButton
android:id="@+id/widget_skip_next"
style="@style/Widget.Auxio.Button.AppWidget"
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:contentDescription="@string/desc_skip_next"
android:src="@drawable/ic_skip_next" />
</android.widget.LinearLayout>
</android.widget.LinearLayout>
</RelativeLayout>
</LinearLayout>

View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_vertical"
android:background="?attr/colorSurface"
android:theme="@style/Theme.Widget">
<android.widget.LinearLayout style="@style/Widget.Auxio.AppWidget.Panel">
<android.widget.TextView
android:id="@+id/widget_song"
style="@style/Widget.Auxio.TextView.Primary.AppWidget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/def_widget_song" />
<android.widget.TextView
android:id="@+id/widget_artist"
style="@style/Widget.Auxio.TextView.Secondary.AppWidget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/def_widget_artist" />
<android.widget.LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_medium">
<android.widget.ImageButton
android:id="@+id/widget_skip_prev"
style="@style/Widget.Auxio.Button.AppWidget"
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/desc_skip_prev"
android:src="@drawable/ic_skip_prev" />
<android.widget.ImageButton
android:id="@+id/widget_play_pause"
style="@style/Widget.Auxio.Button.AppWidget"
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/desc_play_pause"
android:src="@drawable/ic_play" />
<android.widget.ImageButton
android:id="@+id/widget_skip_next"
style="@style/Widget.Auxio.Button.AppWidget"
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/desc_skip_next"
android:src="@drawable/ic_skip_next" />
</android.widget.LinearLayout>
</android.widget.LinearLayout>
</LinearLayout>

View file

@ -7,6 +7,6 @@
android:previewLayout="@layout/widget_small"
android:resizeMode="horizontal|vertical"
android:minWidth="180dp"
android:minHeight="110dp"
android:minHeight="180dp"
android:updatePeriodMillis="0"
android:widgetCategory="home_screen" />

View file

@ -2,7 +2,7 @@
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget_small"
android:minWidth="180dp"
android:minHeight="110dp"
android:minHeight="180dp"
android:minResizeWidth="180dp"
android:minResizeHeight="110dp"
android:previewImage="@drawable/ui_widget_preview"