Merge branch 'ImproveCurrentPlayingUI' into 'develop'

Draft: improve feedback of currentPlayingLayout

See merge request ultrasonic/ultrasonic!1047
This commit is contained in:
Maxmystere 2023-10-08 13:30:07 +00:00
commit e967139f94
3 changed files with 123 additions and 70 deletions

View File

@ -73,6 +73,7 @@ import org.moire.ultrasonic.util.ShortcutUtil
import org.moire.ultrasonic.util.Storage import org.moire.ultrasonic.util.Storage
import org.moire.ultrasonic.util.UncaughtExceptionHandler import org.moire.ultrasonic.util.UncaughtExceptionHandler
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.util.Util.ifNotNull
import timber.log.Timber import timber.log.Timber
/** /**
@ -179,11 +180,7 @@ class NavigationActivity : AppCompatActivity() {
currentFragmentId = destination.id currentFragmentId = destination.id
// Handle the hiding of the NowPlaying fragment when the Player is active // Handle the hiding of the NowPlaying fragment when the Player is active
if (currentFragmentId == R.id.playerFragment) { computeNowPlayingVisibility()
hideNowPlaying()
} else {
if (!nowPlayingHidden) showNowPlaying()
}
} }
// Determine if this is a first run // Determine if this is a first run
@ -199,15 +196,16 @@ class NavigationActivity : AppCompatActivity() {
Util.ensurePermissionToPostNotification(this) Util.ensurePermissionToPostNotification(this)
rxBusSubscription += RxBus.dismissNowPlayingCommandObservable.subscribe { rxBusSubscription += RxBus.dismissNowPlayingCommandObservable.subscribe {
nowPlayingHidden = true computeNowPlayingVisibility(false)
hideNowPlaying()
} }
rxBusSubscription += RxBus.playerStateObservable.subscribe { rxBusSubscription += RxBus.playerStateObservable.subscribe {
if (it.state == STATE_READY) // If state is ready then nowPlaying will be visible again
showNowPlaying() if (it.state == STATE_READY) {
else computeNowPlayingVisibility(true)
hideNowPlaying() } else {
computeNowPlayingVisibility()
}
} }
rxBusSubscription += RxBus.themeChangedEventObservable.subscribe { rxBusSubscription += RxBus.themeChangedEventObservable.subscribe {
@ -314,8 +312,7 @@ class NavigationActivity : AppCompatActivity() {
// Lifecycle support's constructor registers some event receivers so it should be created early // Lifecycle support's constructor registers some event receivers so it should be created early
lifecycleSupport.onCreate() lifecycleSupport.onCreate()
if (!nowPlayingHidden) showNowPlaying() computeNowPlayingVisibility()
else hideNowPlaying()
} }
/* /*
@ -555,9 +552,14 @@ class NavigationActivity : AppCompatActivity() {
} }
} }
private fun showNowPlaying() { private fun computeNowPlayingVisibility(forceNewVisibility: Boolean? = null) {
forceNewVisibility.ifNotNull { nowPlayingHidden = !it }
if (!Settings.showNowPlaying) { if (!Settings.showNowPlaying) {
hideNowPlaying() nowPlayingView?.visibility = View.GONE
return
}
if (nowPlayingHidden) {
nowPlayingView?.visibility = View.GONE
return return
} }
@ -566,7 +568,7 @@ class NavigationActivity : AppCompatActivity() {
nowPlayingHidden = false nowPlayingHidden = false
// Do not show for Player fragment // Do not show for Player fragment
if (currentFragmentId == R.id.playerFragment) { if (currentFragmentId == R.id.playerFragment) {
hideNowPlaying() nowPlayingView?.visibility = View.GONE
return return
} }
@ -578,15 +580,11 @@ class NavigationActivity : AppCompatActivity() {
nowPlayingView?.visibility = View.VISIBLE nowPlayingView?.visibility = View.VISIBLE
} }
} else { } else {
hideNowPlaying() nowPlayingView?.visibility = View.GONE
} }
} }
} }
private fun hideNowPlaying() {
nowPlayingView?.visibility = View.GONE
}
private fun setMenuForServerCapabilities() { private fun setMenuForServerCapabilities() {
val isOnline = !ActiveServerProvider.isOffline() val isOnline = !ActiveServerProvider.isOffline()
val activeServer = activeServerProvider.getActiveServer() val activeServer = activeServerProvider.getActiveServer()

View File

@ -10,18 +10,19 @@ package org.moire.ultrasonic.fragment
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.navigation.Navigation import androidx.navigation.Navigation
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import java.lang.Exception
import kotlin.math.abs
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
@ -38,9 +39,88 @@ import timber.log.Timber
* Contains the mini-now playing information box displayed at the bottom of the screen * Contains the mini-now playing information box displayed at the bottom of the screen
*/ */
class NowPlayingFragment : Fragment() { class NowPlayingFragment : Fragment() {
private var isInitialized = false
private lateinit var nowPlayingCollectionAdapter: NowPlayingCollectionAdapter
private lateinit var viewPager: ViewPager2
private var rxBusSubscription: Disposable? = null
private var downX = 0f private val mediaPlayerManager: MediaPlayerManager by inject()
private var downY = 0f private val pageChangeCallback = PageChangeCallback()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.now_playing_wrapper, container, false)
}
override fun onResume() {
super.onResume()
if (mediaPlayerManager.currentMediaItemIndex >= 0)
viewPager.setCurrentItem(mediaPlayerManager.currentMediaItemIndex, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
nowPlayingCollectionAdapter = NowPlayingCollectionAdapter(this, mediaPlayerManager)
viewPager = view.findViewById(R.id.pager)
viewPager.currentItem = mediaPlayerManager.currentMediaItemIndex
viewPager.adapter = nowPlayingCollectionAdapter
isInitialized = false
// Subscribe to updates on current Item
rxBusSubscription = RxBus.playerStateObservable.subscribe {
if (it.state == Player.STATE_READY) {
viewPager.setCurrentItem(it.index, true)
isInitialized = true
}
}
viewPager.registerOnPageChangeCallback(pageChangeCallback)
}
override fun onDestroy() {
super.onDestroy()
rxBusSubscription?.dispose()
viewPager.unregisterOnPageChangeCallback(pageChangeCallback)
}
private inner class PageChangeCallback : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
if (!isInitialized) return
val newIndex = mediaPlayerManager.getUnshuffledIndexOf(position)
if (mediaPlayerManager.currentMediaItemIndex != newIndex) {
mediaPlayerManager.seekTo(newIndex, 0)
}
}
}
}
class NowPlayingCollectionAdapter(
fragment: Fragment,
private val playerManager: MediaPlayerManager
) : FragmentStateAdapter(fragment) {
override fun getItemCount(): Int {
return playerManager.mediaItemCount
}
override fun getItemId(position: Int): Long {
return playerManager.getMediaItemAt(playerManager.getUnshuffledIndexOf(position))
.hashCode().toLong()
}
override fun createFragment(position: Int): Fragment {
// Return a NEW fragment instance in createFragment(int)
val mediaItem = playerManager.getMediaItemAt(playerManager.getUnshuffledIndexOf(position))
return NowPlayingChildFragment(mediaItem)
}
}
/**
* Contains the mini-now playing information box displayed at the bottom of the screen
*/
class NowPlayingChildFragment(private val mediaItem: MediaItem?) : Fragment() {
private var playButton: MaterialButton? = null private var playButton: MaterialButton? = null
private var nowPlayingAlbumArtImage: ImageView? = null private var nowPlayingAlbumArtImage: ImageView? = null
@ -79,7 +159,7 @@ class NowPlayingFragment : Fragment() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
rxBusSubscription!!.dispose() rxBusSubscription?.dispose()
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
@ -91,7 +171,7 @@ class NowPlayingFragment : Fragment() {
playButton!!.setIconResource(R.drawable.media_start) playButton!!.setIconResource(R.drawable.media_start)
} }
val file = mediaPlayerManager.currentMediaItem?.toTrack() val file = mediaItem?.toTrack()
if (file != null) { if (file != null) {
val title = file.title val title = file.title
@ -121,53 +201,14 @@ class NowPlayingFragment : Fragment() {
} }
} }
requireView().setOnTouchListener { _: View?, event: MotionEvent -> requireView().setOnClickListener {
handleOnTouch(event) Navigation.findNavController(requireActivity(), R.id.nav_host_fragment)
.navigate(R.id.playerFragment)
} }
// This empty onClickListener is necessary for the onTouchListener to work
requireView().setOnClickListener { }
playButton!!.setOnClickListener { mediaPlayerManager.togglePlayPause() } playButton!!.setOnClickListener { mediaPlayerManager.togglePlayPause() }
} catch (all: Exception) { } catch (all: Exception) {
Timber.w(all, "Failed to get notification cover art") Timber.w(all, "Failed to get notification cover art")
} }
} }
private fun handleOnTouch(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
downX = event.x
downY = event.y
}
MotionEvent.ACTION_UP -> {
val upX = event.x
val upY = event.y
val deltaX = downX - upX
val deltaY = downY - upY
if (abs(deltaX) > MIN_DISTANCE) {
// left or right
if (deltaX < 0) {
mediaPlayerManager.seekToPrevious()
}
if (deltaX > 0) {
mediaPlayerManager.seekToNext()
}
} else if (abs(deltaY) > MIN_DISTANCE) {
if (deltaY < 0) {
RxBus.dismissNowPlayingCommandPublisher.onNext(Unit)
}
} else {
Navigation.findNavController(requireActivity(), R.id.nav_host_fragment)
.navigate(R.id.playerFragment)
}
}
}
return false
}
companion object {
private const val MIN_DISTANCE = 30
}
} }

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
a:id="@+id/now_playing_wrapper"
a:layout_width="fill_parent"
a:layout_height="wrap_content">
<androidx.viewpager2.widget.ViewPager2
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>