music: rework loading ui flow

Rework the UI flow for music loading to be more coherent with runtime
rescanning.

The loading progress is now shown as a card on the bottom of the
screen. This way, app use is not completely crippled when the app has
to rescan the music library, albeit the shuffle button still has to
be disabled during this period.
This commit is contained in:
OxygenCobalt 2022-07-03 14:47:10 -06:00
parent e1c55d5ddc
commit 4f8fc8008c
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
4 changed files with 80 additions and 75 deletions

View file

@ -98,7 +98,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
binding.homeToolbar.alpha = 1f - (abs(offset.toFloat()) / (range.toFloat() / 2)) binding.homeToolbar.alpha = 1f - (abs(offset.toFloat()) / (range.toFloat() / 2))
binding.homeContent.updatePadding( binding.homePager.updatePadding(
bottom = binding.homeAppbar.totalScrollRange + offset) bottom = binding.homeAppbar.totalScrollRange + offset)
} }
} }
@ -107,7 +107,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
updateTabConfiguration() updateTabConfiguration()
binding.homeIndexingContainer.setOnApplyWindowInsetsListener { view, insets -> binding.homeIndexingWrapper.setOnApplyWindowInsetsListener { view, insets ->
view.updatePadding(bottom = insets.systemBarInsetsCompat.bottom) view.updatePadding(bottom = insets.systemBarInsetsCompat.bottom)
insets insets
} }
@ -141,7 +141,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
collect(homeModel.isFastScrolling, ::updateFastScrolling) collect(homeModel.isFastScrolling) { updateFab() }
collect(homeModel.recreateTabs, ::handleRecreateTabs) collect(homeModel.recreateTabs, ::handleRecreateTabs)
collectImmediately(homeModel.currentTab, ::updateCurrentTab) collectImmediately(homeModel.currentTab, ::updateCurrentTab)
collectImmediately(indexerModel.state, ::handleIndexerState) collectImmediately(indexerModel.state, ::handleIndexerState)
@ -190,23 +190,6 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
return true return true
} }
private fun updateFastScrolling(isFastScrolling: Boolean) {
val binding = requireBinding()
// Make sure an update here doesn't mess up the FAB state when it comes to the
// loader response.
val state = indexerModel.state.value
if (!(state is Indexer.State.Complete && state.response is Indexer.Response.Ok)) {
return
}
if (isFastScrolling) {
binding.homeFab.hide()
} else {
binding.homeFab.show()
}
}
private fun updateCurrentTab(tab: DisplayMode) { private fun updateCurrentTab(tab: DisplayMode) {
// Make sure that we update the scrolling view and allowed menu items whenever // Make sure that we update the scrolling view and allowed menu items whenever
// the tab changes. // the tab changes.
@ -274,27 +257,23 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
private fun handleIndexerState(state: Indexer.State?) { private fun handleIndexerState(state: Indexer.State?) {
val binding = requireBinding() val binding = requireBinding()
when (state) { when (state) {
is Indexer.State.Complete -> handleIndexerResponse(binding, state.response) is Indexer.State.Complete -> handleIndexerResponse(binding, state.response)
is Indexer.State.Indexing -> handleIndexingState(binding, state.indexing) is Indexer.State.Indexing -> handleIndexingState(binding, state.indexing)
null -> { null -> {
logD("Indexer is in indeterminate state") logD("Indexer is in indeterminate state")
binding.homeFab.hide()
binding.homeIndexingContainer.visibility = View.INVISIBLE binding.homeIndexingContainer.visibility = View.INVISIBLE
binding.homePager.visibility = View.INVISIBLE
} }
} }
updateFab()
} }
private fun handleIndexerResponse(binding: FragmentHomeBinding, response: Indexer.Response) { private fun handleIndexerResponse(binding: FragmentHomeBinding, response: Indexer.Response) {
if (response is Indexer.Response.Ok) { if (response is Indexer.Response.Ok) {
binding.homeFab.show() binding.homeFab.show()
binding.homeIndexingContainer.visibility = View.INVISIBLE binding.homeIndexingContainer.visibility = View.INVISIBLE
binding.homePager.visibility = View.VISIBLE
} else { } else {
binding.homeFab.hide()
binding.homePager.visibility = View.INVISIBLE
binding.homeIndexingContainer.visibility = View.VISIBLE binding.homeIndexingContainer.visibility = View.VISIBLE
logD("Received non-ok response $response") logD("Received non-ok response $response")
@ -336,8 +315,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
} }
private fun handleIndexingState(binding: FragmentHomeBinding, indexing: Indexer.Indexing) { private fun handleIndexingState(binding: FragmentHomeBinding, indexing: Indexer.Indexing) {
binding.homeFab.hide() updateFab()
binding.homePager.visibility = View.INVISIBLE
binding.homeIndexingContainer.visibility = View.VISIBLE binding.homeIndexingContainer.visibility = View.VISIBLE
binding.homeIndexingProgress.visibility = View.VISIBLE binding.homeIndexingProgress.visibility = View.VISIBLE
binding.homeIndexingAction.visibility = View.INVISIBLE binding.homeIndexingAction.visibility = View.INVISIBLE
@ -359,6 +337,18 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
} }
} }
private fun updateFab() {
val binding = requireBinding()
val fastScrolling = homeModel.isFastScrolling.value
val state = indexerModel.state.value
if (fastScrolling ||
!(state is Indexer.State.Complete && state.response is Indexer.Response.Ok)) {
binding.homeFab.hide()
} else {
binding.homeFab.show()
}
}
private fun handleNavigation(item: Music?) { private fun handleNavigation(item: Music?) {
// Note: You will want to add a post call to this if you want to re-introduce a collapsing // Note: You will want to add a post call to this if you want to re-introduce a collapsing
// toolbar. // toolbar.

View file

@ -23,6 +23,7 @@ import android.content.pm.PackageManager
import android.database.Cursor import android.database.Cursor
import android.os.Build import android.os.Build
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import kotlin.coroutines.cancellation.CancellationException
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.backend.Api21MediaStoreBackend import org.oxycblt.auxio.music.backend.Api21MediaStoreBackend
import org.oxycblt.auxio.music.backend.Api29MediaStoreBackend import org.oxycblt.auxio.music.backend.Api29MediaStoreBackend
@ -121,7 +122,7 @@ class Indexer {
this.callback = null this.callback = null
} }
fun index(context: Context) { suspend fun index(context: Context) {
val generation = synchronized(this) { ++currentGeneration } val generation = synchronized(this) { ++currentGeneration }
val notGranted = val notGranted =
@ -181,7 +182,9 @@ class Indexer {
@Synchronized @Synchronized
private fun emitIndexing(indexing: Indexing?, generation: Long) { private fun emitIndexing(indexing: Indexing?, generation: Long) {
if (currentGeneration != generation) { if (currentGeneration != generation) {
return // Not the running task anymore, cancel this co-routine
// We do this instead of using yield since it is *far* cheaper.
throw CancellationException()
} }
indexingState = indexing indexingState = indexing
@ -198,7 +201,9 @@ class Indexer {
@Synchronized @Synchronized
private fun emitCompletion(response: Response, generation: Long) { private fun emitCompletion(response: Response, generation: Long) {
if (currentGeneration != generation) { if (currentGeneration != generation) {
return // Not the running task anymore, cancel this co-routine
// We do this instead of using yield since it is *far* cheaper.
throw CancellationException()
} }
lastResponse = response lastResponse = response

View file

@ -46,8 +46,6 @@ import org.oxycblt.auxio.util.logD
* @author OxygenCobalt * @author OxygenCobalt
* *
* TODO: Add file observing * TODO: Add file observing
*
* TODO: Rework UI flow once again
*/ */
class IndexerService : Service(), Indexer.Controller, Settings.Callback { class IndexerService : Service(), Indexer.Controller, Settings.Callback {
private val indexer = Indexer.getInstance() private val indexer = Indexer.getInstance()

View file

@ -29,60 +29,72 @@
</org.oxycblt.auxio.ui.coordinator.EdgeAppBarLayout> </org.oxycblt.auxio.ui.coordinator.EdgeAppBarLayout>
<FrameLayout <androidx.viewpager2.widget.ViewPager2
android:id="@+id/home_content" android:id="@+id/home_pager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:layout="@layout/fragment_home_list" />
<androidx.constraintlayout.widget.ConstraintLayout <FrameLayout
android:id="@+id/home_indexing_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:animateLayoutChanges="true"
android:clipToPadding="false">
<com.google.android.material.card.MaterialCardView
android:id="@+id/home_indexing_container" android:id="@+id/home_indexing_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:animateLayoutChanges="true" android:layout_margin="@dimen/spacing_medium">
android:paddingStart="@dimen/spacing_medium"
android:paddingEnd="@dimen/spacing_medium">
<TextView <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/home_indexing_status"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/spacing_medium" android:animateLayoutChanges="true">
android:gravity="center"
android:textAppearance="@style/TextAppearance.Auxio.BodyLarge"
app:layout_constraintBottom_toTopOf="@+id/home_indexing_action"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Status" />
<com.google.android.material.progressindicator.LinearProgressIndicator <TextView
android:id="@+id/home_indexing_progress" android:id="@+id/home_indexing_status"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:indeterminate="true" android:layout_margin="@dimen/spacing_medium"
app:indeterminateAnimationType="disjoint" android:gravity="center"
app:layout_constraintBottom_toBottomOf="@+id/home_indexing_action" android:textAppearance="@style/TextAppearance.Auxio.BodyLarge"
app:layout_constraintTop_toTopOf="@+id/home_indexing_action" app:layout_constraintBottom_toTopOf="@+id/home_indexing_action"
app:trackColor="@color/sel_track" /> app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Status" />
<Button <com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/home_indexing_action" android:id="@+id/home_indexing_progress"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/lbl_retry" android:layout_marginStart="@dimen/spacing_medium"
android:visibility="invisible" android:layout_marginEnd="@dimen/spacing_medium"
app:layout_constraintBottom_toBottomOf="parent" android:indeterminate="true"
app:layout_constraintTop_toBottomOf="@+id/home_indexing_status" /> app:indeterminateAnimationType="disjoint"
app:layout_constraintBottom_toBottomOf="@+id/home_indexing_action"
app:layout_constraintTop_toTopOf="@+id/home_indexing_action"
app:trackColor="@color/sel_track" />
</androidx.constraintlayout.widget.ConstraintLayout> <Button
android:id="@+id/home_indexing_action"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/lbl_retry"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
android:layout_marginBottom="@dimen/spacing_medium"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_indexing_status" />
<androidx.viewpager2.widget.ViewPager2 </androidx.constraintlayout.widget.ConstraintLayout>
android:id="@+id/home_pager"
android:layout_width="match_parent" </com.google.android.material.card.MaterialCardView>
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:layout="@layout/fragment_home_list" />
</FrameLayout> </FrameLayout>