Use MotionLayout to animate bottom sheet opening

This commit is contained in:
Christophe Henry 2023-09-24 11:26:52 +02:00
parent b924a0c655
commit 056e3a4d66
12 changed files with 271 additions and 136 deletions

View File

@ -181,7 +181,7 @@ dependencies {
implementation("androidx.recyclerview:recyclerview:1.2.1") implementation("androidx.recyclerview:recyclerview:1.2.1")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("com.google.android.material:material:1.8.0") implementation("com.google.android.material:material:1.8.0")
implementation("com.android.support.constraint:constraint-layout:2.0.4") implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("com.google.android.exoplayer:exoplayer-core:2.18.1") implementation("com.google.android.exoplayer:exoplayer-core:2.18.1")
implementation("com.google.android.exoplayer:exoplayer-ui:2.18.1") implementation("com.google.android.exoplayer:exoplayer-ui:2.18.1")

View File

@ -3,6 +3,7 @@ package audio.funkwhale.ffa.activities
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Fragment
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -49,7 +50,6 @@ import audio.funkwhale.ffa.utils.authorize
import audio.funkwhale.ffa.utils.log import audio.funkwhale.ffa.utils.log
import audio.funkwhale.ffa.utils.logError import audio.funkwhale.ffa.utils.logError
import audio.funkwhale.ffa.utils.mustNormalizeUrl import audio.funkwhale.ffa.utils.mustNormalizeUrl
import audio.funkwhale.ffa.utils.onApi
import audio.funkwhale.ffa.utils.toast import audio.funkwhale.ffa.utils.toast
import audio.funkwhale.ffa.utils.wait import audio.funkwhale.ffa.utils.wait
import com.github.kittinunf.fuel.Fuel import com.github.kittinunf.fuel.Fuel
@ -60,10 +60,6 @@ import com.google.gson.Gson
import com.preference.PowerPreference import com.preference.PowerPreference
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.channels.consume
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.java.KoinJavaComponent.inject import org.koin.java.KoinJavaComponent.inject
@ -87,32 +83,28 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
AppContext.init(this) AppContext.init(this)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
binding.nowPlayingBottomSheet.addBottomSheetCallback( (supportFragmentManager.findFragmentById(R.id.now_playing) as NowPlayingFragment).apply {
object : BottomSheetBehavior.BottomSheetCallback() { onDetailsMenuItemClicked { binding.nowPlayingBottomSheet.close() }
override fun onStateChanged(bottomSheet: View, newState: Int) { binding.nowPlayingBottomSheet.addBottomSheetCallback(
// Set the proper margin on the other child object : BottomSheetBehavior.BottomSheetCallback() {
val anim = if (newState == BottomSheetBehavior.STATE_HIDDEN) { override fun onStateChanged(bottomSheet: View, newState: Int) {
ValueAnimator.ofInt(binding.nowPlayingBottomSheet.peekHeight, 0) // Add padding to the main fragment so that player control don't overlap
} else { // artists and albums
ValueAnimator.ofInt(0, binding.nowPlayingBottomSheet.peekHeight) addSiblingFragmentPadding()
} }
anim.apply { override fun onSlide(bottomSheet: View, slideOffset: Float) {
duration = 200 // Animate the cover and other elements of the bottom sheet
addUpdateListener { onBottomSheetDrag(slideOffset)
val params =
binding.navHostFragmentWrapper.layoutParams as CoordinatorLayout.LayoutParams
params.setMargins(0, 0, 0, it.animatedValue as Int)
}
start()
} }
} }
)
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {} addSiblingFragmentPadding()
}
)
setContentView(binding.root) setContentView(binding.root)
@ -132,8 +124,11 @@ class MainActivity : AppCompatActivity() {
lifecycleScope.launch { lifecycleScope.launch {
RequestBus.send(Request.GetQueue).wait<Response.Queue>()?.let { RequestBus.send(Request.GetQueue).wait<Response.Queue>()?.let {
if(it.queue.isNotEmpty()) binding.nowPlayingBottomSheet.show() if (it.queue.isNotEmpty() && binding.nowPlayingBottomSheet.isHidden) {
else binding.nowPlayingBottomSheet.hide() binding.nowPlayingBottomSheet.show()
} else if (it.queue.isEmpty()) {
binding.nowPlayingBottomSheet.hide()
}
} }
// Watch the event bus only after to prevent concurrency in displaying the bottom sheet // Watch the event bus only after to prevent concurrency in displaying the bottom sheet
watchEventBus() watchEventBus()
@ -265,6 +260,20 @@ class MainActivity : AppCompatActivity() {
return true return true
} }
private fun addSiblingFragmentPadding() {
val anim = if (binding.nowPlayingBottomSheet.isHidden) {
ValueAnimator.ofInt(binding.nowPlayingBottomSheet.peekHeight, 0)
} else {
ValueAnimator.ofInt(0, binding.nowPlayingBottomSheet.peekHeight)
}
anim.duration = 200
anim.addUpdateListener {
binding.navHostFragmentWrapper.setPadding(0, 0, 0, it.animatedValue as Int)
}
anim.start()
}
private fun launchDialog(fragment: DialogFragment) = private fun launchDialog(fragment: DialogFragment) =
fragment.show(supportFragmentManager.beginTransaction(), "") fragment.show(supportFragmentManager.beginTransaction(), "")
@ -297,7 +306,7 @@ class MainActivity : AppCompatActivity() {
CommandBus.get().flowWithLifecycle( CommandBus.get().flowWithLifecycle(
this@MainActivity.lifecycle, Lifecycle.State.RESUMED this@MainActivity.lifecycle, Lifecycle.State.RESUMED
).collect { command -> ).collect { command ->
when(command) { when (command) {
is Command.StartService -> startService(command.command) is Command.StartService -> startService(command.command)
is Command.RefreshTrack -> refreshTrack(command.track) is Command.RefreshTrack -> refreshTrack(command.track)
is Command.AddToPlaylist -> AddToPlaylistDialog.show( is Command.AddToPlaylist -> AddToPlaylistDialog.show(
@ -306,6 +315,7 @@ class MainActivity : AppCompatActivity() {
lifecycleScope, lifecycleScope,
command.tracks command.tracks
) )
else -> {} else -> {}
} }
} }

View File

@ -18,7 +18,6 @@ import audio.funkwhale.ffa.model.Track
import audio.funkwhale.ffa.repositories.FavoritedRepository import audio.funkwhale.ffa.repositories.FavoritedRepository
import audio.funkwhale.ffa.repositories.FavoritesRepository import audio.funkwhale.ffa.repositories.FavoritesRepository
import audio.funkwhale.ffa.repositories.Repository import audio.funkwhale.ffa.repositories.Repository
import audio.funkwhale.ffa.utils.BottomSheetIneractable
import audio.funkwhale.ffa.utils.Command import audio.funkwhale.ffa.utils.Command
import audio.funkwhale.ffa.utils.CommandBus import audio.funkwhale.ffa.utils.CommandBus
import audio.funkwhale.ffa.utils.CoverArt import audio.funkwhale.ffa.utils.CoverArt
@ -30,24 +29,17 @@ import audio.funkwhale.ffa.utils.maybeNormalizeUrl
import audio.funkwhale.ffa.utils.toIntOrElse import audio.funkwhale.ffa.utils.toIntOrElse
import audio.funkwhale.ffa.utils.untilNetwork import audio.funkwhale.ffa.utils.untilNetwork
import audio.funkwhale.ffa.viewmodel.NowPlayingViewModel import audio.funkwhale.ffa.viewmodel.NowPlayingViewModel
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.lang.Float.max
class NowPlayingFragment : Fragment(R.layout.fragment_now_playing) { class NowPlayingFragment: Fragment(R.layout.fragment_now_playing) {
private val binding by lazy { FragmentNowPlayingBinding.bind(requireView()) } private val binding by lazy { FragmentNowPlayingBinding.bind(requireView()) }
private val viewModel by viewModels<NowPlayingViewModel>() private val viewModel by viewModels<NowPlayingViewModel>()
private val favoriteRepository by lazy { FavoritesRepository(requireContext()) } private val favoriteRepository by lazy { FavoritesRepository(requireContext()) }
private val favoritedRepository by lazy { FavoritedRepository(requireContext()) } private val favoritedRepository by lazy { FavoritedRepository(requireContext()) }
private val bottomSheet: BottomSheetIneractable? by lazy { private var onDetailsMenuItemClickedCb: () -> Unit = {}
var view = this.view?.parent
while (view != null) {
if(view is BottomSheetIneractable) return@lazy view
view = view.parent
}
null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.lifecycleOwner = viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner
@ -83,7 +75,10 @@ class NowPlayingFragment : Fragment(R.layout.fragment_now_playing) {
nowPlayingDetailsAddToPlaylist.setOnClickListener { onAddToPlaylist() } nowPlayingDetailsAddToPlaylist.setOnClickListener { onAddToPlaylist() }
} }
binding.nowPlayingDetailsInfo.setOnClickListener { openInfoMenu() }
with(binding.header) { with(binding.header) {
lifecycleOwner = viewLifecycleOwner
isBuffering = viewModel.isBuffering isBuffering = viewModel.isBuffering
isPlaying = viewModel.isPlaying isPlaying = viewModel.isPlaying
progress = viewModel.progress progress = viewModel.progress
@ -100,8 +95,6 @@ class NowPlayingFragment : Fragment(R.layout.fragment_now_playing) {
} }
} }
binding.nowPlayingDetailsInfo.setOnClickListener { openInfoMenu() }
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
CommandBus.get().collect { onCommand(it) } CommandBus.get().collect { onCommand(it) }
} }
@ -115,6 +108,14 @@ class NowPlayingFragment : Fragment(R.layout.fragment_now_playing) {
} }
} }
fun onBottomSheetDrag(value: Float) {
binding.nowPlayingRoot.progress = max(value, 0f)
}
fun onDetailsMenuItemClicked(cb: () -> Unit) {
onDetailsMenuItemClickedCb = cb
}
private fun toggleRepeatMode() { private fun toggleRepeatMode() {
val cachedRepeatMode = FFACache.getLine(requireContext(), "repeat").toIntOrElse(0) val cachedRepeatMode = FFACache.getLine(requireContext(), "repeat").toIntOrElse(0)
@ -170,19 +171,10 @@ class NowPlayingFragment : Fragment(R.layout.fragment_now_playing) {
private fun onTrackChange(track: Track?) { private fun onTrackChange(track: Track?) {
if (track == null) { if (track == null) {
binding.header.nowPlayingCover.setImageResource(R.drawable.cover) binding.header.nowPlayingCover.setImageResource(R.drawable.cover)
binding.nowPlayingDetailCover.setImageResource(R.drawable.cover)
return return
} }
CoverArt.withContext(requireContext(), maybeNormalizeUrl(track.album?.cover())) CoverArt.withContext(requireContext(), maybeNormalizeUrl(track.album?.cover()))
.fit()
.centerCrop()
.into(binding.nowPlayingDetailCover)
CoverArt.withContext(requireContext(), maybeNormalizeUrl(track.album?.cover()))
.fit()
.centerCrop()
.transform(RoundedCornersTransformation(16, 0))
.into(binding.header.nowPlayingCover) .into(binding.header.nowPlayingCover)
} }
@ -199,7 +191,7 @@ class NowPlayingFragment : Fragment(R.layout.fragment_now_playing) {
inflate(R.menu.track_info) inflate(R.menu.track_info)
setOnMenuItemClickListener { setOnMenuItemClickListener {
bottomSheet?.close() onDetailsMenuItemClickedCb()
when (it.itemId) { when (it.itemId) {
R.id.track_info_artist -> findNavController().navigate( R.id.track_info_artist -> findNavController().navigate(

View File

@ -2,20 +2,21 @@ package audio.funkwhale.ffa.views
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.util.TypedValue
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.res.use import androidx.core.content.res.use
import audio.funkwhale.ffa.R import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.utils.BottomSheetIneractable import audio.funkwhale.ffa.utils.BottomSheetIneractable
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
import com.google.android.material.card.MaterialCardView
class NowPlayingBottomSheet @JvmOverloads constructor( class NowPlayingBottomSheet @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MaterialCardView(context, attrs), BottomSheetIneractable { ) : FrameLayout(context, attrs, defStyleAttr), BottomSheetIneractable {
private val behavior = BottomSheetBehavior<NowPlayingBottomSheet>() private val behavior = BottomSheetBehavior<NowPlayingBottomSheet>()
private val targetHeaderId: Int private val targetHeaderId: Int
@ -27,6 +28,14 @@ class NowPlayingBottomSheet @JvmOverloads constructor(
).use { ).use {
it.getResourceId(R.styleable.NowPlaying_target_header, NO_ID) it.getResourceId(R.styleable.NowPlaying_target_header, NO_ID)
} }
// Put default peek height to actionBarSize so it is not 0
val tv = TypedValue()
if (context.theme.resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
behavior.peekHeight = TypedValue.complexToDimensionPixelSize(
tv.data, resources.displayMetrics
)
}
} }
override fun setLayoutParams(params: ViewGroup.LayoutParams?) { override fun setLayoutParams(params: ViewGroup.LayoutParams?) {
@ -37,7 +46,7 @@ class NowPlayingBottomSheet @JvmOverloads constructor(
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec) super.onMeasure(widthMeasureSpec, heightMeasureSpec)
findViewById<View>(targetHeaderId)?.apply { findViewById<View>(targetHeaderId)?.apply {
behavior.setPeekHeight(this.measuredHeight, false) behavior.setPeekHeight(this.height, false)
this.setOnClickListener { this@NowPlayingBottomSheet.toggle() } this.setOnClickListener { this@NowPlayingBottomSheet.toggle() }
} ?: hide() } ?: hide()
} }

View File

@ -2,8 +2,25 @@ package audio.funkwhale.ffa.views
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View
import androidx.appcompat.widget.AppCompatImageButton
import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatImageView
open class SquareView : View {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, style: Int) : super(context, attrs, style)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val dimension = if(measuredWidth == 0 && measuredHeight > 0) measuredHeight else measuredWidth
setMeasuredDimension(dimension, dimension)
}
}
open class SquareImageView : AppCompatImageView { open class SquareImageView : AppCompatImageView {
constructor(context: Context) : super(context) constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

View File

@ -9,9 +9,8 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:background="@color/surface"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_weight="10"> app:layout_constraintBottom_toTopOf="@id/appbar_wrapper">
<LinearLayout <LinearLayout
android:id="@+id/nav_host_fragment_wrapper" android:id="@+id/nav_host_fragment_wrapper"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -38,21 +37,13 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:layout="@layout/partial_queue" /> tools:layout="@layout/partial_queue" />
</LinearLayout> </LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/appbar_wrapper"
app:layout_constraintTop_toTopOf="parent">
<audio.funkwhale.ffa.views.NowPlayingBottomSheet <audio.funkwhale.ffa.views.NowPlayingBottomSheet
android:id="@+id/now_playing_bottom_sheet" android:id="@+id/now_playing_bottom_sheet"
style="?attr/bottomSheetStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:cardCornerRadius="3dp" android:background="@color/elevatedSurface"
app:cardElevation="12dp" app:target_header="@id/constraint_layout_placeholder">
app:target_header="@id/header">
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:id="@+id/now_playing" android:id="@+id/now_playing"
android:name="audio.funkwhale.ffa.fragments.NowPlayingFragment" android:name="audio.funkwhale.ffa.fragments.NowPlayingFragment"

View File

@ -3,56 +3,63 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
<data>
<import type="androidx.lifecycle.LiveData" />
<import type="android.view.View" />
<import type="android.graphics.drawable.Drawable" />
<variable name="isBuffering" type="LiveData&lt;Boolean>" />
<variable name="isPlaying" type="LiveData&lt;Boolean>" />
<variable name="progress" type="LiveData&lt;Integer>" />
<variable name="currentTrackTitle" type="LiveData&lt;String>" />
<variable name="currentTrackArtist" type="LiveData&lt;String>" />
</data>
<androidx.constraintlayout.motion.widget.MotionLayout
android:id="@+id/now_playing_root" android:id="@+id/now_playing_root"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/elevatedSurface"> app:layoutDescription="@xml/fragment_now_playing_scene">
<include <include android:id="@+id/header" layout="@layout/partial_now_playing_header" />
android:id="@+id/header"
layout="@layout/partial_now_playing_header" />
<audio.funkwhale.ffa.views.SquareImageView <audio.funkwhale.ffa.views.SquareView
android:id="@+id/now_playing_detail_cover" android:id="@+id/detail_image_placeholder"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:scaleType="fitCenter" app:layout_constraintTop_toBottomOf="@id/now_playing_progress"
app:layout_constraintTop_toBottomOf="@id/header"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@drawable/cover" />
tools:src="@tools:sample/avatars" />
<ImageButton <ImageButton
android:id="@+id/now_playing_details_info" android:id="@+id/now_playing_details_info"
style="@style/IconButton" style="@style/IconButton"
android:layout_width="32dp" android:layout_width="32dp"
android:layout_height="32dp" android:layout_height="32dp"
android:layout_gravity="top|end"
android:layout_margin="8dp" android:layout_margin="8dp"
android:background="@drawable/circle" android:background="@drawable/circle"
android:contentDescription="@string/alt_track_info" android:contentDescription="@string/alt_track_info"
android:src="@drawable/more" android:src="@drawable/more"
app:layout_constraintEnd_toEndOf="@id/now_playing_detail_cover" app:layout_constraintEnd_toEndOf="@id/detail_image_placeholder"
app:layout_constraintTop_toTopOf="@id/now_playing_detail_cover" app:layout_constraintTop_toTopOf="@id/detail_image_placeholder"
app:tint="@color/controlForeground" app:tint="@color/controlForeground"
/> />
<include <include
android:id="@+id/controls" android:id="@+id/controls"
layout="@layout/partial_now_playing_controls" layout="@layout/partial_now_playing_controls"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginStart="4dp" android:layout_marginStart="8dp"
android:layout_marginEnd="4dp" android:layout_marginEnd="8dp"
app:layout_constraintTop_toBottomOf="@id/header" app:layout_constraintTop_toTopOf="@id/detail_image_placeholder"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/now_playing_detail_cover" app:layout_constraintStart_toEndOf="@id/detail_image_placeholder"
android:alpha="0"
android:background="@color/elevatedSurface"
/> />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.motion.widget.MotionLayout>
</layout> </layout>

View File

@ -11,11 +11,11 @@
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/appbar_wrapper"> app:layout_constraintBottom_toTopOf="@id/appbar_wrapper">
<FrameLayout <FrameLayout
android:id="@+id/nav_host_fragment_wrapper"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
> android:id="@+id/nav_host_fragment_wrapper">
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment" android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment" android:name="androidx.navigation.fragment.NavHostFragment"
@ -30,18 +30,17 @@
<audio.funkwhale.ffa.views.NowPlayingBottomSheet <audio.funkwhale.ffa.views.NowPlayingBottomSheet
android:id="@+id/now_playing_bottom_sheet" android:id="@+id/now_playing_bottom_sheet"
style="?attr/bottomSheetStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:cardCornerRadius="3dp" android:background="@color/elevatedSurface"
app:cardElevation="12dp" app:target_header="@id/constraint_layout_placeholder">
app:target_header="@id/header">
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:id="@+id/now_playing" android:id="@+id/now_playing"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:name="audio.funkwhale.ffa.fragments.NowPlayingFragment" android:name="audio.funkwhale.ffa.fragments.NowPlayingFragment"
tools:layout="@layout/fragment_now_playing"/> tools:layout="@layout/fragment_now_playing"
/>
</audio.funkwhale.ffa.views.NowPlayingBottomSheet> </audio.funkwhale.ffa.views.NowPlayingBottomSheet>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,25 +1,35 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout <layout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout <data>
<import type="androidx.lifecycle.LiveData" />
<import type="android.view.View" />
<import type="android.graphics.drawable.Drawable" />
<variable name="isBuffering" type="LiveData&lt;Boolean>" />
<variable name="isPlaying" type="LiveData&lt;Boolean>" />
<variable name="progress" type="LiveData&lt;Integer>" />
<variable name="currentTrackTitle" type="LiveData&lt;String>" />
<variable name="currentTrackArtist" type="LiveData&lt;String>" />
</data>
<androidx.constraintlayout.motion.widget.MotionLayout
android:id="@+id/now_playing_root" android:id="@+id/now_playing_root"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/elevatedSurface"> app:layoutDescription="@xml/fragment_now_playing_scene">
<include <include
android:id="@+id/header" android:id="@+id/header"
layout="@layout/partial_now_playing_header" /> layout="@layout/partial_now_playing_header"
/>
<audio.funkwhale.ffa.views.SquareImageView <audio.funkwhale.ffa.views.SquareView
android:id="@+id/now_playing_detail_cover" android:id="@+id/detail_image_placeholder"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@+id/header" app:layout_constraintTop_toBottomOf="@id/now_playing_progress"
app:srcCompat="@drawable/cover"
tools:src="@tools:sample/avatars"
/> />
<ImageButton <ImageButton
@ -27,23 +37,23 @@
android:layout_width="32dp" android:layout_width="32dp"
android:layout_height="32dp" android:layout_height="32dp"
android:layout_margin="8dp" android:layout_margin="8dp"
app:layout_constraintEnd_toEndOf="@id/now_playing_detail_cover" app:layout_constraintEnd_toEndOf="@id/detail_image_placeholder"
app:layout_constraintTop_toTopOf="@id/now_playing_detail_cover" app:layout_constraintTop_toTopOf="@id/detail_image_placeholder"
style="@style/IconButton" style="@style/IconButton"
android:layout_gravity="top|end"
android:background="@drawable/circle" android:background="@drawable/circle"
android:contentDescription="@string/alt_track_info" android:contentDescription="@string/alt_track_info"
android:src="@drawable/more" android:src="@drawable/more"
app:tint="@color/controlForeground" /> app:tint="@color/controlForeground"
/>
<include <include
android:id="@+id/controls" android:id="@+id/controls"
layout="@layout/partial_now_playing_controls" layout="@layout/partial_now_playing_controls"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="0dp"
android:layout_margin="8dp" android:layout_margin="8dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/now_playing_detail_cover" app:layout_constraintTop_toBottomOf="@id/detail_image_placeholder"
/> />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.motion.widget.MotionLayout>
</layout> </layout>

View File

@ -16,9 +16,11 @@
<variable name="isPlaying" type="LiveData&lt;Boolean>" /> <variable name="isPlaying" type="LiveData&lt;Boolean>" />
<variable name="progress" type="LiveData&lt;Integer>" /> <variable name="progress" type="LiveData&lt;Integer>" />
</data> </data>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:alpha="0">
<TextView <TextView
android:id="@+id/current_playing_details_title" android:id="@+id/current_playing_details_title"
android:layout_width="0dp" android:layout_width="0dp"
@ -84,8 +86,8 @@
android:id="@+id/now_playing_details_progress" android:id="@+id/now_playing_details_progress"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:max="100"
android:layout_margin="8dp" android:layout_margin="8dp"
android:max="100"
android:progress="@{progress, default=40}" android:progress="@{progress, default=40}"
android:progressBackgroundTint="#cacaca" android:progressBackgroundTint="#cacaca"
android:progressTint="@color/controlForeground" android:progressTint="@color/controlForeground"

View File

@ -14,29 +14,34 @@
<variable name="currentTrackArtist" type="LiveData&lt;String>" /> <variable name="currentTrackArtist" type="LiveData&lt;String>" />
</data> </data>
<androidx.constraintlayout.widget.ConstraintLayout <merge>
android:layout_width="match_parent" <!-- Placeholder for setting constraints and interacting -->
android:layout_height="?attr/actionBarSize"> <View
android:id="@+id/constraint_layout_placeholder"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_constraintTop_toTopOf="parent"
/>
<com.google.android.material.progressindicator.LinearProgressIndicator <com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/now_playing_progress" android:id="@+id/now_playing_progress"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="@id/constraint_layout_placeholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:progress="@{progress, default=40}" android:progress="@{progress, default=40}"
android:progressTint="@color/colorPrimaryDark" /> android:progressTint="@color/colorPrimaryDark"
/>
<audio.funkwhale.ffa.views.SquareImageView <audio.funkwhale.ffa.views.SquareImageView
android:id="@+id/now_playing_cover" android:id="@+id/now_playing_cover"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="@id/constraint_layout_placeholder"
app:layout_constraintTop_toBottomOf="@+id/now_playing_progress" app:layout_constraintTop_toBottomOf="@id/now_playing_progress"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="@id/constraint_layout_placeholder"
app:srcCompat="@drawable/cover" app:srcCompat="@drawable/cover"
tools:src="@tools:sample/avatars" /> tools:src="@tools:sample/avatars"
/>
<ProgressBar <ProgressBar
android:id="@+id/now_playing_buffering" android:id="@+id/now_playing_buffering"
@ -46,9 +51,8 @@
app:layout_constraintTop_toTopOf="@id/now_playing_cover" app:layout_constraintTop_toTopOf="@id/now_playing_cover"
app:layout_constraintBottom_toBottomOf="@id/now_playing_cover" app:layout_constraintBottom_toBottomOf="@id/now_playing_cover"
app:layout_constraintEnd_toEndOf="@id/now_playing_cover" app:layout_constraintEnd_toEndOf="@id/now_playing_cover"
android:indeterminate="true" android:visibility="@{isBuffering ? View.VISIBLE : View.INVISIBLE, default=invisible}"
android:indeterminateTint="@color/controlForeground" />
android:visibility="@{isBuffering ? View.VISIBLE : View.INVISIBLE, default=invisible}" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/header_controls" android:id="@+id/header_controls"
@ -56,9 +60,10 @@
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintHorizontal_weight="10" app:layout_constraintHorizontal_weight="10"
app:layout_constraintStart_toEndOf="@id/now_playing_cover" app:layout_constraintStart_toEndOf="@id/now_playing_cover"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="@id/constraint_layout_placeholder"
app:layout_constraintTop_toBottomOf="@id/now_playing_progress" app:layout_constraintTop_toBottomOf="@id/now_playing_progress"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="@id/constraint_layout_placeholder"
android:background="@color/elevatedSurface"
android:padding="4dp"> android:padding="4dp">
<TextView <TextView
@ -71,7 +76,8 @@
android:text="@{currentTrackTitle}" android:text="@{currentTrackTitle}"
android:ellipsize="end" android:ellipsize="end"
android:lines="1" android:lines="1"
tools:text="Supermassive Black Hole" /> tools:text="Supermassive Black Hole"
/>
<TextView <TextView
android:layout_width="0dp" android:layout_width="0dp"
@ -82,7 +88,8 @@
android:ellipsize="end" android:ellipsize="end"
android:lines="1" android:lines="1"
android:text="@{currentTrackArtist}" android:text="@{currentTrackArtist}"
tools:text="Muse" /> tools:text="Muse"
/>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/now_playing_toggle" android:id="@+id/now_playing_toggle"
@ -91,7 +98,8 @@
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_constraintEnd_toStartOf="@id/now_playing_next" app:layout_constraintEnd_toStartOf="@id/now_playing_next"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
app:icon="@{isPlaying ? @drawable/pause : @drawable/play, default=@drawable/play}" /> app:icon="@{isPlaying ? @drawable/pause : @drawable/play, default=@drawable/play}"
/>
<ImageButton <ImageButton
android:id="@+id/now_playing_next" android:id="@+id/now_playing_next"
@ -100,7 +108,9 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
style="@style/IconButton" style="@style/IconButton"
android:contentDescription="@string/control_next" android:contentDescription="@string/control_next"
android:src="@drawable/next" /> android:src="@drawable/next"
/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </merge>
</layout> </layout>

View File

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<MotionScene
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
<ConstraintSet android:id="@+id/start">
<Constraint android:id="@id/now_playing_details_info">
<PropertySet android:alpha="0" android:visibility="invisible" />
</Constraint>
<Constraint android:id="@id/header_controls">
<PropertySet android:alpha="1" android:visibility="visible" />
</Constraint>
<Constraint android:id="@id/constraint_layout_placeholder">
<PropertySet android:visibility="visible" />
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@id/now_playing_cover"
motion:layout_constraintEnd_toEndOf="@id/detail_image_placeholder"
motion:layout_constraintStart_toStartOf="@id/detail_image_placeholder"
motion:layout_constraintTop_toBottomOf="@id/detail_image_placeholder"
motion:layout_constraintTop_toTopOf="@id/detail_image_placeholder"
motion:transitionEasing="accelerate"
/>
<Constraint android:id="@id/now_playing_progress">
<PropertySet android:alpha="0" android:visibility="gone" />
</Constraint>
<Constraint android:id="@id/header_controls">
<PropertySet android:alpha="0" android:visibility="invisible" />
</Constraint>
<Constraint android:id="@id/constraint_layout_placeholder">
<PropertySet android:visibility="invisible" />
</Constraint>
<Constraint android:id="@id/now_playing_details_info">
<PropertySet android:alpha="1" android:visibility="visible"/>
</Constraint>
<Constraint android:id="@id/controls">
<PropertySet android:alpha="1" />
</Constraint>
</ConstraintSet>
<Transition
motion:constraintSetEnd="@id/end"
motion:constraintSetStart="@+id/start"
>
<KeyFrameSet>
<KeyPosition
motion:percentX="1"
motion:framePosition="50"
motion:motionTarget="@id/now_playing_cover"
motion:curveFit="spline"
/>
<KeyAttribute
android:alpha="0"
motion:framePosition="10"
motion:motionTarget="@id/header_controls"
/>
<KeyPosition
motion:percentX="1"
motion:framePosition="50"
motion:motionTarget="@id/header_controls"
motion:curveFit="spline"
/>
<KeyAttribute
android:alpha="0"
motion:framePosition="10"
motion:motionTarget="@id/now_playing_progress"
/>
<KeyAttribute
android:alpha="0"
motion:framePosition="90"
motion:motionTarget="@id/now_playing_details_info"
/>
<KeyAttribute
android:alpha="0"
motion:framePosition="90"
motion:motionTarget="@id/controls"
/>
</KeyFrameSet>
</Transition>
</MotionScene>