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.ui.MainNavigationAction
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.ui.SelectionViewModel
|
||||
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||
import org.oxycblt.auxio.util.collect
|
||||
|
@ -75,6 +76,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
||||
private val navModel: NavigationViewModel by activityViewModels()
|
||||
private val homeModel: HomeViewModel by androidActivityViewModels()
|
||||
private val selectionModel: SelectionViewModel by activityViewModels()
|
||||
private val musicModel: MusicViewModel by activityViewModels()
|
||||
|
||||
// lifecycleObject builds this in the creation step, so doing this is okay.
|
||||
|
@ -109,7 +111,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
addOnOffsetChangedListener { _, offset ->
|
||||
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(
|
||||
bottom = binding.homeAppbar.totalScrollRange + offset)
|
||||
|
@ -158,6 +160,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
collectImmediately(homeModel.currentTab, ::updateCurrentTab)
|
||||
collectImmediately(homeModel.songs, homeModel.isFastScrolling, ::updateFab)
|
||||
collectImmediately(musicModel.indexerState, ::handleIndexerState)
|
||||
collectImmediately(selectionModel.selected, ::updateSelection)
|
||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||
}
|
||||
|
||||
|
@ -276,7 +279,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
|
||||
private fun updateTabConfiguration() {
|
||||
val binding = requireBinding()
|
||||
val toolbarParams = binding.homeToolbar.layoutParams as AppBarLayout.LayoutParams
|
||||
val toolbarParams = binding.homeToolbarOverlay.layoutParams as AppBarLayout.LayoutParams
|
||||
if (homeModel.tabs.size == 1) {
|
||||
// A single tag makes the tab layout redundant, hide it and disable the collapsing
|
||||
// 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?) {
|
||||
val action =
|
||||
when (item) {
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package org.oxycblt.auxio.image
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
|
@ -61,6 +62,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
private val playingIndicator: IndicatorView
|
||||
private val selectionIndicator: ImageView
|
||||
|
||||
private var fadeAnimator: ValueAnimator? = null
|
||||
|
||||
init {
|
||||
// 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.
|
||||
|
@ -156,27 +159,40 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
}
|
||||
|
||||
private fun invalidateSelectionIndicator() {
|
||||
val targetVis: Int
|
||||
val targetAlpha: Float
|
||||
val targetDuration: Long
|
||||
|
||||
if (isActivated) {
|
||||
targetVis = VISIBLE
|
||||
targetAlpha = 1f
|
||||
targetDuration =
|
||||
context.resources.getInteger(R.integer.anim_fade_enter_duration).toLong()
|
||||
} else {
|
||||
targetVis = INVISIBLE
|
||||
targetAlpha = 0f
|
||||
targetDuration =
|
||||
context.resources.getInteger(R.integer.anim_fade_exit_duration).toLong()
|
||||
}
|
||||
|
||||
if (selectionIndicator.visibility == targetVis) {
|
||||
if (selectionIndicator.alpha == targetAlpha) {
|
||||
return
|
||||
}
|
||||
|
||||
TransitionManager.beginDelayedTransition(
|
||||
this, MaterialFade().apply { duration = targetDuration })
|
||||
if (!isLaidOut) {
|
||||
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) {
|
||||
|
|
|
@ -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,6 +13,11 @@
|
|||
style="@style/Widget.Auxio.AppBarLayout"
|
||||
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
|
||||
android:id="@+id/home_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -21,6 +26,8 @@
|
|||
app:menu="@menu/menu_home"
|
||||
app:title="@string/info_app_name" />
|
||||
|
||||
</org.oxycblt.auxio.ui.SelectionToolbarOverlay>
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/home_tabs"
|
||||
android:layout_width="match_parent"
|
||||
|
|
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_equalizer">Equalizer</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_selected">Shuffle selected</string>
|
||||
|
||||
<string name="lbl_queue">Queue</string>
|
||||
<string name="lbl_play_next">Play next</string>
|
||||
|
@ -338,6 +340,9 @@
|
|||
<!-- Format Namespace | Value formatting/plurals -->
|
||||
<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 -->
|
||||
<string name="fmt_disc_no">Disc %d</string>
|
||||
|
||||
|
|
Loading…
Reference in a new issue