Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,48 +29,31 @@ import androidx.media3.common.util.Log
import android.view.View
import android.view.WindowInsets
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.BackEventCompat
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.graphics.Insets
import androidx.core.view.HapticFeedbackConstantsCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.doOnLayout
import androidx.core.view.doOnNextLayout
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.findFragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.session.MediaController
import androidx.preference.PreferenceManager
import coil3.asDrawable
import coil3.imageLoader
import coil3.request.Disposable
import coil3.request.ImageRequest
import coil3.request.error
import coil3.size.Scale
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
import com.google.android.material.button.MaterialButton
import com.google.android.material.motion.MaterialBottomContainerBackHelper
import org.akanework.gramophone.BuildConfig
import org.akanework.gramophone.R
import org.akanework.gramophone.logic.clone
import org.akanework.gramophone.logic.fadInAnimation
import org.akanework.gramophone.logic.fadOutAnimation
import org.akanework.gramophone.logic.getBooleanStrict
import org.akanework.gramophone.logic.playOrPause
import org.akanework.gramophone.logic.startAnimation
import org.akanework.gramophone.logic.ui.MyBottomSheetBehavior
import org.akanework.gramophone.ui.MainActivity


class PlayerBottomSheet private constructor(
context: Context, attributeSet: AttributeSet?, defStyleAttr: Int, defStyleRes: Int
) : FrameLayout(context, attributeSet, defStyleAttr, defStyleRes),
Expand All @@ -87,7 +70,7 @@ class PlayerBottomSheet private constructor(
private var lyricsBackHelper: MaterialBottomContainerBackHelper? = null
private var bottomSheetBackCallback: OnBackPressedCallback? = null
val fullPlayer: FullBottomSheet
private val previewPlayer: View
val previewPlayer: View

private val activity
get() = context as MainActivity
Expand Down Expand Up @@ -131,7 +114,8 @@ class PlayerBottomSheet private constructor(
}
}

