ui: add animated playing indicator [#218]
Make the playing indicator animate when playback is ongoing. Previously state issues stopped me from doing this, but apparently this time I miraculously got it working. Yay. Resolves #218.
This commit is contained in:
parent
227a258eca
commit
acaf679000
25 changed files with 879 additions and 287 deletions
|
@ -3,6 +3,9 @@
|
|||
## dev
|
||||
|
||||
#### What's New
|
||||
- Improved playing indicators [#218]
|
||||
- Search and library now show playing indicators
|
||||
- Playing indicators are now animated when playback is ongoing
|
||||
- Added smooth seeking
|
||||
- Queue now has a fast scroller
|
||||
|
||||
|
|
|
@ -69,6 +69,8 @@ dependencies {
|
|||
// --- SUPPORT ---
|
||||
|
||||
// General
|
||||
// 1.4.0 is used in order to avoid a ripple bug in material components
|
||||
implementation "androidx.appcompat:appcompat:1.4.0"
|
||||
implementation "androidx.core:core-ktx:1.8.0"
|
||||
implementation "androidx.activity:activity-ktx:1.6.0-rc01"
|
||||
implementation "androidx.fragment:fragment-ktx:1.5.2"
|
||||
|
|
|
@ -96,7 +96,8 @@ class AlbumDetailFragment :
|
|||
|
||||
collectImmediately(detailModel.currentAlbum, ::handleItemChange)
|
||||
collectImmediately(detailModel.albumData, detailAdapter::submitList)
|
||||
collectImmediately(playbackModel.song, playbackModel.parent, ::updatePlayback)
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||
}
|
||||
|
||||
|
@ -135,6 +136,7 @@ class AlbumDetailFragment :
|
|||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
if (item is Song) {
|
||||
musicMenu(anchor, R.menu.menu_album_song_actions, item)
|
||||
return
|
||||
}
|
||||
|
||||
error("Unexpected datatype when opening menu: ${item::class.java}")
|
||||
|
@ -244,7 +246,7 @@ class AlbumDetailFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?) {
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
val binding = requireBinding()
|
||||
|
||||
for (item in binding.detailToolbar.menu.children) {
|
||||
|
@ -257,10 +259,10 @@ class AlbumDetailFragment :
|
|||
}
|
||||
|
||||
if (parent is Album && parent.id == unlikelyToBeNull(detailModel.currentAlbum.value).id) {
|
||||
detailAdapter.activateSong(song)
|
||||
detailAdapter.updateIndicator(song, isPlaying)
|
||||
} else {
|
||||
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
||||
detailAdapter.activateSong(null)
|
||||
detailAdapter.updateIndicator(null, isPlaying)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -91,7 +91,8 @@ class ArtistDetailFragment :
|
|||
|
||||
collectImmediately(detailModel.currentArtist, ::handleItemChange)
|
||||
collectImmediately(detailModel.artistData, detailAdapter::submitList)
|
||||
collectImmediately(playbackModel.song, playbackModel.parent, ::updatePlayback)
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||
}
|
||||
|
||||
|
@ -200,20 +201,17 @@ class ArtistDetailFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?) {
|
||||
if (parent is Artist && parent.id == unlikelyToBeNull(detailModel.currentArtist.value).id) {
|
||||
detailAdapter.activateSong(song)
|
||||
} else {
|
||||
// Ignore song playback not from the artist
|
||||
detailAdapter.activateSong(null)
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
var item: Item? = null
|
||||
|
||||
if (parent is Album) {
|
||||
item = parent
|
||||
}
|
||||
|
||||
if (parent is Album &&
|
||||
parent.artist.id == unlikelyToBeNull(detailModel.currentArtist.value).id) {
|
||||
detailAdapter.activateAlbum(parent)
|
||||
} else {
|
||||
// Ignore album playback not from the artist
|
||||
detailAdapter.activateAlbum(null)
|
||||
if (parent is Artist && parent.id == unlikelyToBeNull(detailModel.currentArtist.value).id) {
|
||||
item = song
|
||||
}
|
||||
|
||||
detailAdapter.updateIndicator(item, isPlaying)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,7 +92,8 @@ class GenreDetailFragment :
|
|||
|
||||
collectImmediately(detailModel.currentGenre, ::handleItemChange)
|
||||
collectImmediately(detailModel.genreData, detailAdapter::submitList)
|
||||
collectImmediately(playbackModel.song, playbackModel.parent, ::updatePlayback)
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||
}
|
||||
|
||||
|
@ -129,10 +130,11 @@ class GenreDetailFragment :
|
|||
}
|
||||
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
when (item) {
|
||||
is Song -> musicMenu(anchor, R.menu.menu_song_actions, item)
|
||||
else -> error("Unexpected datatype when opening menu: ${item::class.java}")
|
||||
if (item is Song) {
|
||||
musicMenu(anchor, R.menu.menu_song_actions, item)
|
||||
}
|
||||
|
||||
error("Unexpected datatype when opening menu: ${item::class.java}")
|
||||
}
|
||||
|
||||
override fun onPlayParent() {
|
||||
|
@ -193,12 +195,12 @@ class GenreDetailFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?) {
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
if (parent is Genre && parent.id == unlikelyToBeNull(detailModel.currentGenre.value).id) {
|
||||
detailAdapter.activateSong(song)
|
||||
detailAdapter.updateIndicator(song, isPlaying)
|
||||
} else {
|
||||
// Ignore song playback not from the genre
|
||||
detailAdapter.activateSong(null)
|
||||
detailAdapter.updateIndicator(null, isPlaying)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding
|
|||
import org.oxycblt.auxio.detail.DiscHeader
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.ui.recycler.IndicatorViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
||||
|
@ -43,7 +44,6 @@ import org.oxycblt.auxio.util.inflater
|
|||
*/
|
||||
class AlbumDetailAdapter(private val listener: Listener) :
|
||||
DetailAdapter<AlbumDetailAdapter.Listener>(listener, DIFFER) {
|
||||
private var currentSong: Song? = null
|
||||
|
||||
override fun getItemViewType(position: Int) =
|
||||
when (differ.currentList[position]) {
|
||||
|
@ -77,18 +77,6 @@ class AlbumDetailAdapter(private val listener: Listener) :
|
|||
}
|
||||
}
|
||||
|
||||
override fun shouldActivateViewHolder(position: Int): Boolean {
|
||||
val item = differ.currentList[position]
|
||||
return item is Song && item.id == currentSong?.id
|
||||
}
|
||||
|
||||
/** Update the [song] that this adapter should indicate playback */
|
||||
fun activateSong(song: Song?) {
|
||||
if (song == currentSong) return
|
||||
activateImpl(differ.currentList, currentSong, song)
|
||||
currentSong = song
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DIFFER =
|
||||
object : SimpleItemCallback<Item>() {
|
||||
|
@ -182,7 +170,7 @@ class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
|
|||
}
|
||||
|
||||
private class AlbumSongViewHolder private constructor(private val binding: ItemAlbumSongBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
IndicatorViewHolder(binding.root) {
|
||||
fun bind(item: Song, listener: MenuItemListener) {
|
||||
// Hide the track number view if the song does not have a track.
|
||||
if (item.track != null) {
|
||||
|
@ -210,6 +198,11 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
|
|||
binding.root.setOnClickListener { listener.onItemClick(item) }
|
||||
}
|
||||
|
||||
override fun updateIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||
binding.root.isActivated = isActive
|
||||
binding.songTrackBg.isPlaying = isPlaying
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ALBUM_SONG
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.oxycblt.auxio.music.Artist
|
|||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.resolveYear
|
||||
import org.oxycblt.auxio.ui.recycler.ArtistViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.IndicatorViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
||||
|
@ -44,8 +45,6 @@ import org.oxycblt.auxio.util.inflater
|
|||
*/
|
||||
class ArtistDetailAdapter(private val listener: Listener) :
|
||||
DetailAdapter<DetailAdapter.Listener>(listener, DIFFER) {
|
||||
private var currentAlbum: Album? = null
|
||||
private var currentSong: Song? = null
|
||||
|
||||
override fun getItemViewType(position: Int) =
|
||||
when (differ.currentList[position]) {
|
||||
|
@ -79,26 +78,6 @@ class ArtistDetailAdapter(private val listener: Listener) :
|
|||
}
|
||||
}
|
||||
|
||||
override fun shouldActivateViewHolder(position: Int): Boolean {
|
||||
val item = differ.currentList[position]
|
||||
return (item is Album && item.id == currentAlbum?.id) ||
|
||||
(item is Song && item.id == currentSong?.id)
|
||||
}
|
||||
|
||||
/** Update the [album] that this adapter should indicate playback */
|
||||
fun activateAlbum(album: Album?) {
|
||||
if (album == currentAlbum) return
|
||||
activateImpl(differ.currentList, currentAlbum, album)
|
||||
currentAlbum = album
|
||||
}
|
||||
|
||||
/** Update the [song] that this adapter should indicate playback */
|
||||
fun activateSong(song: Song?) {
|
||||
if (song == currentSong) return
|
||||
activateImpl(differ.currentList, currentSong, song)
|
||||
currentSong = song
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DIFFER =
|
||||
object : SimpleItemCallback<Item>() {
|
||||
|
@ -158,7 +137,7 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
|
|||
private class ArtistAlbumViewHolder
|
||||
private constructor(
|
||||
private val binding: ItemParentBinding,
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
) : IndicatorViewHolder(binding.root) {
|
||||
fun bind(item: Album, listener: MenuItemListener) {
|
||||
binding.parentImage.bind(item)
|
||||
binding.parentName.text = item.resolveName(binding.context)
|
||||
|
@ -171,6 +150,11 @@ private constructor(
|
|||
binding.root.setOnClickListener { listener.onItemClick(item) }
|
||||
}
|
||||
|
||||
override fun updateIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||
binding.root.isActivated = isActive
|
||||
binding.parentImage.isPlaying = isPlaying
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST_ALBUM
|
||||
|
||||
|
@ -188,7 +172,7 @@ private constructor(
|
|||
private class ArtistSongViewHolder
|
||||
private constructor(
|
||||
private val binding: ItemSongBinding,
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
) : IndicatorViewHolder(binding.root) {
|
||||
fun bind(item: Song, listener: MenuItemListener) {
|
||||
binding.songAlbumCover.bind(item)
|
||||
binding.songName.text = item.resolveName(binding.context)
|
||||
|
@ -201,6 +185,11 @@ private constructor(
|
|||
binding.root.setOnClickListener { listener.onItemClick(item) }
|
||||
}
|
||||
|
||||
override fun updateIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||
binding.root.isActivated = isActive
|
||||
binding.songAlbumCover.isPlaying = isPlaying
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST_SONG
|
||||
|
||||
|
|
|
@ -26,9 +26,9 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.databinding.ItemSortHeaderBinding
|
||||
import org.oxycblt.auxio.detail.SortHeader
|
||||
import org.oxycblt.auxio.ui.recycler.ActivationAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.Header
|
||||
import org.oxycblt.auxio.ui.recycler.HeaderViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.IndicatorAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
||||
|
@ -38,7 +38,9 @@ import org.oxycblt.auxio.util.inflater
|
|||
abstract class DetailAdapter<L : DetailAdapter.Listener>(
|
||||
private val listener: L,
|
||||
diffCallback: DiffUtil.ItemCallback<Item>
|
||||
) : ActivationAdapter<RecyclerView.ViewHolder>() {
|
||||
) : IndicatorAdapter<RecyclerView.ViewHolder>() {
|
||||
private var isPlaying = false
|
||||
|
||||
@Suppress("LeakingThis") override fun getItemCount() = differ.currentList.size
|
||||
|
||||
override fun getItemViewType(position: Int) =
|
||||
|
@ -77,7 +79,7 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
|
|||
|
||||
protected val differ = AsyncListDiffer(this, diffCallback)
|
||||
|
||||
val currentList: List<Item>
|
||||
override val currentList: List<Item>
|
||||
get() = differ.currentList
|
||||
|
||||
fun submitList(list: List<Item>) {
|
||||
|
|
|
@ -40,6 +40,7 @@ import org.oxycblt.auxio.util.inflater
|
|||
class GenreDetailAdapter(private val listener: Listener) :
|
||||
DetailAdapter<DetailAdapter.Listener>(listener, DIFFER) {
|
||||
private var currentSong: Song? = null
|
||||
private var isPlaying = false
|
||||
|
||||
override fun getItemViewType(position: Int) =
|
||||
when (differ.currentList[position]) {
|
||||
|
@ -70,18 +71,6 @@ class GenreDetailAdapter(private val listener: Listener) :
|
|||
}
|
||||
}
|
||||
|
||||
override fun shouldActivateViewHolder(position: Int): Boolean {
|
||||
val item = differ.currentList[position]
|
||||
return item is Song && item.id == currentSong?.id
|
||||
}
|
||||
|
||||
/** Update the [song] that this adapter should indicate playback */
|
||||
fun activateSong(song: Song?) {
|
||||
if (song == currentSong) return
|
||||
activateImpl(differ.currentList, currentSong, song)
|
||||
currentSong = song
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DIFFER =
|
||||
object : SimpleItemCallback<Item>() {
|
||||
|
|
|
@ -29,8 +29,8 @@ import org.oxycblt.auxio.music.Music
|
|||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.ui.recycler.ActivationAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.AlbumViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.IndicatorAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
||||
|
@ -56,7 +56,7 @@ class AlbumListFragment : HomeListFragment<Album>() {
|
|||
}
|
||||
|
||||
collectImmediately(homeModel.albums, homeAdapter::replaceList)
|
||||
collectImmediately(playbackModel.parent, ::handleParent)
|
||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::handleParent)
|
||||
}
|
||||
|
||||
override fun getPopup(pos: Int): String? {
|
||||
|
@ -109,19 +109,21 @@ class AlbumListFragment : HomeListFragment<Album>() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleParent(parent: MusicParent?) {
|
||||
private fun handleParent(parent: MusicParent?, isPlaying: Boolean) {
|
||||
if (parent is Album) {
|
||||
homeAdapter.activateAlbum(parent)
|
||||
homeAdapter.updateIndicator(parent, isPlaying)
|
||||
} else {
|
||||
// Ignore playback not from albums
|
||||
homeAdapter.activateAlbum(null)
|
||||
homeAdapter.updateIndicator(null, isPlaying)
|
||||
}
|
||||
}
|
||||
|
||||
private class AlbumAdapter(private val listener: MenuItemListener) :
|
||||
ActivationAdapter<AlbumViewHolder>() {
|
||||
IndicatorAdapter<AlbumViewHolder>() {
|
||||
private val differ = SyncListDiffer(this, AlbumViewHolder.DIFFER)
|
||||
private var currentAlbum: Album? = null
|
||||
|
||||
override val currentList: List<Item>
|
||||
get() = differ.currentList
|
||||
|
||||
override fun getItemCount() = differ.currentList.size
|
||||
|
||||
|
@ -136,20 +138,8 @@ class AlbumListFragment : HomeListFragment<Album>() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun shouldActivateViewHolder(position: Int): Boolean {
|
||||
val item = differ.currentList[position]
|
||||
return item.id == currentAlbum?.id
|
||||
}
|
||||
|
||||
fun replaceList(newList: List<Album>) {
|
||||
differ.replaceList(newList)
|
||||
}
|
||||
|
||||
/** Update the [album] that this adapter should indicate playback */
|
||||
fun activateAlbum(album: Album?) {
|
||||
if (album == currentAlbum) return
|
||||
activateImpl(differ.currentList, currentAlbum, album)
|
||||
currentAlbum = album
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,8 +27,8 @@ import org.oxycblt.auxio.music.Music
|
|||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.ui.recycler.ActivationAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.ArtistViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.IndicatorAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
||||
|
@ -51,7 +51,7 @@ class ArtistListFragment : HomeListFragment<Artist>() {
|
|||
}
|
||||
|
||||
collectImmediately(homeModel.artists, homeAdapter::replaceList)
|
||||
collectImmediately(playbackModel.parent, ::handleParent)
|
||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::handleParent)
|
||||
}
|
||||
|
||||
override fun getPopup(pos: Int): String? {
|
||||
|
@ -85,19 +85,21 @@ class ArtistListFragment : HomeListFragment<Artist>() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleParent(parent: MusicParent?) {
|
||||
private fun handleParent(parent: MusicParent?, isPlaying: Boolean) {
|
||||
if (parent is Artist) {
|
||||
homeAdapter.activateArtist(parent)
|
||||
homeAdapter.updateIndicator(parent, isPlaying)
|
||||
} else {
|
||||
// Ignore playback not from artists
|
||||
homeAdapter.activateArtist(null)
|
||||
homeAdapter.updateIndicator(null, isPlaying)
|
||||
}
|
||||
}
|
||||
|
||||
private class ArtistAdapter(private val listener: MenuItemListener) :
|
||||
ActivationAdapter<ArtistViewHolder>() {
|
||||
IndicatorAdapter<ArtistViewHolder>() {
|
||||
private val differ = SyncListDiffer(this, ArtistViewHolder.DIFFER)
|
||||
private var currentArtist: Artist? = null
|
||||
|
||||
override val currentList: List<Item>
|
||||
get() = differ.currentList
|
||||
|
||||
override fun getItemCount() = differ.currentList.size
|
||||
|
||||
|
@ -116,20 +118,8 @@ class ArtistListFragment : HomeListFragment<Artist>() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun shouldActivateViewHolder(position: Int): Boolean {
|
||||
val item = differ.currentList[position]
|
||||
return item.id == currentArtist?.id
|
||||
}
|
||||
|
||||
fun replaceList(newList: List<Artist>) {
|
||||
differ.replaceList(newList)
|
||||
}
|
||||
|
||||
/** Update the [artist] that this adapter should indicate playback */
|
||||
fun activateArtist(artist: Artist?) {
|
||||
if (artist == currentArtist) return
|
||||
activateImpl(differ.currentList, currentArtist, artist)
|
||||
currentArtist = artist
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,8 +27,8 @@ import org.oxycblt.auxio.music.Music
|
|||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.ui.recycler.ActivationAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.GenreViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.IndicatorAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
||||
|
@ -51,7 +51,7 @@ class GenreListFragment : HomeListFragment<Genre>() {
|
|||
}
|
||||
|
||||
collectImmediately(homeModel.genres, homeAdapter::replaceList)
|
||||
collectImmediately(playbackModel.parent, ::handlePlayback)
|
||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::handlePlayback)
|
||||
}
|
||||
|
||||
override fun getPopup(pos: Int): String? {
|
||||
|
@ -85,19 +85,21 @@ class GenreListFragment : HomeListFragment<Genre>() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handlePlayback(parent: MusicParent?) {
|
||||
private fun handlePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||
if (parent is Genre) {
|
||||
homeAdapter.activateGenre(parent)
|
||||
homeAdapter.updateIndicator(parent, isPlaying)
|
||||
} else {
|
||||
// Ignore playback not from genres
|
||||
homeAdapter.activateGenre(null)
|
||||
homeAdapter.updateIndicator(null, isPlaying)
|
||||
}
|
||||
}
|
||||
|
||||
private class GenreAdapter(private val listener: MenuItemListener) :
|
||||
ActivationAdapter<GenreViewHolder>() {
|
||||
IndicatorAdapter<GenreViewHolder>() {
|
||||
private val differ = SyncListDiffer(this, GenreViewHolder.DIFFER)
|
||||
private var currentGenre: Genre? = null
|
||||
|
||||
override val currentList: List<Item>
|
||||
get() = differ.currentList
|
||||
|
||||
override fun getItemCount() = differ.currentList.size
|
||||
|
||||
|
@ -112,20 +114,8 @@ class GenreListFragment : HomeListFragment<Genre>() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun shouldActivateViewHolder(position: Int): Boolean {
|
||||
val item = differ.currentList[position]
|
||||
return item.id == currentGenre?.id
|
||||
}
|
||||
|
||||
fun replaceList(newList: List<Genre>) {
|
||||
differ.replaceList(newList)
|
||||
}
|
||||
|
||||
/** Update the [genre] that this adapter should indicate playback */
|
||||
fun activateGenre(genre: Genre?) {
|
||||
if (genre == currentGenre) return
|
||||
activateImpl(differ.currentList, currentGenre, genre)
|
||||
currentGenre = genre
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ import org.oxycblt.auxio.music.Song
|
|||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.ui.recycler.ActivationAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.IndicatorAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.recycler.SongViewHolder
|
||||
|
@ -58,7 +58,8 @@ class SongListFragment : HomeListFragment<Song>() {
|
|||
}
|
||||
|
||||
collectImmediately(homeModel.songs, homeAdapter::replaceList)
|
||||
collectImmediately(playbackModel.song, playbackModel.parent, ::handlePlayback)
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback)
|
||||
}
|
||||
|
||||
override fun getPopup(pos: Int): String? {
|
||||
|
@ -113,19 +114,21 @@ class SongListFragment : HomeListFragment<Song>() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handlePlayback(song: Song?, parent: MusicParent?) {
|
||||
private fun handlePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
if (parent == null) {
|
||||
homeAdapter.activateSong(song)
|
||||
homeAdapter.updateIndicator(song, isPlaying)
|
||||
} else {
|
||||
// Ignore playback that is not from all songs
|
||||
homeAdapter.activateSong(null)
|
||||
homeAdapter.updateIndicator(null, isPlaying)
|
||||
}
|
||||
}
|
||||
|
||||
private class SongAdapter(private val listener: MenuItemListener) :
|
||||
ActivationAdapter<SongViewHolder>() {
|
||||
IndicatorAdapter<SongViewHolder>() {
|
||||
private val differ = SyncListDiffer(this, SongViewHolder.DIFFER)
|
||||
private var currentSong: Song? = null
|
||||
|
||||
override val currentList: List<Item>
|
||||
get() = differ.currentList
|
||||
|
||||
override fun getItemCount() = differ.currentList.size
|
||||
|
||||
|
@ -140,20 +143,8 @@ class SongListFragment : HomeListFragment<Song>() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun shouldActivateViewHolder(position: Int): Boolean {
|
||||
val item = differ.currentList[position]
|
||||
return item.id == currentSong?.id
|
||||
}
|
||||
|
||||
fun replaceList(newList: List<Song>) {
|
||||
differ.replaceList(newList)
|
||||
}
|
||||
|
||||
/** Update the [song] that this adapter should indicate playback */
|
||||
fun activateSong(song: Song?) {
|
||||
if (song == currentSong) return
|
||||
activateImpl(differ.currentList, currentSong, song)
|
||||
currentSong = song
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,6 @@ import org.oxycblt.auxio.music.Artist
|
|||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.util.getColorCompat
|
||||
import org.oxycblt.auxio.util.getDrawableCompat
|
||||
|
||||
/**
|
||||
* Effectively a super-charged [StyledImageView].
|
||||
|
@ -52,7 +51,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
private val cornerRadius: Float
|
||||
private val inner: StyledImageView
|
||||
private var customView: View? = null
|
||||
private val indicator: StyledImageView
|
||||
private val indicator: IndicatorView
|
||||
|
||||
init {
|
||||
// Android wants you to make separate attributes for each view type, but will
|
||||
|
@ -63,11 +62,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
styledAttrs.recycle()
|
||||
|
||||
inner = StyledImageView(context, attrs)
|
||||
indicator =
|
||||
StyledImageView(context).apply {
|
||||
cornerRadius = this@ImageGroup.cornerRadius
|
||||
staticIcon = context.getDrawableCompat(R.drawable.ic_currently_playing_24)
|
||||
}
|
||||
indicator = IndicatorView(context).apply { cornerRadius = this@ImageGroup.cornerRadius }
|
||||
|
||||
addView(inner)
|
||||
}
|
||||
|
@ -101,6 +96,14 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
invalidateIndicator()
|
||||
}
|
||||
|
||||
fun updateIndicator(isActive: Boolean, isPlaying: Boolean) {}
|
||||
|
||||
var isPlaying: Boolean
|
||||
get() = indicator.isPlaying
|
||||
set(value) {
|
||||
indicator.isPlaying = value
|
||||
}
|
||||
|
||||
override fun setEnabled(enabled: Boolean) {
|
||||
super.setEnabled(enabled)
|
||||
invalidateIndicator()
|
||||
|
@ -109,14 +112,14 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
private fun invalidateIndicator() {
|
||||
if (isActivated) {
|
||||
alpha = 1f
|
||||
indicator.alpha = 1f
|
||||
customView?.alpha = 0f
|
||||
inner.alpha = 0f
|
||||
indicator.alpha = 1f
|
||||
} else {
|
||||
alpha = if (isEnabled) 1f else 0.5f
|
||||
indicator.alpha = 0f
|
||||
customView?.alpha = 1f
|
||||
inner.alpha = 1f
|
||||
indicator.alpha = 0f
|
||||
}
|
||||
}
|
||||
|
||||
|
|
127
app/src/main/java/org/oxycblt/auxio/image/IndicatorView.kt
Normal file
127
app/src/main/java/org/oxycblt/auxio/image/IndicatorView.kt
Normal file
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Auxio Project
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.image
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.AnimationDrawable
|
||||
import android.util.AttributeSet
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.core.widget.ImageViewCompat
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import kotlin.math.max
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.getColorCompat
|
||||
import org.oxycblt.auxio.util.getDrawableCompat
|
||||
|
||||
/**
|
||||
* View that displays the playback indicator. Nominally emulates [StyledImageView], but is
|
||||
* much different internally as an animated icon can't be wrapped within StyledDrawable without
|
||||
* causing insane issues.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class IndicatorView
|
||||
@JvmOverloads
|
||||
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||
AppCompatImageView(context, attrs, defStyleAttr) {
|
||||
private val playingIndicatorDrawable =
|
||||
context.getDrawableCompat(R.drawable.ic_playing_indicator_24) as AnimationDrawable
|
||||
|
||||
private val pausedIndicatorDrawable = context.getDrawableCompat(R.drawable.ic_paused_indicator_24)
|
||||
|
||||
private val indicatorMatrix = Matrix()
|
||||
private val indicatorMatrixSrc = RectF()
|
||||
private val indicatorMatrixDst = RectF()
|
||||
|
||||
private val settings = Settings(context)
|
||||
|
||||
var cornerRadius = 0f
|
||||
set(value) {
|
||||
field = value
|
||||
(background as? MaterialShapeDrawable)?.let { bg ->
|
||||
if (settings.roundMode) {
|
||||
bg.setCornerSize(value)
|
||||
} else {
|
||||
bg.setCornerSize(0f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// Use clipToOutline and a background drawable to crop images. While Coil's transformation
|
||||
// could theoretically be used to round corners, the corner radius is dependent on the
|
||||
// dimensions of the image, which will result in inconsistent corners across different
|
||||
// album covers unless we resize all covers to be the same size. clipToOutline is both
|
||||
// cheaper and more elegant. As a side-note, this also allows us to re-use the same
|
||||
// background for both the tonal background color and the corner rounding.
|
||||
clipToOutline = true
|
||||
background =
|
||||
MaterialShapeDrawable().apply {
|
||||
fillColor = context.getColorCompat(R.color.sel_cover_bg)
|
||||
setCornerSize(cornerRadius)
|
||||
}
|
||||
|
||||
scaleType = ScaleType.MATRIX
|
||||
ImageViewCompat.setImageTintList(this, context.getColorCompat(R.color.sel_on_cover_bg))
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
|
||||
val iconSize = max(measuredWidth, measuredHeight) / 2
|
||||
|
||||
imageMatrix =
|
||||
indicatorMatrix.apply {
|
||||
reset()
|
||||
drawable?.let { drawable ->
|
||||
// Android is too good to allow us to set a fixed image size, so we instead need
|
||||
// to define a matrix to scale an image directly.
|
||||
|
||||
// First scale the icon up to the desired size.
|
||||
indicatorMatrixSrc.set(
|
||||
0f,
|
||||
0f,
|
||||
drawable.intrinsicWidth.toFloat(),
|
||||
drawable.intrinsicHeight.toFloat())
|
||||
indicatorMatrixDst.set(0f, 0f, iconSize.toFloat(), iconSize.toFloat())
|
||||
indicatorMatrix.setRectToRect(
|
||||
indicatorMatrixSrc, indicatorMatrixDst, Matrix.ScaleToFit.CENTER)
|
||||
|
||||
// Then actually center it into the icon, which the previous call does not
|
||||
// actually do.
|
||||
indicatorMatrix.postTranslate(
|
||||
(measuredWidth - iconSize) / 2f, (measuredHeight - iconSize) / 2f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isPlaying: Boolean
|
||||
get() = drawable == playingIndicatorDrawable
|
||||
set(value) {
|
||||
if (value) {
|
||||
playingIndicatorDrawable.start()
|
||||
setImageDrawable(playingIndicatorDrawable)
|
||||
} else {
|
||||
playingIndicatorDrawable.stop()
|
||||
setImageDrawable(pausedIndicatorDrawable)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -35,6 +35,7 @@ class QueueAdapter(private val listener: QueueItemListener) :
|
|||
RecyclerView.Adapter<QueueSongViewHolder>() {
|
||||
private var differ = SyncListDiffer(this, QueueSongViewHolder.DIFFER)
|
||||
private var currentIndex = 0
|
||||
private var isPlaying = false
|
||||
|
||||
override fun getItemCount() = differ.currentList.size
|
||||
|
||||
|
@ -54,7 +55,7 @@ class QueueAdapter(private val listener: QueueItemListener) :
|
|||
}
|
||||
|
||||
viewHolder.isEnabled = position > currentIndex
|
||||
viewHolder.isActivated = position == currentIndex
|
||||
viewHolder.updateIndicator(position == currentIndex, isPlaying)
|
||||
}
|
||||
|
||||
fun submitList(newList: List<Song>) {
|
||||
|
@ -65,18 +66,32 @@ class QueueAdapter(private val listener: QueueItemListener) :
|
|||
differ.replaceList(newList)
|
||||
}
|
||||
|
||||
fun updateIndex(index: Int) {
|
||||
fun updateIndicator(index: Int, isPlaying: Boolean) {
|
||||
var updatedIndex = false
|
||||
|
||||
if (index != currentIndex) {
|
||||
when {
|
||||
index < currentIndex -> {
|
||||
val lastIndex = currentIndex
|
||||
currentIndex = index
|
||||
notifyItemRangeChanged(0, lastIndex + 1, PAYLOAD_UPDATE_INDEX)
|
||||
}
|
||||
index > currentIndex -> {
|
||||
else -> {
|
||||
currentIndex = index
|
||||
notifyItemRangeChanged(0, currentIndex + 1, PAYLOAD_UPDATE_INDEX)
|
||||
}
|
||||
}
|
||||
|
||||
updatedIndex = true
|
||||
}
|
||||
|
||||
if (this.isPlaying != isPlaying) {
|
||||
this.isPlaying = isPlaying
|
||||
|
||||
if (!updatedIndex) {
|
||||
notifyItemChanged(index, PAYLOAD_UPDATE_INDEX)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -92,7 +107,7 @@ interface QueueItemListener {
|
|||
class QueueSongViewHolder
|
||||
private constructor(
|
||||
private val binding: ItemQueueSongBinding,
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
) : IndicatorViewHolder(binding.root) {
|
||||
val bodyView: View
|
||||
get() = binding.body
|
||||
val backgroundView: View
|
||||
|
@ -146,11 +161,9 @@ private constructor(
|
|||
binding.songDragHandle.isEnabled = value
|
||||
}
|
||||
|
||||
var isActivated: Boolean
|
||||
get() = binding.interactBody.isActivated
|
||||
set(value) {
|
||||
// Activation does not affect clicking, make everything activated.
|
||||
binding.interactBody.isActivated = value
|
||||
override fun updateIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||
binding.interactBody.isActivated = isActive
|
||||
binding.songAlbumCover.isPlaying = isPlaying
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -27,7 +27,9 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
|
@ -38,6 +40,7 @@ import org.oxycblt.auxio.util.logD
|
|||
*/
|
||||
class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemListener {
|
||||
private val queueModel: QueueViewModel by activityViewModels()
|
||||
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
||||
private val queueAdapter = QueueAdapter(this)
|
||||
private val touchHelper: ItemTouchHelper by lifecycleObject {
|
||||
ItemTouchHelper(QueueDragCallback(queueModel))
|
||||
|
@ -63,7 +66,8 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
|
|||
|
||||
// --- VIEWMODEL SETUP ----
|
||||
|
||||
collectImmediately(queueModel.queue, queueModel.index, ::updateQueue)
|
||||
collectImmediately(
|
||||
queueModel.queue, queueModel.index, playbackModel.isPlaying, ::updateQueue)
|
||||
}
|
||||
|
||||
override fun onDestroyBinding(binding: FragmentQueueBinding) {
|
||||
|
@ -79,7 +83,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
|
|||
touchHelper.startDrag(viewHolder)
|
||||
}
|
||||
|
||||
private fun updateQueue(queue: List<Song>, index: Int) {
|
||||
private fun updateQueue(queue: List<Song>, index: Int, isPlaying: Boolean) {
|
||||
val binding = requireBinding()
|
||||
|
||||
val replaceQueue = queueModel.replaceQueue
|
||||
|
@ -111,7 +115,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
|
|||
|
||||
queueModel.finishScrollTo()
|
||||
|
||||
queueAdapter.updateIndex(index)
|
||||
queueAdapter.updateIndicator(index, isPlaying)
|
||||
}
|
||||
|
||||
private fun invalidateDivider() {
|
||||
|
|
|
@ -24,24 +24,20 @@ import org.oxycblt.auxio.music.Album
|
|||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.ui.recycler.ActivationAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.AlbumViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.ArtistViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.GenreViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.Header
|
||||
import org.oxycblt.auxio.ui.recycler.HeaderViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.IndicatorAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
||||
import org.oxycblt.auxio.ui.recycler.SongViewHolder
|
||||
|
||||
class SearchAdapter(private val listener: MenuItemListener) :
|
||||
ActivationAdapter<RecyclerView.ViewHolder>() {
|
||||
IndicatorAdapter<RecyclerView.ViewHolder>() {
|
||||
private val differ = AsyncListDiffer(this, DIFFER)
|
||||
private var currentSong: Song? = null
|
||||
private var currentAlbum: Album? = null
|
||||
private var currentArtist: Artist? = null
|
||||
private var currentGenre: Genre? = null
|
||||
|
||||
override fun getItemCount() = differ.currentList.size
|
||||
|
||||
|
@ -83,44 +79,11 @@ class SearchAdapter(private val listener: MenuItemListener) :
|
|||
}
|
||||
}
|
||||
|
||||
override fun shouldActivateViewHolder(position: Int): Boolean {
|
||||
val item = differ.currentList[position]
|
||||
|
||||
return (item is Song && item.id == currentSong?.id) ||
|
||||
(item is Album && item.id == currentAlbum?.id) ||
|
||||
(item is Artist && item.id == currentArtist?.id) ||
|
||||
(item is Genre && item.id == currentGenre?.id)
|
||||
}
|
||||
|
||||
val currentList: List<Item>
|
||||
override val currentList: List<Item>
|
||||
get() = differ.currentList
|
||||
|
||||
fun submitList(list: List<Item>, callback: () -> Unit) = differ.submitList(list, callback)
|
||||
|
||||
fun activateSong(song: Song?) {
|
||||
if (song == currentSong) return
|
||||
activateImpl(differ.currentList, currentSong, song)
|
||||
currentSong = song
|
||||
}
|
||||
|
||||
fun activateAlbum(album: Album?) {
|
||||
if (album == currentAlbum) return
|
||||
activateImpl(differ.currentList, currentAlbum, album)
|
||||
currentAlbum = album
|
||||
}
|
||||
|
||||
fun activateArtist(artist: Artist?) {
|
||||
if (artist == currentArtist) return
|
||||
activateImpl(differ.currentList, currentArtist, artist)
|
||||
currentArtist = artist
|
||||
}
|
||||
|
||||
fun activateGenre(genre: Genre?) {
|
||||
if (genre == currentGenre) return
|
||||
activateImpl(differ.currentList, currentGenre, genre)
|
||||
currentGenre = genre
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DIFFER =
|
||||
object : SimpleItemCallback<Item>() {
|
||||
|
|
|
@ -112,7 +112,8 @@ class SearchFragment :
|
|||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
collectImmediately(searchModel.searchResults, ::handleResults)
|
||||
collectImmediately(playbackModel.song, playbackModel.parent, ::handlePlayback)
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback)
|
||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||
}
|
||||
|
||||
|
@ -165,34 +166,8 @@ class SearchFragment :
|
|||
binding.searchRecycler.isInvisible = results.isEmpty()
|
||||
}
|
||||
|
||||
private fun handlePlayback(song: Song?, parent: MusicParent?) {
|
||||
if (parent == null) {
|
||||
searchAdapter.activateSong(song)
|
||||
} else {
|
||||
// Ignore playback not from all songs
|
||||
searchAdapter.activateSong(null)
|
||||
}
|
||||
|
||||
if (parent is Album) {
|
||||
searchAdapter.activateAlbum(parent)
|
||||
} else {
|
||||
// Ignore playback not from albums
|
||||
searchAdapter.activateAlbum(null)
|
||||
}
|
||||
|
||||
if (parent is Artist) {
|
||||
searchAdapter.activateArtist(parent)
|
||||
} else {
|
||||
// Ignore playback not from artists
|
||||
searchAdapter.activateArtist(null)
|
||||
}
|
||||
|
||||
if (parent is Genre) {
|
||||
searchAdapter.activateGenre(parent)
|
||||
} else {
|
||||
// Ignore playback not from artists
|
||||
searchAdapter.activateGenre(null)
|
||||
}
|
||||
private fun handlePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
searchAdapter.updateIndicator(parent ?: song, isPlaying)
|
||||
}
|
||||
|
||||
private fun handleNavigation(item: Music?) {
|
||||
|
|
|
@ -173,42 +173,84 @@ abstract class SimpleItemCallback<T : Item> : DiffUtil.ItemCallback<T>() {
|
|||
}
|
||||
}
|
||||
|
||||
abstract class ActivationAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
|
||||
// TODO: Base adapter that automates current list stuff for span size lookup
|
||||
// TODO: Dialog view holder that automates the dumb sizing hack I have to do
|
||||
|
||||
abstract class IndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
|
||||
private var isPlaying = false
|
||||
private var currentItem: Item? = null
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int) = throw UnsupportedOperationException()
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int, payloads: List<Any>) {
|
||||
holder.itemView.isActivated = shouldActivateViewHolder(position)
|
||||
if (holder is IndicatorViewHolder) {
|
||||
val item = currentList[position]
|
||||
val currentItem = currentItem
|
||||
holder.updateIndicator(
|
||||
currentItem != null &&
|
||||
item.javaClass == currentItem.javaClass &&
|
||||
item.id == currentItem.id,
|
||||
isPlaying)
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun shouldActivateViewHolder(position: Int): Boolean
|
||||
abstract val currentList: List<Item>
|
||||
|
||||
fun updateIndicator(item: Item?, isPlaying: Boolean) {
|
||||
var updatedItem = false
|
||||
|
||||
if (currentItem != item) {
|
||||
val oldItem = currentItem
|
||||
currentItem = item
|
||||
|
||||
protected inline fun <reified T : Item> activateImpl(
|
||||
currentList: List<Item>,
|
||||
oldItem: T?,
|
||||
newItem: T?
|
||||
) {
|
||||
if (oldItem != null) {
|
||||
val pos = currentList.indexOfFirst { item -> item.id == oldItem.id && item is T }
|
||||
val pos =
|
||||
currentList.indexOfFirst {
|
||||
it.javaClass == oldItem.javaClass && it.id == oldItem.id
|
||||
}
|
||||
|
||||
if (pos > -1) {
|
||||
notifyItemChanged(pos, PAYLOAD_ACTIVATION_CHANGED)
|
||||
notifyItemChanged(pos, PAYLOAD_INDICATOR_CHANGED)
|
||||
} else {
|
||||
logW("oldItem was not in adapter data")
|
||||
}
|
||||
}
|
||||
|
||||
if (newItem != null) {
|
||||
val pos = currentList.indexOfFirst { item -> item is T && item.id == newItem.id }
|
||||
if (item != null) {
|
||||
val pos =
|
||||
currentList.indexOfFirst { it.javaClass == item.javaClass && it.id == item.id }
|
||||
|
||||
if (pos > -1) {
|
||||
notifyItemChanged(pos, PAYLOAD_ACTIVATION_CHANGED)
|
||||
notifyItemChanged(pos, PAYLOAD_INDICATOR_CHANGED)
|
||||
} else {
|
||||
logW("newItem was not in adapter data")
|
||||
}
|
||||
}
|
||||
|
||||
updatedItem = true
|
||||
}
|
||||
|
||||
if (this.isPlaying != isPlaying) {
|
||||
this.isPlaying = isPlaying
|
||||
|
||||
if (!updatedItem && item != null) {
|
||||
val pos =
|
||||
currentList.indexOfFirst { it.javaClass == item.javaClass && it.id == item.id }
|
||||
|
||||
if (pos > -1) {
|
||||
notifyItemChanged(pos, PAYLOAD_INDICATOR_CHANGED)
|
||||
} else {
|
||||
logW("newItem was not in adapter data")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val PAYLOAD_ACTIVATION_CHANGED = Any()
|
||||
val PAYLOAD_INDICATOR_CHANGED = Any()
|
||||
}
|
||||
}
|
||||
|
||||
abstract class IndicatorViewHolder(root: View) : RecyclerView.ViewHolder(root) {
|
||||
abstract fun updateIndicator(isActive: Boolean, isPlaying: Boolean)
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ import org.oxycblt.auxio.util.inflater
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
IndicatorViewHolder(binding.root) {
|
||||
fun bind(item: Song, listener: MenuItemListener) {
|
||||
binding.songAlbumCover.bind(item)
|
||||
binding.songName.text = item.resolveName(binding.context)
|
||||
|
@ -50,6 +50,11 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
|||
binding.root.setOnClickListener { listener.onItemClick(item) }
|
||||
}
|
||||
|
||||
override fun updateIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||
binding.root.isActivated = isActive
|
||||
binding.songAlbumCover.isPlaying = isPlaying
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_SONG
|
||||
|
||||
|
@ -71,7 +76,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
|||
class AlbumViewHolder
|
||||
private constructor(
|
||||
private val binding: ItemParentBinding,
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
) : IndicatorViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: Album, listener: MenuItemListener) {
|
||||
binding.parentImage.bind(item)
|
||||
|
@ -85,6 +90,11 @@ private constructor(
|
|||
binding.root.setOnClickListener { listener.onItemClick(item) }
|
||||
}
|
||||
|
||||
override fun updateIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||
binding.root.isActivated = isActive
|
||||
binding.parentImage.isPlaying = isPlaying
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ALBUM
|
||||
|
||||
|
@ -105,7 +115,7 @@ private constructor(
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
class ArtistViewHolder private constructor(private val binding: ItemParentBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
IndicatorViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: Artist, listener: MenuItemListener) {
|
||||
binding.parentImage.bind(item)
|
||||
|
@ -123,6 +133,11 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
|
|||
binding.root.setOnClickListener { listener.onItemClick(item) }
|
||||
}
|
||||
|
||||
override fun updateIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||
binding.root.isActivated = isActive
|
||||
binding.parentImage.isPlaying = isPlaying
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST
|
||||
|
||||
|
@ -145,7 +160,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
|
|||
class GenreViewHolder
|
||||
private constructor(
|
||||
private val binding: ItemParentBinding,
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
) : IndicatorViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: Genre, listener: MenuItemListener) {
|
||||
binding.parentImage.bind(item)
|
||||
|
@ -160,6 +175,11 @@ private constructor(
|
|||
binding.root.setOnClickListener { listener.onItemClick(item) }
|
||||
}
|
||||
|
||||
override fun updateIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||
binding.root.isActivated = isActive
|
||||
binding.parentImage.isPlaying = isPlaying
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_GENRE
|
||||
|
||||
|
|
|
@ -144,6 +144,18 @@ fun <T1, T2> Fragment.collectImmediately(
|
|||
launch { combine.collect { block(it.first, it.second) } }
|
||||
}
|
||||
|
||||
/** Like [collectImmediately], but with three [StateFlow] values. */
|
||||
fun <T1, T2, T3> Fragment.collectImmediately(
|
||||
a: StateFlow<T1>,
|
||||
b: StateFlow<T2>,
|
||||
c: StateFlow<T3>,
|
||||
block: (T1, T2, T3) -> Unit
|
||||
) {
|
||||
block(a.value, b.value, c.value)
|
||||
val combine = combine(a, b, c) { a1, b2, c3 -> Triple(a1, b2, c3) }
|
||||
launch { combine.collect { block(it.first, it.second, it.third) } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches [block] in a lifecycle-aware coroutine once [state] is reached. This is primarily a
|
||||
* shortcut intended to correctly launch a co-routine on a fragment in a way that won't cause
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M4,20V12H8V20ZM10,20V4H14V20ZM16,20V9H20V20Z" />
|
||||
</vector>
|
11
app/src/main/res/drawable/ic_paused_indicator_24.xml
Normal file
11
app/src/main/res/drawable/ic_paused_indicator_24.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M 3.9997559 17.999935 L 3.9997559 19.999813 L 8.0000285 19.999813 L 8.0000285 17.999935 L 3.9997559 17.999935 z M 9.9999064 17.999935 L 9.9999064 19.999813 L 14.000179 19.999813 L 14.000179 17.999935 L 9.9999064 17.999935 z M 16.000057 17.999935 L 16.000057 19.999813 L 19.999813 19.999813 L 19.999813 17.999935 L 16.000057 17.999935 z " />
|
||||
</vector>
|
492
app/src/main/res/drawable/ic_playing_indicator_24.xml
Normal file
492
app/src/main/res/drawable/ic_playing_indicator_24.xml
Normal file
|
@ -0,0 +1,492 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
|
||||
<!--
|
||||
Yes, this whole file is all 30 frames of Spotify's equalizer animation
|
||||
merged with the material equalizer icon, with each vector inlrined using
|
||||
aapt:attr so that it does not clutter the drawable folder.
|
||||
-->
|
||||
|
||||
<!-- Frame 1 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M10,20h4L14,4h-4v16zM4,20h4v-8L4,12v8zM16,9v11h4L20,9h-4z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 2 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,3.9997559 c 0,5.3333524 0,10.6667051 0,16.0000571 1.3334246,0 2.6668486,0 4.0002726,0 0,-5.333352 0,-10.6667047 0,-16.0000571 -1.333424,0 -2.666848,0 -4.0002726,0 z M 3.9997559,10.999845 c 0,2.999989 0,5.999979 0,8.999968 1.3334242,0 2.6668484,0 4.0002726,0 0,-2.999989 0,-5.999979 0,-8.999968 -1.3334242,0 -2.6668484,0 -4.0002726,0 z m 12.0003011,0 c 0,2.999989 0,5.999979 0,8.999968 1.333252,0 2.666504,0 3.999756,0 0,-2.999989 0,-5.999979 0,-8.999968 -1.333252,0 -2.666504,0 -3.999756,0 z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 3 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,5.0002116 c 0,4.9998674 0,9.9997344 0,14.9996014 1.3334246,0 2.6668486,0 4.0002726,0 0,-4.999867 0,-9.999734 0,-14.9996014 -1.333424,0 -2.666848,0 -4.0002726,0 z M 3.9997559,9.9999064 c 0,3.3333026 0,6.6666046 0,9.9999066 1.3334242,0 2.6668484,0 4.0002726,0 0,-3.333302 0,-6.666604 0,-9.9999066 -1.3334242,0 -2.6668484,0 -4.0002726,0 z M 16.000057,14.000179 c 0,1.999878 0,3.999756 0,5.999634 1.333252,0 2.666504,0 3.999756,0 0,-1.999878 0,-3.999756 0,-5.999634 -1.333252,0 -2.666504,0 -3.999756,0 z " />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 4 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,5.0002116 c 0,4.9998674 0,9.9997344 0,14.9996014 1.3334246,0 2.6668486,0 4.0002726,0 0,-4.999867 0,-9.999734 0,-14.9996014 -1.333424,0 -2.666848,0 -4.0002726,0 z M 3.9997559,9 c 0,3.666604 0,7.333209 0,10.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-3.666604 0,-7.333209 0,-10.999813 C 6.6666043,9 5.3331801,9 3.9997559,9 Z m 12.0003011,7 c 0,1.333271 0,2.666542 0,3.999813 1.333252,0 2.666504,0 3.999756,0 0,-1.333271 0,-2.666542 0,-3.999813 -1.333252,0 -2.666504,0 -3.999756,0 z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 5 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,7 c 0,4.333271 0,8.666542 0,12.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-4.333271 0,-8.666542 0,-12.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z m -6.0001505,4 c 0,2.999938 0,5.999875 0,8.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-2.999938 0,-5.999875 0,-8.999813 -1.3334242,0 -2.6668484,0 -4.0002726,0 z m 12.0003011,6 c 0,0.999938 0,1.999875 0,2.999813 1.333252,0 2.666504,0 3.999756,0 0,-0.999938 0,-1.999875 0,-2.999813 -1.333252,0 -2.666504,0 -3.999756,0 z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 6 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,9 c 0,3.666604 0,7.333209 0,10.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-3.666604 0,-7.333209 0,-10.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z m -6.0001505,1 c 0,3.333271 0,6.666542 0,9.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-3.333271 0,-6.666542 0,-9.999813 -1.3334242,0 -2.6668484,0 -4.0002726,0 z M 16,18 c 0.0000190,0.666604 0.0000380,1.333209 0.0000570,1.999813 1.333252,0 2.666504,0 3.999756,0 0,-0.666604 0,-1.333209 0,-1.999813 C 18.666542,18 17.333271,18 16,18 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 7 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,11 c 0,2.999938 0,5.999875 0,8.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-2.999938 0,-5.999875 0,-8.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z M 3.9997559,9 c 0,3.666604 0,7.333209 0,10.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-3.666604 0,-7.333209 0,-10.999813 C 6.6666043,9 5.3331801,9 3.9997559,9 Z M 16,18 c 0.0000190,0.666604 0.0000380,1.333209 0.0000570,1.999813 1.333252,0 2.666504,0 3.999756,0 0,-0.666604 0,-1.333209 0,-1.999813 C 18.666542,18 17.333271,18 16,18 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 8 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,13 c 0,2.333271 0,4.666542 0,6.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-2.333271 0,-4.666542 0,-6.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z M 3.9997559,8 c 0,3.999938 0,7.999875 0,11.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-3.999938 0,-7.999875 0,-11.999813 C 6.6666043,8 5.3331801,8 3.9997559,8 Z M 16,18 c 0.0000190,0.666604 0.0000380,1.333209 0.0000570,1.999813 1.333252,0 2.666504,0 3.999756,0 0,-0.666604 0,-1.333209 0,-1.999813 C 18.666542,18 17.333271,18 16,18 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 9 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,16 c 0,1.333271 0,2.666542 0,3.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-1.333271 0,-2.666542 0,-3.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z M 3.9997559,7 c 0,4.333271 0,8.666542 0,12.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-4.333271 0,-8.666542 0,-12.999813 C 6.6666043,7 5.3331801,7 3.9997559,7 Z M 16,17 c 0.0000190,0.999938 0.0000380,1.999875 0.0000570,2.999813 1.333252,0 2.666504,0 3.999756,0 0,-0.999938 0,-1.999875 0,-2.999813 C 18.666542,17 17.333271,17 16,17 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 10 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,17 c 0,0.999938 0,1.999875 0,2.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-0.999938 0,-1.999875 0,-2.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z M 3.9997559,6 c 0,4.666604 0,9.333209 0,13.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-4.666604 0,-9.333209 0,-13.999813 C 6.6666043,6 5.3331801,6 3.9997559,6 Z M 16,17 c 0.0000190,0.999938 0.0000380,1.999875 0.0000570,2.999813 1.333252,0 2.666504,0 3.999756,0 0,-0.999938 0,-1.999875 0,-2.999813 C 18.666542,17 17.333271,17 16,17 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 11 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,18 c 0,0.666604 0,1.333209 0,1.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-0.666604 0,-1.333209 0,-1.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z M 3.9997559,5 c 0,4.9999377 0,9.999875 0,14.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-4.999938 0,-9.9998753 0,-14.999813 C 6.6666043,5 5.3331801,5 3.9997559,5 Z M 16,15 c 0.0000190,1.666604 0.0000380,3.333209 0.0000570,4.999813 1.333252,0 2.666504,0 3.999756,0 0,-1.666604 0,-3.333209 0,-4.999813 C 18.666542,15 17.333271,15 16,15 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 12 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,18 c 0,0.666604 0,1.333209 0,1.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-0.666604 0,-1.333209 0,-1.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z M 3.9997559,5 c 0,4.9999377 0,9.999875 0,14.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-4.999938 0,-9.9998753 0,-14.999813 C 6.6666043,5 5.3331801,5 3.9997559,5 Z M 16,14 c 0.0000190,1.999938 0.0000380,3.999875 0.0000570,5.999813 1.333252,0 2.666504,0 3.999756,0 0,-1.999938 0,-3.999875 0,-5.999813 C 18.666542,14 17.333271,14 16,14 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 13 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,17 c 0,0.999938 0,1.999875 0,2.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-0.999938 0,-1.999875 0,-2.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z M 3.9997559,4 c 0,5.333271 0,10.666542 0,15.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-5.333271 0,-10.666542 0,-15.999813 C 6.6666043,4 5.3331801,4 3.9997559,4 Z M 16,12 c 0.0000190,2.666604 0.0000380,5.333209 0.0000570,7.999813 1.333252,0 2.666504,0 3.999756,0 0,-2.666604 0,-5.333209 0,-7.999813 C 18.666542,12 17.333271,12 16,12 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 14 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,17 c 0,0.999938 0,1.999875 0,2.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-0.999938 0,-1.999875 0,-2.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z M 3.9997559,4 c 0,5.333271 0,10.666542 0,15.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-5.333271 0,-10.666542 0,-15.999813 C 6.6666043,4 5.3331801,4 3.9997559,4 Z M 16,11 c 0.0000190,2.999938 0.0000380,5.999875 0.0000570,8.999813 1.333252,0 2.666504,0 3.999756,0 0,-2.999938 0,-5.999875 0,-8.999813 C 18.666542,11 17.333271,11 16,11 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 15 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,17 c 0,0.999938 0,1.999875 0,2.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-0.999938 0,-1.999875 0,-2.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z M 3.9997559,4 c 0,5.333271 0,10.666542 0,15.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-5.333271 0,-10.666542 0,-15.999813 C 6.6666043,4 5.3331801,4 3.9997559,4 Z M 16,10 c 0.0000190,3.333271 0.0000380,6.666542 0.0000570,9.999813 1.333252,0 2.666504,0 3.999756,0 0,-3.333271 0,-6.666542 0,-9.999813 C 18.666542,10 17.333271,10 16,10 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 16 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,15 c 0,1.666604 0,3.333209 0,4.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-1.666604 0,-3.333209 0,-4.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z M 3.9997559,5 c 0,4.9999377 0,9.999875 0,14.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-4.999938 0,-9.9998753 0,-14.999813 C 6.6666043,5 5.3331801,5 3.9997559,5 Z M 16,10 c 0.0000190,3.333271 0.0000380,6.666542 0.0000570,9.999813 1.333252,0 2.666504,0 3.999756,0 0,-3.333271 0,-6.666542 0,-9.999813 C 18.666542,10 17.333271,10 16,10 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 17 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,14 c 0,1.999938 0,3.999875 0,5.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-1.999938 0,-3.999875 0,-5.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z M 3.9997559,5 c 0,4.9999377 0,9.999875 0,14.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-4.999938 0,-9.9998753 0,-14.999813 C 6.6666043,5 5.3331801,5 3.9997559,5 Z M 16,9 c 0.0000190,3.666604 0.0000380,7.333209 0.0000570,10.999813 1.333252,0 2.666504,0 3.999756,0 0,-3.666604 0,-7.333209 0,-10.999813 C 18.666542,9 17.333271,9 16,9 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 18 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,14 c 0,1.999938 0,3.999875 0,5.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-1.999938 0,-3.999875 0,-5.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z M 3.9997559,5 c 0,4.9999377 0,9.999875 0,14.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-4.999938 0,-9.9998753 0,-14.999813 C 6.6666043,5 5.3331801,5 3.9997559,5 Z M 16,9 c 0.0000190,3.666604 0.0000380,7.333209 0.0000570,10.999813 1.333252,0 2.666504,0 3.999756,0 0,-3.666604 0,-7.333209 0,-10.999813 C 18.666542,9 17.333271,9 16,9 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 19 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 13.999663,10 c 0,3.333271 0,6.666542 0,9.999813 -1.333424,0 -2.666849,0 -4.0002731,0 0,-3.333271 0,-6.666542 0,-9.999813 1.3334241,0 2.6668491,0 4.0002731,0 z m 5.999906,0 c 0.0000810,3.333271 0.0001630,6.666542 0.0002440,9.999813 -1.333424,0 -2.666849,0 -4.000273,0 0,-3.333271 0,-6.666542 0,-9.999813 1.333343,0 2.666686,0 4.000029,0 z M 7.9995689,9 c -0.0000190,3.666604 -0.0000380,7.333209 -0.0000570,10.999813 -1.333252,0 -2.666504,0 -3.999756,0 0,-3.666604 0,-7.333209 0,-10.999813 1.333271,0 2.666542,0 3.999813,0 z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 20 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,10 c 0,3.333271 0,6.666542 0,9.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-3.333271 0,-6.666542 0,-9.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z M 4,13 c -0.0000814,2.333271 -0.0001627,4.666542 -0.0002441,6.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-2.333271 0,-4.666542 0,-6.999813 C 6.6666857,13 5.3333428,13 4,13 Z M 16,9 c 0.0000190,3.666604 0.0000380,7.333209 0.0000570,10.999813 1.333252,0 2.666504,0 3.999756,0 0,-3.666604 0,-7.333209 0,-10.999813 C 18.666542,9 17.333271,9 16,9 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 21 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,9 c 0,3.666604 0,7.333209 0,10.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-3.666604 0,-7.333209 0,-10.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z M 4,15 c -0.0000814,1.666604 -0.0001627,3.333209 -0.0002441,4.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-1.666604 0,-3.333209 0,-4.999813 C 6.6666857,15 5.3333428,15 4,15 Z M 16,8 c 0.0000190,3.999938 0.0000380,7.999875 0.0000570,11.999813 1.333252,0 2.666504,0 3.999756,0 0,-3.999938 0,-7.999875 0,-11.999813 C 18.666542,8 17.333271,8 16,8 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 22 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,10 c 0,3.333271 0,6.666542 0,9.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-3.333271 0,-6.666542 0,-9.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z M 4,17 c -0.0000814,0.999938 -0.0001627,1.999875 -0.0002441,2.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-0.999938 0,-1.999875 0,-2.999813 C 6.6666857,17 5.3333428,17 4,17 Z M 16,7 c 0.0000190,4.333271 0.0000380,8.666542 0.0000570,12.999813 1.333252,0 2.666504,0 3.999756,0 0,-4.333271 0,-8.666542 0,-12.999813 C 18.666542,7 17.333271,7 16,7 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 23 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,9 c 0,3.666604 0,7.333209 0,10.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-3.666604 0,-7.333209 0,-10.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z M 4,17 c -0.0000814,0.999938 -0.0001627,1.999875 -0.0002441,2.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-0.999938 0,-1.999875 0,-2.999813 C 6.6666857,17 5.3333428,17 4,17 Z M 16,6 c 0.0000190,4.666604 0.0000380,9.333209 0.0000570,13.999813 1.333252,0 2.666504,0 3.999756,0 0,-4.666604 0,-9.333209 0,-13.999813 C 18.666542,6 17.333271,6 16,6 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 24 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,9 c 0,3.666604 0,7.333209 0,10.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-3.666604 0,-7.333209 0,-10.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z M 4,18 c -0.0000814,0.666604 -0.0001627,1.333209 -0.0002441,1.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-0.666604 0,-1.333209 0,-1.999813 C 6.6666857,18 5.3333428,18 4,18 Z M 16,5 c 0.0000190,4.9999377 0.0000380,9.999875 0.0000570,14.999813 1.333252,0 2.666504,0 3.999756,0 0,-4.999938 0,-9.9998753 0,-14.999813 C 18.666542,5 17.333271,5 16,5 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 25 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,9 c 0,3.666604 0,7.333209 0,10.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-3.666604 0,-7.333209 0,-10.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z M 4,18 c -0.0000814,0.666604 -0.0001627,1.333209 -0.0002441,1.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-0.666604 0,-1.333209 0,-1.999813 C 6.6666857,18 5.3333428,18 4,18 Z M 16,5 c 0.0000190,4.9999377 0.0000380,9.999875 0.0000570,14.999813 1.333252,0 2.666504,0 3.999756,0 0,-4.999938 0,-9.9998753 0,-14.999813 C 18.666542,5 17.333271,5 16,5 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 26 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 9.9999064,8 c 0,3.999938 0,7.999875 0,11.999813 1.3334246,0 2.6668486,0 4.0002726,0 0,-3.999938 0,-7.999875 0,-11.999813 -1.333424,0 -2.666848,0 -4.0002726,0 z M 4,17 c -0.0000814,0.999938 -0.0001627,1.999875 -0.0002441,2.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-0.999938 0,-1.999875 0,-2.999813 C 6.6666857,17 5.3333428,17 4,17 Z M 16,4 c 0.0000190,5.333271 0.0000380,10.666542 0.0000570,15.999813 1.333252,0 2.666504,0 3.999756,0 0,-5.333271 0,-10.666542 0,-15.999813 C 18.666542,4 17.333271,4 16,4 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 27 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 10,7 c -0.0000312,4.333271 -0.0000624,8.666542 -0.0000936,12.999813 1.3334246,0 2.6668486,0 4.0002726,0 C 14.000119,15.666542 14.00006,11.333271 14,7 12.666667,7 11.333333,7 10,7 Z M 4,17 c -0.0000814,0.999938 -0.0001627,1.999875 -0.0002441,2.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-0.999938 0,-1.999875 0,-2.999813 C 6.6666857,17 5.3333428,17 4,17 Z M 16,4 c 0.0000190,5.333271 0.0000380,10.666542 0.0000570,15.999813 1.333252,0 2.666504,0 3.999756,0 0,-5.333271 0,-10.666542 0,-15.999813 C 18.666542,4 17.333271,4 16,4 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 28 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 10,6 c -0.0000312,4.666604 -0.0000624,9.333209 -0.0000936,13.999813 1.3334246,0 2.6668486,0 4.0002726,0 C 14.000119,15.333209 14.00006,10.666604 14,6 12.666667,6 11.333333,6 10,6 Z M 4,16 c -0.0000814,1.333271 -0.0001627,2.666542 -0.0002441,3.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-1.333271 0,-2.666542 0,-3.999813 C 6.6666857,16 5.3333428,16 4,16 Z M 16,5 c 0.0000190,4.9999377 0.0000380,9.999875 0.0000570,14.999813 1.333252,0 2.666504,0 3.999756,0 0,-4.999938 0,-9.9998753 0,-14.999813 C 18.666542,5 17.333271,5 16,5 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 29 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 10,5 c -0.0000312,4.9999377 -0.0000624,9.999875 -0.0000936,14.999813 1.3334246,0 2.6668486,0 4.0002726,0 C 14.000119,14.999875 14.00006,9.9999377 14,5 12.666667,5 11.333333,5 10,5 Z m -6,9 c -0.0000814,1.999938 -0.0001627,3.999875 -0.0002441,5.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-1.999938 0,-3.999875 0,-5.999813 C 6.6666857,14 5.3333428,14 4,14 Z M 16,5 c 0.0000190,4.9999377 0.0000380,9.999875 0.0000570,14.999813 1.333252,0 2.666504,0 3.999756,0 0,-4.999938 0,-9.9998753 0,-14.999813 C 18.666542,5 17.333271,5 16,5 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
|
||||
<!-- Frame 30 -->
|
||||
<item android:duration="30">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m 10,5 c -0.0000312,4.9999377 -0.0000624,9.999875 -0.0000936,14.999813 1.3334246,0 2.6668486,0 4.0002726,0 C 14.000119,14.999875 14.00006,9.9999377 14,5 12.666667,5 11.333333,5 10,5 Z m -6,8 c -0.0000814,2.333271 -0.0001627,4.666542 -0.0002441,6.999813 1.3334242,0 2.6668484,0 4.0002726,0 0,-2.333271 0,-4.666542 0,-6.999813 C 6.6666857,13 5.3333428,13 4,13 Z M 16,6 c 0.0000190,4.666604 0.0000380,9.333209 0.0000570,13.999813 1.333252,0 2.666504,0 3.999756,0 0,-4.666604 0,-9.333209 0,-13.999813 C 18.666542,6 17.333271,6 16,6 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
</item>
|
||||
</animation-list>
|
||||
|
||||
|
Loading…
Reference in a new issue