widgets: rework tiny widget
Rework the tiny widget to cram even more information into it. The tiny widget is nominally meant for edge cases like exceptionally small screens or landscape mode, but apparently it's triggered on some devices in normal use because of platform fragmentation and OEM insanity. Update the tiny widget layout with some new buttons in order to make it more usable in mnormal use. This is still nowehre near ideal. For example, when triggering the layout on my device, it ends up squishing the buttons. But it should probably work better outside of those edge cases.
This commit is contained in:
parent
485c35d74c
commit
eb293022e8
16 changed files with 85 additions and 91 deletions
|
@ -39,7 +39,8 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
|||
/**
|
||||
* The single [AppCompatActivity] for Auxio.
|
||||
*
|
||||
* TODO: Add a new view for crashes with a stack trace
|
||||
* TODO: Add crash reporting and error screens. This likely has to be an external activity, so it is
|
||||
* blocked by eliminating exitProcess from the app.
|
||||
*
|
||||
* TODO: Custom language support
|
||||
*
|
||||
|
|
|
@ -44,8 +44,6 @@ import org.oxycblt.auxio.util.logD
|
|||
* A wrapper around the home fragment that shows the playback fragment and controls the more
|
||||
* high-level navigation features.
|
||||
* @author OxygenCobalt
|
||||
*
|
||||
* TODO: Add a new view with a stack trace whenever the music loading process fails.
|
||||
*/
|
||||
class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
|
|
|
@ -41,6 +41,7 @@ import org.oxycblt.auxio.music.Song
|
|||
class BitmapProvider(private val context: Context) {
|
||||
private var currentRequest: Request? = null
|
||||
|
||||
/* If this provider is currently attempting to load something. */
|
||||
val isBusy: Boolean
|
||||
get() = currentRequest?.run { !disposable.isDisposed } ?: false
|
||||
|
||||
|
@ -53,7 +54,7 @@ class BitmapProvider(private val context: Context) {
|
|||
currentRequest = null
|
||||
|
||||
val request =
|
||||
target.setupRequest(
|
||||
target.onConfigRequest(
|
||||
ImageRequest.Builder(context)
|
||||
.data(song)
|
||||
.size(Size.ORIGINAL)
|
||||
|
@ -76,8 +77,15 @@ class BitmapProvider(private val context: Context) {
|
|||
|
||||
private data class Request(val disposable: Disposable, val callback: Target)
|
||||
|
||||
/** Represents the target for a request. */
|
||||
interface Target {
|
||||
fun setupRequest(builder: ImageRequest.Builder): ImageRequest.Builder = builder
|
||||
/** Modify the default request with custom attributes. */
|
||||
fun onConfigRequest(builder: ImageRequest.Builder): ImageRequest.Builder = builder
|
||||
|
||||
/**
|
||||
* Called when the loading process is completed. [bitmap] will be null if there was an
|
||||
* error.
|
||||
*/
|
||||
fun onCompleted(bitmap: Bitmap?)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,9 +71,8 @@ class AlbumArtFetcher private constructor(private val context: Context, private
|
|||
}
|
||||
|
||||
class AlbumFactory : Fetcher.Factory<Album> {
|
||||
override fun create(data: Album, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||
return AlbumArtFetcher(options.context, data)
|
||||
}
|
||||
override fun create(data: Album, options: Options, imageLoader: ImageLoader) =
|
||||
AlbumArtFetcher(options.context, data)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,14 +89,12 @@ private constructor(
|
|||
override suspend fun fetch(): FetchResult? {
|
||||
val albums = Sort.ByName(true).albums(artist.albums)
|
||||
val results = albums.mapAtMost(4) { album -> fetchArt(context, album) }
|
||||
|
||||
return createMosaic(context, results, size)
|
||||
}
|
||||
|
||||
class Factory : Fetcher.Factory<Artist> {
|
||||
override fun create(data: Artist, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||
return ArtistImageFetcher(options.context, options.size, data)
|
||||
}
|
||||
override fun create(data: Artist, options: Options, imageLoader: ImageLoader) =
|
||||
ArtistImageFetcher(options.context, options.size, data)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,14 +112,12 @@ private constructor(
|
|||
// Don't sort here to preserve compatibility with previous versions of this image.
|
||||
val albums = genre.songs.groupBy { it.album }.keys
|
||||
val results = albums.mapAtMost(4) { album -> fetchArt(context, album) }
|
||||
|
||||
return createMosaic(context, results, size)
|
||||
}
|
||||
|
||||
class Factory : Fetcher.Factory<Genre> {
|
||||
override fun create(data: Genre, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||
return GenreImageFetcher(options.context, options.size, data)
|
||||
}
|
||||
override fun create(data: Genre, options: Options, imageLoader: ImageLoader) =
|
||||
GenreImageFetcher(options.context, options.size, data)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -119,9 +119,7 @@ sealed class Sort(open val isAscending: Boolean) {
|
|||
genres.sortWith(compareByDynamic(NameComparator()) { it })
|
||||
}
|
||||
|
||||
override fun ascending(newIsAscending: Boolean): Sort {
|
||||
return ByName(newIsAscending)
|
||||
}
|
||||
override fun ascending(newIsAscending: Boolean) = ByName(newIsAscending)
|
||||
}
|
||||
|
||||
/** Sort by the album of an item, only supported by [Song] */
|
||||
|
@ -140,9 +138,7 @@ sealed class Sort(open val isAscending: Boolean) {
|
|||
compareBy(NameComparator()) { it }))
|
||||
}
|
||||
|
||||
override fun ascending(newIsAscending: Boolean): Sort {
|
||||
return ByAlbum(newIsAscending)
|
||||
}
|
||||
override fun ascending(newIsAscending: Boolean) = ByAlbum(newIsAscending)
|
||||
}
|
||||
|
||||
/** Sort by the artist of an item, only supported by [Album] and [Song] */
|
||||
|
@ -171,9 +167,7 @@ sealed class Sort(open val isAscending: Boolean) {
|
|||
compareBy(NameComparator()) { it }))
|
||||
}
|
||||
|
||||
override fun ascending(newIsAscending: Boolean): Sort {
|
||||
return ByArtist(newIsAscending)
|
||||
}
|
||||
override fun ascending(newIsAscending: Boolean) = ByArtist(newIsAscending)
|
||||
}
|
||||
|
||||
/** Sort by the year of an item, only supported by [Album] and [Song] */
|
||||
|
@ -200,9 +194,7 @@ sealed class Sort(open val isAscending: Boolean) {
|
|||
compareBy(NameComparator()) { it }))
|
||||
}
|
||||
|
||||
override fun ascending(newIsAscending: Boolean): Sort {
|
||||
return ByYear(newIsAscending)
|
||||
}
|
||||
override fun ascending(newIsAscending: Boolean) = ByYear(newIsAscending)
|
||||
}
|
||||
|
||||
/** Sort by the duration of the item. Supports all items. */
|
||||
|
@ -237,9 +229,7 @@ sealed class Sort(open val isAscending: Boolean) {
|
|||
compareByDynamic { it.durationSecs }, compareBy(NameComparator()) { it }))
|
||||
}
|
||||
|
||||
override fun ascending(newIsAscending: Boolean): Sort {
|
||||
return ByDuration(newIsAscending)
|
||||
}
|
||||
override fun ascending(newIsAscending: Boolean) = ByDuration(newIsAscending)
|
||||
}
|
||||
|
||||
/** Sort by the amount of songs. Only applicable to music parents. */
|
||||
|
@ -268,9 +258,7 @@ sealed class Sort(open val isAscending: Boolean) {
|
|||
compareByDynamic { it.songs.size }, compareBy(NameComparator()) { it }))
|
||||
}
|
||||
|
||||
override fun ascending(newIsAscending: Boolean): Sort {
|
||||
return ByCount(newIsAscending)
|
||||
}
|
||||
override fun ascending(newIsAscending: Boolean) = ByCount(newIsAscending)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -293,9 +281,7 @@ sealed class Sort(open val isAscending: Boolean) {
|
|||
compareBy(NameComparator()) { it }))
|
||||
}
|
||||
|
||||
override fun ascending(newIsAscending: Boolean): Sort {
|
||||
return ByDisc(newIsAscending)
|
||||
}
|
||||
override fun ascending(newIsAscending: Boolean) = ByDisc(newIsAscending)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -317,9 +303,7 @@ sealed class Sort(open val isAscending: Boolean) {
|
|||
compareBy(NameComparator()) { it }))
|
||||
}
|
||||
|
||||
override fun ascending(newIsAscending: Boolean): Sort {
|
||||
return ByTrack(newIsAscending)
|
||||
}
|
||||
override fun ascending(newIsAscending: Boolean) = ByTrack(newIsAscending)
|
||||
}
|
||||
|
||||
val intCode: Int
|
||||
|
|
|
@ -41,7 +41,6 @@ import org.oxycblt.auxio.music.Song
|
|||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import org.oxycblt.auxio.util.getColorStateListSafe
|
||||
import org.oxycblt.auxio.util.getDrawableSafe
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* An [AppCompatImageView] that applies many of the stylistic choices that Auxio uses regarding
|
||||
|
@ -130,7 +129,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
logD(src.alpha)
|
||||
src.bounds.set(canvas.clipBounds)
|
||||
val adjustWidth = src.bounds.width() / 4
|
||||
val adjustHeight = src.bounds.height() / 4
|
||||
|
|
|
@ -35,13 +35,13 @@ fun createDefaultWidget(context: Context): RemoteViews {
|
|||
}
|
||||
|
||||
/**
|
||||
* The tiny widget is for an edge-case situation where a 2xN widget happens to be smaller than
|
||||
* 100dp. It just shows the cover, titles, and a button.
|
||||
* The tiny widget is for an edge-case situation where a widget falls under the size class of the
|
||||
* small widget, either via landscape mode or exceptionally small screens.
|
||||
*/
|
||||
fun createTinyWidget(context: Context, state: WidgetComponent.WidgetState): RemoteViews {
|
||||
return createViews(context, R.layout.widget_tiny)
|
||||
.applyMeta(context, state)
|
||||
.applyPlayControls(context, state)
|
||||
.applyBasicControls(context, state)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -50,6 +50,10 @@ class WidgetComponent(private val context: Context) :
|
|||
init {
|
||||
playbackManager.addCallback(this)
|
||||
settingsManager.addCallback(this)
|
||||
|
||||
if (playbackManager.isInitialized) {
|
||||
update()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -76,7 +80,7 @@ class WidgetComponent(private val context: Context) :
|
|||
provider.load(
|
||||
song,
|
||||
object : BitmapProvider.Target {
|
||||
override fun setupRequest(builder: ImageRequest.Builder): ImageRequest.Builder {
|
||||
override fun onConfigRequest(builder: ImageRequest.Builder): ImageRequest.Builder {
|
||||
// The widget has two distinct styles that we must transform the album art to
|
||||
// accommodate:
|
||||
// - Before Android 12, the widget has hard edges, so we don't need to round
|
||||
|
|
|
@ -122,6 +122,13 @@ class WidgetProvider : AppWidgetProvider() {
|
|||
val name = ComponentName(context, WidgetProvider::class.java)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
for (id in getAppWidgetIds(name)) {
|
||||
val options = getAppWidgetOptions(id)
|
||||
logD(
|
||||
options.getParcelableArrayList<SizeF>(AppWidgetManager.OPTION_APPWIDGET_SIZES)
|
||||
?: "no sizes")
|
||||
}
|
||||
|
||||
// Widgets are automatically responsive on Android 12, no need to do anything.
|
||||
updateAppWidget(name, RemoteViews(views))
|
||||
} else {
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6 -1.41,-1.41z" />
|
||||
android:pathData="M 18.119999,7.0600006 12,13.166666 5.8800004,7.0600006 4.0000006,8.9400004 12,16.939999 19.999999,8.9400004 Z" />
|
||||
</vector>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
a terrible API.
|
||||
-->
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:fillColor="?attr/colorPrimary"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,16.5c-2.49,0 -4.5,-2.01 -4.5,-4.5S9.51,7.5 12,7.5s4.5,2.01 4.5,4.5 -2.01,4.5 -4.5,4.5zM12,11c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1z" />
|
||||
<path
|
||||
android:fillColor="?attr/colorSurface"
|
||||
|
|
|
@ -8,14 +8,7 @@
|
|||
android:theme="@style/Theme.Widget">
|
||||
|
||||
<!--
|
||||
For this widget form to work, we need to scale the ImageView across a 1:1 aspect ratio.
|
||||
However, since we are working with a RemoteViews instance, we can't just use ConstraintLayout
|
||||
to achieve this. We can use RelativeLayout, but there's no way to force an aspect ratio with
|
||||
that layout. However, if we create an invisible ImageView that contains a massive fixed-size
|
||||
drawable and then clamp our main ImageView to it, we can make the view scale on a 1:1 aspect
|
||||
ratio.
|
||||
|
||||
This is easily one of the worst layout hacks I've done, but it seems to work.
|
||||
See widget_small.xml for an explanation for the ImageView setup
|
||||
-->
|
||||
|
||||
<android.widget.ImageView
|
||||
|
|
|
@ -8,14 +8,7 @@
|
|||
android:theme="@style/Theme.Widget">
|
||||
|
||||
<!--
|
||||
For this widget form to work, we need to scale the ImageView across a 1:1 aspect ratio.
|
||||
However, since we are working with a RemoteViews instance, we can't just use ConstraintLayout
|
||||
to achieve this. We can use RelativeLayout, but there's no way to force an aspect ratio with
|
||||
that layout. However, if we create an invisible ImageView that contains a massive fixed-size
|
||||
drawable and then clamp our main ImageView to it, we can make the view scale on a 1:1 aspect
|
||||
ratio.
|
||||
|
||||
This is easily one of the worst layout hacks I've done, but it seems to work.
|
||||
See widget_small.xml for an explanation for the ImageView setup
|
||||
-->
|
||||
|
||||
<android.widget.ImageView
|
||||
|
|
|
@ -8,11 +8,6 @@
|
|||
android:orientation="vertical"
|
||||
android:theme="@style/Theme.Widget">
|
||||
|
||||
<!--
|
||||
This is a throwaway layout designed for the rare edge-case where a 2xN widget is shown
|
||||
in landscape mode.
|
||||
-->
|
||||
|
||||
<android.widget.LinearLayout
|
||||
style="@style/Widget.Auxio.AppWidget.Panel"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -22,7 +17,8 @@
|
|||
|
||||
<android.widget.ImageView
|
||||
android:id="@+id/widget_cover"
|
||||
style="@style/Widget.Auxio.Image.Medium"
|
||||
android:layout_width="88dp"
|
||||
android:layout_height="88dp"
|
||||
android:layout_marginEnd="@dimen/spacing_medium"
|
||||
android:contentDescription="@string/desc_no_cover"
|
||||
android:scaleType="centerCrop"
|
||||
|
@ -37,28 +33,51 @@
|
|||
<android.widget.TextView
|
||||
android:id="@+id/widget_song"
|
||||
style="@style/Widget.Auxio.TextView.Primary.AppWidget"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
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_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/def_widget_artist" />
|
||||
|
||||
</android.widget.LinearLayout>
|
||||
<android.widget.LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_small"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<android.widget.ImageButton
|
||||
android:id="@+id/widget_skip_prev"
|
||||
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
|
||||
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.PlaybackButton.AppWidget"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="@string/desc_play_pause"
|
||||
android:minWidth="@dimen/size_btn_small"
|
||||
android:minHeight="@dimen/size_btn_small"
|
||||
android:src="@drawable/sel_playing_state" />
|
||||
android:src="@drawable/ic_play" />
|
||||
|
||||
<android.widget.ImageButton
|
||||
android:id="@+id/widget_skip_next"
|
||||
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
|
||||
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>
|
||||
</android.widget.LinearLayout>
|
||||
</android.widget.LinearLayout>
|
||||
|
|
|
@ -8,14 +8,7 @@
|
|||
android:theme="@style/Theme.Widget">
|
||||
|
||||
<!--
|
||||
For this widget form to work, we need to scale the ImageView across a 1:1 aspect ratio.
|
||||
However, since we are working with a RemoteViews instance, we can't just use ConstraintLayout
|
||||
to achieve this. We can use RelativeLayout, but there's no way to force an aspect ratio with
|
||||
that layout. However, if we create an invisible ImageView that contains a massive fixed-size
|
||||
drawable and then clamp our main ImageView to it, we can make the view scale on a 1:1 aspect
|
||||
ratio.
|
||||
|
||||
This is easily one of the worst layout hacks I've done, but it seems to work.
|
||||
See widget_small.xml for an explanation for the ImageView setup
|
||||
-->
|
||||
|
||||
<android.widget.ImageView
|
||||
|
|
|
@ -178,6 +178,7 @@
|
|||
<string name="clr_orange">Orange</string>
|
||||
<string name="clr_brown">Brown</string>
|
||||
<string name="clr_grey">Grey</string>
|
||||
<string name="clr_dynamic">Dynamic</string>
|
||||
|
||||
<!-- Format Namespace | Value formatting/plurals -->
|
||||
<string name="fmt_disc_no">Disc %d</string>
|
||||
|
|
Loading…
Reference in a new issue