diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index 8f10725f8..c38d36349 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -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(), 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(), 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(), 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(), 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(), Toolbar.OnMenuI } } + private fun updateSelection(selected: List) { + requireBinding().homeToolbarOverlay.updateSelectionAmount(selected.size) + } + private fun handleNavigation(item: Music?) { val action = when (item) { diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt index 6cedab82d..55183c42b 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt @@ -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) { diff --git a/app/src/main/java/org/oxycblt/auxio/ui/SelectionToolbarOverlay.kt b/app/src/main/java/org/oxycblt/auxio/ui/SelectionToolbarOverlay.kt new file mode 100644 index 000000000..3e0cca415 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/ui/SelectionToolbarOverlay.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_play_next_24.xml b/app/src/main/res/drawable/ic_play_next_24.xml new file mode 100644 index 000000000..6a3d37c13 --- /dev/null +++ b/app/src/main/res/drawable/ic_play_next_24.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 90d6cc2a2..2b2ca753b 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -13,13 +13,20 @@ style="@style/Widget.Auxio.AppBarLayout" android:fitsSystemWindows="true"> - + android:layout_height="wrap_content"> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e73f10825..24730dcba 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -96,7 +96,9 @@ Now playing Equalizer Play + Play selected Shuffle + Shuffle selected Queue Play next @@ -338,6 +340,9 @@ + + %d Selected + Disc %d