Skip to content

Commit 7af0a14

Browse files
lukstbitmikehardy
authored andcommitted
Introduce LoadingDialogFragment
A [DialogFragment] that can be used to show a "loading" state in the ui
1 parent 54624cb commit 7af0a14

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/****************************************************************************************
2+
* Copyright (c) 2025 lukstbit <[email protected]> *
3+
* *
4+
* This program is free software; you can redistribute it and/or modify it under *
5+
* the terms of the GNU General Public License as published by the Free Software *
6+
* Foundation; either version 3 of the License, or (at your option) any later *
7+
* version. *
8+
* *
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY *
10+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A *
11+
* PARTICULAR PURPOSE. See the GNU General Public License for more details. *
12+
* *
13+
* You should have received a copy of the GNU General Public License along with *
14+
* this program. If not, see <http://www.gnu.org/licenses/>. *
15+
****************************************************************************************/
16+
17+
package com.ichi2.anki.dialogs
18+
19+
import android.app.Dialog
20+
import android.os.Bundle
21+
import android.widget.TextView
22+
import androidx.annotation.MainThread
23+
import androidx.appcompat.app.AlertDialog
24+
import androidx.core.os.bundleOf
25+
import androidx.fragment.app.DialogFragment
26+
import androidx.fragment.app.commitNow
27+
import com.google.android.material.loadingindicator.LoadingIndicator
28+
import com.ichi2.anki.AnkiActivity
29+
import com.ichi2.anki.R
30+
import com.ichi2.utils.cancelable
31+
import com.ichi2.utils.create
32+
import com.ichi2.utils.customView
33+
34+
/**
35+
* Simple [DialogFragment] to be used to show a "loading" ui state.
36+
*/
37+
class LoadingDialogFragment : DialogFragment() {
38+
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
39+
val dialogView = layoutInflater.inflate(R.layout.fragment_loading, null)
40+
val canBeCancelled = arguments?.getBoolean(KEY_CANCELLABLE) ?: true
41+
dialogView.findViewById<TextView>(R.id.text).text =
42+
arguments?.getString(KEY_MESSAGE) ?: getString(R.string.dialog_processing)
43+
return AlertDialog
44+
.Builder(requireContext())
45+
.create {
46+
customView(dialogView)
47+
cancelable(canBeCancelled)
48+
}.apply { setCanceledOnTouchOutside(canBeCancelled) }
49+
}
50+
51+
companion object {
52+
const val TAG = "LoadingDialogFragment"
53+
private const val KEY_MESSAGE = "key_message"
54+
private const val KEY_CANCELLABLE = "key_cancellable"
55+
56+
/**
57+
* Creates an instance of [LoadingDialogFragment].
58+
* @param message optional message for the loading operation, will default to
59+
* [R.string.dialog_processing] if not provided
60+
* @param cancellable if the dialog should be cancellable or not (also affects cancel when
61+
* touching outside the dialog window)
62+
*/
63+
fun newInstance(
64+
message: String? = null,
65+
cancellable: Boolean = true,
66+
) = LoadingDialogFragment().apply {
67+
arguments =
68+
bundleOf(
69+
KEY_MESSAGE to message,
70+
KEY_CANCELLABLE to cancellable,
71+
)
72+
}
73+
}
74+
}
75+
76+
/**
77+
* Shows a [LoadingDialogFragment].
78+
* This method will look for a previous instance of the dialog and reuse it(by changing the message)
79+
* if it's already showing and the new call doesn't modify input parameters(ex. [cancellable]). In
80+
* any other cases, the old instance will be removed and a new one will be used.
81+
*
82+
* Note: Multiple calls of this method will result in a single dialog being shown, this also
83+
* implies that a call to [dismissLoadingDialog] will dismiss the dialogs of all calls. Callers need
84+
* to handle this scenario by combining the loading states and manually handling showing/dismissing.
85+
*
86+
* @param message the message to show along with the [LoadingIndicator] or null to default to
87+
* use "Processing..."
88+
* @param cancellable true if the dialog should be cancellable, false otherwise. This will also
89+
* apply to the [AlertDialog.setCanceledOnTouchOutside] property
90+
*/
91+
@MainThread
92+
fun AnkiActivity.showLoadingDialog(
93+
message: String? = null,
94+
cancellable: Boolean = true,
95+
) {
96+
val fragment =
97+
supportFragmentManager.findFragmentByTag(LoadingDialogFragment.TAG) as? LoadingDialogFragment
98+
val isAlreadyShowing = fragment?.dialog?.isShowing == true
99+
if (isAlreadyShowing) {
100+
// if a dialog is already showing and it has the same input params then just update the
101+
// dialog with the new message, otherwise remove the instance
102+
val fragmentView = fragment.view // sanity check
103+
if (fragmentView != null && cancellable == fragment.isCancelable) {
104+
fragmentView.findViewById<TextView>(R.id.text)?.text =
105+
message ?: getString(R.string.dialog_processing)
106+
return
107+
}
108+
}
109+
// remove the previous fragment if an instance is found but it is not showing or different input
110+
// parameters were requested for the new dialog fragment
111+
removeImmediately(fragment)
112+
val loadingDialog = LoadingDialogFragment.newInstance(message, cancellable)
113+
loadingDialog.show(supportFragmentManager, LoadingDialogFragment.TAG)
114+
}
115+
116+
/** Synchronously removes the provided [LoadingDialogFragment] if valid(not null) */
117+
private fun AnkiActivity.removeImmediately(fragment: LoadingDialogFragment?) {
118+
if (fragment != null) {
119+
supportFragmentManager.commitNow { remove(fragment) }
120+
}
121+
}
122+
123+
/**
124+
* Dismisses and removes the current displayed [LoadingDialogFragment] if one is present.
125+
*/
126+
fun AnkiActivity.dismissLoadingDialog() {
127+
val fragment =
128+
supportFragmentManager.findFragmentByTag(LoadingDialogFragment.TAG) as? LoadingDialogFragment
129+
removeImmediately(fragment)
130+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools"
4+
android:layout_width="match_parent"
5+
android:layout_height="wrap_content"
6+
xmlns:app="http://schemas.android.com/apk/res-auto"
7+
android:minHeight="?attr/listPreferredItemHeight"
8+
android:paddingVertical="24dp"
9+
android:paddingHorizontal="32dp">
10+
11+
<com.google.android.material.loadingindicator.LoadingIndicator
12+
android:id="@+id/loading_indicator"
13+
android:layout_width="wrap_content"
14+
android:layout_height="wrap_content"
15+
app:layout_constraintTop_toTopOf="parent"
16+
app:layout_constraintBottom_toBottomOf="parent"
17+
app:layout_constraintStart_toStartOf="parent"
18+
app:layout_constraintEnd_toStartOf="@+id/text"
19+
app:layout_constraintHorizontal_chainStyle="packed"
20+
app:layout_constraintHorizontal_bias="0.0"
21+
/>
22+
23+
<com.ichi2.ui.FixedTextView
24+
android:id="@+id/text"
25+
android:layout_width="wrap_content"
26+
android:layout_height="wrap_content"
27+
android:layout_marginStart="16dp"
28+
app:layout_constraintTop_toTopOf="parent"
29+
app:layout_constraintBottom_toBottomOf="parent"
30+
app:layout_constraintStart_toEndOf="@id/loading_indicator"
31+
app:layout_constraintEnd_toEndOf="parent"
32+
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
33+
tools:text="Processing..."
34+
/>
35+
</androidx.constraintlayout.widget.ConstraintLayout>

0 commit comments

Comments
 (0)