From 800a2991f33895bd2e1952b25adae83bd41b18ee Mon Sep 17 00:00:00 2001 From: maltaisn Date: Sun, 5 Jan 2020 13:08:49 -0500 Subject: [PATCH 1/3] Fixed dialogTitle setting having no effect --- lib/src/main/kotlin/com/maltaisn/icondialog/IconDialog.kt | 4 ++++ .../main/kotlin/com/maltaisn/icondialog/IconDialogContract.kt | 1 + .../kotlin/com/maltaisn/icondialog/IconDialogPresenter.kt | 1 + 3 files changed, 6 insertions(+) diff --git a/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialog.kt b/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialog.kt index 74542c6..ca56c5c 100644 --- a/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialog.kt +++ b/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialog.kt @@ -259,6 +259,10 @@ class IconDialog : DialogFragment(), IconDialogContract.View { titleTxv.isVisible = visible } + override fun updateTitle(titleRes: Int) { + titleTxv.text = getString(titleRes) + } + override fun setSearchBarVisible(visible: Boolean) { searchImv.isVisible = visible searchEdt.isVisible = visible diff --git a/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialogContract.kt b/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialogContract.kt index 26a33ff..9adf076 100644 --- a/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialogContract.kt +++ b/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialogContract.kt @@ -38,6 +38,7 @@ internal interface IconDialogContract { fun setSelectionResult(selected: List) fun setTitleVisible(visible: Boolean) + fun updateTitle(@StringRes titleRes: Int) fun setSearchBarVisible(visible: Boolean) fun setClearSearchBtnVisible(visible: Boolean) fun setClearBtnVisible(visible: Boolean) diff --git a/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialogPresenter.kt b/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialogPresenter.kt index 80803b6..aca491c 100644 --- a/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialogPresenter.kt +++ b/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialogPresenter.kt @@ -70,6 +70,7 @@ internal class IconDialogPresenter : IconDialogContract.Presenter { // Initialize view state view.apply { + updateTitle(settings.dialogTitle) setFooterVisible(settings.showSelectBtn) setSelectBtnEnabled(selectedIconIds.isNotEmpty()) setClearBtnVisible(settings.showSelectBtn && settings.showClearBtn && selectedIconIds.isNotEmpty()) From 7d4fd2cd4eac950d398cd7fdeca87b777b6d9c39 Mon Sep 17 00:00:00 2001 From: maltaisn Date: Sun, 5 Jan 2020 13:27:41 -0500 Subject: [PATCH 2/3] Added progress indicator in icon dialog and allowed icon pack to be null - Now icon dialog periodically tries to get icon pack and show progress if it's null. In the meantime, it's the app responsibility to load it. - Also added "fake loading time" check to test this in demo app, and removed existing loading indicator. --- .../maltaisn/icondialog/demo/MainFragment.kt | 38 ++----- app/src/main/res/layout/fragment_main.xml | 2 - app/src/main/res/layout/layout_options.xml | 25 ++--- app/src/main/res/values/strings.xml | 1 + .../com/maltaisn/icondialog/IconDialog.kt | 28 +++++- .../maltaisn/icondialog/IconDialogContract.kt | 8 +- .../icondialog/IconDialogPresenter.kt | 99 ++++++++++++++----- lib/src/main/res/layout/icd_dialog_icon.xml | 14 ++- lib/src/main/res/values/styles.xml | 1 - .../icondialog/IconDialogPresenterTest.kt | 7 ++ 10 files changed, 148 insertions(+), 75 deletions(-) diff --git a/app/src/main/kotlin/com/maltaisn/icondialog/demo/MainFragment.kt b/app/src/main/kotlin/com/maltaisn/icondialog/demo/MainFragment.kt index 7d7907e..575d012 100644 --- a/app/src/main/kotlin/com/maltaisn/icondialog/demo/MainFragment.kt +++ b/app/src/main/kotlin/com/maltaisn/icondialog/demo/MainFragment.kt @@ -28,8 +28,6 @@ import androidx.annotation.ArrayRes import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.content.res.AppCompatResources import androidx.core.graphics.drawable.DrawableCompat -import androidx.core.view.isInvisible -import androidx.core.view.isVisible import androidx.core.widget.addTextChangedListener import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager @@ -55,12 +53,10 @@ class MainFragment : Fragment(), IconDialog.Callback { private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) private var packLoadJob: Job? = null - private var progressVisbJob: Job? = null private lateinit var iconDialog: IconDialog private lateinit var iconsAdapter: IconsAdapter - private lateinit var iconPackPb: ProgressBar - private lateinit var fab: FloatingActionButton + private lateinit var fakeLoadingCheck: CheckBox private lateinit var iconPackLoader: IconPackLoader @@ -84,7 +80,6 @@ class MainFragment : Fragment(), IconDialog.Callback { setupDropdown(view.findViewById(R.id.dropdown_icon_pack), R.array.icon_packs) { changeIconPack(it) } - iconPackPb = view.findViewById(R.id.pb_icon_pack) var titleVisbIndex = 2 setupDropdown(view.findViewById(R.id.dropdown_title_visibility), R.array.title_visibility) { @@ -142,16 +137,15 @@ class MainFragment : Fragment(), IconDialog.Callback { }) } + fakeLoadingCheck = view.findViewById(R.id.chk_fake_loading) + val iconsRcv: RecyclerView = view.findViewById(R.id.rcv_icon_list) iconsAdapter = IconsAdapter() iconsRcv.adapter = iconsAdapter iconsRcv.layoutManager = LinearLayoutManager(context) - fab = view.findViewById(R.id.fab) - fab.isVisible = (app.iconPack != null) + val fab: FloatingActionButton = view.findViewById(R.id.fab) fab.setOnClickListener { - if (app.iconPack == null) return@setOnClickListener - // Create new settings and set them. iconDialog.settings = IconDialogSettings { this.iconFilter = iconFilter @@ -241,29 +235,17 @@ class MainFragment : Fragment(), IconDialog.Callback { // Load drawables pack.loadDrawables(iconPackLoader.drawableLoader) + if (fakeLoadingCheck.isChecked) { + delay(4000) + } + pack } - iconPackPb.isInvisible = true - fab.show() - updateSelectedIcons() - progressVisbJob?.cancel() - progressVisbJob = null packLoadJob = null } - - // Start new job to show progress after some delay. - progressVisbJob?.cancel() - progressVisbJob = coroutineScope.launch(Dispatchers.Default) { - delay(250) - withContext(Dispatchers.Main) { - iconPackPb.isInvisible = false - fab.hide() - } - progressVisbJob = null - } } private fun changeIconPack(index: Int) { @@ -293,8 +275,8 @@ class MainFragment : Fragment(), IconDialog.Callback { } // Called by icon dialog to get the icon pack. - override val iconDialogIconPack: IconPack - get() = app.iconPack!! + override val iconDialogIconPack: IconPack? + get() = app.iconPack override fun onIconDialogIconsSelected(dialog: IconDialog, icons: List) { // Called by icon dialog when icons were selected. diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index e43a928..a317021 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -105,9 +105,7 @@ android:layout_margin="16dp" android:layout_gravity="bottom|end" android:src="@drawable/ic_edit" - android:visibility="gone" app:fabSize="normal" - tools:visibility="visible" /> diff --git a/app/src/main/res/layout/layout_options.xml b/app/src/main/res/layout/layout_options.xml index 46ae5b3..f432e60 100644 --- a/app/src/main/res/layout/layout_options.xml +++ b/app/src/main/res/layout/layout_options.xml @@ -63,21 +63,6 @@ /> - - + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9ff64bb..a367d3d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,6 +29,7 @@ Show select button Show clear button Dark theme + Fake loading time Default Font Awesome diff --git a/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialog.kt b/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialog.kt index ca56c5c..1437cf8 100644 --- a/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialog.kt +++ b/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialog.kt @@ -58,7 +58,7 @@ class IconDialog : DialogFragment(), IconDialogContract.View { /** The settings used for the dialog. */ override lateinit var settings: IconDialogSettings - override val iconPack: IconPack + override val iconPack: IconPack? get() = callback.iconDialogIconPack /** @@ -77,6 +77,7 @@ class IconDialog : DialogFragment(), IconDialogContract.View { private lateinit var searchEdt: EditText private lateinit var searchClearBtn: ImageView private lateinit var noResultTxv: TextView + private lateinit var progressBar: ProgressBar private lateinit var footerDiv: View private lateinit var selectBtn: Button private lateinit var cancelBtn: Button @@ -86,6 +87,9 @@ class IconDialog : DialogFragment(), IconDialogContract.View { private lateinit var listAdapter: IconAdapter private lateinit var listLayout: IconLayoutManager + private lateinit var progressHandler: Handler + private var progressCallback: Runnable? = null + private lateinit var searchHandler: Handler private val searchCallback = Runnable { presenter?.onSearchQueryEntered(searchEdt.text.toString()) @@ -117,6 +121,7 @@ class IconDialog : DialogFragment(), IconDialogContract.View { iconColorSelected = getColor(it.getResourceId(R.styleable.IconDialog_icdSelectedIconColor, 0)) } + progressHandler = Handler() searchHandler = Handler() // Create the dialog view @@ -127,6 +132,7 @@ class IconDialog : DialogFragment(), IconDialogContract.View { searchEdt = dialogView.findViewById(R.id.icd_edt_search) searchClearBtn = dialogView.findViewById(R.id.icd_imv_clear_search) noResultTxv = dialogView.findViewById(R.id.icd_txv_no_result) + progressBar = dialogView.findViewById(R.id.icd_progress_bar) // Search searchEdt.addTextChangedListener { @@ -235,6 +241,17 @@ class IconDialog : DialogFragment(), IconDialogContract.View { presenter?.onDialogCancelled() } + override fun postDelayed(delay: Long, action: () -> Unit) { + val callback = Runnable(action) + progressHandler.post(callback) + progressCallback = callback + } + + override fun cancelCallbacks() { + progressHandler.removeCallbacks(progressCallback ?: return) + progressCallback = null + } + override fun exit() { dismiss() } @@ -281,6 +298,10 @@ class IconDialog : DialogFragment(), IconDialogContract.View { noResultTxv.isVisible = visible } + override fun setProgressBarVisible(visible: Boolean) { + progressBar.isVisible = visible + } + override fun setFooterVisible(visible: Boolean) { clearBtn.isVisible = visible cancelBtn.isVisible = visible @@ -408,8 +429,11 @@ class IconDialog : DialogFragment(), IconDialogContract.View { /** * The icon pack to be displayed by the dialog. * All icon drawables in the pack must have been loaded, or they won't be displayed. + * + * If `null` is returned, the icon dialog will periodically try to get the icon + * pack while showing a progress indicator, until it no longer returns `null`. */ - val iconDialogIconPack: IconPack + val iconDialogIconPack: IconPack? /** * Called when icons are selected and user confirms the selection. diff --git a/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialogContract.kt b/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialogContract.kt index 9adf076..16ce8c4 100644 --- a/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialogContract.kt +++ b/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialogContract.kt @@ -17,6 +17,7 @@ package com.maltaisn.icondialog import android.os.Bundle +import androidx.annotation.StringRes import com.maltaisn.icondialog.data.Category import com.maltaisn.icondialog.data.Icon import com.maltaisn.icondialog.pack.IconPack @@ -27,21 +28,24 @@ internal interface IconDialogContract { interface View { val settings: IconDialogSettings - val iconPack: IconPack + val iconPack: IconPack? val selectedIconIds: List val locale: Locale + fun postDelayed(delay: Long, action: () -> Unit) + fun cancelCallbacks() + fun exit() fun hideKeyboard() fun setCancelResult() fun setSelectionResult(selected: List) - fun setTitleVisible(visible: Boolean) fun updateTitle(@StringRes titleRes: Int) fun setSearchBarVisible(visible: Boolean) fun setClearSearchBtnVisible(visible: Boolean) fun setClearBtnVisible(visible: Boolean) + fun setProgressBarVisible(visible: Boolean) fun setNoResultLabelVisible(visible: Boolean) fun setFooterVisible(visible: Boolean) fun removeLayoutPadding() diff --git a/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialogPresenter.kt b/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialogPresenter.kt index aca491c..103a688 100644 --- a/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialogPresenter.kt +++ b/lib/src/main/kotlin/com/maltaisn/icondialog/IconDialogPresenter.kt @@ -31,7 +31,7 @@ internal class IconDialogPresenter : IconDialogContract.Presenter { private val settings: IconDialogSettings get() = view!!.settings - private val iconPack: IconPack + private val iconPack: IconPack? get() = view!!.iconPack private val listItems = mutableListOf() @@ -47,6 +47,43 @@ internal class IconDialogPresenter : IconDialogContract.Presenter { selectedIconIds.clear() searchQuery = "" + if (iconPack == null) { + // Set view state to show progress with no action available. + view.apply { + updateTitle(settings.dialogTitle) + setFooterVisible(settings.showSelectBtn) + setProgressBarVisible(true) + setSelectBtnEnabled(false) + setClearBtnVisible(false) + setNoResultLabelVisible(false) + notifyAllIconsChanged() + } + updateSearchAndTitleVisibility() + + // Wait until icon pack is not null. + waitForIconPack(view, state) + + } else { + // Initialize view state. + initialize(view, state) + } + } + + /** Periodically query icon pack and if it's not null, initialize view. */ + private fun waitForIconPack(view: View, state: Bundle?) { + view.postDelayed(ICON_PACK_CHECK_DELAY) { + if (iconPack != null) { + initialize(view, state) + } else { + waitForIconPack(view, state) + } + } + } + + /** Initialize the presenter and the view. */ + private fun initialize(view: View, state: Bundle?) { + val iconPack = iconPack ?: error("Icon pack must be initialized.") + if (state == null) { // Check if initial selection is valid. val selection = view.selectedIconIds.toMutableList() @@ -69,29 +106,18 @@ internal class IconDialogPresenter : IconDialogContract.Presenter { } // Initialize view state - view.apply { - updateTitle(settings.dialogTitle) - setFooterVisible(settings.showSelectBtn) - setSelectBtnEnabled(selectedIconIds.isNotEmpty()) - setClearBtnVisible(settings.showSelectBtn && settings.showClearBtn && selectedIconIds.isNotEmpty()) - setNoResultLabelVisible(false) - - val searchVisible = settings.searchVisibility == SearchVisibility.ALWAYS - && iconPack.locales.isNotEmpty() - || settings.searchVisibility == SearchVisibility.IF_LANGUAGE_AVAILABLE - && view.locale.language in iconPack.locales.map { it.language } - val titleVisible = settings.titleVisibility == TitleVisibility.ALWAYS - || settings.titleVisibility == TitleVisibility.IF_SEARCH_HIDDEN && !searchVisible - setSearchBarVisible(searchVisible) - setTitleVisible(titleVisible) - if (!searchVisible && !titleVisible) { - removeLayoutPadding() - } - - setClearSearchBtnVisible(searchQuery.isNotEmpty()) + updateSearchAndTitleVisibility() + view.let { + it.updateTitle(settings.dialogTitle) + it.setFooterVisible(settings.showSelectBtn) + it.setSelectBtnEnabled(selectedIconIds.isNotEmpty()) + it.setClearBtnVisible(settings.showSelectBtn && settings.showClearBtn && selectedIconIds.isNotEmpty()) + it.setProgressBarVisible(false) + it.setNoResultLabelVisible(false) + it.setClearSearchBtnVisible(searchQuery.isNotEmpty()) if (settings.headersVisibility == HeadersVisibility.STICKY) { - addStickyHeaderDecoration() + it.addStickyHeaderDecoration() } } @@ -107,6 +133,7 @@ internal class IconDialogPresenter : IconDialogContract.Presenter { } override fun detach() { + view?.cancelCallbacks() view = null } @@ -139,8 +166,11 @@ internal class IconDialogPresenter : IconDialogContract.Presenter { } override fun onDialogCancelled() { - view?.setCancelResult() - view?.exit() + view?.let { + it.cancelCallbacks() + it.setCancelResult() + it.exit() + } } override fun onSearchQueryChanged(query: String) { @@ -279,6 +309,7 @@ internal class IconDialogPresenter : IconDialogContract.Presenter { } private fun confirmSelection() { + val iconPack = iconPack ?: return view?.setSelectionResult(selectedIconIds.map { iconPack.getIcon(it)!! }) view?.exit() } @@ -295,6 +326,7 @@ internal class IconDialogPresenter : IconDialogContract.Presenter { */ private fun updateList() { // Get icons matching search + val iconPack = iconPack ?: return val icons = settings.iconFilter.queryIcons(iconPack, searchQuery) // Sort icons by category, then use icon filter for secondary sorting rules. @@ -329,6 +361,23 @@ internal class IconDialogPresenter : IconDialogContract.Presenter { view?.setNoResultLabelVisible(listItems.isEmpty()) } + private fun updateSearchAndTitleVisibility() { + val view = view ?: return + val locales = iconPack?.locales ?: emptyList() + + val searchVisible = settings.searchVisibility == SearchVisibility.ALWAYS + && locales.isNotEmpty() + || settings.searchVisibility == SearchVisibility.IF_LANGUAGE_AVAILABLE + && view.locale.language in locales.map { it.language } + val titleVisible = settings.titleVisibility == TitleVisibility.ALWAYS + || settings.titleVisibility == TitleVisibility.IF_SEARCH_HIDDEN && !searchVisible + view.setSearchBarVisible(searchVisible) + view.setTitleVisible(titleVisible) + if (!searchVisible && !titleVisible && iconPack != null) { + view.removeLayoutPadding() + } + } + private interface Item { val id: Long } @@ -347,6 +396,8 @@ internal class IconDialogPresenter : IconDialogContract.Presenter { internal const val ITEM_TYPE_ICON = 0 internal const val ITEM_TYPE_HEADER = 1 internal const val ITEM_TYPE_STICKY_HEADER = 2 + + private const val ICON_PACK_CHECK_DELAY = 100L } } diff --git a/lib/src/main/res/layout/icd_dialog_icon.xml b/lib/src/main/res/layout/icd_dialog_icon.xml index b0b8881..4ae4644 100644 --- a/lib/src/main/res/layout/icd_dialog_icon.xml +++ b/lib/src/main/res/layout/icd_dialog_icon.xml @@ -33,10 +33,10 @@ android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" + app:layout_constraintBottom_toTopOf="@id/icd_edt_search" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toTopOf="@id/icd_edt_search" app:layout_constraintVertical_chainStyle="spread_inside" /> @@ -112,6 +112,18 @@ tools:spanCount="6" /> + + 8dp 4dp 4dp - @string/icd_title diff --git a/lib/src/test/kotlin/com/maltaisn/icondialog/IconDialogPresenterTest.kt b/lib/src/test/kotlin/com/maltaisn/icondialog/IconDialogPresenterTest.kt index 23b65fd..e9e637c 100644 --- a/lib/src/test/kotlin/com/maltaisn/icondialog/IconDialogPresenterTest.kt +++ b/lib/src/test/kotlin/com/maltaisn/icondialog/IconDialogPresenterTest.kt @@ -65,6 +65,13 @@ internal class IconDialogPresenterTest { // [5] Category 2 header // [6] Icon 3 + @Test + fun `attach icon pack null`() { + whenever(view.iconPack).doReturn(null) + presenter.attach(view, null) + verify(view).setProgressBarVisible(true) + verify(view).postDelayed(any(), any()) + } @Test(expected = IllegalStateException::class) fun `attach wrong icon id error`() { From f0d4090a37ef4eacbdedf30f41b70113ddc4efb0 Mon Sep 17 00:00:00 2001 From: maltaisn Date: Sun, 5 Jan 2020 13:49:45 -0500 Subject: [PATCH 3/3] Updated changelog and version --- CHANGELOG.md | 4 ++++ gradle.properties | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bcd5c8..98dee68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### v3.1.0 +- Added progress bar in dialog for when icon pack is not loaded yet. +- Fixed `IconDialogSettings.dialogTitle` having no effect. + # v3.0.0 - Complete rewrite in Kotlin with MVP architecture. - Now based on Google's Material Components with out of the box support for dark theme. diff --git a/gradle.properties b/gradle.properties index 47a0219..409dab5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Library -libVersion=3.0.0 +libVersion=3.0.1 # Icon packs iconPackDefaultVersion=1.0.0