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:
Nik Clayton 2024-03-13 10:03:10 +01:00 committed by GitHub
parent 006c358053
commit cfc15c8f64
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 66 additions and 79 deletions

View File

@ -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()

View File

@ -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)
}

View File

@ -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>

View File

@ -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"

View File

@ -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 -->