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:
parent
c32b31cd2e
commit
04e25eb90a
7 changed files with 200 additions and 15 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
app/src/main/res/drawable/ic_play_next_24.xml
Normal file
11
app/src/main/res/drawable/ic_play_next_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: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>
|
|
@ -13,13 +13,20 @@
|
||||||
style="@style/Widget.Auxio.AppBarLayout"
|
style="@style/Widget.Auxio.AppBarLayout"
|
||||||
android:fitsSystemWindows="true">
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
<org.oxycblt.auxio.ui.SelectionToolbarOverlay
|
||||||
android:id="@+id/home_toolbar"
|
android:id="@+id/home_toolbar_overlay"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
app:layout_scrollFlags="scroll|enterAlways"
|
|
||||||
app:menu="@menu/menu_home"
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
app:title="@string/info_app_name" />
|
android:id="@+id/home_toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_scrollFlags="scroll|enterAlways"
|
||||||
|
app:menu="@menu/menu_home"
|
||||||
|
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"
|
||||||
|
|
21
app/src/main/res/menu/menu_selection_actions.xml
Normal file
21
app/src/main/res/menu/menu_selection_actions.xml
Normal 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>
|
|
@ -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>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue