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
|
||||
- Audio Focus / Headset Management
|
||||
- No internet connectivity whatsoever
|
||||
- No rounded album corners
|
||||
- No rounded album covers
|
||||
|
||||
## To possibly come in the future:
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ dependencies {
|
|||
// --- SUPPORT ---
|
||||
|
||||
// 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.fragment:fragment-ktx:1.3.6'
|
||||
|
||||
|
@ -74,7 +74,7 @@ dependencies {
|
|||
implementation "androidx.viewpager2:viewpager2:1.1.0-beta01"
|
||||
|
||||
// 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-java8:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||
|
|
|
@ -26,8 +26,6 @@ import android.view.ViewGroup
|
|||
import android.widget.Button
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
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.activityViewModels
|
||||
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.MusicViewModel
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.util.applyEdge
|
||||
import org.oxycblt.auxio.util.applyMaterialDrawable
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
|
@ -65,25 +61,26 @@ class MainFragment : Fragment() {
|
|||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
binding.applyEdge { bars ->
|
||||
binding.mainPlayback.updatePadding(bottom = bars.bottom)
|
||||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
if (playbackModel.song.value != null) {
|
||||
binding.mainBarLayout.showBar()
|
||||
} else {
|
||||
binding.mainBarLayout.hideBar()
|
||||
}
|
||||
|
||||
binding.mainPlayback.applyMaterialDrawable()
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
playbackModel.song.observe(viewLifecycleOwner) { song ->
|
||||
if (song != null) {
|
||||
binding.mainBarLayout.showBar()
|
||||
} else {
|
||||
binding.mainBarLayout.hideBar()
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
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.
|
||||
musicModel.loaderResponse.observe(viewLifecycleOwner) { response ->
|
||||
// Handle the loader response.
|
||||
|
|
|
@ -24,7 +24,6 @@ import androidx.activity.OnBackPressedCallback
|
|||
import androidx.annotation.MenuRes
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.view.forEach
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
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.ui.SortMode
|
||||
import org.oxycblt.auxio.ui.memberBinding
|
||||
import org.oxycblt.auxio.util.applyEdge
|
||||
import org.oxycblt.auxio.util.applyEdgeRespectingBar
|
||||
import org.oxycblt.auxio.util.isLandscape
|
||||
|
||||
/**
|
||||
|
@ -49,12 +46,6 @@ abstract class DetailFragment : Fragment() {
|
|||
protected val binding by memberBinding(FragmentDetailBinding::inflate)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -95,7 +95,7 @@ class ExcludedViewModel(context: Context) : ViewModel() {
|
|||
fun isModified() = dbPaths != paths.value
|
||||
|
||||
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)) {
|
||||
"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 androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.iterator
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.core.view.updatePaddingRelative
|
||||
import androidx.fragment.app.Fragment
|
||||
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.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.SortMode
|
||||
import org.oxycblt.auxio.util.applyEdge
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logE
|
||||
|
||||
|
@ -73,19 +71,12 @@ class HomeFragment : Fragment() {
|
|||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val binding = FragmentHomeBinding.inflate(inflater)
|
||||
var bottomPadding = 0
|
||||
val sortItem: MenuItem
|
||||
|
||||
// --- UI SETUP ---
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
binding.applyEdge { bars ->
|
||||
bottomPadding = bars.bottom
|
||||
updateFabPadding(binding, bottomPadding)
|
||||
binding.homeAppbar.updatePadding(top = bars.top)
|
||||
}
|
||||
|
||||
binding.homeAppbar.apply {
|
||||
// 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
|
||||
|
@ -298,10 +289,6 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
playbackModel.song.observe(viewLifecycleOwner) {
|
||||
updateFabPadding(binding, bottomPadding)
|
||||
}
|
||||
|
||||
logD("Fragment Created.")
|
||||
|
||||
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) {
|
||||
DisplayMode.SHOW_SONGS -> R.id.home_song_list
|
||||
DisplayMode.SHOW_ALBUMS -> R.id.home_album_list
|
||||
|
|
|
@ -30,6 +30,7 @@ import android.view.MotionEvent
|
|||
import android.view.View
|
||||
import android.view.ViewConfiguration
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowInsets
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
|
@ -66,6 +67,7 @@ import kotlin.math.abs
|
|||
* - Added drag listener
|
||||
* - TODO: Added documentation
|
||||
* - TODO: Popup will center itself to the thumb when possible
|
||||
* - TODO: Stabilize how I'm using padding
|
||||
*/
|
||||
class FastScrollRecyclerView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
|
@ -107,6 +109,14 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
|||
hideScrollbar()
|
||||
}
|
||||
|
||||
private val initialPadding = Rect(
|
||||
paddingLeft, paddingTop, paddingRight, paddingBottom
|
||||
)
|
||||
|
||||
private val scrollerPadding = Rect(
|
||||
0, 0, 0, 0
|
||||
)
|
||||
|
||||
init {
|
||||
val thumbDrawable = R.drawable.ui_scroll_thumb.resolveDrawable(context)
|
||||
|
||||
|
@ -207,7 +217,10 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
|||
width - paddingRight - thumbWidth
|
||||
}
|
||||
|
||||
trackView.layout(trackLeft, paddingTop, trackLeft + thumbWidth, height - paddingBottom)
|
||||
trackView.layout(
|
||||
trackLeft, paddingTop, trackLeft + thumbWidth,
|
||||
height - scrollerPadding.bottom
|
||||
)
|
||||
|
||||
val thumbLeft = if (isRtl) {
|
||||
paddingLeft
|
||||
|
@ -237,14 +250,14 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
|||
|
||||
val widthMeasureSpec = ViewGroup.getChildMeasureSpec(
|
||||
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
||||
paddingLeft + paddingRight + thumbWidth + popupLayoutParams.leftMargin +
|
||||
popupLayoutParams.rightMargin,
|
||||
scrollerPadding.left + scrollerPadding.right + thumbWidth +
|
||||
popupLayoutParams.leftMargin + popupLayoutParams.rightMargin,
|
||||
popupLayoutParams.width
|
||||
)
|
||||
|
||||
val heightMeasureSpec = ViewGroup.getChildMeasureSpec(
|
||||
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
|
||||
paddingTop + paddingBottom + popupLayoutParams.topMargin +
|
||||
scrollerPadding.top + scrollerPadding.bottom + popupLayoutParams.topMargin +
|
||||
popupLayoutParams.bottomMargin,
|
||||
popupLayoutParams.height
|
||||
)
|
||||
|
@ -255,9 +268,9 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
|||
val popupWidth = popupView.measuredWidth
|
||||
val popupHeight = popupView.measuredHeight
|
||||
val popupLeft = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
|
||||
paddingLeft + thumbWidth + popupLayoutParams.leftMargin
|
||||
scrollerPadding.left + thumbWidth + popupLayoutParams.leftMargin
|
||||
} 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
|
||||
|
@ -280,8 +293,8 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
|||
|
||||
val popupTop = MathUtils.clamp(
|
||||
thumbTop + thumbAnchorY - popupAnchorY,
|
||||
paddingTop + popupLayoutParams.topMargin,
|
||||
height - paddingBottom - popupLayoutParams.bottomMargin - popupHeight
|
||||
scrollerPadding.top + popupLayoutParams.topMargin,
|
||||
height - scrollerPadding.bottom - popupLayoutParams.bottomMargin - popupHeight
|
||||
)
|
||||
|
||||
popupView.layout(
|
||||
|
@ -310,6 +323,17 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
|||
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() {
|
||||
if (!canScroll() || childCount == 0) {
|
||||
return
|
||||
|
@ -348,7 +372,7 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
|||
val scrollOffset = paddingTop + (itemPos * itemHeight) - itemTop
|
||||
|
||||
// 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:
|
||||
// [proportion of scroll position to scroll range] * [total thumb range]
|
||||
|
@ -375,7 +399,7 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
|||
if (isInViewTouchTarget(thumbView, eventX, eventY)) {
|
||||
dragStartThumbOffset = thumbOffset
|
||||
} else {
|
||||
dragStartThumbOffset = (eventY - paddingTop - thumbHeight / 2f).toInt()
|
||||
dragStartThumbOffset = (eventY - scrollerPadding.top - thumbHeight / 2f).toInt()
|
||||
scrollToThumbOffset(dragStartThumbOffset)
|
||||
}
|
||||
|
||||
|
@ -392,7 +416,7 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
|||
dragStartThumbOffset = thumbOffset
|
||||
} else {
|
||||
dragStartY = eventY
|
||||
dragStartThumbOffset = (eventY - paddingTop - thumbHeight / 2f).toInt()
|
||||
dragStartThumbOffset = (eventY - scrollerPadding.top - thumbHeight / 2f).toInt()
|
||||
scrollToThumbOffset(dragStartThumbOffset)
|
||||
}
|
||||
setDragging(true)
|
||||
|
@ -577,7 +601,7 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
|||
|
||||
private val thumbOffsetRange: Int
|
||||
get() {
|
||||
return height - paddingTop - paddingBottom - thumbHeight
|
||||
return height - scrollerPadding.top - scrollerPadding.bottom - thumbHeight
|
||||
}
|
||||
|
||||
private val itemCount: Int
|
||||
|
|
|
@ -31,7 +31,6 @@ import org.oxycblt.auxio.home.HomeViewModel
|
|||
import org.oxycblt.auxio.music.BaseModel
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.memberBinding
|
||||
import org.oxycblt.auxio.util.applyEdgeRespectingBar
|
||||
import org.oxycblt.auxio.util.applySpans
|
||||
|
||||
/**
|
||||
|
@ -67,7 +66,6 @@ abstract class HomeListFragment : Fragment() {
|
|||
adapter = homeAdapter
|
||||
setHasFixedSize(true)
|
||||
applySpans()
|
||||
applyEdgeRespectingBar(playbackModel, viewLifecycleOwner)
|
||||
}
|
||||
|
||||
// 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.playback.state.LoopMode
|
||||
import org.oxycblt.auxio.ui.memberBinding
|
||||
import org.oxycblt.auxio.util.applyEdge
|
||||
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.
|
||||
|
@ -60,11 +60,15 @@ class PlaybackFragment : Fragment() {
|
|||
binding.playbackModel = playbackModel
|
||||
binding.detailModel = detailModel
|
||||
|
||||
binding.applyEdge { bars ->
|
||||
binding.root.setOnApplyWindowInsetsListener { v, insets ->
|
||||
val bars = insets.systemBarsCompat
|
||||
|
||||
binding.root.updatePadding(
|
||||
top = bars.top,
|
||||
bottom = bars.bottom
|
||||
)
|
||||
|
||||
insets
|
||||
}
|
||||
|
||||
binding.playbackToolbar.apply {
|
||||
|
|
|
@ -22,14 +22,12 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
||||
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
|
||||
|
@ -58,11 +56,6 @@ class QueueFragment : Fragment() {
|
|||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
binding.applyEdge { bars ->
|
||||
binding.queueAppbar.updatePadding(top = bars.top)
|
||||
binding.queueRecycler.updatePadding(bottom = bars.bottom)
|
||||
}
|
||||
|
||||
binding.queueToolbar.setNavigationOnClickListener {
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.Fragment
|
||||
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.ui.DisplayMode
|
||||
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.getSystemServiceSafe
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -77,10 +74,6 @@ class SearchFragment : Fragment() {
|
|||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
binding.applyEdge { bars ->
|
||||
binding.searchAppbar.updatePadding(top = bars.top)
|
||||
}
|
||||
|
||||
binding.searchToolbar.apply {
|
||||
val itemId = when (searchModel.filterMode) {
|
||||
DisplayMode.SHOW_SONGS -> R.id.option_filter_songs
|
||||
|
@ -128,8 +121,6 @@ class SearchFragment : Fragment() {
|
|||
applySpans { pos ->
|
||||
searchAdapter.currentList[pos] is Header
|
||||
}
|
||||
|
||||
applyEdgeRespectingBar(playbackModel, viewLifecycleOwner)
|
||||
}
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
|
|
|
@ -36,9 +36,9 @@ import org.oxycblt.auxio.BuildConfig
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentAboutBinding
|
||||
import org.oxycblt.auxio.home.HomeViewModel
|
||||
import org.oxycblt.auxio.util.applyEdge
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.showToast
|
||||
import org.oxycblt.auxio.util.systemBarsCompat
|
||||
|
||||
/**
|
||||
* A [BottomSheetDialogFragment] that shows Auxio's about screen.
|
||||
|
@ -54,9 +54,9 @@ class AboutFragment : Fragment() {
|
|||
): View {
|
||||
val binding = FragmentAboutBinding.inflate(layoutInflater)
|
||||
|
||||
binding.applyEdge { bars ->
|
||||
binding.aboutAppbar.updatePadding(top = bars.top)
|
||||
binding.aboutContents.updatePadding(bottom = bars.bottom)
|
||||
binding.aboutContents.setOnApplyWindowInsetsListener { v, insets ->
|
||||
binding.aboutContents.updatePadding(bottom = insets.systemBarsCompat.bottom)
|
||||
insets
|
||||
}
|
||||
|
||||
binding.aboutToolbar.setNavigationOnClickListener {
|
||||
|
|
|
@ -22,11 +22,9 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.oxycblt.auxio.databinding.FragmentSettingsBinding
|
||||
import org.oxycblt.auxio.util.applyEdge
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
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.IntListPreference
|
||||
import org.oxycblt.auxio.settings.tabs.TabCustomizeDialog
|
||||
import org.oxycblt.auxio.util.applyEdge
|
||||
import org.oxycblt.auxio.util.isNight
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.showToast
|
||||
import org.oxycblt.auxio.util.systemBarsCompat
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
clipToPadding = false
|
||||
|
||||
applyEdge { bars ->
|
||||
updatePadding(bottom = bars.bottom)
|
||||
setOnApplyWindowInsetsListener { v, insets ->
|
||||
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.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.WindowInsets
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
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
|
||||
|
@ -61,6 +64,20 @@ class LiftAppBarLayout @JvmOverloads constructor(
|
|||
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() {
|
||||
super.onDetachedFromWindow()
|
||||
|
||||
|
|
|
@ -32,14 +32,10 @@ import androidx.annotation.ColorInt
|
|||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply edge-to-edge tweaks to the root of a [ViewBinding].
|
||||
* @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 {
|
||||
val WindowInsets.systemBarsCompat: Rect get() {
|
||||
return when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
||||
setOnApplyWindowInsetsListener { _, insets ->
|
||||
val bars = insets.getInsets(WindowInsets.Type.systemBars()).run {
|
||||
Rect(left, top, right, bottom)
|
||||
}
|
||||
|
||||
onApply(bars)
|
||||
|
||||
insets
|
||||
getInsets(WindowInsets.Type.systemBars()).run {
|
||||
Rect(left, top, right, bottom)
|
||||
}
|
||||
}
|
||||
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> {
|
||||
setOnApplyWindowInsetsListener { _, insets ->
|
||||
@Suppress("DEPRECATION")
|
||||
val bars = Rect(
|
||||
insets.systemWindowInsetLeft,
|
||||
insets.systemWindowInsetTop,
|
||||
insets.systemWindowInsetRight,
|
||||
insets.systemWindowInsetBottom
|
||||
)
|
||||
|
||||
onApply(bars)
|
||||
insets
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
Rect(
|
||||
systemWindowInsetLeft,
|
||||
systemWindowInsetTop,
|
||||
systemWindowInsetRight,
|
||||
systemWindowInsetBottom
|
||||
)
|
||||
}
|
||||
|
||||
// Not on a version that supports edge-to-edge [yet], don't do anything
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
else -> Rect(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,10 +41,14 @@ private fun createViews(
|
|||
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_artist, state.song.album.artist.resolvedName)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
private fun RemoteViews.applyCover(context: Context, state: WidgetState): RemoteViews {
|
||||
if (state.albumArt != null) {
|
||||
setImageViewBitmap(R.id.widget_cover, state.albumArt)
|
||||
setContentDescription(
|
||||
|
@ -54,9 +58,10 @@ private fun RemoteViews.applyMeta(context: Context, state: WidgetState) {
|
|||
setImageViewResource(R.id.widget_cover, R.drawable.ic_song)
|
||||
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(
|
||||
R.id.widget_skip_prev,
|
||||
context.newBroadcastIntent(
|
||||
|
@ -86,53 +91,21 @@ private fun RemoteViews.applyControls(context: Context, state: WidgetState) {
|
|||
R.drawable.ic_play
|
||||
}
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
fun createDefaultWidget(context: Context): RemoteViews {
|
||||
return createViews(context, R.layout.widget_default)
|
||||
}
|
||||
private fun RemoteViews.applyFullControls(context: Context, state: WidgetState): RemoteViews {
|
||||
applyControls(context, state)
|
||||
|
||||
fun createTinyWidget(context: Context, state: WidgetState): RemoteViews {
|
||||
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(
|
||||
setOnClickPendingIntent(
|
||||
R.id.widget_loop,
|
||||
context.newBroadcastIntent(
|
||||
PlaybackService.ACTION_LOOP
|
||||
)
|
||||
)
|
||||
|
||||
views.setOnClickPendingIntent(
|
||||
setOnClickPendingIntent(
|
||||
R.id.widget_shuffle,
|
||||
context.newBroadcastIntent(
|
||||
PlaybackService.ACTION_SHUFFLE
|
||||
|
@ -154,8 +127,46 @@ fun createLargeWidget(context: Context, state: WidgetState): RemoteViews {
|
|||
LoopMode.TRACK -> R.drawable.ic_loop_one
|
||||
}
|
||||
|
||||
views.setImageViewResource(R.id.widget_shuffle, shuffleRes)
|
||||
views.setImageViewResource(R.id.widget_loop, loopRes)
|
||||
setImageViewResource(R.id.widget_shuffle, shuffleRes)
|
||||
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:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_medium"
|
||||
android:layout_marginEnd="@dimen/spacing_medium"
|
||||
app:layout_constraintBottom_toTopOf="@+id/detail_subhead"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
android:background="?attr/colorSurface"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
<org.oxycblt.auxio.ui.LiftAppBarLayout
|
||||
android:id="@+id/about_appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -22,7 +22,7 @@
|
|||
style="@style/Widget.Auxio.Toolbar.Icon.Down"
|
||||
app:title="@string/lbl_about" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
</org.oxycblt.auxio.ui.LiftAppBarLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/about_contents"
|
||||
|
|
|
@ -4,32 +4,39 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
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_height="match_parent">
|
||||
|
||||
<org.oxycblt.auxio.ui.LiftAppBarLayout
|
||||
android:id="@+id/detail_appbar"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurface"
|
||||
app:liftOnScroll="true"
|
||||
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/detail_toolbar"
|
||||
style="@style/Widget.Auxio.Toolbar.Icon" />
|
||||
<org.oxycblt.auxio.ui.LiftAppBarLayout
|
||||
android:id="@+id/detail_appbar"
|
||||
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
|
||||
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" />
|
||||
</org.oxycblt.auxio.ui.LiftAppBarLayout>
|
||||
|
||||
</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>
|
|
@ -47,16 +47,23 @@
|
|||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||
tools:layout="@layout/fragment_home_list" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/home_fab"
|
||||
<org.oxycblt.auxio.home.FloatingActionButtonContainer
|
||||
android:id="@+id/home_fab_container"
|
||||
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: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>
|
||||
</layout>
|
|
@ -8,6 +8,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="88dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||
tools:listitem="@layout/item_artist" />
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".MainFragment">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/main_layout"
|
||||
<org.oxycblt.auxio.playback.PlaybackBarLayout
|
||||
android:id="@+id/main_bar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true"
|
||||
|
@ -15,20 +15,9 @@
|
|||
android:id="@+id/explore_nav_host"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="match_parent"
|
||||
app:navGraph="@navigation/nav_explore"
|
||||
tools:layout="@layout/fragment_home" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
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>
|
||||
</org.oxycblt.auxio.playback.PlaybackBarLayout>
|
||||
</layout>
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
</org.oxycblt.auxio.ui.LiftAppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
<org.oxycblt.auxio.ui.EdgeRecyclerView
|
||||
android:id="@+id/queue_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
</org.oxycblt.auxio.ui.LiftAppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
<org.oxycblt.auxio.ui.EdgeRecyclerView
|
||||
android:id="@+id/search_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_medium"
|
||||
android:layout_marginEnd="@dimen/spacing_medium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
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="colorPrimary">?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>
|
||||
</style>
|
||||
</resources>
|
|
@ -60,7 +60,7 @@
|
|||
<item name="colorSurface">@android:color/system_accent1_50</item>
|
||||
<item name="colorPrimary">?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>
|
||||
</style>
|
||||
</resources>
|
|
@ -4,4 +4,9 @@
|
|||
<attr name="entries" format="reference" />
|
||||
<attr name="entryValues" format="reference" />
|
||||
</declare-styleable>
|
||||
|
||||
<attr format="enum" name="layout_role">
|
||||
<enum name="content" value="0" />
|
||||
<enum name="floating" value="1" />
|
||||
</attr>
|
||||
</resources>
|
Loading…
Reference in a new issue