search: improve ui
Improve the search UI by making it edge-to-edge and adding the liftOnScroll idiom. It does come with the caveat of walking on eggshells to get the liftOnScroll code working, but its okay. It may be improved in the future.
This commit is contained in:
parent
e142c17fca
commit
a4d2a8d48c
6 changed files with 53 additions and 16 deletions
|
@ -22,6 +22,7 @@ import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.annotation.MenuRes
|
import androidx.annotation.MenuRes
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
@ -30,6 +31,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.memberBinding
|
import org.oxycblt.auxio.ui.memberBinding
|
||||||
|
import org.oxycblt.auxio.util.applyEdge
|
||||||
import org.oxycblt.auxio.util.isLandscape
|
import org.oxycblt.auxio.util.isLandscape
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,6 +44,10 @@ abstract class DetailFragment : Fragment() {
|
||||||
protected val binding by memberBinding(FragmentDetailBinding::inflate)
|
protected val binding by memberBinding(FragmentDetailBinding::inflate)
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
binding.applyEdge { bars ->
|
||||||
|
binding.detailAppbar.updatePadding(top = bars.top)
|
||||||
|
}
|
||||||
|
|
||||||
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
|
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,6 @@ import androidx.navigation.fragment.findNavController
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import org.oxycblt.auxio.MainFragmentDirections
|
import org.oxycblt.auxio.MainFragmentDirections
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
@ -44,6 +43,7 @@ import org.oxycblt.auxio.ui.DisplayMode
|
||||||
import org.oxycblt.auxio.util.applyEdge
|
import org.oxycblt.auxio.util.applyEdge
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
|
import org.oxycblt.auxio.util.makeScrollingViewFade
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main "Launching Point" fragment of Auxio, allowing navigation to the detail
|
* The main "Launching Point" fragment of Auxio, allowing navigation to the detail
|
||||||
|
@ -74,16 +74,7 @@ class HomeFragment : Fragment() {
|
||||||
binding.homeAppbar.updatePadding(top = bars.top)
|
binding.homeAppbar.updatePadding(top = bars.top)
|
||||||
}
|
}
|
||||||
|
|
||||||
// There is basically no way to prevent the toolbar to draw under the status bar when
|
binding.homeAppbar.makeScrollingViewFade(binding.homeToolbar)
|
||||||
// it collapses, so do the next best thing and fade it out so it doesn't stick out like
|
|
||||||
// a sore thumb. As a side effect, this looks really cool.
|
|
||||||
binding.homeAppbar.addOnOffsetChangedListener(
|
|
||||||
AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
|
|
||||||
binding.homeToolbar.apply {
|
|
||||||
alpha = (height + verticalOffset) / height.toFloat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
binding.homeToolbar.setOnMenuItemClickListener { item ->
|
binding.homeToolbar.setOnMenuItemClickListener { item ->
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
|
|
|
@ -27,6 +27,7 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
import kotlinx.coroutines.NonDisposableHandle.parent
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
||||||
import org.oxycblt.auxio.music.BaseModel
|
import org.oxycblt.auxio.music.BaseModel
|
||||||
|
@ -105,6 +106,7 @@ class QueueFragment : Fragment() {
|
||||||
lastShuffle = isShuffling
|
lastShuffle = isShuffling
|
||||||
|
|
||||||
binding.queueRecycler.scrollToPosition(0)
|
binding.queueRecycler.scrollToPosition(0)
|
||||||
|
binding.queueAppbar.isLifted = false // Make sure lifted state changes.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import androidx.core.view.postDelayed
|
import androidx.core.view.postDelayed
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
@ -42,9 +43,11 @@ import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
|
import org.oxycblt.auxio.util.applyEdge
|
||||||
import org.oxycblt.auxio.util.applySpans
|
import org.oxycblt.auxio.util.applySpans
|
||||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
import org.oxycblt.auxio.util.getSystemServiceSafe
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
import org.oxycblt.auxio.util.makeScrollingViewFade
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Fragment] that allows for the searching of the entire music library.
|
* A [Fragment] that allows for the searching of the entire music library.
|
||||||
|
@ -79,6 +82,12 @@ class SearchFragment : Fragment() {
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
|
|
||||||
|
binding.applyEdge { bars ->
|
||||||
|
binding.searchAppbar.updatePadding(top = bars.top)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.searchAppbar.makeScrollingViewFade(binding.searchToolbar)
|
||||||
|
|
||||||
binding.searchToolbar.apply {
|
binding.searchToolbar.apply {
|
||||||
val itemId = when (searchModel.filterMode) {
|
val itemId = when (searchModel.filterMode) {
|
||||||
DisplayMode.SHOW_SONGS -> R.id.option_filter_songs
|
DisplayMode.SHOW_SONGS -> R.id.option_filter_songs
|
||||||
|
@ -131,18 +140,18 @@ class SearchFragment : Fragment() {
|
||||||
|
|
||||||
searchModel.searchResults.observe(viewLifecycleOwner) { results ->
|
searchModel.searchResults.observe(viewLifecycleOwner) { results ->
|
||||||
searchAdapter.submitList(results) {
|
searchAdapter.submitList(results) {
|
||||||
|
// We've just scrolled back to the top, reset the lifted state
|
||||||
|
// TODO: Maybe find a better way to keep scroll state when the search
|
||||||
|
// results didn't actually change.
|
||||||
binding.searchRecycler.scrollToPosition(0)
|
binding.searchRecycler.scrollToPosition(0)
|
||||||
|
binding.searchAppbar.isLifted = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (results.isEmpty()) {
|
if (results.isEmpty()) {
|
||||||
// If the data is empty, then the ability for the toolbar to collapse
|
|
||||||
// on scroll should be disabled.
|
|
||||||
binding.searchAppbar.setExpanded(true)
|
binding.searchAppbar.setExpanded(true)
|
||||||
binding.searchRecycler.visibility = View.GONE
|
binding.searchRecycler.visibility = View.GONE
|
||||||
toolbarParams.scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL
|
|
||||||
} else {
|
} else {
|
||||||
binding.searchRecycler.visibility = View.VISIBLE
|
binding.searchRecycler.visibility = View.VISIBLE
|
||||||
toolbarParams.scrollFlags = defaultParams
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,11 +32,13 @@ import androidx.annotation.AttrRes
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.ColorRes
|
import androidx.annotation.ColorRes
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -140,6 +142,33 @@ fun @receiver:AttrRes Int.resolveAttr(context: Context): Int {
|
||||||
return color.resolveColor(context)
|
return color.resolveColor(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make this [AppBarLayout] fade a scrolling [view] out when it collapses.
|
||||||
|
* This is mostly because I am unable to figure out how to get a collapsing view not
|
||||||
|
* to draw under the status bar in edge-to-edge mode.
|
||||||
|
*/
|
||||||
|
fun AppBarLayout.makeScrollingViewFade(view: View) {
|
||||||
|
addOnOffsetChangedListener(
|
||||||
|
AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
|
||||||
|
view.alpha = (view.height + verticalOffset) / view.height.toFloat()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force-update this [AppBarLayout]'s lifted state. This is useful when the dataset changes
|
||||||
|
* and the lifted state must be updated.
|
||||||
|
*/
|
||||||
|
fun AppBarLayout.updateLiftedState(recycler: RecyclerView) {
|
||||||
|
post {
|
||||||
|
val coordinator = (parent as CoordinatorLayout)
|
||||||
|
|
||||||
|
(layoutParams as CoordinatorLayout.LayoutParams).behavior?.onNestedPreScroll(
|
||||||
|
coordinator, this, recycler, 0, 0, IntArray(2), 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply edge-to-edge tweaks to the root of a [ViewBinding].
|
* Apply edge-to-edge tweaks to the root of a [ViewBinding].
|
||||||
* @param onApply What to do when the system bar insets are provided
|
* @param onApply What to do when the system bar insets are provided
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/colorSurface"
|
android:background="?attr/colorSurface"
|
||||||
android:elevation="@dimen/elevation_normal">
|
app:liftOnScroll="true">
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
<androidx.appcompat.widget.Toolbar
|
||||||
android:id="@+id/search_toolbar"
|
android:id="@+id/search_toolbar"
|
||||||
|
|
Loading…
Reference in a new issue