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.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.ProgressBar
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
@ -42,7 +39,6 @@ import app.pachli.adapter.TabAdapter
|
||||||
import app.pachli.appstore.EventHub
|
import app.pachli.appstore.EventHub
|
||||||
import app.pachli.appstore.MainTabsChangedEvent
|
import app.pachli.appstore.MainTabsChangedEvent
|
||||||
import app.pachli.core.activity.BaseActivity
|
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.show
|
||||||
import app.pachli.core.common.extensions.viewBinding
|
import app.pachli.core.common.extensions.viewBinding
|
||||||
import app.pachli.core.common.extensions.visible
|
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.model.MastoList
|
||||||
import app.pachli.core.network.retrofit.MastodonApi
|
import app.pachli.core.network.retrofit.MastodonApi
|
||||||
import app.pachli.databinding.ActivityTabPreferenceBinding
|
import app.pachli.databinding.ActivityTabPreferenceBinding
|
||||||
import app.pachli.util.getDimension
|
import app.pachli.databinding.DialogSelectListBinding
|
||||||
import app.pachli.util.unsafeLazy
|
import app.pachli.util.unsafeLazy
|
||||||
import at.connyduck.sparkbutton.helpers.Utils
|
import at.connyduck.sparkbutton.helpers.Utils
|
||||||
import com.github.michaelbull.result.onFailure
|
import com.github.michaelbull.result.onFailure
|
||||||
|
@ -67,10 +63,7 @@ import com.google.android.material.transition.MaterialContainerTransform
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.CoroutineStart
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.awaitCancellation
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@ -296,26 +289,13 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val statusLayout = LinearLayout(this)
|
val selectListBinding = DialogSelectListBinding.inflate(layoutInflater, null, false)
|
||||||
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 dialog = AlertDialog.Builder(this)
|
val dialog = AlertDialog.Builder(this)
|
||||||
.setTitle(R.string.select_list_title)
|
.setTitle(R.string.select_list_title)
|
||||||
.setNeutralButton(R.string.select_list_manage, null)
|
.setNeutralButton(R.string.select_list_manage, null)
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.setView(statusLayout)
|
.setView(selectListBinding.root)
|
||||||
.setAdapter(adapter) { _, position ->
|
.setAdapter(adapter) { _, position ->
|
||||||
adapter.getItem(position)?.let { item ->
|
adapter.getItem(position)?.let { item ->
|
||||||
val newTab = TabViewData.from(TabData.UserList(item.id, item.title))
|
val newTab = TabViewData.from(TabData.UserList(item.id, item.title))
|
||||||
|
@ -326,8 +306,7 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener {
|
||||||
}
|
}
|
||||||
}.create()
|
}.create()
|
||||||
|
|
||||||
val showProgressBarJob = getProgressBarJob(progress, 500)
|
selectListBinding.progressBar.show()
|
||||||
showProgressBarJob.start()
|
|
||||||
|
|
||||||
// Set the "Manage lists" button listener after creating the dialog. This ensures
|
// 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
|
// 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))
|
startActivity(ListActivityIntent(applicationContext))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.show()
|
dialog.show()
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
listsRepository.lists.collect { result ->
|
listsRepository.lists.collect { result ->
|
||||||
result.onSuccess { lists ->
|
result.onSuccess { lists ->
|
||||||
if (lists is Lists.Loaded) {
|
if (lists is Lists.Loaded) {
|
||||||
showProgressBarJob.cancel()
|
selectListBinding.progressBar.hide()
|
||||||
adapter.clear()
|
adapter.clear()
|
||||||
adapter.addAll(lists.lists.sortedWith(compareByListTitle))
|
adapter.addAll(lists.lists.sortedWith(compareByListTitle))
|
||||||
if (lists.lists.isEmpty()) noListsText.show()
|
if (lists.lists.isEmpty()) selectListBinding.noLists.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.onFailure {
|
result.onFailure {
|
||||||
dialog.hide()
|
selectListBinding.progressBar.hide()
|
||||||
|
dialog.dismiss()
|
||||||
Snackbar.make(binding.root, R.string.error_list_load, Snackbar.LENGTH_LONG).show()
|
Snackbar.make(binding.root, R.string.error_list_load, Snackbar.LENGTH_LONG).show()
|
||||||
Timber.w(it.throwable, "failed to load lists")
|
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 {
|
private fun validateHashtag(input: CharSequence?): Boolean {
|
||||||
val trimmedInput = input?.trim() ?: ""
|
val trimmedInput = input?.trim() ?: ""
|
||||||
return trimmedInput.isNotEmpty() && hashtagRegex.matcher(trimmedInput).matches()
|
return trimmedInput.isNotEmpty() && hashtagRegex.matcher(trimmedInput).matches()
|
||||||
|
|
|
@ -23,7 +23,6 @@ import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.annotation.CheckResult
|
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import androidx.fragment.app.viewModels
|
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.divider.MaterialDividerItemDecoration
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineStart
|
|
||||||
import kotlinx.coroutines.awaitCancellation
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@ -130,9 +126,6 @@ class ViewThreadFragment :
|
||||||
|
|
||||||
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
|
|
||||||
var initialProgressBar = getProgressBarJob(binding.initialProgressBar, 500)
|
|
||||||
var threadProgressBar = getProgressBarJob(binding.threadProgressBar, 500)
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
viewModel.uiState.collect { uiState ->
|
viewModel.uiState.collect { uiState ->
|
||||||
when (uiState) {
|
when (uiState) {
|
||||||
|
@ -142,8 +135,7 @@ class ViewThreadFragment :
|
||||||
binding.recyclerView.hide()
|
binding.recyclerView.hide()
|
||||||
binding.statusView.hide()
|
binding.statusView.hide()
|
||||||
|
|
||||||
initialProgressBar = getProgressBarJob(binding.initialProgressBar, 500)
|
binding.initialProgressBar.show()
|
||||||
initialProgressBar.start()
|
|
||||||
}
|
}
|
||||||
is ThreadUiState.LoadingThread -> {
|
is ThreadUiState.LoadingThread -> {
|
||||||
if (uiState.statusViewDatum == null) {
|
if (uiState.statusViewDatum == null) {
|
||||||
|
@ -152,9 +144,8 @@ class ViewThreadFragment :
|
||||||
return@collect
|
return@collect
|
||||||
}
|
}
|
||||||
|
|
||||||
initialProgressBar.cancel()
|
binding.initialProgressBar.hide()
|
||||||
threadProgressBar = getProgressBarJob(binding.threadProgressBar, 500)
|
binding.threadProgressBar.show()
|
||||||
threadProgressBar.start()
|
|
||||||
|
|
||||||
if (viewModel.isInitialLoad) {
|
if (viewModel.isInitialLoad) {
|
||||||
adapter.submitList(listOf(uiState.statusViewDatum))
|
adapter.submitList(listOf(uiState.statusViewDatum))
|
||||||
|
@ -170,8 +161,8 @@ class ViewThreadFragment :
|
||||||
}
|
}
|
||||||
is ThreadUiState.Error -> {
|
is ThreadUiState.Error -> {
|
||||||
Timber.w(uiState.throwable, "failed to load status")
|
Timber.w(uiState.throwable, "failed to load status")
|
||||||
initialProgressBar.cancel()
|
binding.initialProgressBar.hide()
|
||||||
threadProgressBar.cancel()
|
binding.threadProgressBar.hide()
|
||||||
|
|
||||||
revealButtonState = RevealButtonState.NO_BUTTON
|
revealButtonState = RevealButtonState.NO_BUTTON
|
||||||
binding.swipeRefreshLayout.isRefreshing = false
|
binding.swipeRefreshLayout.isRefreshing = false
|
||||||
|
@ -188,7 +179,7 @@ class ViewThreadFragment :
|
||||||
return@collect
|
return@collect
|
||||||
}
|
}
|
||||||
|
|
||||||
threadProgressBar.cancel()
|
binding.threadProgressBar.hide()
|
||||||
|
|
||||||
adapter.submitList(uiState.statusViewData) {
|
adapter.submitList(uiState.statusViewData) {
|
||||||
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) && viewModel.isInitialLoad) {
|
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) && viewModel.isInitialLoad) {
|
||||||
|
@ -209,7 +200,7 @@ class ViewThreadFragment :
|
||||||
binding.statusView.hide()
|
binding.statusView.hide()
|
||||||
}
|
}
|
||||||
is ThreadUiState.Refreshing -> {
|
is ThreadUiState.Refreshing -> {
|
||||||
threadProgressBar.cancel()
|
binding.threadProgressBar.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -275,28 +266,6 @@ class ViewThreadFragment :
|
||||||
requireActivity().title = getString(R.string.title_view_thread)
|
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() {
|
override fun onRefresh() {
|
||||||
viewModel.refresh(thisThreadsStatusId)
|
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>
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<ProgressBar
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
android:id="@+id/initialProgressBar"
|
android:id="@+id/initialProgressBar"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
@ -84,6 +84,8 @@
|
||||||
<item name="graphViewStyle">@style/Pachli.Widget.GraphView</item>
|
<item name="graphViewStyle">@style/Pachli.Widget.GraphView</item>
|
||||||
|
|
||||||
<item name="snackbarTextViewStyle">@style/snackbar_text</item>
|
<item name="snackbarTextViewStyle">@style/snackbar_text</item>
|
||||||
|
|
||||||
|
<item name="linearProgressIndicatorStyle">@style/Pachli.Widget.Material3.LinearProgressIndicator</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme" parent="Theme.Pachli" />
|
<style name="AppTheme" parent="Theme.Pachli" />
|
||||||
|
@ -169,6 +171,11 @@
|
||||||
<item name="lineWidth">2sp</item>
|
<item name="lineWidth">2sp</item>
|
||||||
</style>
|
</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" />
|
<style name="Theme.Pachli.Black" parent="Base.Theme.Black" />
|
||||||
|
|
||||||
<!-- customize the shape of the avatars in account selection list -->
|
<!-- customize the shape of the avatars in account selection list -->
|
||||||
|
|
Loading…
Reference in New Issue