diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt
index 442dde45f..8021652a0 100644
--- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt
@@ -28,10 +28,13 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
+import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import org.oxycblt.auxio.databinding.FragmentMainBinding
+import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.MusicViewModel
+import org.oxycblt.auxio.playback.PlaybackBarLayout
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.logD
@@ -39,8 +42,9 @@ import org.oxycblt.auxio.util.logD
* A wrapper around the home fragment that shows the playback fragment and controls
* the more high-level navigation features.
*/
-class MainFragment : Fragment() {
+class MainFragment : Fragment(), PlaybackBarLayout.ActionCallback {
private val playbackModel: PlaybackViewModel by activityViewModels()
+ private val detailModel: DetailViewModel by activityViewModels()
private val musicModel: MusicViewModel by activityViewModels()
override fun onCreateView(
@@ -63,22 +67,26 @@ class MainFragment : Fragment() {
// --- VIEWMODEL SETUP ---
- if (playbackModel.song.value != null) {
- binding.mainBarLayout.showBar()
- } else {
- binding.mainBarLayout.hideBar()
- }
+ binding.mainBarLayout.setActionCallback(this)
+
+ binding.mainBarLayout.setSong(playbackModel.song.value)
+ binding.mainBarLayout.setPlaying(playbackModel.isPlaying.value!!)
+ binding.mainBarLayout.setPosition(playbackModel.position.value!!)
playbackModel.song.observe(viewLifecycleOwner) { song ->
- if (song != null) {
- binding.mainBarLayout.showBar()
- } else {
- binding.mainBarLayout.hideBar()
- }
+ binding.mainBarLayout.setSong(song)
}
- // Initialize music loading. Unlike MainFragment, we can not only do this here on startup
- // but also show a SnackBar in a reasonable place in this fragment.
+ playbackModel.isPlaying.observe(viewLifecycleOwner) { isPlaying ->
+ binding.mainBarLayout.setPlaying(isPlaying)
+ }
+
+ playbackModel.position.observe(viewLifecycleOwner) { pos ->
+ binding.mainBarLayout.setPosition(pos)
+ }
+
+ // Initialize music loading. Do it here so that it shows on every fragment that this
+ // one contains.
musicModel.loadMusic(requireContext())
// Handle the music loader response.
@@ -132,4 +140,18 @@ class MainFragment : Fragment() {
return binding.root
}
+
+ override fun onPlayPauseClick() {
+ playbackModel.invertPlayingStatus()
+ }
+
+ override fun onNavToPlayback() {
+ findNavController().navigate(
+ MainFragmentDirections.actionGoToPlayback()
+ )
+ }
+
+ override fun onNavToItem() {
+ detailModel.navToItem(playbackModel.song.value ?: return)
+ }
}
diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt
index b1898588c..ccd1a78c7 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt
@@ -42,6 +42,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.canScroll
import org.oxycblt.auxio.util.resolveAttr
import org.oxycblt.auxio.util.resolveDrawable
+import org.oxycblt.auxio.util.systemBarsCompat
import kotlin.math.abs
/**
@@ -324,12 +325,14 @@ class FastScrollRecyclerView @JvmOverloads constructor(
}
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
+ val bars = insets.systemBarsCompat
+
setPadding(
initialPadding.left, initialPadding.top, initialPadding.right,
- initialPadding.bottom + insets.systemWindowInsetBottom
+ initialPadding.bottom + bars.bottom
)
- scrollerPadding.bottom = insets.systemWindowInsetBottom
+ scrollerPadding.bottom = bars.bottom
return insets
}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt
deleted file mode 100644
index 51f828605..000000000
--- a/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (c) 2021 Auxio Project
- * CompactPlaybackFragment.kt is part of Auxio.
- *
- * 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 .
- */
-
-package org.oxycblt.auxio.playback
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.navigation.fragment.findNavController
-import org.oxycblt.auxio.MainFragmentDirections
-import org.oxycblt.auxio.databinding.FragmentCompactPlaybackBinding
-import org.oxycblt.auxio.detail.DetailViewModel
-import org.oxycblt.auxio.util.logD
-
-/**
- * A [Fragment] that displays the currently played song at a glance, with some basic controls.
- * Extends into [PlaybackFragment] when clicked on.
- *
- * Instantiation is done by FragmentContainerView, **do not instantiate this fragment manually.**
- * @author OxygenCobalt
- * TODO: Add more controls to this view depending on screen width
- */
-class CompactPlaybackFragment : Fragment() {
- private val playbackModel: PlaybackViewModel by activityViewModels()
- private val detailModel: DetailViewModel by activityViewModels()
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- val binding = FragmentCompactPlaybackBinding.inflate(inflater)
-
- // --- UI SETUP ---
-
- binding.lifecycleOwner = viewLifecycleOwner
- binding.playbackModel = playbackModel
- binding.executePendingBindings()
-
- binding.root.apply {
- setOnClickListener {
- findNavController().navigate(
- MainFragmentDirections.actionGoToPlayback()
- )
- }
-
- setOnLongClickListener {
- detailModel.navToItem(playbackModel.song.value!!)
- true
- }
- }
-
- // --- VIEWMODEL SETUP ---
-
- playbackModel.song.observe(viewLifecycleOwner) { song ->
- if (song != null) {
- logD("Updating song display to ${song.name}")
-
- binding.song = song
- binding.playbackProgress.max = song.seconds.toInt()
- }
- }
-
- playbackModel.isPlaying.observe(viewLifecycleOwner) { isPlaying ->
- binding.playbackPlayPause.isActivated = isPlaying
- }
-
- logD("Fragment Created")
-
- return binding.root
- }
-}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackView.kt b/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackView.kt
new file mode 100644
index 000000000..8509ca4de
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackView.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2021 Auxio Project
+ * CompactPlaybackView.kt is part of Auxio.
+ *
+ * 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 .
+ */
+
+package org.oxycblt.auxio.playback
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.drawable.RippleDrawable
+import android.util.AttributeSet
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.google.android.material.shape.MaterialShapeDrawable
+import org.oxycblt.auxio.R
+import org.oxycblt.auxio.databinding.ViewCompactPlaybackBinding
+import org.oxycblt.auxio.music.Song
+import org.oxycblt.auxio.util.inflater
+import org.oxycblt.auxio.util.resolveAttr
+import org.oxycblt.auxio.util.resolveDrawable
+
+/**
+ * A view displaying the playback state in a compact manner. This is only meant to be used
+ * by [PlaybackBarLayout].
+ */
+class CompactPlaybackView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = -1
+) : ConstraintLayout(context, attrs, defStyleAttr) {
+ private val binding = ViewCompactPlaybackBinding.inflate(context.inflater, this, true)
+ private var mCallback: PlaybackBarLayout.ActionCallback? = null
+
+ init {
+ id = R.id.playback_bar
+
+ elevation = resources.getDimensionPixelSize(R.dimen.elevation_normal).toFloat()
+
+ // To get a MaterialShapeDrawable to co-exist with a ripple drawable, we need to layer
+ // this drawable on top of the existing ripple drawable. RippleDrawable actually inherits
+ // LayerDrawable though, so we can do this. However, adding a new drawable layer directly
+ // is only available on API 23+, but we're on API 21. So we create a drawable resource with
+ // an empty drawable with a hard-coded ID, filling the drawable in with a
+ // MaterialShapeDrawable at runtime and allowing this code to work on API 21.
+ background = R.drawable.ui_shape_ripple.resolveDrawable(context).apply {
+ val backgroundDrawable = MaterialShapeDrawable.createWithElevationOverlay(context).apply {
+ elevation = this@CompactPlaybackView.elevation
+ fillColor = ColorStateList.valueOf(R.attr.colorSurface.resolveAttr(context))
+ }
+
+ (this as RippleDrawable).setDrawableByLayerId(
+ android.R.id.background, backgroundDrawable
+ )
+ }
+
+ isClickable = true
+ isFocusable = true
+
+ setOnClickListener {
+ mCallback?.onNavToPlayback()
+ }
+
+ setOnLongClickListener {
+ mCallback?.onNavToItem()
+ true
+ }
+
+ binding.playbackPlayPause.setOnClickListener {
+ mCallback?.onPlayPauseClick()
+ }
+ }
+
+ fun setSong(song: Song) {
+ binding.song = song
+ }
+
+ fun setPlaying(isPlaying: Boolean) {
+ binding.playbackPlayPause.isActivated = isPlaying
+ }
+
+ fun setPosition(position: Long) {
+ if (binding.song == null) {
+ binding.playbackProgress.progress = 0
+ return
+ }
+
+ binding.playbackProgress.progress = position.toInt()
+ }
+
+ fun setCallback(callback: PlaybackBarLayout.ActionCallback) {
+ mCallback = callback
+ }
+
+ fun clearCallback() {
+ mCallback = null
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarLayout.kt
index 1db401be3..e2b9d5c9a 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarLayout.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarLayout.kt
@@ -19,25 +19,18 @@
package org.oxycblt.auxio.playback
import android.content.Context
-import android.content.res.ColorStateList
import android.graphics.Insets
import android.os.Build
-import android.os.Parcelable
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
-import android.widget.FrameLayout
import androidx.annotation.AttrRes
import androidx.annotation.StyleRes
-import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
-import com.google.android.material.shape.MaterialShapeDrawable
-import org.oxycblt.auxio.R
-import org.oxycblt.auxio.util.logD
-import org.oxycblt.auxio.util.resolveAttr
+import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.systemBarsCompat
/**
@@ -46,9 +39,6 @@ import org.oxycblt.auxio.util.systemBarsCompat
* this class was primarily written by me and I plan to expand this layout to become part of
* the playback navigation process.
*
- * TODO: Migrate CompactPlaybackFragment to a view. This is okay, as updates can be delivered
- * via MainFragment and it would fix the issue where the actual layout won't measure until
- * the fragment is shown.
* TODO: Implement animation
* TODO: Implement the swipe-up behavior. This needs to occur, as the way the main fragment
* saves state results in'
@@ -59,37 +49,18 @@ class PlaybackBarLayout @JvmOverloads constructor(
@AttrRes defStyleAttr: Int = 0,
@StyleRes defStyleRes: Int = 0
) : ViewGroup(context, attrs, defStyleAttr, defStyleRes) {
- private val barLayout = FrameLayout(context)
- private val playbackFragment = CompactPlaybackFragment()
+ private val playbackView = CompactPlaybackView(context)
private var lastInsets: WindowInsets? = null
init {
- addView(barLayout)
-
- barLayout.apply {
- id = R.id.main_playback
-
- elevation = resources.getDimensionPixelSize(R.dimen.elevation_normal).toFloat()
+ addView(playbackView)
+ playbackView.apply {
(layoutParams as LayoutParams).apply {
width = ViewGroup.LayoutParams.MATCH_PARENT
height = ViewGroup.LayoutParams.WRAP_CONTENT
isBar = true
}
-
- background = MaterialShapeDrawable.createWithElevationOverlay(context).apply {
- elevation = barLayout.elevation
- fillColor = ColorStateList.valueOf(R.attr.colorSurface.resolveAttr(context))
- }
- }
-
- if (!isInEditMode) {
- (context as AppCompatActivity).supportFragmentManager.apply {
- this
- .beginTransaction()
- .replace(R.id.main_playback, playbackFragment)
- .commit()
- }
}
}
@@ -101,15 +72,15 @@ class PlaybackBarLayout @JvmOverloads constructor(
setMeasuredDimension(widthSize, heightSize)
- val barParams = barLayout.layoutParams as LayoutParams
+ val barParams = playbackView.layoutParams as LayoutParams
val barWidthSpec = getChildMeasureSpec(widthMeasureSpec, 0, barParams.width)
val barHeightSpec = getChildMeasureSpec(heightMeasureSpec, 0, barParams.height)
- barLayout.measure(barWidthSpec, barHeightSpec)
+ playbackView.measure(barWidthSpec, barHeightSpec)
updateWindowInsets()
- val barHeightAdjusted = (barLayout.measuredHeight * barParams.offset).toInt()
+ val barHeightAdjusted = (playbackView.measuredHeight * barParams.offset).toInt()
val contentWidth = measuredWidth
val contentHeight = measuredHeight - barHeightAdjusted
@@ -129,13 +100,13 @@ class PlaybackBarLayout @JvmOverloads constructor(
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
- val barHeight = if (barLayout.isVisible) {
- barLayout.measuredHeight
+ val barHeight = if (playbackView.isVisible) {
+ playbackView.measuredHeight
} else {
0
}
- val barHeightAdjusted = (barHeight * (barLayout.layoutParams as LayoutParams).offset).toInt()
+ val barHeightAdjusted = (barHeight * (playbackView.layoutParams as LayoutParams).offset).toInt()
for (child in children) {
if (child.visibility == View.GONE) continue
@@ -154,7 +125,7 @@ class PlaybackBarLayout @JvmOverloads constructor(
}
override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets {
- barLayout.updatePadding(bottom = insets.systemBarsCompat.bottom)
+ playbackView.updatePadding(bottom = insets.systemBarsCompat.bottom)
lastInsets = insets
updateWindowInsets()
@@ -162,6 +133,12 @@ class PlaybackBarLayout @JvmOverloads constructor(
return insets
}
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+
+ playbackView.clearCallback()
+ }
+
private fun updateWindowInsets() {
val insets = lastInsets
@@ -171,8 +148,8 @@ class PlaybackBarLayout @JvmOverloads constructor(
}
private fun mutateInsets(insets: WindowInsets): WindowInsets {
- val barParams = barLayout.layoutParams as LayoutParams
- val childConsumedInset = (barLayout.measuredHeight * barParams.offset).toInt()
+ val barParams = playbackView.layoutParams as LayoutParams
+ val childConsumedInset = (playbackView.measuredHeight * barParams.offset).toInt()
val bars = insets.systemBarsCompat
@@ -194,8 +171,29 @@ class PlaybackBarLayout @JvmOverloads constructor(
return insets
}
- fun showBar() {
- val barParams = barLayout.layoutParams as LayoutParams
+ fun setSong(song: Song?) {
+ if (song != null) {
+ showBar()
+ playbackView.setSong(song)
+ } else {
+ hideBar()
+ }
+ }
+
+ fun setPlaying(isPlaying: Boolean) {
+ playbackView.setPlaying(isPlaying)
+ }
+
+ fun setPosition(position: Long) {
+ playbackView.setPosition(position)
+ }
+
+ fun setActionCallback(callback: ActionCallback) {
+ playbackView.setCallback(callback)
+ }
+
+ private fun showBar() {
+ val barParams = playbackView.layoutParams as LayoutParams
if (barParams.offset == 1f) {
return
@@ -210,8 +208,8 @@ class PlaybackBarLayout @JvmOverloads constructor(
invalidate()
}
- fun hideBar() {
- val barParams = barLayout.layoutParams as LayoutParams
+ private fun hideBar() {
+ val barParams = playbackView.layoutParams as LayoutParams
if (barParams.offset == 0f) {
return
@@ -260,4 +258,10 @@ class PlaybackBarLayout @JvmOverloads constructor(
constructor(source: ViewGroup.LayoutParams) : super(source)
}
+
+ interface ActionCallback {
+ fun onPlayPauseClick()
+ fun onNavToItem()
+ fun onNavToPlayback()
+ }
}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt
index 2e3112c83..fa4492a2c 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt
@@ -419,7 +419,6 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
* Restore playback on startup. This can do one of two things:
* - Play a file intent that was given by MainActivity in [playWithUri]
* - Restore the last playback state if there is no active file intent.
- * TODO: Re-add this to HomeFragment once state can be restored
*/
fun setupPlayback(context: Context) {
val intentUri = mIntentUri
diff --git a/app/src/main/java/org/oxycblt/auxio/util/ViewUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/ViewUtil.kt
index 7729585e9..76bfd2a13 100644
--- a/app/src/main/java/org/oxycblt/auxio/util/ViewUtil.kt
+++ b/app/src/main/java/org/oxycblt/auxio/util/ViewUtil.kt
@@ -138,6 +138,10 @@ fun @receiver:AttrRes Int.resolveAttr(context: Context): Int {
return color.resolveColor(context)
}
+/**
+ * Resolve window insets in a version-aware manner. This can be used to apply padding to
+ * a view that properly
+ */
val WindowInsets.systemBarsCompat: Rect get() {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
diff --git a/app/src/main/res/drawable/ui_shape_ripple.xml b/app/src/main/res/drawable/ui_shape_ripple.xml
new file mode 100644
index 000000000..8cca1f401
--- /dev/null
+++ b/app/src/main/res/drawable/ui_shape_ripple.xml
@@ -0,0 +1,14 @@
+
+
+
+ -
+
+
+
+
+ -
+
+
+
diff --git a/app/src/main/res/layout/fragment_detail.xml b/app/src/main/res/layout/fragment_detail.xml
index 689d2e915..9663b4bbe 100644
--- a/app/src/main/res/layout/fragment_detail.xml
+++ b/app/src/main/res/layout/fragment_detail.xml
@@ -32,7 +32,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
- app:layout_role="content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:listitem="@layout/item_detail" />
diff --git a/app/src/main/res/layout/fragment_compact_playback.xml b/app/src/main/res/layout/view_compact_playback.xml
similarity index 88%
rename from app/src/main/res/layout/fragment_compact_playback.xml
rename to app/src/main/res/layout/view_compact_playback.xml
index 3b88f2b9d..fc83fc9a8 100644
--- a/app/src/main/res/layout/fragment_compact_playback.xml
+++ b/app/src/main/res/layout/view_compact_playback.xml
@@ -2,7 +2,7 @@
+ tools:context=".playback.CompactPlaybackView">
@@ -10,19 +10,17 @@
name="song"
type="org.oxycblt.auxio.music.Song" />
-
-
+ android:focusable="true"
+ tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
@@ -82,12 +79,12 @@
android:id="@+id/playback_progress"
android:layout_width="match_parent"
android:layout_height="@dimen/size_stroke_large"
+ android:max="@{(int) song.seconds}"
style="@style/Widget.Auxio.ProgressBar"
- android:progress="@{playbackModel.positionAsProgress}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:progress="70" />
-
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_playback_bar.xml b/app/src/main/res/layout/view_playback_bar.xml
deleted file mode 100644
index f062a19b5..000000000
--- a/app/src/main/res/layout/view_playback_bar.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index 7e92b50ea..21bbd907d 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -4,9 +4,4 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
index 8ee55ee73..6351210a8 100644
--- a/app/src/main/res/values/ids.xml
+++ b/app/src/main/res/values/ids.xml
@@ -1,5 +1,8 @@
+
+
+