activity.controllerViewModel.addRecreationalPlayerListener(activity.lifecycle, this) {
activity.controllerViewModel.addControllerCallback(activity.lifecycle) { _, _ ->
instance?.addListener(this@PlayerBottomSheet)
onMediaItemTransition(
instance?.currentMediaItem,
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ package org.akanework.gramophone.ui.components

import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.ImageView
import android.widget.TextView
import android.view.View
import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.HapticFeedbackConstantsCompat
import androidx.core.view.ViewCompat
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.session.MediaController
import androidx.viewpager2.widget.ViewPager2
import coil3.asDrawable
import coil3.imageLoader
import coil3.request.Disposable
Expand All @@ -25,93 +27,173 @@ import org.akanework.gramophone.logic.startAnimation
import org.akanework.gramophone.ui.MainActivity

class PreviewBottomSheet(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) :
ConstraintLayout(context, attrs, defStyleAttr, defStyleRes), Player.Listener {
private val activity
get() = context as MainActivity
private val instance: MediaController?
get() = activity.getPlayer()
private val bottomSheetPreviewCover: ImageView
private val bottomSheetPreviewTitle: TextView
private val bottomSheetPreviewSubtitle: TextView
private val bottomSheetPreviewControllerButton: MaterialButton
private val bottomSheetPreviewNextButton: MaterialButton
private var lastDisposable: Disposable? = null

constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
this(context, attrs, defStyleAttr, 0)

constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)

init {
inflate(context, R.layout.preview_player, this)
bottomSheetPreviewTitle = findViewById(R.id.preview_song_name)
bottomSheetPreviewSubtitle = findViewById(R.id.preview_artist_name)
bottomSheetPreviewCover = findViewById(R.id.preview_album_cover)
bottomSheetPreviewControllerButton = findViewById(R.id.preview_control)
bottomSheetPreviewNextButton = findViewById(R.id.preview_next)

bottomSheetPreviewControllerButton.setOnClickListener {
ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK)
instance?.playOrPause()
}

bottomSheetPreviewNextButton.setOnClickListener {
ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK)
instance?.seekToNext()
}

activity.controllerViewModel.addRecreationalPlayerListener(activity.lifecycle, this) {
onPlaybackStateChanged(instance?.playbackState ?: Player.STATE_IDLE)
onMediaItemTransition(
instance?.currentMediaItem,
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED
)
}
}

override fun onIsPlayingChanged(isPlaying: Boolean) {
onPlaybackStateChanged(instance?.playbackState ?: Player.STATE_IDLE)
}

override fun onPlaybackStateChanged(playbackState: Int) {
if (playbackState == Player.STATE_BUFFERING) return
val myTag = bottomSheetPreviewControllerButton.getTag(R.id.play_next) as Int?
if (instance?.isPlaying == true && myTag != 1) {
bottomSheetPreviewControllerButton.icon =
AppCompatResources.getDrawable(context, R.drawable.play_anim)
bottomSheetPreviewControllerButton.icon.startAnimation()
bottomSheetPreviewControllerButton.setTag(R.id.play_next, 1)
} else if (instance?.isPlaying == false && myTag != 2) {
bottomSheetPreviewControllerButton.icon =
AppCompatResources.getDrawable(context, R.drawable.pause_anim)
bottomSheetPreviewControllerButton.icon.startAnimation()
bottomSheetPreviewControllerButton.setTag(R.id.play_next, 2)
}
}

override fun onMediaItemTransition(
mediaItem: MediaItem?,
reason: @Player.MediaItemTransitionReason Int
) {
if ((instance?.mediaItemCount ?: 0) > 0) {
lastDisposable?.dispose()
lastDisposable = context.imageLoader.enqueue(ImageRequest.Builder(context).apply {
target(onSuccess = {
bottomSheetPreviewCover.setImageDrawable(it.asDrawable(context.resources))
}, onError = {
bottomSheetPreviewCover.setImageDrawable(it?.asDrawable(context.resources))
}) // do not react to onStart() which sets placeholder
data(mediaItem?.mediaMetadata?.artworkUri)
scale(Scale.FILL)
allowHardware(bottomSheetPreviewCover.isHardwareAccelerated)
error(R.drawable.ic_default_cover)
}.build())
bottomSheetPreviewTitle.text = mediaItem?.mediaMetadata?.title
bottomSheetPreviewSubtitle.text =
mediaItem?.mediaMetadata?.artist ?: context.getString(R.string.unknown_artist)
} else {
lastDisposable?.dispose()
lastDisposable = null
}
}
ConstraintLayout(context, attrs, defStyleAttr, defStyleRes), Player.Listener {
private val activity
get() = context as MainActivity
private val instance: MediaController?
get() = activity.getPlayer()
private val bottomSheetPreviewCover: ImageView
private val bottomSheetPreviewControllerButton: MaterialButton
private val bottomSheetPreviewNextButton: MaterialButton
private val pager: ViewPager2
private var pagerAllowLeftSwipe = true
private var pagerAllowRightSwipe = true
private val adapter: PreviewPlayerPagerAdapter
private var titles = listOf("", "", "")
private var artists = listOf("", "", "")
private var lastDisposable: Disposable? = null
private var userScrollInProgress = false

constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
this(context, attrs, defStyleAttr, 0)

constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)

init {
inflate(context, R.layout.preview_player, this)
bottomSheetPreviewCover = findViewById(R.id.preview_album_cover)
bottomSheetPreviewControllerButton = findViewById(R.id.preview_control)
bottomSheetPreviewNextButton = findViewById(R.id.preview_next)
bottomSheetPreviewControllerButton.setOnClickListener {
ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK)
instance?.playOrPause()
}

bottomSheetPreviewNextButton.setOnClickListener {
ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK)
instance?.seekToNext()
}

pager = findViewById(R.id.preview_player_pager)
adapter = PreviewPlayerPagerAdapter(context)
pager.adapter = adapter
updatePages()
pager.setCurrentItem(1, false)

pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrollStateChanged(state: Int) {
super.onPageScrollStateChanged(state)
when (state) {
ViewPager2.SCROLL_STATE_DRAGGING -> userScrollInProgress = true
ViewPager2.SCROLL_STATE_IDLE -> {
if (userScrollInProgress) {
userScrollInProgress = false
handlePageSettled()
}
}
}
}
})

pager.getChildAt(0).setOnTouchListener(object: View.OnTouchListener {
var initX = 0f

override fun onTouch(v: View, event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> initX = event.x
MotionEvent.ACTION_MOVE -> {
val diff = event.x - initX
if (diff > 0 && !pagerAllowLeftSwipe) return true
if (diff < 0 && !pagerAllowRightSwipe) return true
}
}
return false
}
})

activity.controllerViewModel.addControllerCallback(activity.lifecycle) { _, _ ->
instance?.addListener(this@PreviewBottomSheet)
onPlaybackStateChanged(instance?.playbackState ?: Player.STATE_IDLE)
onMediaItemTransition(
instance?.currentMediaItem,
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED
)
}
}

