ui: create dedicated playback bar layout
Create a dedicated playback bar layout. This replaces the old janky observer system with something that handles state better and is just more elegant.
This commit is contained in:
parent
4f4f6654c0
commit
68782fadac
33 changed files with 544 additions and 278 deletions
|
@ -46,7 +46,7 @@ I primarily built Auxio for myself, but you can use it too, I guess.
|
||||||
- Search Functionality
|
- Search Functionality
|
||||||
- Audio Focus / Headset Management
|
- Audio Focus / Headset Management
|
||||||
- No internet connectivity whatsoever
|
- No internet connectivity whatsoever
|
||||||
- No rounded album corners
|
- No rounded album covers
|
||||||
|
|
||||||
## To possibly come in the future:
|
## To possibly come in the future:
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ dependencies {
|
||||||
// --- SUPPORT ---
|
// --- SUPPORT ---
|
||||||
|
|
||||||
// General
|
// General
|
||||||
implementation "androidx.core:core-ktx:1.6.0"
|
implementation "androidx.core:core-ktx:1.7.0"
|
||||||
implementation "androidx.activity:activity-ktx:1.3.1"
|
implementation "androidx.activity:activity-ktx:1.3.1"
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.3.6'
|
implementation 'androidx.fragment:fragment-ktx:1.3.6'
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ dependencies {
|
||||||
implementation "androidx.viewpager2:viewpager2:1.1.0-beta01"
|
implementation "androidx.viewpager2:viewpager2:1.1.0-beta01"
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
def lifecycle_version = "2.3.1"
|
def lifecycle_version = "2.4.0"
|
||||||
implementation "androidx.lifecycle:lifecycle-common:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-common:$lifecycle_version"
|
||||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||||
|
|
|
@ -26,8 +26,6 @@ import android.view.ViewGroup
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.core.view.isVisible
|
|
||||||
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 com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
@ -35,8 +33,6 @@ import org.oxycblt.auxio.databinding.FragmentMainBinding
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.util.applyEdge
|
|
||||||
import org.oxycblt.auxio.util.applyMaterialDrawable
|
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -65,25 +61,26 @@ class MainFragment : Fragment() {
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
|
|
||||||
binding.applyEdge { bars ->
|
// --- VIEWMODEL SETUP ---
|
||||||
binding.mainPlayback.updatePadding(bottom = bars.bottom)
|
|
||||||
|
if (playbackModel.song.value != null) {
|
||||||
|
binding.mainBarLayout.showBar()
|
||||||
|
} else {
|
||||||
|
binding.mainBarLayout.hideBar()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.mainPlayback.applyMaterialDrawable()
|
playbackModel.song.observe(viewLifecycleOwner) { song ->
|
||||||
|
if (song != null) {
|
||||||
// --- VIEWMODEL SETUP ---
|
binding.mainBarLayout.showBar()
|
||||||
|
} else {
|
||||||
|
binding.mainBarLayout.hideBar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize music loading. Unlike MainFragment, we can not only do this here on startup
|
// Initialize music loading. Unlike MainFragment, we can not only do this here on startup
|
||||||
// but also show a SnackBar in a reasonable place in this fragment.
|
// but also show a SnackBar in a reasonable place in this fragment.
|
||||||
musicModel.loadMusic(requireContext())
|
musicModel.loadMusic(requireContext())
|
||||||
|
|
||||||
// Change CompactPlaybackFragment's visibility here so that an animation occurs.
|
|
||||||
binding.mainPlayback.isVisible = playbackModel.song.value != null
|
|
||||||
|
|
||||||
playbackModel.song.observe(viewLifecycleOwner) { song ->
|
|
||||||
binding.mainPlayback.isVisible = song != null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle the music loader response.
|
// Handle the music loader response.
|
||||||
musicModel.loaderResponse.observe(viewLifecycleOwner) { response ->
|
musicModel.loaderResponse.observe(viewLifecycleOwner) { response ->
|
||||||
// Handle the loader response.
|
// Handle the loader response.
|
||||||
|
|
|
@ -24,7 +24,6 @@ import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.annotation.MenuRes
|
import androidx.annotation.MenuRes
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.core.view.forEach
|
import androidx.core.view.forEach
|
||||||
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
|
||||||
|
@ -35,8 +34,6 @@ import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.SortMode
|
import org.oxycblt.auxio.ui.SortMode
|
||||||
import org.oxycblt.auxio.ui.memberBinding
|
import org.oxycblt.auxio.ui.memberBinding
|
||||||
import org.oxycblt.auxio.util.applyEdge
|
|
||||||
import org.oxycblt.auxio.util.applyEdgeRespectingBar
|
|
||||||
import org.oxycblt.auxio.util.isLandscape
|
import org.oxycblt.auxio.util.isLandscape
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -49,12 +46,6 @@ 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.detailRecycler.applyEdgeRespectingBar(playbackModel, viewLifecycleOwner)
|
|
||||||
|
|
||||||
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
|
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,7 @@ class ExcludedViewModel(context: Context) : ViewModel() {
|
||||||
fun isModified() = dbPaths != paths.value
|
fun isModified() = dbPaths != paths.value
|
||||||
|
|
||||||
class Factory(private val context: Context) : ViewModelProvider.Factory {
|
class Factory(private val context: Context) : ViewModelProvider.Factory {
|
||||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
check(modelClass.isAssignableFrom(ExcludedViewModel::class.java)) {
|
check(modelClass.isAssignableFrom(ExcludedViewModel::class.java)) {
|
||||||
"ExcludedViewModel.Factory does not support this class"
|
"ExcludedViewModel.Factory does not support this class"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 Auxio Project
|
||||||
|
* EdgeFloatingActionButton.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.home
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.WindowInsets
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import org.oxycblt.auxio.util.systemBarsCompat
|
||||||
|
|
||||||
|
class FloatingActionButtonContainer @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = -1
|
||||||
|
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||||
|
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||||
|
updatePadding(bottom = insets.systemBarsCompat.bottom)
|
||||||
|
|
||||||
|
return insets
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,7 +25,6 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.view.iterator
|
import androidx.core.view.iterator
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.core.view.updatePaddingRelative
|
import androidx.core.view.updatePaddingRelative
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
@ -52,7 +51,6 @@ 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.SortMode
|
import org.oxycblt.auxio.ui.SortMode
|
||||||
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
|
||||||
|
|
||||||
|
@ -73,19 +71,12 @@ class HomeFragment : Fragment() {
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
val binding = FragmentHomeBinding.inflate(inflater)
|
val binding = FragmentHomeBinding.inflate(inflater)
|
||||||
var bottomPadding = 0
|
|
||||||
val sortItem: MenuItem
|
val sortItem: MenuItem
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
|
|
||||||
binding.applyEdge { bars ->
|
|
||||||
bottomPadding = bars.bottom
|
|
||||||
updateFabPadding(binding, bottomPadding)
|
|
||||||
binding.homeAppbar.updatePadding(top = bars.top)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.homeAppbar.apply {
|
binding.homeAppbar.apply {
|
||||||
// I have no idea how to clip the collapsing toolbar while still making the elevation
|
// I have no idea how to clip the collapsing toolbar while still making the elevation
|
||||||
// overlay bleed into the status bar, so I take the easy way out and just fade the
|
// overlay bleed into the status bar, so I take the easy way out and just fade the
|
||||||
|
@ -298,10 +289,6 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackModel.song.observe(viewLifecycleOwner) {
|
|
||||||
updateFabPadding(binding, bottomPadding)
|
|
||||||
}
|
|
||||||
|
|
||||||
logD("Fragment Created.")
|
logD("Fragment Created.")
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
|
@ -323,23 +310,6 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateFabPadding(
|
|
||||||
binding: FragmentHomeBinding,
|
|
||||||
bottomPadding: Int
|
|
||||||
) {
|
|
||||||
// To get our FAB to work with edge-to-edge, we need keep track of the bar view and update
|
|
||||||
// the padding based off of that. However, we can't use the shared method here since FABs
|
|
||||||
// don't respect padding, so we duplicate the code here except with the margins instead.
|
|
||||||
val fabParams = binding.homeFab.layoutParams as CoordinatorLayout.LayoutParams
|
|
||||||
val baseSpacing = resources.getDimensionPixelSize(R.dimen.spacing_medium)
|
|
||||||
|
|
||||||
if (playbackModel.song.value == null) {
|
|
||||||
fabParams.bottomMargin = baseSpacing + bottomPadding
|
|
||||||
} else {
|
|
||||||
fabParams.bottomMargin = baseSpacing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val DisplayMode.viewId: Int get() = when (this) {
|
private val DisplayMode.viewId: Int get() = when (this) {
|
||||||
DisplayMode.SHOW_SONGS -> R.id.home_song_list
|
DisplayMode.SHOW_SONGS -> R.id.home_song_list
|
||||||
DisplayMode.SHOW_ALBUMS -> R.id.home_album_list
|
DisplayMode.SHOW_ALBUMS -> R.id.home_album_list
|
||||||
|
|
|
@ -30,6 +30,7 @@ import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewConfiguration
|
import android.view.ViewConfiguration
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.WindowInsets
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.widget.AppCompatTextView
|
import androidx.appcompat.widget.AppCompatTextView
|
||||||
|
@ -66,6 +67,7 @@ import kotlin.math.abs
|
||||||
* - Added drag listener
|
* - Added drag listener
|
||||||
* - TODO: Added documentation
|
* - TODO: Added documentation
|
||||||
* - TODO: Popup will center itself to the thumb when possible
|
* - TODO: Popup will center itself to the thumb when possible
|
||||||
|
* - TODO: Stabilize how I'm using padding
|
||||||
*/
|
*/
|
||||||
class FastScrollRecyclerView @JvmOverloads constructor(
|
class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
@ -107,6 +109,14 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
hideScrollbar()
|
hideScrollbar()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val initialPadding = Rect(
|
||||||
|
paddingLeft, paddingTop, paddingRight, paddingBottom
|
||||||
|
)
|
||||||
|
|
||||||
|
private val scrollerPadding = Rect(
|
||||||
|
0, 0, 0, 0
|
||||||
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val thumbDrawable = R.drawable.ui_scroll_thumb.resolveDrawable(context)
|
val thumbDrawable = R.drawable.ui_scroll_thumb.resolveDrawable(context)
|
||||||
|
|
||||||
|
@ -207,7 +217,10 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
width - paddingRight - thumbWidth
|
width - paddingRight - thumbWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
trackView.layout(trackLeft, paddingTop, trackLeft + thumbWidth, height - paddingBottom)
|
trackView.layout(
|
||||||
|
trackLeft, paddingTop, trackLeft + thumbWidth,
|
||||||
|
height - scrollerPadding.bottom
|
||||||
|
)
|
||||||
|
|
||||||
val thumbLeft = if (isRtl) {
|
val thumbLeft = if (isRtl) {
|
||||||
paddingLeft
|
paddingLeft
|
||||||
|
@ -237,14 +250,14 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
|
|
||||||
val widthMeasureSpec = ViewGroup.getChildMeasureSpec(
|
val widthMeasureSpec = ViewGroup.getChildMeasureSpec(
|
||||||
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
||||||
paddingLeft + paddingRight + thumbWidth + popupLayoutParams.leftMargin +
|
scrollerPadding.left + scrollerPadding.right + thumbWidth +
|
||||||
popupLayoutParams.rightMargin,
|
popupLayoutParams.leftMargin + popupLayoutParams.rightMargin,
|
||||||
popupLayoutParams.width
|
popupLayoutParams.width
|
||||||
)
|
)
|
||||||
|
|
||||||
val heightMeasureSpec = ViewGroup.getChildMeasureSpec(
|
val heightMeasureSpec = ViewGroup.getChildMeasureSpec(
|
||||||
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
|
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
|
||||||
paddingTop + paddingBottom + popupLayoutParams.topMargin +
|
scrollerPadding.top + scrollerPadding.bottom + popupLayoutParams.topMargin +
|
||||||
popupLayoutParams.bottomMargin,
|
popupLayoutParams.bottomMargin,
|
||||||
popupLayoutParams.height
|
popupLayoutParams.height
|
||||||
)
|
)
|
||||||
|
@ -255,9 +268,9 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
val popupWidth = popupView.measuredWidth
|
val popupWidth = popupView.measuredWidth
|
||||||
val popupHeight = popupView.measuredHeight
|
val popupHeight = popupView.measuredHeight
|
||||||
val popupLeft = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
|
val popupLeft = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
|
||||||
paddingLeft + thumbWidth + popupLayoutParams.leftMargin
|
scrollerPadding.left + thumbWidth + popupLayoutParams.leftMargin
|
||||||
} else {
|
} else {
|
||||||
width - paddingRight - thumbWidth - popupLayoutParams.rightMargin - popupWidth
|
width - scrollerPadding.right - thumbWidth - popupLayoutParams.rightMargin - popupWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
// We handle RTL separately, so it's okay if Gravity.RIGHT is used here
|
// We handle RTL separately, so it's okay if Gravity.RIGHT is used here
|
||||||
|
@ -280,8 +293,8 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
|
|
||||||
val popupTop = MathUtils.clamp(
|
val popupTop = MathUtils.clamp(
|
||||||
thumbTop + thumbAnchorY - popupAnchorY,
|
thumbTop + thumbAnchorY - popupAnchorY,
|
||||||
paddingTop + popupLayoutParams.topMargin,
|
scrollerPadding.top + popupLayoutParams.topMargin,
|
||||||
height - paddingBottom - popupLayoutParams.bottomMargin - popupHeight
|
height - scrollerPadding.bottom - popupLayoutParams.bottomMargin - popupHeight
|
||||||
)
|
)
|
||||||
|
|
||||||
popupView.layout(
|
popupView.layout(
|
||||||
|
@ -310,6 +323,17 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
didRelayout = changed
|
didRelayout = changed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||||
|
setPadding(
|
||||||
|
initialPadding.left, initialPadding.top, initialPadding.right,
|
||||||
|
initialPadding.bottom + insets.systemWindowInsetBottom
|
||||||
|
)
|
||||||
|
|
||||||
|
scrollerPadding.bottom = insets.systemWindowInsetBottom
|
||||||
|
|
||||||
|
return insets
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateScrollbarState() {
|
private fun updateScrollbarState() {
|
||||||
if (!canScroll() || childCount == 0) {
|
if (!canScroll() || childCount == 0) {
|
||||||
return
|
return
|
||||||
|
@ -348,7 +372,7 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
val scrollOffset = paddingTop + (itemPos * itemHeight) - itemTop
|
val scrollOffset = paddingTop + (itemPos * itemHeight) - itemTop
|
||||||
|
|
||||||
// The range of pixels where the thumb is not present
|
// The range of pixels where the thumb is not present
|
||||||
val thumbOffsetRange = height - paddingTop - paddingBottom - thumbHeight
|
val thumbOffsetRange = height - scrollerPadding.top - scrollerPadding.bottom - thumbHeight
|
||||||
|
|
||||||
// Finally, we can calculate the thumb position, which is just:
|
// Finally, we can calculate the thumb position, which is just:
|
||||||
// [proportion of scroll position to scroll range] * [total thumb range]
|
// [proportion of scroll position to scroll range] * [total thumb range]
|
||||||
|
@ -375,7 +399,7 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
if (isInViewTouchTarget(thumbView, eventX, eventY)) {
|
if (isInViewTouchTarget(thumbView, eventX, eventY)) {
|
||||||
dragStartThumbOffset = thumbOffset
|
dragStartThumbOffset = thumbOffset
|
||||||
} else {
|
} else {
|
||||||
dragStartThumbOffset = (eventY - paddingTop - thumbHeight / 2f).toInt()
|
dragStartThumbOffset = (eventY - scrollerPadding.top - thumbHeight / 2f).toInt()
|
||||||
scrollToThumbOffset(dragStartThumbOffset)
|
scrollToThumbOffset(dragStartThumbOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -392,7 +416,7 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
dragStartThumbOffset = thumbOffset
|
dragStartThumbOffset = thumbOffset
|
||||||
} else {
|
} else {
|
||||||
dragStartY = eventY
|
dragStartY = eventY
|
||||||
dragStartThumbOffset = (eventY - paddingTop - thumbHeight / 2f).toInt()
|
dragStartThumbOffset = (eventY - scrollerPadding.top - thumbHeight / 2f).toInt()
|
||||||
scrollToThumbOffset(dragStartThumbOffset)
|
scrollToThumbOffset(dragStartThumbOffset)
|
||||||
}
|
}
|
||||||
setDragging(true)
|
setDragging(true)
|
||||||
|
@ -577,7 +601,7 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
|
|
||||||
private val thumbOffsetRange: Int
|
private val thumbOffsetRange: Int
|
||||||
get() {
|
get() {
|
||||||
return height - paddingTop - paddingBottom - thumbHeight
|
return height - scrollerPadding.top - scrollerPadding.bottom - thumbHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
private val itemCount: Int
|
private val itemCount: Int
|
||||||
|
|
|
@ -31,7 +31,6 @@ import org.oxycblt.auxio.home.HomeViewModel
|
||||||
import org.oxycblt.auxio.music.BaseModel
|
import org.oxycblt.auxio.music.BaseModel
|
||||||
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.applyEdgeRespectingBar
|
|
||||||
import org.oxycblt.auxio.util.applySpans
|
import org.oxycblt.auxio.util.applySpans
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -67,7 +66,6 @@ abstract class HomeListFragment : Fragment() {
|
||||||
adapter = homeAdapter
|
adapter = homeAdapter
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
applySpans()
|
applySpans()
|
||||||
applyEdgeRespectingBar(playbackModel, viewLifecycleOwner)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure that this RecyclerView has data before startup
|
// Make sure that this RecyclerView has data before startup
|
||||||
|
|
|
@ -0,0 +1,245 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 Auxio Project
|
||||||
|
* PlaybackBarLayout.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.playback
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.graphics.Insets
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.WindowInsets
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.annotation.AttrRes
|
||||||
|
import androidx.annotation.StyleRes
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.children
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
|
import org.oxycblt.auxio.util.resolveAttr
|
||||||
|
import org.oxycblt.auxio.util.systemBarsCompat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A layout that manages the bottom playback bar while still enabling edge-to-edge to work
|
||||||
|
* properly. The mechanism is mostly inspired by Material Files' PersistentBarLayout, however
|
||||||
|
* this class was primarily written by me and I plan to expand this layout to become part of
|
||||||
|
* the playback navigation process.
|
||||||
|
*/
|
||||||
|
class PlaybackBarLayout @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
@AttrRes defStyleAttr: Int = 0,
|
||||||
|
@StyleRes defStyleRes: Int = 0
|
||||||
|
) : ViewGroup(context, attrs, defStyleAttr, defStyleRes) {
|
||||||
|
private val barLayout = FrameLayout(context)
|
||||||
|
private val playbackFragment = CompactPlaybackFragment()
|
||||||
|
private var lastInsets: WindowInsets? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
addView(barLayout)
|
||||||
|
|
||||||
|
barLayout.apply {
|
||||||
|
id = R.id.main_playback
|
||||||
|
|
||||||
|
elevation = resources.getDimensionPixelSize(R.dimen.elevation_normal).toFloat()
|
||||||
|
|
||||||
|
(layoutParams as LayoutParams).apply {
|
||||||
|
width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
isBar = true
|
||||||
|
}
|
||||||
|
|
||||||
|
background = MaterialShapeDrawable.createWithElevationOverlay(context).apply {
|
||||||
|
elevation = barLayout.elevation
|
||||||
|
fillColor = ColorStateList.valueOf(R.attr.colorSurface.resolveAttr(context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
|
||||||
|
if (isInEditMode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// By default, using a FragmentContainerView in this view will result in
|
||||||
|
// the fragment disappearing on a recreate. Who knows why.
|
||||||
|
(context as AppCompatActivity).supportFragmentManager.apply {
|
||||||
|
this
|
||||||
|
.beginTransaction()
|
||||||
|
.replace(R.id.main_playback, playbackFragment)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||||
|
|
||||||
|
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
|
||||||
|
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
|
||||||
|
|
||||||
|
setMeasuredDimension(widthSize, heightSize)
|
||||||
|
|
||||||
|
val barParams = barLayout.layoutParams as LayoutParams
|
||||||
|
|
||||||
|
val barWidthSpec = getChildMeasureSpec(widthMeasureSpec, 0, barParams.width)
|
||||||
|
val barHeightSpec = getChildMeasureSpec(heightMeasureSpec, 0, barParams.height)
|
||||||
|
barLayout.measure(barWidthSpec, barHeightSpec)
|
||||||
|
|
||||||
|
val barHeightAdjusted = (barLayout.measuredHeight * barParams.offset).toInt()
|
||||||
|
|
||||||
|
val contentWidth = measuredWidth
|
||||||
|
val contentHeight = measuredHeight - barHeightAdjusted
|
||||||
|
|
||||||
|
for (child in children) {
|
||||||
|
if (child.visibility == View.GONE) continue
|
||||||
|
|
||||||
|
val childParams = child.layoutParams as LayoutParams
|
||||||
|
|
||||||
|
if (!childParams.isBar) {
|
||||||
|
val childWidthSpec = MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY)
|
||||||
|
val childHeightSpec = MeasureSpec.makeMeasureSpec(contentHeight, MeasureSpec.EXACTLY)
|
||||||
|
|
||||||
|
child.measure(childWidthSpec, childHeightSpec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWindowInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
|
||||||
|
val barHeight = if (barLayout.isVisible) {
|
||||||
|
barLayout.measuredHeight
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
val barHeightAdjusted = (barHeight * (barLayout.layoutParams as LayoutParams).offset).toInt()
|
||||||
|
|
||||||
|
for (child in children) {
|
||||||
|
if (child.visibility == View.GONE) continue
|
||||||
|
|
||||||
|
val childParams = child.layoutParams as LayoutParams
|
||||||
|
|
||||||
|
if (childParams.isBar) {
|
||||||
|
child.layout(
|
||||||
|
0, height - barHeightAdjusted,
|
||||||
|
width, height + (barHeight - barHeightAdjusted)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
child.layout(0, 0, child.measuredWidth, child.measuredHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||||
|
lastInsets = insets
|
||||||
|
|
||||||
|
barLayout.updatePadding(bottom = insets.systemBarsCompat.bottom)
|
||||||
|
updateWindowInsets()
|
||||||
|
|
||||||
|
return insets
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateWindowInsets() {
|
||||||
|
val insets = lastInsets
|
||||||
|
|
||||||
|
if (insets != null) {
|
||||||
|
super.dispatchApplyWindowInsets(mutateInsets(insets))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mutateInsets(insets: WindowInsets): WindowInsets {
|
||||||
|
if (barLayout.isVisible) {
|
||||||
|
val barParams = barLayout.layoutParams as LayoutParams
|
||||||
|
val childConsumedInset = (barLayout.measuredHeight * barParams.offset).toInt()
|
||||||
|
|
||||||
|
logD(childConsumedInset)
|
||||||
|
|
||||||
|
val bars = insets.systemBarsCompat
|
||||||
|
|
||||||
|
// TODO: Q support
|
||||||
|
when {
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
||||||
|
return WindowInsets.Builder(insets)
|
||||||
|
.setInsets(
|
||||||
|
WindowInsets.Type.systemBars(),
|
||||||
|
Insets.of(
|
||||||
|
bars.left, bars.top,
|
||||||
|
bars.right, (bars.bottom - childConsumedInset).coerceAtLeast(0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return insets
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showBar() {
|
||||||
|
(barLayout.layoutParams as LayoutParams).offset = 1f
|
||||||
|
updateWindowInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideBar() {
|
||||||
|
(barLayout.layoutParams as LayoutParams).offset = 0f
|
||||||
|
updateWindowInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- LAYOUT PARAMS ---
|
||||||
|
|
||||||
|
override fun generateLayoutParams(attrs: AttributeSet): ViewGroup.LayoutParams {
|
||||||
|
return LayoutParams(context, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun generateLayoutParams(
|
||||||
|
layoutParams: ViewGroup.LayoutParams
|
||||||
|
): ViewGroup.LayoutParams =
|
||||||
|
when (layoutParams) {
|
||||||
|
is LayoutParams -> LayoutParams(layoutParams)
|
||||||
|
is MarginLayoutParams -> LayoutParams(layoutParams)
|
||||||
|
else -> LayoutParams(layoutParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun generateDefaultLayoutParams(): ViewGroup.LayoutParams =
|
||||||
|
LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||||
|
|
||||||
|
override fun checkLayoutParams(layoutParams: ViewGroup.LayoutParams): Boolean =
|
||||||
|
layoutParams is LayoutParams && super.checkLayoutParams(layoutParams)
|
||||||
|
|
||||||
|
@Suppress("UNUSED")
|
||||||
|
class LayoutParams : ViewGroup.LayoutParams {
|
||||||
|
var isBar = false
|
||||||
|
var offset = 0f
|
||||||
|
|
||||||
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
|
|
||||||
|
constructor(width: Int, height: Int) : super(width, height)
|
||||||
|
|
||||||
|
constructor(source: LayoutParams) : super(source)
|
||||||
|
|
||||||
|
constructor(source: ViewGroup.LayoutParams) : super(source)
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,8 +32,8 @@ import org.oxycblt.auxio.databinding.FragmentPlaybackBinding
|
||||||
import org.oxycblt.auxio.detail.DetailViewModel
|
import org.oxycblt.auxio.detail.DetailViewModel
|
||||||
import org.oxycblt.auxio.playback.state.LoopMode
|
import org.oxycblt.auxio.playback.state.LoopMode
|
||||||
import org.oxycblt.auxio.ui.memberBinding
|
import org.oxycblt.auxio.ui.memberBinding
|
||||||
import org.oxycblt.auxio.util.applyEdge
|
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
import org.oxycblt.auxio.util.systemBarsCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Fragment] that displays more information about the song, along with more media controls.
|
* A [Fragment] that displays more information about the song, along with more media controls.
|
||||||
|
@ -60,11 +60,15 @@ class PlaybackFragment : Fragment() {
|
||||||
binding.playbackModel = playbackModel
|
binding.playbackModel = playbackModel
|
||||||
binding.detailModel = detailModel
|
binding.detailModel = detailModel
|
||||||
|
|
||||||
binding.applyEdge { bars ->
|
binding.root.setOnApplyWindowInsetsListener { v, insets ->
|
||||||
|
val bars = insets.systemBarsCompat
|
||||||
|
|
||||||
binding.root.updatePadding(
|
binding.root.updatePadding(
|
||||||
top = bars.top,
|
top = bars.top,
|
||||||
bottom = bars.bottom
|
bottom = bars.bottom
|
||||||
)
|
)
|
||||||
|
|
||||||
|
insets
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.playbackToolbar.apply {
|
binding.playbackToolbar.apply {
|
||||||
|
|
|
@ -22,14 +22,12 @@ 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.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
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.util.applyEdge
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Fragment] that contains both the user queue and the next queue, with the ability to
|
* A [Fragment] that contains both the user queue and the next queue, with the ability to
|
||||||
|
@ -58,11 +56,6 @@ class QueueFragment : Fragment() {
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
|
|
||||||
binding.applyEdge { bars ->
|
|
||||||
binding.queueAppbar.updatePadding(top = bars.top)
|
|
||||||
binding.queueRecycler.updatePadding(bottom = bars.bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.queueToolbar.setNavigationOnClickListener {
|
binding.queueToolbar.setNavigationOnClickListener {
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ 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,8 +41,6 @@ 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.applyEdgeRespectingBar
|
|
||||||
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
|
||||||
|
@ -77,10 +74,6 @@ class SearchFragment : Fragment() {
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
|
|
||||||
binding.applyEdge { bars ->
|
|
||||||
binding.searchAppbar.updatePadding(top = bars.top)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
|
@ -128,8 +121,6 @@ class SearchFragment : Fragment() {
|
||||||
applySpans { pos ->
|
applySpans { pos ->
|
||||||
searchAdapter.currentList[pos] is Header
|
searchAdapter.currentList[pos] is Header
|
||||||
}
|
}
|
||||||
|
|
||||||
applyEdgeRespectingBar(playbackModel, viewLifecycleOwner)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
|
@ -36,9 +36,9 @@ import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentAboutBinding
|
import org.oxycblt.auxio.databinding.FragmentAboutBinding
|
||||||
import org.oxycblt.auxio.home.HomeViewModel
|
import org.oxycblt.auxio.home.HomeViewModel
|
||||||
import org.oxycblt.auxio.util.applyEdge
|
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
|
import org.oxycblt.auxio.util.systemBarsCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [BottomSheetDialogFragment] that shows Auxio's about screen.
|
* A [BottomSheetDialogFragment] that shows Auxio's about screen.
|
||||||
|
@ -54,9 +54,9 @@ class AboutFragment : Fragment() {
|
||||||
): View {
|
): View {
|
||||||
val binding = FragmentAboutBinding.inflate(layoutInflater)
|
val binding = FragmentAboutBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
binding.applyEdge { bars ->
|
binding.aboutContents.setOnApplyWindowInsetsListener { v, insets ->
|
||||||
binding.aboutAppbar.updatePadding(top = bars.top)
|
binding.aboutContents.updatePadding(bottom = insets.systemBarsCompat.bottom)
|
||||||
binding.aboutContents.updatePadding(bottom = bars.bottom)
|
insets
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.aboutToolbar.setNavigationOnClickListener {
|
binding.aboutToolbar.setNavigationOnClickListener {
|
||||||
|
|
|
@ -22,11 +22,9 @@ 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.core.view.updatePadding
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import org.oxycblt.auxio.databinding.FragmentSettingsBinding
|
import org.oxycblt.auxio.databinding.FragmentSettingsBinding
|
||||||
import org.oxycblt.auxio.util.applyEdge
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A container [Fragment] for the settings menu.
|
* A container [Fragment] for the settings menu.
|
||||||
|
@ -46,13 +44,6 @@ class SettingsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.applyEdge { bars ->
|
|
||||||
binding.settingsAppbar.updatePadding(top = bars.top)
|
|
||||||
|
|
||||||
// The padding + clipToPadding method does not work with a
|
|
||||||
// FragmentContainerView. Do it directly in SettingsListFragment instead.
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.settingsAppbar.liftOnScrollTargetViewId = androidx.preference.R.id.recycler_view
|
binding.settingsAppbar.liftOnScrollTargetViewId = androidx.preference.R.id.recycler_view
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
|
|
|
@ -39,10 +39,10 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.settings.pref.IntListPrefDialog
|
import org.oxycblt.auxio.settings.pref.IntListPrefDialog
|
||||||
import org.oxycblt.auxio.settings.pref.IntListPreference
|
import org.oxycblt.auxio.settings.pref.IntListPreference
|
||||||
import org.oxycblt.auxio.settings.tabs.TabCustomizeDialog
|
import org.oxycblt.auxio.settings.tabs.TabCustomizeDialog
|
||||||
import org.oxycblt.auxio.util.applyEdge
|
|
||||||
import org.oxycblt.auxio.util.isNight
|
import org.oxycblt.auxio.util.isNight
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
|
import org.oxycblt.auxio.util.systemBarsCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The actual fragment containing the settings menu. Inherits [PreferenceFragmentCompat].
|
* The actual fragment containing the settings menu. Inherits [PreferenceFragmentCompat].
|
||||||
|
@ -65,8 +65,10 @@ class SettingsListFragment : PreferenceFragmentCompat() {
|
||||||
view.findViewById<RecyclerView>(androidx.preference.R.id.recycler_view).apply {
|
view.findViewById<RecyclerView>(androidx.preference.R.id.recycler_view).apply {
|
||||||
clipToPadding = false
|
clipToPadding = false
|
||||||
|
|
||||||
applyEdge { bars ->
|
setOnApplyWindowInsetsListener { v, insets ->
|
||||||
updatePadding(bottom = bars.bottom)
|
updatePadding(bottom = insets.systemBarsCompat.bottom)
|
||||||
|
|
||||||
|
insets
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
40
app/src/main/java/org/oxycblt/auxio/ui/EdgeRecyclerView.kt
Normal file
40
app/src/main/java/org/oxycblt/auxio/ui/EdgeRecyclerView.kt
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 Auxio Project
|
||||||
|
* EdgeRecyclerView.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.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.WindowInsets
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.oxycblt.auxio.util.systemBarsCompat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [RecyclerView] that automatically applies insets to itself.
|
||||||
|
*/
|
||||||
|
class EdgeRecyclerView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = -1
|
||||||
|
) : RecyclerView(context, attrs, defStyleAttr) {
|
||||||
|
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||||
|
updatePadding(bottom = insets.systemBarsCompat.bottom)
|
||||||
|
return insets
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,11 +23,14 @@ import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.ViewTreeObserver
|
import android.view.ViewTreeObserver
|
||||||
|
import android.view.WindowInsets
|
||||||
import androidx.annotation.StyleRes
|
import androidx.annotation.StyleRes
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
|
import org.oxycblt.auxio.util.systemBarsCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An [AppBarLayout] that fixes a bug with the default implementation where the lifted state
|
* An [AppBarLayout] that fixes a bug with the default implementation where the lifted state
|
||||||
|
@ -61,6 +64,20 @@ class LiftAppBarLayout @JvmOverloads constructor(
|
||||||
viewTreeObserver.addOnPreDrawListener(onPreDraw)
|
viewTreeObserver.addOnPreDrawListener(onPreDraw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||||
|
super.dispatchApplyWindowInsets(insets)
|
||||||
|
|
||||||
|
return onApplyWindowInsets(insets)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||||
|
super.onApplyWindowInsets(insets)
|
||||||
|
|
||||||
|
updatePadding(top = insets.systemBarsCompat.top)
|
||||||
|
|
||||||
|
return insets
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDetachedFromWindow() {
|
override fun onDetachedFromWindow() {
|
||||||
super.onDetachedFromWindow()
|
super.onDetachedFromWindow()
|
||||||
|
|
||||||
|
|
|
@ -32,14 +32,10 @@ import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.ColorRes
|
import androidx.annotation.ColorRes
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
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 com.google.android.material.shape.MaterialShapeDrawable
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply a [MaterialShapeDrawable] to this view, automatically initializing the elevation overlay
|
* Apply a [MaterialShapeDrawable] to this view, automatically initializing the elevation overlay
|
||||||
|
@ -142,84 +138,24 @@ fun @receiver:AttrRes Int.resolveAttr(context: Context): Int {
|
||||||
return color.resolveColor(context)
|
return color.resolveColor(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
val WindowInsets.systemBarsCompat: Rect get() {
|
||||||
* Apply edge-to-edge tweaks to the root of a [ViewBinding].
|
return when {
|
||||||
* @param onApply What to do when the system bar insets are provided
|
|
||||||
*/
|
|
||||||
fun ViewBinding.applyEdge(onApply: (Rect) -> Unit) {
|
|
||||||
root.applyEdge(onApply)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply edge-to-edge tweaks to a [View].
|
|
||||||
* @param onApply What to do when the system bar insets are provided
|
|
||||||
*/
|
|
||||||
fun View.applyEdge(onApply: (Rect) -> Unit) {
|
|
||||||
when {
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
||||||
setOnApplyWindowInsetsListener { _, insets ->
|
getInsets(WindowInsets.Type.systemBars()).run {
|
||||||
val bars = insets.getInsets(WindowInsets.Type.systemBars()).run {
|
Rect(left, top, right, bottom)
|
||||||
Rect(left, top, right, bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
onApply(bars)
|
|
||||||
|
|
||||||
insets
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> {
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> {
|
||||||
setOnApplyWindowInsetsListener { _, insets ->
|
@Suppress("DEPRECATION")
|
||||||
@Suppress("DEPRECATION")
|
Rect(
|
||||||
val bars = Rect(
|
systemWindowInsetLeft,
|
||||||
insets.systemWindowInsetLeft,
|
systemWindowInsetTop,
|
||||||
insets.systemWindowInsetTop,
|
systemWindowInsetRight,
|
||||||
insets.systemWindowInsetRight,
|
systemWindowInsetBottom
|
||||||
insets.systemWindowInsetBottom
|
)
|
||||||
)
|
|
||||||
|
|
||||||
onApply(bars)
|
|
||||||
insets
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not on a version that supports edge-to-edge [yet], don't do anything
|
else -> Rect(0, 0, 0, 0)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stopgap measure to make edge-to-edge work on views that also have a playback bar.
|
|
||||||
* The issue is that while we can apply padding initially, the padding will still be applied
|
|
||||||
* when the bar is shown, which is very ungood. We mitigate this by just checking the song state
|
|
||||||
* and removing the padding if there is one, which is a stupidly fragile band-aid but it
|
|
||||||
* works.
|
|
||||||
*
|
|
||||||
* TODO: Dumpster this and replace it with a dedicated layout. Only issue with that is how
|
|
||||||
* nested our layouts are, which basically forces us to do recursion magic. Hai Zhang's Material
|
|
||||||
* Files layout may help in this task.
|
|
||||||
*/
|
|
||||||
fun View.applyEdgeRespectingBar(
|
|
||||||
playbackModel: PlaybackViewModel,
|
|
||||||
viewLifecycleOwner: LifecycleOwner,
|
|
||||||
initialPadding: Int = 0
|
|
||||||
) {
|
|
||||||
var bottomPadding = 0
|
|
||||||
|
|
||||||
applyEdge {
|
|
||||||
bottomPadding = it.bottom
|
|
||||||
|
|
||||||
if (playbackModel.song.value == null) {
|
|
||||||
updatePadding(bottom = bottomPadding)
|
|
||||||
} else {
|
|
||||||
updatePadding(bottom = initialPadding)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
playbackModel.song.observe(viewLifecycleOwner) { song ->
|
|
||||||
if (song == null) {
|
|
||||||
updatePadding(bottom = bottomPadding)
|
|
||||||
} else {
|
|
||||||
updatePadding(bottom = initialPadding)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,10 +41,14 @@ private fun createViews(
|
||||||
return views
|
return views
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun RemoteViews.applyMeta(context: Context, state: WidgetState) {
|
private fun RemoteViews.applyMeta(state: WidgetState): RemoteViews {
|
||||||
setTextViewText(R.id.widget_song, state.song.name)
|
setTextViewText(R.id.widget_song, state.song.name)
|
||||||
setTextViewText(R.id.widget_artist, state.song.album.artist.resolvedName)
|
setTextViewText(R.id.widget_artist, state.song.album.artist.resolvedName)
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RemoteViews.applyCover(context: Context, state: WidgetState): RemoteViews {
|
||||||
if (state.albumArt != null) {
|
if (state.albumArt != null) {
|
||||||
setImageViewBitmap(R.id.widget_cover, state.albumArt)
|
setImageViewBitmap(R.id.widget_cover, state.albumArt)
|
||||||
setContentDescription(
|
setContentDescription(
|
||||||
|
@ -54,9 +58,10 @@ private fun RemoteViews.applyMeta(context: Context, state: WidgetState) {
|
||||||
setImageViewResource(R.id.widget_cover, R.drawable.ic_song)
|
setImageViewResource(R.id.widget_cover, R.drawable.ic_song)
|
||||||
setContentDescription(R.id.widget_cover, context.getString(R.string.desc_no_cover))
|
setContentDescription(R.id.widget_cover, context.getString(R.string.desc_no_cover))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun RemoteViews.applyControls(context: Context, state: WidgetState) {
|
return this
|
||||||
|
}
|
||||||
|
private fun RemoteViews.applyControls(context: Context, state: WidgetState): RemoteViews {
|
||||||
setOnClickPendingIntent(
|
setOnClickPendingIntent(
|
||||||
R.id.widget_skip_prev,
|
R.id.widget_skip_prev,
|
||||||
context.newBroadcastIntent(
|
context.newBroadcastIntent(
|
||||||
|
@ -86,53 +91,21 @@ private fun RemoteViews.applyControls(context: Context, state: WidgetState) {
|
||||||
R.drawable.ic_play
|
R.drawable.ic_play
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createDefaultWidget(context: Context): RemoteViews {
|
private fun RemoteViews.applyFullControls(context: Context, state: WidgetState): RemoteViews {
|
||||||
return createViews(context, R.layout.widget_default)
|
applyControls(context, state)
|
||||||
}
|
|
||||||
|
|
||||||
fun createTinyWidget(context: Context, state: WidgetState): RemoteViews {
|
setOnClickPendingIntent(
|
||||||
val views = createViews(context, R.layout.widget_tiny)
|
|
||||||
views.applyMeta(context, state)
|
|
||||||
views.applyControls(context, state)
|
|
||||||
return views
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createWideWidget(context: Context, state: WidgetState): RemoteViews {
|
|
||||||
val views = createViews(context, R.layout.widget_wide)
|
|
||||||
views.applyMeta(context, state)
|
|
||||||
views.applyControls(context, state)
|
|
||||||
return views
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createSmallWidget(context: Context, state: WidgetState): RemoteViews {
|
|
||||||
val views = createViews(context, R.layout.widget_small)
|
|
||||||
views.applyMeta(context, state)
|
|
||||||
views.applyControls(context, state)
|
|
||||||
return views
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createMediumWidget(context: Context, state: WidgetState): RemoteViews {
|
|
||||||
val views = createViews(context, R.layout.widget_medium)
|
|
||||||
views.applyMeta(context, state)
|
|
||||||
views.applyControls(context, state)
|
|
||||||
return views
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createLargeWidget(context: Context, state: WidgetState): RemoteViews {
|
|
||||||
val views = createViews(context, R.layout.widget_large)
|
|
||||||
views.applyMeta(context, state)
|
|
||||||
views.applyControls(context, state)
|
|
||||||
|
|
||||||
views.setOnClickPendingIntent(
|
|
||||||
R.id.widget_loop,
|
R.id.widget_loop,
|
||||||
context.newBroadcastIntent(
|
context.newBroadcastIntent(
|
||||||
PlaybackService.ACTION_LOOP
|
PlaybackService.ACTION_LOOP
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
views.setOnClickPendingIntent(
|
setOnClickPendingIntent(
|
||||||
R.id.widget_shuffle,
|
R.id.widget_shuffle,
|
||||||
context.newBroadcastIntent(
|
context.newBroadcastIntent(
|
||||||
PlaybackService.ACTION_SHUFFLE
|
PlaybackService.ACTION_SHUFFLE
|
||||||
|
@ -154,8 +127,46 @@ fun createLargeWidget(context: Context, state: WidgetState): RemoteViews {
|
||||||
LoopMode.TRACK -> R.drawable.ic_loop_one
|
LoopMode.TRACK -> R.drawable.ic_loop_one
|
||||||
}
|
}
|
||||||
|
|
||||||
views.setImageViewResource(R.id.widget_shuffle, shuffleRes)
|
setImageViewResource(R.id.widget_shuffle, shuffleRes)
|
||||||
views.setImageViewResource(R.id.widget_loop, loopRes)
|
setImageViewResource(R.id.widget_loop, loopRes)
|
||||||
|
|
||||||
return views
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createDefaultWidget(context: Context): RemoteViews {
|
||||||
|
return createViews(context, R.layout.widget_default)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createTinyWidget(context: Context, state: WidgetState): RemoteViews {
|
||||||
|
return createViews(context, R.layout.widget_tiny)
|
||||||
|
.applyMeta(state)
|
||||||
|
.applyCover(context, state)
|
||||||
|
.applyControls(context, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createWideWidget(context: Context, state: WidgetState): RemoteViews {
|
||||||
|
return createViews(context, R.layout.widget_wide)
|
||||||
|
.applyMeta(state)
|
||||||
|
.applyCover(context, state)
|
||||||
|
.applyFullControls(context, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createSmallWidget(context: Context, state: WidgetState): RemoteViews {
|
||||||
|
return createViews(context, R.layout.widget_small)
|
||||||
|
.applyMeta(state)
|
||||||
|
.applyControls(context, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createMediumWidget(context: Context, state: WidgetState): RemoteViews {
|
||||||
|
return createViews(context, R.layout.widget_medium)
|
||||||
|
.applyMeta(state)
|
||||||
|
.applyCover(context, state)
|
||||||
|
.applyControls(context, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createLargeWidget(context: Context, state: WidgetState): RemoteViews {
|
||||||
|
return createViews(context, R.layout.widget_large)
|
||||||
|
.applyMeta(state)
|
||||||
|
.applyCover(context, state)
|
||||||
|
.applyFullControls(context, state)
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,6 @@
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/spacing_medium"
|
android:layout_marginStart="@dimen/spacing_medium"
|
||||||
android:layout_marginEnd="@dimen/spacing_medium"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/detail_subhead"
|
app:layout_constraintBottom_toTopOf="@+id/detail_subhead"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.5"
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
android:background="?attr/colorSurface"
|
android:background="?attr/colorSurface"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<org.oxycblt.auxio.ui.LiftAppBarLayout
|
||||||
android:id="@+id/about_appbar"
|
android:id="@+id/about_appbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
style="@style/Widget.Auxio.Toolbar.Icon.Down"
|
style="@style/Widget.Auxio.Toolbar.Icon.Down"
|
||||||
app:title="@string/lbl_about" />
|
app:title="@string/lbl_about" />
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</org.oxycblt.auxio.ui.LiftAppBarLayout>
|
||||||
|
|
||||||
<androidx.core.widget.NestedScrollView
|
<androidx.core.widget.NestedScrollView
|
||||||
android:id="@+id/about_contents"
|
android:id="@+id/about_contents"
|
||||||
|
|
|
@ -4,32 +4,39 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
tools:context=".detail.DetailFragment">
|
tools:context=".detail.DetailFragment">
|
||||||
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
<org.oxycblt.auxio.playback.PlaybackBarLayout
|
||||||
|
android:id="@+id/bar_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<org.oxycblt.auxio.ui.LiftAppBarLayout
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
android:id="@+id/detail_appbar"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent">
|
||||||
android:background="?attr/colorSurface"
|
|
||||||
app:liftOnScroll="true"
|
|
||||||
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
<org.oxycblt.auxio.ui.LiftAppBarLayout
|
||||||
android:id="@+id/detail_toolbar"
|
android:id="@+id/detail_appbar"
|
||||||
style="@style/Widget.Auxio.Toolbar.Icon" />
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/colorSurface"
|
||||||
|
app:liftOnScroll="true"
|
||||||
|
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
||||||
|
|
||||||
</org.oxycblt.auxio.ui.LiftAppBarLayout>
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/detail_toolbar"
|
||||||
|
style="@style/Widget.Auxio.Toolbar.Icon" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
</org.oxycblt.auxio.ui.LiftAppBarLayout>
|
||||||
android:id="@+id/detail_recycler"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
|
||||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
|
||||||
tools:listitem="@layout/item_detail" />
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
<org.oxycblt.auxio.ui.EdgeRecyclerView
|
||||||
|
android:id="@+id/detail_recycler"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
app:layout_role="content"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||||
|
tools:listitem="@layout/item_detail" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
</org.oxycblt.auxio.playback.PlaybackBarLayout>
|
||||||
</layout>
|
</layout>
|
|
@ -47,16 +47,23 @@
|
||||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||||
tools:layout="@layout/fragment_home_list" />
|
tools:layout="@layout/fragment_home_list" />
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<org.oxycblt.auxio.home.FloatingActionButtonContainer
|
||||||
android:id="@+id/home_fab"
|
android:id="@+id/home_fab_container"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:src="@drawable/ic_shuffle"
|
|
||||||
android:layout_margin="@dimen/spacing_medium"
|
|
||||||
android:contentDescription="@string/desc_shuffle_all"
|
|
||||||
app:layout_anchor="@id/home_pager"
|
app:layout_anchor="@id/home_pager"
|
||||||
app:tint="?attr/colorControlNormal"
|
app:layout_anchorGravity="bottom|end">
|
||||||
app:layout_anchorGravity="bottom|end" />
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/home_fab"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ic_shuffle"
|
||||||
|
android:layout_margin="@dimen/spacing_medium"
|
||||||
|
android:contentDescription="@string/desc_shuffle_all"
|
||||||
|
app:tint="?attr/colorOnPrimaryContainer" />
|
||||||
|
|
||||||
|
</org.oxycblt.auxio.home.FloatingActionButtonContainer>
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
</layout>
|
</layout>
|
|
@ -8,6 +8,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
|
android:paddingBottom="88dp"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||||
tools:listitem="@layout/item_artist" />
|
tools:listitem="@layout/item_artist" />
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
tools:context=".MainFragment">
|
tools:context=".MainFragment">
|
||||||
|
|
||||||
<LinearLayout
|
<org.oxycblt.auxio.playback.PlaybackBarLayout
|
||||||
android:id="@+id/main_layout"
|
android:id="@+id/main_bar_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:animateLayoutChanges="true"
|
android:animateLayoutChanges="true"
|
||||||
|
@ -15,20 +15,9 @@
|
||||||
android:id="@+id/explore_nav_host"
|
android:id="@+id/explore_nav_host"
|
||||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="match_parent"
|
||||||
android:layout_weight="1"
|
|
||||||
app:navGraph="@navigation/nav_explore"
|
app:navGraph="@navigation/nav_explore"
|
||||||
tools:layout="@layout/fragment_home" />
|
tools:layout="@layout/fragment_home" />
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
</org.oxycblt.auxio.playback.PlaybackBarLayout>
|
||||||
android:id="@+id/main_playback"
|
|
||||||
android:name="org.oxycblt.auxio.playback.CompactPlaybackFragment"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/colorSurface"
|
|
||||||
android:clipToPadding="true"
|
|
||||||
android:elevation="@dimen/elevation_normal"
|
|
||||||
tools:layout="@layout/fragment_compact_playback" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</layout>
|
</layout>
|
|
@ -29,7 +29,7 @@
|
||||||
|
|
||||||
</org.oxycblt.auxio.ui.LiftAppBarLayout>
|
</org.oxycblt.auxio.ui.LiftAppBarLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<org.oxycblt.auxio.ui.EdgeRecyclerView
|
||||||
android:id="@+id/queue_recycler"
|
android:id="@+id/queue_recycler"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
|
|
||||||
</org.oxycblt.auxio.ui.LiftAppBarLayout>
|
</org.oxycblt.auxio.ui.LiftAppBarLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<org.oxycblt.auxio.ui.EdgeRecyclerView
|
||||||
android:id="@+id/search_recycler"
|
android:id="@+id/search_recycler"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
|
|
@ -27,7 +27,6 @@
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/spacing_medium"
|
android:layout_marginTop="@dimen/spacing_medium"
|
||||||
android:layout_marginEnd="@dimen/spacing_medium"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.5"
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
|
11
app/src/main/res/layout/view_playback_bar.xml
Normal file
11
app/src/main/res/layout/view_playback_bar.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/main_playback"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/colorSurface"
|
||||||
|
android:clipToPadding="true"
|
||||||
|
android:elevation="@dimen/elevation_normal"
|
||||||
|
tools:layout="@layout/fragment_compact_playback" />
|
|
@ -60,7 +60,7 @@
|
||||||
<item name="colorSurface">@android:color/system_accent2_800</item>
|
<item name="colorSurface">@android:color/system_accent2_800</item>
|
||||||
<item name="colorPrimary">?android:attr/colorAccent</item>
|
<item name="colorPrimary">?android:attr/colorAccent</item>
|
||||||
<item name="colorSecondary">?android:attr/colorAccent</item>
|
<item name="colorSecondary">?android:attr/colorAccent</item>
|
||||||
<item name="colorControlNormal">@color/m3_sys_color_dynamic_dark_inverse_surface</item>
|
<item name="colorControlNormal">?android:attr/colorControlNormal</item>
|
||||||
<item name="colorControlHighlight">?android:attr/colorControlHighlight</item>
|
<item name="colorControlHighlight">?android:attr/colorControlHighlight</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
|
@ -60,7 +60,7 @@
|
||||||
<item name="colorSurface">@android:color/system_accent1_50</item>
|
<item name="colorSurface">@android:color/system_accent1_50</item>
|
||||||
<item name="colorPrimary">?android:attr/colorAccent</item>
|
<item name="colorPrimary">?android:attr/colorAccent</item>
|
||||||
<item name="colorSecondary">?android:attr/colorAccent</item>
|
<item name="colorSecondary">?android:attr/colorAccent</item>
|
||||||
<item name="colorControlNormal">@color/m3_sys_color_dynamic_light_inverse_surface</item>
|
<item name="colorControlNormal">?android:attr/colorControlNormal</item>
|
||||||
<item name="colorControlHighlight">?android:attr/colorControlHighlight</item>
|
<item name="colorControlHighlight">?android:attr/colorControlHighlight</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
|
@ -4,4 +4,9 @@
|
||||||
<attr name="entries" format="reference" />
|
<attr name="entries" format="reference" />
|
||||||
<attr name="entryValues" format="reference" />
|
<attr name="entryValues" format="reference" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
|
||||||
|
<attr format="enum" name="layout_role">
|
||||||
|
<enum name="content" value="0" />
|
||||||
|
<enum name="floating" value="1" />
|
||||||
|
</attr>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in a new issue