ui: add toolbar visual skeleton for selection

Add an overlay toolbar to indicate when selection is occuring.

This has no functionality as of right now.
This commit is contained in:
Alexander Capehart 2022-12-16 11:48:21 -07:00
parent c32b31cd2e
commit 04e25eb90a
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
7 changed files with 200 additions and 15 deletions

View file

@ -58,6 +58,7 @@ import org.oxycblt.auxio.music.system.Indexer
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.SelectionViewModel
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collect
@ -75,6 +76,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val navModel: NavigationViewModel by activityViewModels() private val navModel: NavigationViewModel by activityViewModels()
private val homeModel: HomeViewModel by androidActivityViewModels() private val homeModel: HomeViewModel by androidActivityViewModels()
private val selectionModel: SelectionViewModel by activityViewModels()
private val musicModel: MusicViewModel by activityViewModels() private val musicModel: MusicViewModel by activityViewModels()
// lifecycleObject builds this in the creation step, so doing this is okay. // lifecycleObject builds this in the creation step, so doing this is okay.
@ -109,7 +111,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
addOnOffsetChangedListener { _, offset -> addOnOffsetChangedListener { _, offset ->
val range = binding.homeAppbar.totalScrollRange val range = binding.homeAppbar.totalScrollRange
binding.homeToolbar.alpha = 1f - (abs(offset.toFloat()) / (range.toFloat() / 2)) binding.homeToolbarOverlay.alpha = 1f - (abs(offset.toFloat()) / (range.toFloat() / 2))
binding.homeContent.updatePadding( binding.homeContent.updatePadding(
bottom = binding.homeAppbar.totalScrollRange + offset) bottom = binding.homeAppbar.totalScrollRange + offset)
@ -158,6 +160,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
collectImmediately(homeModel.currentTab, ::updateCurrentTab) collectImmediately(homeModel.currentTab, ::updateCurrentTab)
collectImmediately(homeModel.songs, homeModel.isFastScrolling, ::updateFab) collectImmediately(homeModel.songs, homeModel.isFastScrolling, ::updateFab)
collectImmediately(musicModel.indexerState, ::handleIndexerState) collectImmediately(musicModel.indexerState, ::handleIndexerState)
collectImmediately(selectionModel.selected, ::updateSelection)
collect(navModel.exploreNavigationItem, ::handleNavigation) collect(navModel.exploreNavigationItem, ::handleNavigation)
} }
@ -276,7 +279,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
private fun updateTabConfiguration() { private fun updateTabConfiguration() {
val binding = requireBinding() val binding = requireBinding()
val toolbarParams = binding.homeToolbar.layoutParams as AppBarLayout.LayoutParams val toolbarParams = binding.homeToolbarOverlay.layoutParams as AppBarLayout.LayoutParams
if (homeModel.tabs.size == 1) { if (homeModel.tabs.size == 1) {
// A single tag makes the tab layout redundant, hide it and disable the collapsing // A single tag makes the tab layout redundant, hide it and disable the collapsing
// behavior. // behavior.
@ -382,6 +385,10 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
} }
} }
private fun updateSelection(selected: List<Music>) {
requireBinding().homeToolbarOverlay.updateSelectionAmount(selected.size)
}
private fun handleNavigation(item: Music?) { private fun handleNavigation(item: Music?) {
val action = val action =
when (item) { when (item) {

View file

@ -17,6 +17,7 @@
package org.oxycblt.auxio.image package org.oxycblt.auxio.image
import android.animation.ValueAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
@ -61,6 +62,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
private val playingIndicator: IndicatorView private val playingIndicator: IndicatorView
private val selectionIndicator: ImageView private val selectionIndicator: ImageView
private var fadeAnimator: ValueAnimator? = null
init { init {
// Android wants you to make separate attributes for each view type, but will // Android wants you to make separate attributes for each view type, but will
// then throw an error if you do because of duplicate attribute names. // then throw an error if you do because of duplicate attribute names.
@ -156,27 +159,40 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
} }
private fun invalidateSelectionIndicator() { private fun invalidateSelectionIndicator() {
val targetVis: Int val targetAlpha: Float
val targetDuration: Long val targetDuration: Long
if (isActivated) { if (isActivated) {
targetVis = VISIBLE targetAlpha = 1f
targetDuration = targetDuration =
context.resources.getInteger(R.integer.anim_fade_enter_duration).toLong() context.resources.getInteger(R.integer.anim_fade_enter_duration).toLong()
} else { } else {
targetVis = INVISIBLE targetAlpha = 0f
targetDuration = targetDuration =
context.resources.getInteger(R.integer.anim_fade_exit_duration).toLong() context.resources.getInteger(R.integer.anim_fade_exit_duration).toLong()
} }
if (selectionIndicator.visibility == targetVis) { if (selectionIndicator.alpha == targetAlpha) {
return return
} }
TransitionManager.beginDelayedTransition( if (!isLaidOut) {
this, MaterialFade().apply { duration = targetDuration }) selectionIndicator.alpha = targetAlpha
return
}
selectionIndicator.visibility = targetVis if (fadeAnimator != null) {
fadeAnimator?.cancel()
fadeAnimator = null
}
fadeAnimator = ValueAnimator.ofFloat(selectionIndicator.alpha, targetAlpha).apply {
duration = targetDuration
addUpdateListener {
selectionIndicator.alpha = it.animatedValue as Float
}
start()
}
} }
fun bind(song: Song) { fun bind(song: Song) {

View file

@ -0,0 +1,118 @@
package org.oxycblt.auxio.ui
import android.animation.ValueAnimator
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.annotation.AttrRes
import androidx.appcompat.widget.Toolbar
import androidx.core.view.isInvisible
import androidx.transition.TransitionManager
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.transition.MaterialFadeThrough
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.logD
import kotlin.math.max
import kotlin.math.min
/**
* A wrapper around a Toolbar that enables an overlaid toolbar showing information about
* an item selection.
* @author OxygenCobalt
*/
class SelectionToolbarOverlay
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
FrameLayout(context, attrs, defStyleAttr){
private lateinit var innerToolbar: MaterialToolbar
private val selectionToolbar = MaterialToolbar(context).apply {
inflateMenu(R.menu.menu_selection_actions)
setNavigationIcon(R.drawable.ic_close_24)
}
private var fadeThroughAnimator: ValueAnimator? = null
override fun onFinishInflate() {
super.onFinishInflate()
check(childCount == 1 && getChildAt(0) is MaterialToolbar) {
"SelectionToolbarOverlay Must have only one MaterialToolbar child"
}
innerToolbar = getChildAt(0) as MaterialToolbar
addView(selectionToolbar)
}
/**
* Add listeners for the selection toolbar.
*/
fun setListeners(onExit: Toolbar.OnMenuItemClickListener,
onMenuItemClick: Toolbar.OnMenuItemClickListener) {
// TODO: Sub
}
/**
* Update the selection amount in the selection Toolbar. This will animate the selection
* Toolbar into focus if there is now a selection to show.
*/
fun updateSelectionAmount(amount: Int): Boolean {
logD("Updating selection amount to $amount")
return if (amount > 0) {
selectionToolbar.title = context.getString(R.string.fmt_selected, amount)
animateToolbarVisibility(true)
} else {
animateToolbarVisibility(false)
}
}
private fun animateToolbarVisibility(selectionVisible: Boolean): Boolean {
// TODO: Animate nicer Material Fade transitions using animators (Normal transitions
// don't work due to translation)
val targetInnerAlpha: Float
val targetSelectionAlpha: Float
if (selectionVisible) {
targetInnerAlpha = 0f
targetSelectionAlpha = 1f
} else {
targetInnerAlpha = 1f
targetSelectionAlpha = 0f
}
if (innerToolbar.alpha == targetInnerAlpha &&
selectionToolbar.alpha == targetSelectionAlpha) {
return false
}
if (!isLaidOut) {
changeToolbarAlpha(targetInnerAlpha)
return true
}
if (fadeThroughAnimator != null) {
fadeThroughAnimator?.cancel()
fadeThroughAnimator = null
}
fadeThroughAnimator = ValueAnimator.ofFloat(innerToolbar.alpha, targetInnerAlpha).apply {
duration = context.resources.getInteger(R.integer.anim_fade_enter_duration).toLong()
addUpdateListener {
changeToolbarAlpha(it.animatedValue as Float)
}
start()
}
return true
}
private fun changeToolbarAlpha(innerAlpha: Float ) {
innerToolbar.apply {
alpha = innerAlpha
isInvisible = innerAlpha == 0f
}
selectionToolbar.apply {
alpha = 1 - innerAlpha
isInvisible = innerAlpha == 1f
}
}
}

View 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:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M13,14H15V11H18V9H15V6H13V9H10V11H13ZM8,18Q7.175,18 6.588,17.413Q6,16.825 6,16V4Q6,3.175 6.588,2.587Q7.175,2 8,2H20Q20.825,2 21.413,2.587Q22,3.175 22,4V16Q22,16.825 21.413,17.413Q20.825,18 20,18ZM8,16H20Q20,16 20,16Q20,16 20,16V4Q20,4 20,4Q20,4 20,4H8Q8,4 8,4Q8,4 8,4V16Q8,16 8,16Q8,16 8,16ZM4,22Q3.175,22 2.588,21.413Q2,20.825 2,20V6H4V20Q4,20 4,20Q4,20 4,20H18V22ZM8,4Q8,4 8,4Q8,4 8,4V16Q8,16 8,16Q8,16 8,16Q8,16 8,16Q8,16 8,16V4Q8,4 8,4Q8,4 8,4Z"/>
</vector>

View file

@ -13,6 +13,11 @@
style="@style/Widget.Auxio.AppBarLayout" style="@style/Widget.Auxio.AppBarLayout"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true">
<org.oxycblt.auxio.ui.SelectionToolbarOverlay
android:id="@+id/home_toolbar_overlay"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/home_toolbar" android:id="@+id/home_toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -21,6 +26,8 @@
app:menu="@menu/menu_home" app:menu="@menu/menu_home"
app:title="@string/info_app_name" /> app:title="@string/info_app_name" />
</org.oxycblt.auxio.ui.SelectionToolbarOverlay>
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/home_tabs" android:id="@+id/home_tabs"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_play_next"
android:title="@string/lbl_play_next"
android:icon="@drawable/ic_play_next_24"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_queue_add"
android:title="@string/lbl_queue_add"
app:showAsAction="never" />
<item
android:id="@+id/action_play"
android:title="@string/lbl_play_selected"
app:showAsAction="never"/>
<item
android:id="@+id/action_shuffle"
android:title="@string/lbl_shuffle_selected"
app:showAsAction="never"/>
</menu>

View file

@ -96,7 +96,9 @@
<string name="lbl_playback">Now playing</string> <string name="lbl_playback">Now playing</string>
<string name="lbl_equalizer">Equalizer</string> <string name="lbl_equalizer">Equalizer</string>
<string name="lbl_play">Play</string> <string name="lbl_play">Play</string>
<string name="lbl_play_selected">Play selected</string>
<string name="lbl_shuffle">Shuffle</string> <string name="lbl_shuffle">Shuffle</string>
<string name="lbl_shuffle_selected">Shuffle selected</string>
<string name="lbl_queue">Queue</string> <string name="lbl_queue">Queue</string>
<string name="lbl_play_next">Play next</string> <string name="lbl_play_next">Play next</string>
@ -338,6 +340,9 @@
<!-- Format Namespace | Value formatting/plurals --> <!-- Format Namespace | Value formatting/plurals -->
<eat-comment /> <eat-comment />
<!-- As in an amount of items that are selected -->
<string name="fmt_selected">%d Selected</string>
<!-- As in "Disc 1", "Disc 2", etc. in a set --> <!-- As in "Disc 1", "Disc 2", etc. in a set -->
<string name="fmt_disc_no">Disc %d</string> <string name="fmt_disc_no">Disc %d</string>