private fun handlePageSettled() {
when (pager.currentItem) {
0 -> {
if (instance?.hasPreviousMediaItem() == true){
ViewCompat.performHapticFeedback(activity.playerBottomSheet.previewPlayer, HapticFeedbackConstantsCompat.CONTEXT_CLICK)
instance?.seekToPrevious()
}
}
2 -> {
if (instance?.hasNextMediaItem() == true){
ViewCompat.performHapticFeedback(activity.playerBottomSheet.previewPlayer, HapticFeedbackConstantsCompat.CONTEXT_CLICK)
instance?.seekToNext()
}
}
else -> return
}
pager.setCurrentItem(1, false)
updatePages()
}

private fun updatePages() {
adapter.updateData(
titles,
artists
)

pagerAllowRightSwipe = (instance?.hasNextMediaItem() == true)
pagerAllowLeftSwipe = (instance?.hasPreviousMediaItem() == true)
}

override fun onIsPlayingChanged(isPlaying: Boolean) {
onPlaybackStateChanged(instance?.playbackState ?: Player.STATE_IDLE)
}

override fun onPlaybackStateChanged(playbackState: Int) {
if (playbackState == Player.STATE_BUFFERING) return
val myTag = bottomSheetPreviewControllerButton.getTag(R.id.play_next) as Int?
if (instance?.isPlaying == true && myTag != 1) {
bottomSheetPreviewControllerButton.icon =
AppCompatResources.getDrawable(context, R.drawable.play_anim)
bottomSheetPreviewControllerButton.icon.startAnimation()
bottomSheetPreviewControllerButton.setTag(R.id.play_next, 1)
} else if (instance?.isPlaying == false && myTag != 2) {
bottomSheetPreviewControllerButton.icon =
AppCompatResources.getDrawable(context, R.drawable.pause_anim)
bottomSheetPreviewControllerButton.icon.startAnimation()
bottomSheetPreviewControllerButton.setTag(R.id.play_next, 2)
}
}

override fun onMediaItemTransition(
mediaItem: MediaItem?,
reason: @Player.MediaItemTransitionReason Int
) {
if ((instance?.mediaItemCount ?: 0) > 0) {
lastDisposable?.dispose()
lastDisposable = context.imageLoader.enqueue(ImageRequest.Builder(context).apply {
target(onSuccess = {
bottomSheetPreviewCover.setImageDrawable(it.asDrawable(context.resources))
}, onError = {
bottomSheetPreviewCover.setImageDrawable(it?.asDrawable(context.resources))
}) // do not react to onStart() which sets placeholder
data(mediaItem?.mediaMetadata?.artworkUri)
scale(Scale.FILL)
allowHardware(bottomSheetPreviewCover.isHardwareAccelerated)
error(R.drawable.ic_default_cover)
}.build())
val prevIndex = (instance?.currentMediaItemIndex?.minus(1) ?: 0).coerceIn(0, instance?.mediaItemCount)
val nextIndex = (instance?.currentMediaItemIndex?.plus(1) ?: 0).coerceIn(0, instance?.mediaItemCount)
titles = listOf(
instance?.getMediaItemAt(prevIndex)?.mediaMetadata?.title?.toString() ?: context.getString(R.string.unknown_title),
mediaItem?.mediaMetadata?.title?.toString() ?: context.getString(R.string.unknown_title),
instance?.getMediaItemAt(nextIndex)?.mediaMetadata?.title?.toString() ?: context.getString(R.string.unknown_title),
)
artists = listOf(
instance?.getMediaItemAt(prevIndex)?.mediaMetadata?.artist?.toString() ?: context.getString(R.string.unknown_artist),
mediaItem?.mediaMetadata?.artist?.toString() ?: context.getString(R.string.unknown_artist),
instance?.getMediaItemAt(nextIndex)?.mediaMetadata?.artist?.toString() ?: context.getString(R.string.unknown_artist),
)
updatePages()
} else {
lastDisposable?.dispose()
lastDisposable = null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.akanework.gramophone.ui.components

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.akanework.gramophone.R

class PreviewPlayerPagerAdapter (private val context: Context) :
RecyclerView.Adapter<PreviewPlayerPagerAdapter.ViewHolder>() {

private var titles = listOf<String>()
private var artists = listOf<String>()

class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val title: MarqueeTextView = view.findViewById(R.id.preview_song_name)
val artist: MarqueeTextView = view.findViewById(R.id.preview_artist_name)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.preview_player_item, parent, false)
return ViewHolder(view)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.title.text = titles[position]
holder.artist.text = artists[position]
}

override fun getItemCount() = titles.size

fun updateData(newTitles: List<String>, newSubtitles: List<String>) {
titles = newTitles
artists = newSubtitles
notifyDataSetChanged()
}
}
Loading