search: improve keyboard management

Remove the janky requestFocus/clearFocus called on SearchFragment
and replace them with InputMethodManager calls. This is generally
more user friendly, especially when returning to search from
navigation.
This commit is contained in:
OxygenCobalt 2021-08-22 17:59:07 -06:00
parent 6c5a68c929
commit 19e2fcbb90
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
26 changed files with 85 additions and 110 deletions

View file

@ -35,6 +35,7 @@ import org.oxycblt.auxio.settings.SettingsManager
/** /**
* The single [AppCompatActivity] for Auxio. * The single [AppCompatActivity] for Auxio.
* TODO: Improve edge-to-edge everywhere
*/ */
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private val playbackModel: PlaybackViewModel by viewModels() private val playbackModel: PlaybackViewModel by viewModels()

View file

@ -18,12 +18,14 @@
package org.oxycblt.auxio.detail package org.oxycblt.auxio.detail
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearSmoothScroller
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.canScroll import org.oxycblt.auxio.canScroll
import org.oxycblt.auxio.detail.adapters.AlbumDetailAdapter import org.oxycblt.auxio.detail.adapters.AlbumDetailAdapter
@ -34,7 +36,6 @@ import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.recycler.CenterSmoothScroller
import org.oxycblt.auxio.showToast import org.oxycblt.auxio.showToast
import org.oxycblt.auxio.ui.ActionMenu import org.oxycblt.auxio.ui.ActionMenu
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
@ -136,7 +137,8 @@ class AlbumDetailFragment : DetailFragment() {
) )
} }
else -> {} else -> {
}
} }
} }
@ -187,4 +189,27 @@ class AlbumDetailFragment : DetailFragment() {
} }
} }
} }
/**
* [LinearSmoothScroller] subclass that centers the item on the screen instead of
* snapping to the top or bottom.
*/
private class CenterSmoothScroller(
context: Context,
target: Int
) : LinearSmoothScroller(context) {
init {
targetPosition = target
}
override fun calculateDtToFit(
viewStart: Int,
viewEnd: Int,
boxStart: Int,
boxEnd: Int,
snapPreference: Int
): Int {
return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2)
}
}
} }

View file

