refactor: Use LinearProgressIndicator with delayed show/hide (#525)
Previous code used a normal ProgressBar and a coroutine to delay hiding/showing the bar for a snappier UI perception. This is built-in functionality in LinearProgressIndicator, so switch to that. While I'm here, implement the "Select list" dialog's layout as a layout resource.
This commit is contained in:
parent
006c358053
commit
cfc15c8f64
|
@ -19,13 +19,10 @@ package app.pachli
|
|||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
|
@ -42,7 +39,6 @@ import app.pachli.adapter.TabAdapter
|
|||
import app.pachli.appstore.EventHub
|
||||
import app.pachli.appstore.MainTabsChangedEvent
|
||||
import app.pachli.core.activity.BaseActivity
|
||||
import app.pachli.core.common.extensions.hide
|
||||
import app.pachli.core.common.extensions.show
|
||||
import app.pachli.core.common.extensions.viewBinding
|
||||
import app.pachli.core.common.extensions.visible
|
||||
|
@ -55,7 +51,7 @@ import app.pachli.core.navigation.ListActivityIntent
|
|||
import app.pachli.core.network.model.MastoList
|
||||
import app.pachli.core.network.retrofit.MastodonApi
|
||||
import app.pachli.databinding.ActivityTabPreferenceBinding
|
||||
import app.pachli.util.getDimension
|
||||
import app.pachli.databinding.DialogSelectListBinding
|
||||
import app.pachli.util.unsafeLazy
|
||||
import at.connyduck.sparkbutton.helpers.Utils
|
||||
import com.github.michaelbull.result.onFailure
|
||||
|
@ -67,10 +63,7 @@ import com.google.android.material.transition.MaterialContainerTransform
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
|
@ -296,26 +289,13 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
|
|||
}
|
||||
}
|
||||
|
||||
val statusLayout = LinearLayout(this)
|
||||
statusLayout.gravity = Gravity.CENTER
|
||||
val progress = ProgressBar(this)
|
||||
val preferredPadding = getDimension(this, androidx.appcompat.R.attr.dialogPreferredPadding)
|
||||
progress.setPadding(preferredPadding, 0, preferredPadding, 0)
|
||||
progress.visible(false)
|
||||
|
||||
val noListsText = TextView(this)
|
||||
noListsText.setPadding(preferredPadding, 0, preferredPadding, 0)
|
||||
noListsText.text = getText(R.string.select_list_empty)
|
||||
noListsText.visible(false)
|
||||
|
||||
statusLayout.addView(progress)
|
||||
statusLayout.addView(noListsText)
|
||||
val selectListBinding = DialogSelectListBinding.inflate(layoutInflater, null, false)
|
||||
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setTitle(R.string.select_list_title)
|
||||
.setNeutralButton(R.string.select_list_manage, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setView(statusLayout)
|
||||
.setView(selectListBinding.root)
|
||||
.setAdapter(adapter) { _, position ->
|
||||
adapter.getItem(position)?.let { item ->
|
||||
val newTab = TabViewData.from(TabData.UserList(item.id, item.title))
|
||||
|
@ -326,8 +306,7 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
|
|||
}
|
||||
}.create()
|
||||
|
||||
val showProgressBarJob = getProgressBarJob(progress, 500)
|
||||
showProgressBarJob.start()
|
||||
selectListBinding.progressBar.show()
|
||||
|
||||
// Set the "Manage lists" button listener after creating the dialog. This ensures
|
||||
// that clicking the button does not dismiss the dialog, so when the user returns
|
||||
|
@ -338,21 +317,23 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
|
|||
startActivity(ListActivityIntent(applicationContext))
|
||||
}
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
|
||||
lifecycleScope.launch {
|
||||
listsRepository.lists.collect { result ->
|
||||
result.onSuccess { lists ->
|
||||
if (lists is Lists.Loaded) {
|
||||
showProgressBarJob.cancel()
|
||||
selectListBinding.progressBar.hide()
|
||||
adapter.clear()
|
||||
adapter.addAll(lists.lists.sortedWith(compareByListTitle))
|
||||
if (lists.lists.isEmpty()) noListsText.show()
|
||||
if (lists.lists.isEmpty()) selectListBinding.noLists.show()
|
||||
}
|
||||
}
|
||||
|
||||
result.onFailure {
|
||||
dialog.hide()
|
||||
selectListBinding.progressBar.hide()
|
||||
dialog.dismiss()
|
||||
Snackbar.make(binding.root, R.string.error_list_load, Snackbar.LENGTH_LONG).show()
|
||||
Timber.w(it.throwable, "failed to load lists")
|
||||
}
|
||||
|
@ -360,18 +341,6 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
|
|||
}
|
||||
}
|
||||
|
||||
private fun getProgressBarJob(progressView: View, delayMs: Long) = this.lifecycleScope.launch(
|
||||
start = CoroutineStart.LAZY,
|
||||
) {
|
||||
try {
|
||||
delay(delayMs)
|
||||
progressView.show()
|
||||
awaitCancellation()
|
||||
} finally {
|
||||
progressView.hide()
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateHashtag(input: CharSequence?): Boolean {
|
||||
val trimmedInput = input?.trim() ?: ""
|
||||
return trimmedInput.isNotEmpty() && hashtagRegex.matcher(trimmedInput).matches()
|
||||
|
|
|
@ -23,7 +23,6 @@ import android.view.MenuInflater
|
|||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.CheckResult
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.fragment.app.viewModels
|
||||
|
@ -53,9 +52,6 @@ import com.google.android.material.color.MaterialColors
|
|||
import com.google.android.material.divider.MaterialDividerItemDecoration
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
|
@ -130,9 +126,6 @@ class ViewThreadFragment :
|
|||
|
||||
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
|
||||
var initialProgressBar = getProgressBarJob(binding.initialProgressBar, 500)
|
||||
var threadProgressBar = getProgressBarJob(binding.threadProgressBar, 500)
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.uiState.collect { uiState ->
|
||||
when (uiState) {
|
||||
|
@ -142,8 +135,7 @@ class ViewThreadFragment :
|
|||
binding.recyclerView.hide()
|
||||
binding.statusView.hide()
|
||||
|
||||
initialProgressBar = getProgressBarJob(binding.initialProgressBar, 500)
|
||||
initialProgressBar.start()
|
||||
binding.initialProgressBar.show()
|
||||
}
|
||||
is ThreadUiState.LoadingThread -> {
|
||||
if (uiState.statusViewDatum == null) {
|
||||
|
@ -152,9 +144,8 @@ class ViewThreadFragment :
|
|||
return@collect
|
||||
}
|
||||
|
||||
initialProgressBar.cancel()
|
||||
threadProgressBar = getProgressBarJob(binding.threadProgressBar, 500)
|
||||
threadProgressBar.start()
|
||||
binding.initialProgressBar.hide()
|
||||
binding.threadProgressBar.show()
|
||||
|
||||
if (viewModel.isInitialLoad) {
|
||||
adapter.submitList(listOf(uiState.statusViewDatum))
|
||||
|
@ -170,8 +161,8 @@ class ViewThreadFragment :
|
|||
}
|
||||
is ThreadUiState.Error -> {
|
||||
Timber.w(uiState.throwable, "failed to load status")
|
||||
initialProgressBar.cancel()
|
||||
threadProgressBar.cancel()
|
||||
binding.initialProgressBar.hide()
|
||||
binding.threadProgressBar.hide()
|
||||
|
||||
revealButtonState = RevealButtonState.NO_BUTTON
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
|
@ -188,7 +179,7 @@ class ViewThreadFragment :
|
|||
return@collect
|
||||
}
|
||||
|
||||
threadProgressBar.cancel()
|
||||
binding.threadProgressBar.hide()
|
||||
|
||||
adapter.submitList(uiState.statusViewData) {
|
||||
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) && viewModel.isInitialLoad) {
|
||||
|
@ -209,7 +200,7 @@ class ViewThreadFragment :
|
|||
binding.statusView.hide()
|
||||
}
|
||||
is ThreadUiState.Refreshing -> {
|
||||
threadProgressBar.cancel()
|
||||
binding.threadProgressBar.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -275,28 +266,6 @@ class ViewThreadFragment :
|
|||
requireActivity().title = getString(R.string.title_view_thread)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a job to implement a delayed-visible progress bar.
|
||||
*
|
||||
* Delaying the visibility of the progress bar can improve user perception of UI speed because
|
||||
* fewer UI elements are appearing and disappearing.
|
||||
*
|
||||
* When started the job will wait `delayMs` then show `view`. If the job is cancelled at
|
||||
* any time `view` is hidden.
|
||||
*/
|
||||
@CheckResult
|
||||
private fun getProgressBarJob(view: View, delayMs: Long) = viewLifecycleOwner.lifecycleScope.launch(
|
||||
start = CoroutineStart.LAZY,
|
||||
) {
|
||||
try {
|
||||
delay(delayMs)
|
||||
view.show()
|
||||
awaitCancellation()
|
||||
} finally {
|
||||
view.hide()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
viewModel.refresh(thisThreadsStatusId)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2024 Pachli Association
|
||||
~
|
||||
~ This file is a part of Pachli.
|
||||
~
|
||||
~ 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.
|
||||
~
|
||||
~ Pachli 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 Pachli; if not,
|
||||
~ see <http://www.gnu.org/licenses>.
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="top">
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="?dialogPreferredPadding"
|
||||
android:paddingEnd="?dialogPreferredPadding"
|
||||
android:layout_gravity="top"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/noLists"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="?dialogPreferredPadding"
|
||||
android:paddingEnd="?dialogPreferredPadding"
|
||||
android:text="@string/select_list_empty"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
|
@ -45,7 +45,7 @@
|
|||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/initialProgressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -84,6 +84,8 @@
|
|||
<item name="graphViewStyle">@style/Pachli.Widget.GraphView</item>
|
||||
|
||||
<item name="snackbarTextViewStyle">@style/snackbar_text</item>
|
||||
|
||||
<item name="linearProgressIndicatorStyle">@style/Pachli.Widget.Material3.LinearProgressIndicator</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme" parent="Theme.Pachli" />
|
||||
|
@ -169,6 +171,11 @@
|
|||
<item name="lineWidth">2sp</item>
|
||||
</style>
|
||||
|
||||
<style name="Pachli.Widget.Material3.LinearProgressIndicator" parent="Widget.Material3.LinearProgressIndicator">
|
||||
<item name="minHideDelay">500</item>
|
||||
<item name="showDelay">500</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Pachli.Black" parent="Base.Theme.Black" />
|
||||
|
||||
<!-- customize the shape of the avatars in account selection list -->
|
||||
|
|
Loading…
Reference in New Issue