@ -33,9 +33,8 @@ import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.recycler.BaseViewHolder
import org.oxycblt.auxio.recycler.DiffCallback import org.oxycblt.auxio.recycler.DiffCallback
import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder
import org.oxycblt.auxio.recycler.viewholders.Highlightable
import org.oxycblt.auxio.setTextColorResource import org.oxycblt.auxio.setTextColorResource
/** /**

View file

@ -37,9 +37,8 @@ import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Header import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.recycler.BaseViewHolder
import org.oxycblt.auxio.recycler.DiffCallback import org.oxycblt.auxio.recycler.DiffCallback
import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder
import org.oxycblt.auxio.recycler.viewholders.Highlightable
import org.oxycblt.auxio.setTextColorResource import org.oxycblt.auxio.setTextColorResource
/** /**

View file

@ -33,9 +33,8 @@ import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.recycler.BaseViewHolder
import org.oxycblt.auxio.recycler.DiffCallback import org.oxycblt.auxio.recycler.DiffCallback
import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder
import org.oxycblt.auxio.recycler.viewholders.Highlightable
import org.oxycblt.auxio.setTextColorResource import org.oxycblt.auxio.setTextColorResource
/** /**

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.recycler.viewholders package org.oxycblt.auxio.detail.adapters
/** /**
* Interface that allows the highlighting of certain ViewHolders * Interface that allows the highlighting of certain ViewHolders

View file

@ -27,10 +27,10 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.recycler.viewholders.AlbumViewHolder import org.oxycblt.auxio.recycler.AlbumViewHolder
import org.oxycblt.auxio.recycler.viewholders.ArtistViewHolder import org.oxycblt.auxio.recycler.ArtistViewHolder
import org.oxycblt.auxio.recycler.viewholders.GenreViewHolder import org.oxycblt.auxio.recycler.GenreViewHolder
import org.oxycblt.auxio.recycler.viewholders.SongViewHolder import org.oxycblt.auxio.recycler.SongViewHolder
class HomeAdapter( class HomeAdapter(
private val doOnClick: (data: BaseModel) -> Unit, private val doOnClick: (data: BaseModel) -> Unit,

View file

@ -47,8 +47,7 @@ import org.oxycblt.auxio.recycler.DisplayMode
* views for each respective fragment. * views for each respective fragment.
* TODO: Re-add sorting (but new and improved) * TODO: Re-add sorting (but new and improved)
* TODO: Add lift-on-scroll eventually [when I can file a bug report or hack it into working] * TODO: Add lift-on-scroll eventually [when I can file a bug report or hack it into working]
* FIXME: Fix issue where for the toolbar will default to its collapsed state for basically no * FIXME: Keep the collapsed state in the ViewModel so we can make sure it stays consistent
* reason
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class HomeFragment : Fragment() { class HomeFragment : Fragment() {

View file

@ -39,6 +39,8 @@ import org.oxycblt.auxio.ui.newMenu
/* /*
* Fragment that contains a list of items specified by a [DisplayMode]. * Fragment that contains a list of items specified by a [DisplayMode].
* TODO: Fix crash from not saving the display mode. This is getting really tiring.
* Just keep the index for the tab we're working with and then just use that w/homeModel.
*/ */
class HomeListFragment : Fragment() { class HomeListFragment : Fragment() {
private val homeModel: HomeViewModel by viewModels() private val homeModel: HomeViewModel by viewModels()

View file

@ -33,12 +33,6 @@ import org.oxycblt.auxio.logD
/** /**
* Class that loads/constructs [Genre]s, [Artist]s, [Album]s, and [Song] objects from the filesystem * Class that loads/constructs [Genre]s, [Artist]s, [Album]s, and [Song] objects from the filesystem
* @author OxygenCobalt * @author OxygenCobalt
*
* FIXME: Here's a catalog of problems that I already know about with this abomination
* - All loading is done at startup [Not efficent for large libraries, would require massive arch retooling to fix]
* - Does not support the album artist tag [Nothing I can do that doesn't involve rolling my own loader]
* - Genre system is a bottleneck [See Above]
* Blame MediaStore, loading anything on this platform is a nightmare.
*/ */
class MusicLoader(private val context: Context) { class MusicLoader(private val context: Context) {
var genres = mutableListOf<Genre>() var genres = mutableListOf<Genre>()

View file

@ -34,9 +34,9 @@ import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Header import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.recycler.BaseViewHolder
import org.oxycblt.auxio.recycler.DiffCallback import org.oxycblt.auxio.recycler.DiffCallback
import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder import org.oxycblt.auxio.recycler.HeaderViewHolder
import org.oxycblt.auxio.recycler.viewholders.HeaderViewHolder
/** /**
* The single adapter for both the Next Queue and the User Queue. * The single adapter for both the Next Queue and the User Queue.

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.recycler.viewholders package org.oxycblt.auxio.recycler
import android.view.View import android.view.View
import androidx.databinding.ViewDataBinding import androidx.databinding.ViewDataBinding

View file

@ -1,43 +0,0 @@
/*
* Copyright (c) 2021 Auxio Project
* CenterSmoothScroller.kt is part of Auxio.
*
* 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.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.recycler
import android.content.Context
import androidx.recyclerview.widget.LinearSmoothScroller
/**
* [LinearSmoothScroller] subclass that centers the item on the screen instead of snapping to the
* top or bottom.
* @author OxygenCobalt
*/
class CenterSmoothScroller(context: Context, target: Int) : LinearSmoothScroller(context) {
init {
targetPosition = target
}
override fun calculateDtToFit(
viewStart: Int,
viewEnd: Int,
boxStart: Int,
boxEnd: Int,
snapPreference: Int
): Int {
return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2)
}
}

View file

@ -18,25 +18,22 @@
package org.oxycblt.auxio.recycler package org.oxycblt.auxio.recycler
import androidx.annotation.DrawableRes
import org.oxycblt.auxio.R
/** /**
* An enum for determining what items to show in a given list. * An enum for determining what items to show in a given list.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
enum class DisplayMode(@DrawableRes val iconRes: Int) { enum class DisplayMode {
SHOW_GENRES(R.drawable.ic_genre), SHOW_GENRES,
SHOW_ARTISTS(R.drawable.ic_artist), SHOW_ARTISTS,
SHOW_ALBUMS(R.drawable.ic_album), SHOW_ALBUMS,
SHOW_SONGS(R.drawable.ic_song); SHOW_SONGS;
companion object { companion object {
const val CONST_SHOW_ALL = 0xA107 private const val CONST_SHOW_ALL = 0xA107
const val CONST_SHOW_GENRES = 0xA108 private const val CONST_SHOW_GENRES = 0xA108
const val CONST_SHOW_ARTISTS = 0xA109 private const val CONST_SHOW_ARTISTS = 0xA109
const val CONST_SHOW_ALBUMS = 0xA10A private const val CONST_SHOW_ALBUMS = 0xA10A
const val CONST_SHOW_SONGS = 0xA10B private const val CONST_SHOW_SONGS = 0xA10B
fun toSearchInt(value: DisplayMode?): Int { fun toSearchInt(value: DisplayMode?): Int {
return when (value) { return when (value) {

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.recycler.viewholders package org.oxycblt.auxio.recycler
import android.content.Context import android.content.Context
import android.view.View import android.view.View

View file

@ -28,12 +28,12 @@ import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Header import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.recycler.AlbumViewHolder
import org.oxycblt.auxio.recycler.ArtistViewHolder
import org.oxycblt.auxio.recycler.DiffCallback import org.oxycblt.auxio.recycler.DiffCallback
import org.oxycblt.auxio.recycler.viewholders.AlbumViewHolder import org.oxycblt.auxio.recycler.GenreViewHolder
import org.oxycblt.auxio.recycler.viewholders.ArtistViewHolder import org.oxycblt.auxio.recycler.HeaderViewHolder
import org.oxycblt.auxio.recycler.viewholders.GenreViewHolder import org.oxycblt.auxio.recycler.SongViewHolder
import org.oxycblt.auxio.recycler.viewholders.HeaderViewHolder
import org.oxycblt.auxio.recycler.viewholders.SongViewHolder
/** /**
* A Multi-ViewHolder adapter that displays the results of a search query. * A Multi-ViewHolder adapter that displays the results of a search query.

View file

@ -52,7 +52,7 @@ import org.oxycblt.auxio.ui.newMenu
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class SearchFragment : Fragment() { class SearchFragment : Fragment() {
// SearchViewModel only scoped to this Fragment // SearchViewModel is only scoped to this Fragment
private val searchModel: SearchViewModel by viewModels() private val searchModel: SearchViewModel by viewModels()
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
private val detailModel: DetailViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels()
@ -64,10 +64,15 @@ class SearchFragment : Fragment() {
): View { ): View {
val binding = FragmentSearchBinding.inflate(inflater) val binding = FragmentSearchBinding.inflate(inflater)
val searchAdapter = SearchAdapter(::onItemSelection, ::newMenu)
val imm = requireContext().getSystemServiceSafe(InputMethodManager::class) val imm = requireContext().getSystemServiceSafe(InputMethodManager::class)
val searchAdapter = SearchAdapter(
doOnClick = { item ->
onItemSelection(item, imm)
},
::newMenu
)
val toolbarParams = binding.searchToolbar.layoutParams as AppBarLayout.LayoutParams val toolbarParams = binding.searchToolbar.layoutParams as AppBarLayout.LayoutParams
val defaultParams = toolbarParams.scrollFlags val defaultParams = toolbarParams.scrollFlags
@ -87,7 +92,7 @@ class SearchFragment : Fragment() {
menu.findItem(itemId).isChecked = true menu.findItem(itemId).isChecked = true
setNavigationOnClickListener { setNavigationOnClickListener {
requireView().rootView.clearFocus() imm.hide()
findNavController().navigateUp() findNavController().navigateUp()
} }
@ -158,6 +163,8 @@ class SearchFragment : Fragment() {
else -> return@observe else -> return@observe
} }
) )
imm.hide()
} }
logD("Fragment created.") logD("Fragment created.")
@ -171,19 +178,22 @@ class SearchFragment : Fragment() {
searchModel.setNavigating(false) searchModel.setNavigating(false)
} }
private fun InputMethodManager.hide() {
hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_IMPLICIT_ONLY)
}
/** /**
* Function that handles when an [item] is selected. * Function that handles when an [item] is selected.
* Handles all datatypes that are selectable. * Handles all datatypes that are selectable.
*/ */
private fun onItemSelection(item: BaseModel) { private fun onItemSelection(item: BaseModel, imm: InputMethodManager) {
if (item is Song) { if (item is Song) {
playbackModel.playSong(item) playbackModel.playSong(item)
return return
} }
// Get rid of the keyboard if we are navigating imm.hide()
requireView().rootView.clearFocus()
if (!searchModel.isNavigating) { if (!searchModel.isNavigating) {
searchModel.setNavigating(true) searchModel.setNavigating(true)

View file

@ -2,7 +2,7 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context=".recycler.viewholders.HeaderViewHolder"> tools:context=".recycler.HeaderViewHolder">
<data> <data>

View file

@ -2,7 +2,7 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context=".recycler.viewholders.AlbumViewHolder"> tools:context=".recycler.AlbumViewHolder">
<data> <data>

View file

@ -16,7 +16,7 @@
<TextView <TextView
android:id="@+id/song_track" android:id="@+id/song_track"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:minWidth="@dimen/width_track_number" android:minWidth="@dimen/size_track_number"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@{@string/desc_track_number(song.track)}" android:contentDescription="@{@string/desc_track_number(song.track)}"
android:gravity="center" android:gravity="center"

View file

@ -2,7 +2,7 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context=".recycler.viewholders.ArtistViewHolder"> tools:context=".recycler.ArtistViewHolder">
<data> <data>

View file

@ -2,7 +2,7 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context=".recycler.viewholders.SongViewHolder"> tools:context=".recycler.SongViewHolder">
<data> <data>

View file

@ -2,7 +2,7 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context=".recycler.viewholders.GenreViewHolder"> tools:context=".recycler.GenreViewHolder">
<data> <data>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context=".recycler.viewholders.HeaderViewHolder"> tools:context=".recycler.HeaderViewHolder">
<data> <data>

View file

@ -2,7 +2,7 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context=".recycler.viewholders.SongViewHolder"> tools:context=".recycler.SongViewHolder">
<data> <data>

View file

@ -1,10 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!--
TODO: Redo these dimens to line up with the 8dp grid. Tiny spacing can be used for
internal elements, but micro spacing needs to be phased out.
-->
<!-- Spacing Namespace | Dimens for padding/margin attributes --> <!-- Spacing Namespace | Dimens for padding/margin attributes -->
<dimen name="spacing_small">8dp</dimen> <dimen name="spacing_small">8dp</dimen>
<dimen name="spacing_medium">16dp</dimen> <dimen name="spacing_medium">16dp</dimen>
@ -13,10 +8,6 @@
<dimen name="spacing_mid_huge">48dp</dimen> <dimen name="spacing_mid_huge">48dp</dimen>
<dimen name="spacing_insane">128dp</dimen> <dimen name="spacing_insane">128dp</dimen>
<!-- Width Namespace | Width for UI elements -->
<dimen name="width_track_number">32dp</dimen>
<dimen name="width_fast_scroll">20dp</dimen>
<!-- Size Namespace | Width & Heights for UI elements --> <!-- Size Namespace | Width & Heights for UI elements -->
<dimen name="size_btn_small">48dp</dimen> <dimen name="size_btn_small">48dp</dimen>
<dimen name="size_btn_large">64dp</dimen> <dimen name="size_btn_large">64dp</dimen>
@ -32,6 +23,8 @@
<dimen name="size_small_unb_ripple">20dp</dimen> <dimen name="size_small_unb_ripple">20dp</dimen>
<dimen name="size_unb_ripple">24dp</dimen> <dimen name="size_unb_ripple">24dp</dimen>
<dimen name="size_track_number">32dp</dimen>
<!-- Text Size Namespace | Text Sizes --> <!-- Text Size Namespace | Text Sizes -->
<dimen name="text_size_small">16sp</dimen> <dimen name="text_size_small">16sp</dimen>
<dimen name="text_size_medium">18sp</dimen> <dimen name="text_size_medium">18sp</dimen>