From a02aff41f153c7d4195bd3db8d245b310dcaea85 Mon Sep 17 00:00:00 2001 From: stevezmtstudios Date: Sun, 23 Aug 2026 00:24:32 +0800 Subject: [PATCH 1/4] feat(player): add rotate cookie button and cover swipe gestures --- .../ui/components/FullBottomSheet.kt | 116 ++++++++++++++++-- app/src/main/res/layout/full_player.xml | 64 ++++++---- app/src/main/res/xml/settings_player.xml | 18 +++ 3 files changed, 166 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/org/akanework/gramophone/ui/components/FullBottomSheet.kt b/app/src/main/java/org/akanework/gramophone/ui/components/FullBottomSheet.kt index 6b73482706..bdd5f64763 100644 --- a/app/src/main/java/org/akanework/gramophone/ui/components/FullBottomSheet.kt +++ b/app/src/main/java/org/akanework/gramophone/ui/components/FullBottomSheet.kt @@ -34,16 +34,20 @@ import android.os.Parcelable import android.text.format.DateFormat import android.util.AttributeSet import android.view.AbsSavedState +import android.view.GestureDetector import android.view.Gravity import android.view.KeyEvent +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.ViewPropertyAnimator import android.view.WindowInsets +import android.view.animation.LinearInterpolator import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.CheckBox import android.widget.EditText +import android.widget.ImageView import android.widget.LinearLayout import android.widget.ListView import android.widget.SeekBar @@ -57,6 +61,7 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.animation.addListener import androidx.core.animation.doOnEnd import androidx.core.content.edit +import kotlin.math.abs import androidx.core.graphics.Insets import androidx.core.graphics.TypefaceCompat import androidx.core.os.BundleCompat @@ -167,6 +172,8 @@ class FullBottomSheet private var runnableRunning = false private var firstTime = false private var enableQualityInfo = false + private var rotateCookieButton = false + private var buttonRotationAnimator: ValueAnimator? = null private val prefs = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) private var currentFormat: AudioFormatDetector.AudioFormats? = null @@ -252,6 +259,7 @@ class FullBottomSheet private val bottomSheetFullTitle: TextView private val bottomSheetFullSubtitle: TextView private val bottomSheetFullControllerButton: MaterialButton + private val bottomSheetFullControllerButtonBg: ImageView private val bottomSheetFullNextButton: MaterialButton private val bottomSheetFullPreviousButton: MaterialButton private val bottomSheetFullDuration: TextView @@ -272,14 +280,56 @@ class FullBottomSheet private val progressDrawable: SquigglyProgress private var pqs: PlaylistQueueSheet? = null + private var coverTouchStartX = 0f + private var coverTouchStartY = 0f + private var coverIsHorizontalSwipe = false + init { inflate(context, R.layout.full_player, this) bottomSheetFullCoverFrame = findViewById(R.id.album_cover_frame) bottomSheetFullCover = findViewById(R.id.full_sheet_cover) + bottomSheetFullCover.setOnTouchListener { v, event -> + val enabled = prefs.getBoolean("swipe_to_switch_track", true) + when (event.action) { + MotionEvent.ACTION_DOWN -> { + coverTouchStartX = event.rawX + coverTouchStartY = event.rawY + coverIsHorizontalSwipe = false + parent?.requestDisallowInterceptTouchEvent(true) + } + MotionEvent.ACTION_MOVE -> { + val dx = abs(event.rawX - coverTouchStartX) + val dy = abs(event.rawY - coverTouchStartY) + if (dx > 20.dpToPx(context) && dx > dy) { + coverIsHorizontalSwipe = true + parent?.requestDisallowInterceptTouchEvent(true) + } + } + MotionEvent.ACTION_UP -> { + val dx = event.rawX - coverTouchStartX + val dy = event.rawY - coverTouchStartY + if (enabled && abs(dx) > abs(dy) && abs(dx) > 50.dpToPx(context)) { + if (dx > 0) { + instance?.seekToPrevious() + } else { + instance?.seekToNext() + } + return@setOnTouchListener true + } else if (!coverIsHorizontalSwipe) { + v.performClick() + } + } + MotionEvent.ACTION_CANCEL -> { + coverIsHorizontalSwipe = false + } + } + true + } bottomSheetFullTitle = findViewById(R.id.full_song_name) bottomSheetFullSubtitle = findViewById(R.id.full_song_artist) bottomSheetFullPreviousButton = findViewById(R.id.sheet_previous_song) bottomSheetFullControllerButton = findViewById(R.id.sheet_mid_button) + bottomSheetFullControllerButtonBg = findViewById(R.id.sheet_mid_button_bg) bottomSheetFullNextButton = findViewById(R.id.sheet_next_song) bottomSheetFullPosition = findViewById(R.id.position) bottomSheetFullDuration = findViewById(R.id.duration) @@ -625,6 +675,11 @@ class FullBottomSheet bottomSheetFullSeekBar.progressTintList = ColorStateList.valueOf(colorPrimary) } + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + stopButtonRotation(false) + } + override fun onSaveInstanceState(): Parcelable { return Bundle().apply { putParcelable("Super", super.onSaveInstanceState()) @@ -711,6 +766,42 @@ class FullBottomSheet if (key == null || key == "cookie_cover") { bottomSheetFullCover.setClip(prefs.getBooleanStrict("cookie_cover", false)) } + if (key == null || key == "rotate_cookie_button") { + rotateCookieButton = prefs.getBooleanStrict("rotate_cookie_button", false) + if (!rotateCookieButton) { + stopButtonRotation(true) + } else if (instance?.isPlaying == true) { + startButtonRotation() + } + } + } + + private fun startButtonRotation() { + if (!rotateCookieButton || instance?.isPlaying != true) return + if (buttonRotationAnimator == null) { + buttonRotationAnimator = ValueAnimator.ofFloat( + bottomSheetFullControllerButtonBg.rotation, + bottomSheetFullControllerButtonBg.rotation + 360f + ).apply { + duration = 36000L + repeatCount = ValueAnimator.INFINITE + interpolator = LinearInterpolator() + addUpdateListener { animator -> + bottomSheetFullControllerButtonBg.rotation = animator.animatedValue as Float + } + start() + } + } else if (buttonRotationAnimator?.isRunning != true) { + buttonRotationAnimator?.start() + } + } + + private fun stopButtonRotation(reset: Boolean = false) { + buttonRotationAnimator?.cancel() + buttonRotationAnimator = null + if (reset) { + bottomSheetFullControllerButtonBg.rotation = 0f + } } private fun showPlaybackSpeedDialog() { @@ -1139,7 +1230,12 @@ class FullBottomSheet ) val secondaryContainerTransition = ValueAnimator.ofArgb( - bottomSheetFullControllerButton.backgroundTintList!!.defaultColor, + bottomSheetFullControllerButtonBg.imageTintList?.defaultColor + ?: MaterialColors.getColor( + ctx, + com.google.android.material.R.attr.colorSecondaryContainer, + -1 + ), colorSecondaryContainer ) @@ -1226,7 +1322,7 @@ class FullBottomSheet secondaryContainerTransition.apply { addUpdateListener { animation -> val progressColor = animation.animatedValue as Int - bottomSheetFullControllerButton.backgroundTintList = + bottomSheetFullControllerButtonBg.imageTintList = ColorStateList.valueOf(progressColor) } duration = BACKGROUND_COLOR_TRANSITION_SEC @@ -1350,7 +1446,7 @@ class FullBottomSheet bottomSheetFullSubtitle.setTextColor( colorSecondary ) - bottomSheetFullControllerButton.backgroundTintList = + bottomSheetFullControllerButtonBg.imageTintList = ColorStateList.valueOf(colorSecondaryContainer) bottomSheetFullControllerButton.iconTint = ColorStateList.valueOf(colorOnSecondaryContainer) @@ -1537,10 +1633,11 @@ class FullBottomSheet wrappedContext ?: context, R.drawable.play_anim ) - bottomSheetFullControllerButton.background = + bottomSheetFullControllerButtonBg.setImageDrawable( AppCompatResources.getDrawable(context, R.drawable.bg_play_anim) + ) bottomSheetFullControllerButton.icon.startAnimation() - bottomSheetFullControllerButton.background.startAnimation() + bottomSheetFullControllerButtonBg.drawable.startAnimation() bottomSheetFullControllerButton.setTag(R.id.play_next, 1) } if (!isUserTracking) { @@ -1551,6 +1648,9 @@ class FullBottomSheet handler.postDelayed(positionRunnable, SLIDER_UPDATE_INTERVAL) } bottomSheetFullCover.startRotation() + if (rotateCookieButton) { + startButtonRotation() + } } else if (playbackState != Player.STATE_BUFFERING) { if (bottomSheetFullControllerButton.getTag(R.id.play_next) as Int? != 2) { bottomSheetFullControllerButton.icon = @@ -1558,12 +1658,14 @@ class FullBottomSheet wrappedContext ?: context, R.drawable.pause_anim ) - bottomSheetFullControllerButton.background = + bottomSheetFullControllerButtonBg.setImageDrawable( AppCompatResources.getDrawable(context, R.drawable.bg_pause_anim) + ) bottomSheetFullControllerButton.icon.startAnimation() - bottomSheetFullControllerButton.background.startAnimation() + bottomSheetFullControllerButtonBg.drawable.startAnimation() bottomSheetFullControllerButton.setTag(R.id.play_next, 2) bottomSheetFullCover.stopRotation() + stopButtonRotation() } if (!isUserTracking) { progressDrawable.animate = false diff --git a/app/src/main/res/layout/full_player.xml b/app/src/main/res/layout/full_player.xml index 0fa6a1c4a3..2e5ae1899e 100644 --- a/app/src/main/res/layout/full_player.xml +++ b/app/src/main/res/layout/full_player.xml @@ -207,7 +207,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="36dp" - app:layout_constraintBottom_toTopOf="@id/sheet_mid_button" + app:layout_constraintBottom_toTopOf="@id/sheet_mid_button_frame" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/slider" @@ -261,29 +261,43 @@ - + app:layout_constraintTop_toBottomOf="@id/duration_frame"> + + + + + diff --git a/app/src/main/res/xml/settings_player.xml b/app/src/main/res/xml/settings_player.xml index f83d70ca24..33e2679833 100644 --- a/app/src/main/res/xml/settings_player.xml +++ b/app/src/main/res/xml/settings_player.xml @@ -85,6 +85,24 @@ android:title="@string/settings_cookie_info" android:widgetLayout="@layout/preference_switch_widget" /> + + + + Date: Sun, 23 Aug 2026 01:58:30 +0800 Subject: [PATCH 2/4] feat(player): add string resources for cookie rotation and cover swipe --- app/src/main/res/values-zh-rCN/strings.xml | 4 ++++ app/src/main/res/values/strings.xml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 9922260080..1ec7a629cf 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -161,6 +161,10 @@ 滚动到歌曲 关闭 跳到上一个 + 旋转播放按钮 + 播放时让饼干样式的播放/暂停按钮细微地转动 + 手势切歌 + 在播放器封面上左右滑动以切换上一曲/下一曲 位置滑块 实验性界面,显示逐字歌词,具有高级功能 返回 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7c3d235619..df0449eb0e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -239,6 +239,10 @@ Bold title Use bold title for full player + Rotate play button + Subtly rotate the cookie-style play/pause button while playing + Swipe to switch track + Swipe left or right on the album cover to switch tracks Experimental From 61dc1d09c9824eef128d4341f651ab850fddbaaf Mon Sep 17 00:00:00 2001 From: stevezmtstudios Date: Mon, 24 Aug 2026 00:10:03 +0800 Subject: [PATCH 3/4] fix(player): add landscape layout support and improve screen layout update link --- .../ui/components/FullBottomSheet.kt | 1190 ++++++++--------- .../res/layout-w600dp-land/full_player.xml | 64 +- 2 files changed, 577 insertions(+), 677 deletions(-) diff --git a/app/src/main/java/org/akanework/gramophone/ui/components/FullBottomSheet.kt b/app/src/main/java/org/akanework/gramophone/ui/components/FullBottomSheet.kt index bdd5f64763..2b71312ad0 100644 --- a/app/src/main/java/org/akanework/gramophone/ui/components/FullBottomSheet.kt +++ b/app/src/main/java/org/akanework/gramophone/ui/components/FullBottomSheet.kt @@ -259,7 +259,7 @@ class FullBottomSheet private val bottomSheetFullTitle: TextView private val bottomSheetFullSubtitle: TextView private val bottomSheetFullControllerButton: MaterialButton - private val bottomSheetFullControllerButtonBg: ImageView + private val bottomSheetFullControllerButtonBg: ImageView? private val bottomSheetFullNextButton: MaterialButton private val bottomSheetFullPreviousButton: MaterialButton private val bottomSheetFullDuration: TextView @@ -277,7 +277,7 @@ class FullBottomSheet private val bottomSheetFullSlider: Slider private val bottomSheetFullCoverFrame: MaterialCardView val bottomSheetFullLyricView: LyricsView by lazy { (parent as ViewGroup).findViewById(R.id.lyric_frame)!! } - private val progressDrawable: SquigglyProgress + private lateinit var progressDrawable: SquigglyProgress private var pqs: PlaylistQueueSheet? = null private var coverTouchStartX = 0f @@ -288,6 +288,74 @@ class FullBottomSheet inflate(context, R.layout.full_player, this) bottomSheetFullCoverFrame = findViewById(R.id.album_cover_frame) bottomSheetFullCover = findViewById(R.id.full_sheet_cover) + bottomSheetFullTitle = findViewById(R.id.full_song_name) + bottomSheetFullSubtitle = findViewById(R.id.full_song_artist) + bottomSheetFullPreviousButton = findViewById(R.id.sheet_previous_song) + bottomSheetFullControllerButton = findViewById(R.id.sheet_mid_button) + bottomSheetFullControllerButtonBg = findViewById(R.id.sheet_mid_button_bg) + bottomSheetFullNextButton = findViewById(R.id.sheet_next_song) + bottomSheetFullPosition = findViewById(R.id.position) + bottomSheetFullDuration = findViewById(R.id.duration) + bottomSheetFullSeekBar = findViewById(R.id.slider_squiggly) + bottomSheetFullSlider = findViewById(R.id.slider_vert) + bottomSheetFullSlideUpButton = findViewById(R.id.slide_down) + bottomSheetShuffleButton = findViewById(R.id.sheet_random) + bottomSheetLoopButton = findViewById(R.id.sheet_loop) + bottomSheetTimerButton = findViewById(R.id.timer) + bottomSheetPlaybackSpeedButton = findViewById(R.id.playback_speed) + bottomSheetFavoriteButton = findViewById(R.id.favor) + bottomSheetPlaylistButton = findViewById(R.id.playlist) + bottomSheetLyricButton = findViewById(R.id.lyrics) + bottomSheetFullQualityDetails = findViewById(R.id.quality_details) + + setupCoverTouchListener() + setupSeekBarAndSlider() + setupControlButtons() + setupCardAndDialogListeners() + setupCustomCommandListeners() + + refreshSettings(null) + prefs.registerOnSharedPreferenceChangeListener(this) + + val colorSecondaryContainer = MaterialColors.getColor( + context, + com.google.android.material.R.attr.colorSecondaryContainer, + -1 + ) + val colorSurface = MaterialColors.getColor( + context, + com.google.android.material.R.attr.colorSurface, + -1 + ) + val backgroundProcessedColor = ColorUtils.getColor( + colorSurface, + ColorUtils.ColorType.COLOR_BACKGROUND_ELEVATED, + context + ) + val colorContrastFainted = ColorUtils.getColor( + colorSecondaryContainer, + ColorUtils.ColorType.COLOR_CONTRAST_FAINTED, + context + ) + setBackgroundColor(backgroundProcessedColor) + bottomSheetFullSlider.trackInactiveTintList = ColorStateList.valueOf(colorContrastFainted) + + activity.controllerViewModel.addRecreationalPlayerListener(activity.lifecycle, this) { + firstTime = true + updateTimer() + onRepeatModeChanged(instance?.repeatMode ?: Player.REPEAT_MODE_OFF) + onShuffleModeEnabledChanged(instance?.shuffleModeEnabled == true) + onPlaybackStateChanged(instance?.playbackState ?: Player.STATE_IDLE) + onMediaItemTransition( + instance?.currentMediaItem, + Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED + ) + onMediaMetadataChanged(instance?.mediaMetadata ?: MediaMetadata.EMPTY) + firstTime = false + } + } + + private fun setupCoverTouchListener() { bottomSheetFullCover.setOnTouchListener { v, event -> val enabled = prefs.getBoolean("swipe_to_switch_track", true) when (event.action) { @@ -325,70 +393,17 @@ class FullBottomSheet } true } - bottomSheetFullTitle = findViewById(R.id.full_song_name) - bottomSheetFullSubtitle = findViewById(R.id.full_song_artist) - bottomSheetFullPreviousButton = findViewById(R.id.sheet_previous_song) - bottomSheetFullControllerButton = findViewById(R.id.sheet_mid_button) - bottomSheetFullControllerButtonBg = findViewById(R.id.sheet_mid_button_bg) - bottomSheetFullNextButton = findViewById(R.id.sheet_next_song) - bottomSheetFullPosition = findViewById(R.id.position) - bottomSheetFullDuration = findViewById(R.id.duration) - bottomSheetFullSeekBar = findViewById(R.id.slider_squiggly) - bottomSheetFullSlider = findViewById(R.id.slider_vert) - bottomSheetFullSlideUpButton = findViewById(R.id.slide_down) - bottomSheetShuffleButton = findViewById(R.id.sheet_random) - bottomSheetLoopButton = findViewById(R.id.sheet_loop) - bottomSheetTimerButton = findViewById(R.id.timer) - bottomSheetPlaybackSpeedButton = findViewById(R.id.playback_speed) - bottomSheetFavoriteButton = findViewById(R.id.favor) - bottomSheetPlaylistButton = findViewById(R.id.playlist) - bottomSheetLyricButton = findViewById(R.id.lyrics) - bottomSheetFullQualityDetails = findViewById(R.id.quality_details) - refreshSettings(null) - prefs.registerOnSharedPreferenceChangeListener(this) - activity.controllerViewModel.customCommandListeners.addCallback(activity.lifecycle) { _, command, _ -> - when (command.customAction) { - GramophonePlaybackService.SERVICE_TIMER_CHANGED -> updateTimer() - - GramophonePlaybackService.SERVICE_GET_LYRICS -> { - val parsedLyrics = instance?.getLyrics() - bottomSheetFullLyricView.updateLyrics(parsedLyrics) - } - - GramophonePlaybackService.SERVICE_GET_AUDIO_FORMAT -> { - val format = instance?.getAudioFormat() - this.currentFormat = format - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || - !handler.hasCallbacks(formatUpdateRunnable) - ) { - // TODO: is 300ms long enough wait for stuff like bitrate? 100ms isn't. - handler.postDelayed(formatUpdateRunnable, 300) - } - } - - else -> { - return@addCallback Futures.immediateFuture(SessionResult(SessionError.ERROR_NOT_SUPPORTED)) - } - } - return@addCallback Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) - } + } - val seekBarProgressWavelength = - context.resources - .getDimensionPixelSize(R.dimen.media_seekbar_progress_wavelength) - .toFloat() - val seekBarProgressAmplitude = - context.resources - .getDimensionPixelSize(R.dimen.media_seekbar_progress_amplitude) - .toFloat() - val seekBarProgressPhase = - context.resources - .getDimensionPixelSize(R.dimen.media_seekbar_progress_phase) - .toFloat() - val seekBarProgressStrokeWidth = - context.resources - .getDimensionPixelSize(R.dimen.media_seekbar_progress_stroke_width) - .toFloat() + private fun setupSeekBarAndSlider() { + val seekBarProgressWavelength = context.resources + .getDimensionPixelSize(R.dimen.media_seekbar_progress_wavelength).toFloat() + val seekBarProgressAmplitude = context.resources + .getDimensionPixelSize(R.dimen.media_seekbar_progress_amplitude).toFloat() + val seekBarProgressPhase = context.resources + .getDimensionPixelSize(R.dimen.media_seekbar_progress_phase).toFloat() + val seekBarProgressStrokeWidth = context.resources + .getDimensionPixelSize(R.dimen.media_seekbar_progress_stroke_width).toFloat() bottomSheetFullSeekBar.progressDrawable = SquigglyProgress().also { progressDrawable = it @@ -400,12 +415,88 @@ class FullBottomSheet it.animate = false } + bottomSheetFullSlider.addOnChangeListener { _, value, isUser -> + if (isUser) { + val dest = instance?.mediaMetadata?.durationMs + if (dest != null) { + bottomSheetFullPosition.text = + CalculationUtils.convertDurationToTimeStamp(value.toLong()) + } + } + } + bottomSheetFullSeekBar.setOnSeekBarChangeListener(touchListener) + bottomSheetFullSlider.addOnSliderTouchListener(touchListener) + } + + private fun setupControlButtons() { + bottomSheetFullControllerButton.setOnClickListener { + ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) + instance?.playOrPause() + } + bottomSheetFullPreviousButton.setOnClickListener { + ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) + instance?.seekToPrevious() + } + bottomSheetFullPreviousButton.setOnLongClickListener { + ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.LONG_PRESS) + instance?.seekBack() + true + } + bottomSheetFullNextButton.setOnClickListener { + ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) + instance?.seekToNext() + } + bottomSheetFullNextButton.setOnLongClickListener { + ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.LONG_PRESS) + instance?.seekForward() + true + } + bottomSheetShuffleButton.addOnCheckedChangeListener { _, isChecked -> + instance?.shuffleModeEnabled = isChecked + } + bottomSheetShuffleButton.setOnClickListener { + ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) + } + bottomSheetLoopButton.setOnClickListener { + ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) + instance?.repeatMode = when (instance?.repeatMode) { + Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ALL + Player.REPEAT_MODE_ALL -> Player.REPEAT_MODE_ONE + Player.REPEAT_MODE_ONE -> Player.REPEAT_MODE_OFF + else -> throw IllegalStateException() + } + } + bottomSheetPlaybackSpeedButton.setOnClickListener { + ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) + if (instance != null) showPlaybackSpeedDialog() + } + bottomSheetFavoriteButton.addOnCheckedChangeListener(this) + bottomSheetPlaylistButton.setOnClickListener { + ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) + if (instance != null) { + pqs = PlaylistQueueSheet(wrappedContext ?: context, activity).also { it.show() } + } + } + bottomSheetFullSlideUpButton.setOnClickListener { + ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) + minimize?.invoke() + } + bottomSheetLyricButton.setOnClickListener { + ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) + bottomSheetFullLyricView.fadInAnimation(LYRIC_FADE_TRANSITION_SEC) + } + bottomSheetTimerButton.setOnClickListener { + ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) + showTimerDialog() + } + } + + private fun setupCardAndDialogListeners() { bottomSheetFullCover.setOnClickListener { activity.startFragment(DetailDialogFragment()) { putString("Id", instance?.currentMediaItem?.mediaId) } } - bottomSheetFullTitle.setOnClickListener { minimize?.invoke() activity.startFragment(GeneralSubFragment()) { @@ -413,7 +504,6 @@ class FullBottomSheet putInt("Item", R.id.album) } } - if (Flags.FORMAT_INFO_DIALOG) { bottomSheetFullQualityDetails.setOnClickListener { MaterialAlertDialogBuilder(wrappedContext ?: context) @@ -426,7 +516,6 @@ class FullBottomSheet .show() } } - bottomSheetFullSubtitle.setOnClickListener { minimize?.invoke() activity.startFragment(ArtistSubFragment()) { @@ -434,213 +523,133 @@ class FullBottomSheet putInt("Item", R.id.artist) } } + } - bottomSheetTimerButton.setOnClickListener { - ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) - val t = instance?.getTimer() - val currentText = if (t?.first != null) context.getString(R.string.timer_expiry, - DateFormat.getTimeFormat(context).format(System.currentTimeMillis() + t.first!!) - ) else if (t?.second == true) context.getString(R.string.timer_expiry_end_of_this_song) - else null - if (currentText != null) { - val dialog = MaterialAlertDialogBuilder(wrappedContext ?: context) - .setTitle(R.string.timer) - .setView(R.layout.dialog_sleep_timer_active) - .setNeutralButton(R.string.unset) { _, _ -> - instance?.setTimer(0, false) - } - .setPositiveButton(android.R.string.ok) { _, _ -> } - .show() - dialog.findViewById(R.id.textView)!!.text = currentText - dialog.findViewById(R.id.checkBox)!!.let { - if (t!!.first == null) { - it.visibility = GONE - } else { - it.isChecked = t.second - it.setOnCheckedChangeListener { _, value -> - val newTime = instance?.getTimer() - instance?.setTimer(newTime?.first ?: 0, value) - } - } + private fun setupCustomCommandListeners() { + activity.controllerViewModel.customCommandListeners.addCallback(activity.lifecycle) { _, command, _ -> + when (command.customAction) { + GramophonePlaybackService.SERVICE_TIMER_CHANGED -> updateTimer() + GramophonePlaybackService.SERVICE_GET_LYRICS -> { + val parsedLyrics = instance?.getLyrics() + bottomSheetFullLyricView.updateLyrics(parsedLyrics) } - } else { - val dialog = MaterialAlertDialogBuilder(wrappedContext ?: context) - .setTitle(R.string.timer) - .setView(R.layout.dialog_sleep_timer) - .setNegativeButton(android.R.string.cancel) { _, _ -> } - .show() - val lv = dialog.findViewById(R.id.listView)!! - val checkbox = dialog.findViewById(R.id.checkBox)!! - checkbox.isChecked = prefs.getBooleanStrict("lastTimerEos", false) - val minutes = listOf(0, 1, 3, 5, 10, 15, 20, 30, 45, 60, 90) - val items = minutes.map { - if (it > 0) - context.resources.getQuantityString( - R.plurals.minutes, it, - it - ) - else - context.resources.getString(R.string.timer_end_of_this_song) - } + context.resources.getString(R.string.other) - lv.adapter = ArrayAdapter( - context, android.R.layout.simple_list_item_1, - items - ) - lv.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> - dialog.dismiss() - if (position == minutes.size) { - lateinit var et: EditText - lateinit var cb: CheckBox - // TODO find out why wrapped context does not work - val dialog2 = MaterialAlertDialogBuilder(context) - .setTitle(R.string.timer) - .setView(R.layout.dialog_sleep_timer_custom) - .setPositiveButton(android.R.string.ok) { _, _ -> - try { - instance?.setTimer((NumberFormat.getInstance().parse( - et.editableText.toString())!!.toFloat() * 60f * - 1000f).toInt(), cb.isChecked) - } catch (_: ParseException) { - // race condition with button enable - } - } - .setNegativeButton(android.R.string.cancel) { _, _ -> } - .show() - val b = dialog2.getButton(DialogInterface.BUTTON_POSITIVE) - b.isEnabled = false - et = dialog2.findViewById(R.id.editText)!! - et.addTextChangedListener { - b.isEnabled = try { - NumberFormat.getInstance().parse(it.toString())!!.toFloat() - true - } catch (_: ParseException) { - false - } - } - cb = dialog2.findViewById(R.id.checkBox)!! - cb.isChecked = checkbox.isChecked - } else { - val duration = minutes[position] * 60 * 1000 - val eos = duration == 0 || checkbox.isChecked - instance?.setTimer(duration, eos) + GramophonePlaybackService.SERVICE_GET_AUDIO_FORMAT -> { + val format = instance?.getAudioFormat() + this.currentFormat = format + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || + !handler.hasCallbacks(formatUpdateRunnable) + ) { + handler.postDelayed(formatUpdateRunnable, 300) } } + else -> { + return@addCallback Futures.immediateFuture(SessionResult(SessionError.ERROR_NOT_SUPPORTED)) + } } + return@addCallback Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) } + } - bottomSheetLoopButton.setOnClickListener { - ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) - instance?.repeatMode = when (instance?.repeatMode) { - Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ALL - Player.REPEAT_MODE_ALL -> Player.REPEAT_MODE_ONE - Player.REPEAT_MODE_ONE -> Player.REPEAT_MODE_OFF - else -> throw IllegalStateException() - } + private fun showTimerDialog() { + val t = instance?.getTimer() + if (t?.first != null || t?.second == true) { + showActiveTimerDialog(t) + } else { + showInactiveTimerDialog() } + } - bottomSheetPlaybackSpeedButton.setOnClickListener { - ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) - if (instance != null) - showPlaybackSpeedDialog() + private fun showActiveTimerDialog(t: Pair) { + val currentText = if (t.first != null) { + context.getString( + R.string.timer_expiry, + DateFormat.getTimeFormat(context).format(System.currentTimeMillis() + t.first!!) + ) + } else { + context.getString(R.string.timer_expiry_end_of_this_song) } - bottomSheetFavoriteButton.addOnCheckedChangeListener(this) - - bottomSheetPlaylistButton.setOnClickListener { - ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) - if (instance != null) - pqs = PlaylistQueueSheet(wrappedContext ?: context, activity).also { it.show() } - } - bottomSheetFullControllerButton.setOnClickListener { - ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) - instance?.playOrPause() - } - bottomSheetFullPreviousButton.setOnClickListener { - ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) - instance?.seekToPrevious() - } - bottomSheetFullPreviousButton.setOnLongClickListener { - ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.LONG_PRESS) - instance?.seekBack() - true - } - bottomSheetFullNextButton.setOnClickListener { - ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) - instance?.seekToNext() - } - bottomSheetFullNextButton.setOnLongClickListener { - ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.LONG_PRESS) - instance?.seekForward() - true - } - bottomSheetShuffleButton.addOnCheckedChangeListener { _, isChecked -> - instance?.shuffleModeEnabled = isChecked - } + val dialog = MaterialAlertDialogBuilder(wrappedContext ?: context) + .setTitle(R.string.timer) + .setView(R.layout.dialog_sleep_timer_active) + .setNeutralButton(R.string.unset) { _, _ -> + instance?.setTimer(0, false) + } + .setPositiveButton(android.R.string.ok) { _, _ -> } + .show() - bottomSheetFullSlider.addOnChangeListener { _, value, isUser -> - if (isUser) { - val dest = instance?.mediaMetadata?.durationMs - if (dest != null) { - bottomSheetFullPosition.text = - CalculationUtils.convertDurationToTimeStamp((value).toLong()) + dialog.findViewById(R.id.textView)!!.text = currentText + dialog.findViewById(R.id.checkBox)!!.let { + if (t.first == null) { + it.visibility = GONE + } else { + it.isChecked = t.second + it.setOnCheckedChangeListener { _, value -> + val newTime = instance?.getTimer() + instance?.setTimer(newTime?.first ?: 0, value) } } } + } - bottomSheetFullSeekBar.setOnSeekBarChangeListener(touchListener) - bottomSheetFullSlider.addOnSliderTouchListener(touchListener) - - bottomSheetFullSlideUpButton.setOnClickListener { - ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) - minimize?.invoke() - } - - bottomSheetLyricButton.setOnClickListener { - ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) - bottomSheetFullLyricView.fadInAnimation(LYRIC_FADE_TRANSITION_SEC) - } + private fun showInactiveTimerDialog() { + val dialog = MaterialAlertDialogBuilder(wrappedContext ?: context) + .setTitle(R.string.timer) + .setView(R.layout.dialog_sleep_timer) + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .show() - bottomSheetShuffleButton.setOnClickListener { - ViewCompat.performHapticFeedback(it, HapticFeedbackConstantsCompat.CONTEXT_CLICK) + val lv = dialog.findViewById(R.id.listView)!! + val checkbox = dialog.findViewById(R.id.checkBox)!! + checkbox.isChecked = prefs.getBooleanStrict("lastTimerEos", false) + val minutes = listOf(0, 1, 3, 5, 10, 15, 20, 30, 45, 60, 90) + val items = minutes.map { + if (it > 0) context.resources.getQuantityString(R.plurals.minutes, it, it) + else context.resources.getString(R.string.timer_end_of_this_song) + } + context.resources.getString(R.string.other) + + lv.adapter = ArrayAdapter(context, android.R.layout.simple_list_item_1, items) + lv.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> + dialog.dismiss() + if (position == minutes.size) { + showCustomTimerDialog(checkbox) + } else { + val duration = minutes[position] * 60 * 1000 + val eos = duration == 0 || checkbox.isChecked + instance?.setTimer(duration, eos) + } } + } - val colorSecondaryContainer = - MaterialColors.getColor( - context, - com.google.android.material.R.attr.colorSecondaryContainer, - -1 - ) - val colorSurface = MaterialColors.getColor( - context, - com.google.android.material.R.attr.colorSurface, - -1 - ) - val backgroundProcessedColor = ColorUtils.getColor( - colorSurface, - ColorUtils.ColorType.COLOR_BACKGROUND_ELEVATED, - context - ) - val colorContrastFainted = ColorUtils.getColor( - colorSecondaryContainer, - ColorUtils.ColorType.COLOR_CONTRAST_FAINTED, - context - ) - setBackgroundColor(backgroundProcessedColor) - bottomSheetFullSlider.trackInactiveTintList = ColorStateList.valueOf(colorContrastFainted) + private fun showCustomTimerDialog(baseCheckbox: CheckBox) { + lateinit var et: EditText + lateinit var cb: CheckBox + val customDialog = MaterialAlertDialogBuilder(context) + .setTitle(R.string.timer) + .setView(R.layout.dialog_sleep_timer_custom) + .setPositiveButton(android.R.string.ok) { _, _ -> + try { + val parsed = NumberFormat.getInstance().parse(et.editableText.toString())!!.toFloat() + instance?.setTimer((parsed * 60f * 1000f).toInt(), cb.isChecked) + } catch (_: ParseException) { + } + } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .show() - activity.controllerViewModel.addRecreationalPlayerListener(activity.lifecycle, this) { - firstTime = true - updateTimer() - onRepeatModeChanged(instance?.repeatMode ?: Player.REPEAT_MODE_OFF) - onShuffleModeEnabledChanged(instance?.shuffleModeEnabled == true) - onPlaybackStateChanged(instance?.playbackState ?: Player.STATE_IDLE) - onMediaItemTransition( - instance?.currentMediaItem, - Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED - ) - onMediaMetadataChanged(instance?.mediaMetadata ?: MediaMetadata.EMPTY) - firstTime = false + val posButton = customDialog.getButton(DialogInterface.BUTTON_POSITIVE) + posButton.isEnabled = false + et = customDialog.findViewById(R.id.editText)!! + et.addTextChangedListener { + posButton.isEnabled = try { + NumberFormat.getInstance().parse(it.toString())!!.toFloat() + true + } catch (_: ParseException) { + false + } } + cb = customDialog.findViewById(R.id.checkBox)!! + cb.isChecked = baseCheckbox.isChecked } override fun onAttachedToWindow() { @@ -725,38 +734,44 @@ class FullBottomSheet } private fun refreshSettings(key: String?) { + refreshProgressBarSetting(key) + refreshQualityInfoSetting(key) + refreshTitleAppearanceSetting(key) + refreshCoverAppearanceSetting(key) + } + + private fun refreshProgressBarSetting(key: String?) { if (key == null || key == "default_progress_bar") { - if (prefs.getBooleanStrict("default_progress_bar", false)) { - bottomSheetFullSlider.visibility = VISIBLE - bottomSheetFullSeekBar.visibility = GONE - } else { - bottomSheetFullSlider.visibility = GONE - bottomSheetFullSeekBar.visibility = VISIBLE - } + val isDefault = prefs.getBooleanStrict("default_progress_bar", false) + bottomSheetFullSlider.visibility = if (isDefault) VISIBLE else GONE + bottomSheetFullSeekBar.visibility = if (isDefault) GONE else VISIBLE } + } + + private fun refreshQualityInfoSetting(key: String?) { if (key == null || key == "audio_quality_info") { enableQualityInfo = prefs.getBooleanStrict("audio_quality_info", false) updateQualityIndicators( - if (enableQualityInfo) - AudioFormatDetector.detectAudioFormat(currentFormat) else null + if (enableQualityInfo) AudioFormatDetector.detectAudioFormat(currentFormat) else null ) } + } + + private fun refreshTitleAppearanceSetting(key: String?) { if (key == null || key == "centered_title") { - if (prefs.getBooleanStrict("centered_title", false)) { - bottomSheetFullTitle.gravity = Gravity.CENTER - bottomSheetFullSubtitle.gravity = Gravity.CENTER - } else { - bottomSheetFullTitle.gravity = Gravity.CENTER_HORIZONTAL or Gravity.START - bottomSheetFullSubtitle.gravity = Gravity.CENTER_HORIZONTAL or Gravity.START - } + val isCentered = prefs.getBooleanStrict("centered_title", false) + val gravity = if (isCentered) Gravity.CENTER else (Gravity.CENTER_HORIZONTAL or Gravity.START) + bottomSheetFullTitle.gravity = gravity + bottomSheetFullSubtitle.gravity = gravity } if (key == null || key == "bold_title") { - if (prefs.getBooleanStrict("bold_title", true)) { - bottomSheetFullTitle.typeface = TypefaceCompat.create(context, null, 600, false) - } else { - bottomSheetFullTitle.typeface = TypefaceCompat.create(context, null, 400, false) - } + val isBold = prefs.getBooleanStrict("bold_title", true) + val weight = if (isBold) 600 else 400 + bottomSheetFullTitle.typeface = TypefaceCompat.create(context, null, weight, false) } + } + + private fun refreshCoverAppearanceSetting(key: String?) { if (key == null || key == "album_round_corner") { bottomSheetFullCoverFrame.radius = prefs.getIntStrict( "album_round_corner", @@ -777,17 +792,18 @@ class FullBottomSheet } private fun startButtonRotation() { + val bg = bottomSheetFullControllerButtonBg ?: return if (!rotateCookieButton || instance?.isPlaying != true) return if (buttonRotationAnimator == null) { buttonRotationAnimator = ValueAnimator.ofFloat( - bottomSheetFullControllerButtonBg.rotation, - bottomSheetFullControllerButtonBg.rotation + 360f + bg.rotation, + bg.rotation + 360f ).apply { duration = 36000L repeatCount = ValueAnimator.INFINITE interpolator = LinearInterpolator() addUpdateListener { animator -> - bottomSheetFullControllerButtonBg.rotation = animator.animatedValue as Float + bg.rotation = animator.animatedValue as Float } start() } @@ -800,7 +816,7 @@ class FullBottomSheet buttonRotationAnimator?.cancel() buttonRotationAnimator = null if (reset) { - bottomSheetFullControllerButtonBg.rotation = 0f + bottomSheetFullControllerButtonBg?.rotation = 0f } } @@ -1134,377 +1150,239 @@ class FullBottomSheet } } - private suspend fun applyColorScheme(animate: Boolean = true) { - val ctx = wrappedContext ?: context - - val colorSurface = MaterialColors.getColor( - ctx, - com.google.android.material.R.attr.colorSurface, - -1 - ) - - val colorOnSurface = MaterialColors.getColor( - ctx, - com.google.android.material.R.attr.colorOnSurface, - -1 - ) - - val colorOnSurfaceVariant = MaterialColors.getColor( - ctx, - com.google.android.material.R.attr.colorOnSurfaceVariant, - -1 - ) - - val colorPrimary = - MaterialColors.getColor( - ctx, - androidx.appcompat.R.attr.colorPrimary, - -1 - ) - - val colorSecondary = - MaterialColors.getColor( - ctx, - com.google.android.material.R.attr.colorSecondary, - -1 - ) - - val colorSecondaryContainer = - MaterialColors.getColor( - ctx, - com.google.android.material.R.attr.colorSecondaryContainer, - -1 - ) - - val colorOnSecondaryContainer = - MaterialColors.getColor( - ctx, - com.google.android.material.R.attr.colorOnSecondaryContainer, - -1 - ) - - val selectorBackground = - AppCompatResources.getColorStateList( - ctx, - R.color.sl_check_button - ) - - val selectorFavBackground = - AppCompatResources.getColorStateList( - ctx, - R.color.sl_fav_button - ) - - val backgroundProcessedColor = ColorUtils.getColor( - colorSurface, - ColorUtils.ColorType.COLOR_BACKGROUND_ELEVATED, - ctx - ) - - val colorContrastFainted = ColorUtils.getColor( - colorSecondaryContainer, - ColorUtils.ColorType.COLOR_CONTRAST_FAINTED, - ctx - ) - + private data class FullPlayerColorScheme( + val surface: Int, + val onSurface: Int, + val onSurfaceVariant: Int, + val primary: Int, + val secondary: Int, + val secondaryContainer: Int, + val onSecondaryContainer: Int, + val selectorBackground: ColorStateList, + val selectorFavBackground: ColorStateList, + val backgroundProcessed: Int, + val contrastFainted: Int, + val lyricsText: Int, + val lyricsHighlightTl: Int + ) + + private fun resolveThemeColors(ctx: Context): FullPlayerColorScheme { + val colorSurface = MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorSurface, -1) + val colorOnSurface = MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorOnSurface, -1) + val colorOnSurfaceVariant = MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorOnSurfaceVariant, -1) + val colorPrimary = MaterialColors.getColor(ctx, androidx.appcompat.R.attr.colorPrimary, -1) + val colorSecondary = MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorSecondary, -1) + val colorSecondaryContainer = MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorSecondaryContainer, -1) + val colorOnSecondaryContainer = MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorOnSecondaryContainer, -1) + val selectorBackground = AppCompatResources.getColorStateList(ctx, R.color.sl_check_button) + val selectorFavBackground = AppCompatResources.getColorStateList(ctx, R.color.sl_fav_button) + val backgroundProcessedColor = ColorUtils.getColor(colorSurface, ColorUtils.ColorType.COLOR_BACKGROUND_ELEVATED, ctx) + val colorContrastFainted = ColorUtils.getColor(colorSecondaryContainer, ColorUtils.ColorType.COLOR_CONTRAST_FAINTED, ctx) val lyricsTextColor = androidx.core.graphics.ColorUtils.compositeColors( androidx.core.graphics.ColorUtils.setAlphaComponent(colorPrimary, 77), backgroundProcessedColor ) - val lyricsHighlightTlColor = androidx.core.graphics.ColorUtils.compositeColors( androidx.core.graphics.ColorUtils.setAlphaComponent(colorPrimary, 200), backgroundProcessedColor ) + return FullPlayerColorScheme( + surface = colorSurface, + onSurface = colorOnSurface, + onSurfaceVariant = colorOnSurfaceVariant, + primary = colorPrimary, + secondary = colorSecondary, + secondaryContainer = colorSecondaryContainer, + onSecondaryContainer = colorOnSecondaryContainer, + selectorBackground = selectorBackground, + selectorFavBackground = selectorFavBackground, + backgroundProcessed = backgroundProcessedColor, + contrastFainted = colorContrastFainted, + lyricsText = lyricsTextColor, + lyricsHighlightTl = lyricsHighlightTlColor + ) + } - if (animate) { - - val surfaceTransition = ValueAnimator.ofArgb( - (background as ColorDrawable).color, - backgroundProcessedColor - ) - - val primaryTransition = ValueAnimator.ofArgb( - bottomSheetFullTitle.textColors.defaultColor, - colorPrimary - ) - - val secondaryContainerTransition = ValueAnimator.ofArgb( - bottomSheetFullControllerButtonBg.imageTintList?.defaultColor - ?: MaterialColors.getColor( - ctx, - com.google.android.material.R.attr.colorSecondaryContainer, - -1 - ), - colorSecondaryContainer - ) - - val onSecondaryContainerTransition = ValueAnimator.ofArgb( - bottomSheetFullControllerButton.iconTint.defaultColor, - colorOnSecondaryContainer - ) - - val colorContrastFaintedTransition = ValueAnimator.ofArgb( - bottomSheetFullSlider.trackInactiveTintList.defaultColor, - colorContrastFainted - ) - - val colorOnSurfaceTransition = ValueAnimator.ofArgb( - bottomSheetLyricButton.iconTint.defaultColor, - colorOnSurface - ) - - val lyricTextColorTransition = ValueAnimator.ofArgb( - bottomSheetFullLyricView.defaultTextColor, - lyricsTextColor - ) - - val lyricHighlightTlColorTransition = ValueAnimator.ofArgb( - bottomSheetFullLyricView.highlightTlTextColor, - lyricsHighlightTlColor - ) - - val loopTransition = ValueAnimator.ofArgb( - bottomSheetLoopButton.iconTint.getColorForState( - bottomSheetLoopButton.drawableState, Color.RED - ), - selectorBackground.getColorForState( - bottomSheetLoopButton.drawableState, Color.RED - ) - ) - - val shuffleTransition = ValueAnimator.ofArgb( - bottomSheetShuffleButton.iconTint.getColorForState( - bottomSheetShuffleButton.drawableState, Color.RED - ), - selectorBackground.getColorForState( - bottomSheetShuffleButton.drawableState, Color.RED - ) - ) - - val favoriteTransition = ValueAnimator.ofArgb( - bottomSheetFavoriteButton.iconTint.getColorForState( - bottomSheetFavoriteButton.drawableState, Color.RED - ), - selectorFavBackground.getColorForState( - bottomSheetFavoriteButton.drawableState, Color.RED - ) - ) - - surfaceTransition.apply { - addUpdateListener { animation -> - setBackgroundColor( - animation.animatedValue as Int - ) - bottomSheetFullLyricView.setBackgroundColor( - animation.animatedValue as Int - ) - } - duration = BACKGROUND_COLOR_TRANSITION_SEC - } - - primaryTransition.apply { - addUpdateListener { animation -> - val progressColor = animation.animatedValue as Int - bottomSheetFullSlider.thumbTintList = - ColorStateList.valueOf(progressColor) - bottomSheetFullSlider.trackActiveTintList = - ColorStateList.valueOf(progressColor) - bottomSheetFullSeekBar.progressTintList = - ColorStateList.valueOf(progressColor) - bottomSheetFullSeekBar.thumbTintList = - ColorStateList.valueOf(progressColor) - bottomSheetFullLyricView.updateHighlightColor(progressColor) - } - duration = BACKGROUND_COLOR_TRANSITION_SEC + private fun buildColorAnimators(ctx: Context, colors: FullPlayerColorScheme): List { + val surfaceTransition = ValueAnimator.ofArgb( + (background as ColorDrawable).color, colors.backgroundProcessed + ).apply { + duration = BACKGROUND_COLOR_TRANSITION_SEC + addUpdateListener { + val color = it.animatedValue as Int + setBackgroundColor(color) + bottomSheetFullLyricView.setBackgroundColor(color) } + } - secondaryContainerTransition.apply { - addUpdateListener { animation -> - val progressColor = animation.animatedValue as Int - bottomSheetFullControllerButtonBg.imageTintList = - ColorStateList.valueOf(progressColor) - } - duration = BACKGROUND_COLOR_TRANSITION_SEC + val primaryTransition = ValueAnimator.ofArgb( + bottomSheetFullTitle.textColors.defaultColor, colors.primary + ).apply { + duration = BACKGROUND_COLOR_TRANSITION_SEC + addUpdateListener { + val color = it.animatedValue as Int + bottomSheetFullSlider.thumbTintList = ColorStateList.valueOf(color) + bottomSheetFullSlider.trackActiveTintList = ColorStateList.valueOf(color) + bottomSheetFullSeekBar.progressTintList = ColorStateList.valueOf(color) + bottomSheetFullSeekBar.thumbTintList = ColorStateList.valueOf(color) + bottomSheetFullLyricView.updateHighlightColor(color) } + } - onSecondaryContainerTransition.apply { - addUpdateListener { animation -> - val progressColor = animation.animatedValue as Int - bottomSheetFullControllerButton.iconTint = - ColorStateList.valueOf(progressColor) - } - duration = BACKGROUND_COLOR_TRANSITION_SEC + val secondaryContainerTransition = ValueAnimator.ofArgb( + bottomSheetFullControllerButtonBg?.imageTintList?.defaultColor + ?: MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorSecondaryContainer, -1), + colors.secondaryContainer + ).apply { + duration = BACKGROUND_COLOR_TRANSITION_SEC + addUpdateListener { + bottomSheetFullControllerButtonBg?.imageTintList = ColorStateList.valueOf(it.animatedValue as Int) } + } - colorContrastFaintedTransition.apply { - addUpdateListener { animation -> - val progressColor = animation.animatedValue as Int - bottomSheetFullSlider.trackInactiveTintList = - ColorStateList.valueOf(progressColor) - } - duration = BACKGROUND_COLOR_TRANSITION_SEC + val onSecondaryContainerTransition = ValueAnimator.ofArgb( + bottomSheetFullControllerButton.iconTint.defaultColor, colors.onSecondaryContainer + ).apply { + duration = BACKGROUND_COLOR_TRANSITION_SEC + addUpdateListener { + bottomSheetFullControllerButton.iconTint = ColorStateList.valueOf(it.animatedValue as Int) } + } - colorOnSurfaceTransition.apply { - addUpdateListener { animation -> - val progressColor = animation.animatedValue as Int - bottomSheetTimerButton.iconTint = - ColorStateList.valueOf(progressColor) - bottomSheetPlaybackSpeedButton.iconTint = - ColorStateList.valueOf(progressColor) - bottomSheetPlaylistButton.iconTint = - ColorStateList.valueOf(progressColor) - bottomSheetLyricButton.iconTint = - ColorStateList.valueOf(progressColor) - bottomSheetFullNextButton.iconTint = - ColorStateList.valueOf(progressColor) - bottomSheetFullPreviousButton.iconTint = - ColorStateList.valueOf(progressColor) - bottomSheetFullSlideUpButton.iconTint = - ColorStateList.valueOf(progressColor) - } - duration = BACKGROUND_COLOR_TRANSITION_SEC + val contrastFaintedTransition = ValueAnimator.ofArgb( + bottomSheetFullSlider.trackInactiveTintList.defaultColor, colors.contrastFainted + ).apply { + duration = BACKGROUND_COLOR_TRANSITION_SEC + addUpdateListener { + bottomSheetFullSlider.trackInactiveTintList = ColorStateList.valueOf(it.animatedValue as Int) } + } - lyricTextColorTransition.apply { - addUpdateListener { animation -> - val progressColor = animation.animatedValue as Int - bottomSheetFullLyricView.updateTextColor(progressColor) - } - duration = BACKGROUND_COLOR_TRANSITION_SEC + val onSurfaceTransition = ValueAnimator.ofArgb( + bottomSheetLyricButton.iconTint.defaultColor, colors.onSurface + ).apply { + duration = BACKGROUND_COLOR_TRANSITION_SEC + addUpdateListener { + val color = ColorStateList.valueOf(it.animatedValue as Int) + bottomSheetTimerButton.iconTint = color + bottomSheetPlaybackSpeedButton.iconTint = color + bottomSheetPlaylistButton.iconTint = color + bottomSheetLyricButton.iconTint = color + bottomSheetFullNextButton.iconTint = color + bottomSheetFullPreviousButton.iconTint = color + bottomSheetFullSlideUpButton.iconTint = color } + } - lyricHighlightTlColorTransition.apply { - addUpdateListener { animation -> - val progressColor = animation.animatedValue as Int - bottomSheetFullLyricView.updateHighlightTlColor(progressColor) - } - duration = BACKGROUND_COLOR_TRANSITION_SEC + val lyricTextTransition = ValueAnimator.ofArgb( + bottomSheetFullLyricView.defaultTextColor, colors.lyricsText + ).apply { + duration = BACKGROUND_COLOR_TRANSITION_SEC + addUpdateListener { + bottomSheetFullLyricView.updateTextColor(it.animatedValue as Int) } + } - loopTransition.apply { - addUpdateListener { animation -> - val progressColor = animation.animatedValue as Int - bottomSheetLoopButton.iconTint = ColorStateList.valueOf(progressColor) - } - duration = BACKGROUND_COLOR_TRANSITION_SEC + val lyricHighlightTlTransition = ValueAnimator.ofArgb( + bottomSheetFullLyricView.highlightTlTextColor, colors.lyricsHighlightTl + ).apply { + duration = BACKGROUND_COLOR_TRANSITION_SEC + addUpdateListener { + bottomSheetFullLyricView.updateHighlightTlColor(it.animatedValue as Int) } + } - shuffleTransition.apply { - addUpdateListener { animation -> - val progressColor = animation.animatedValue as Int - bottomSheetShuffleButton.iconTint = ColorStateList.valueOf(progressColor) - } - duration = BACKGROUND_COLOR_TRANSITION_SEC + val loopTransition = ValueAnimator.ofArgb( + bottomSheetLoopButton.iconTint.getColorForState(bottomSheetLoopButton.drawableState, Color.RED), + colors.selectorBackground.getColorForState(bottomSheetLoopButton.drawableState, Color.RED) + ).apply { + duration = BACKGROUND_COLOR_TRANSITION_SEC + addUpdateListener { + bottomSheetLoopButton.iconTint = ColorStateList.valueOf(it.animatedValue as Int) } + } - favoriteTransition.apply { - addUpdateListener { animation -> - val progressColor = animation.animatedValue as Int - bottomSheetFavoriteButton.iconTint = ColorStateList.valueOf(progressColor) - } - duration = BACKGROUND_COLOR_TRANSITION_SEC + val shuffleTransition = ValueAnimator.ofArgb( + bottomSheetShuffleButton.iconTint.getColorForState(bottomSheetShuffleButton.drawableState, Color.RED), + colors.selectorBackground.getColorForState(bottomSheetShuffleButton.drawableState, Color.RED) + ).apply { + duration = BACKGROUND_COLOR_TRANSITION_SEC + addUpdateListener { + bottomSheetShuffleButton.iconTint = ColorStateList.valueOf(it.animatedValue as Int) } + } - withContext(Dispatchers.Main) { - surfaceTransition.start() - primaryTransition.start() - secondaryContainerTransition.start() - onSecondaryContainerTransition.start() - colorContrastFaintedTransition.start() - colorOnSurfaceTransition.start() - lyricTextColorTransition.start() - lyricHighlightTlColorTransition.start() - loopTransition.start() - shuffleTransition.start() - favoriteTransition.start() - - // Note: Animator.addListener isn't thread-safe on all Android versions, ensure we - // stay on the main thread to avoid crashes. - surfaceTransition.awaitEnd() - primaryTransition.awaitEnd() - secondaryContainerTransition.awaitEnd() - onSecondaryContainerTransition.awaitEnd() - colorContrastFaintedTransition.awaitEnd() - colorOnSurfaceTransition.awaitEnd() - lyricTextColorTransition.awaitEnd() - lyricHighlightTlColorTransition.awaitEnd() - loopTransition.awaitEnd() - shuffleTransition.awaitEnd() - favoriteTransition.awaitEnd() + val favoriteTransition = ValueAnimator.ofArgb( + bottomSheetFavoriteButton.iconTint.getColorForState(bottomSheetFavoriteButton.drawableState, Color.RED), + colors.selectorFavBackground.getColorForState(bottomSheetFavoriteButton.drawableState, Color.RED) + ).apply { + duration = BACKGROUND_COLOR_TRANSITION_SEC + addUpdateListener { + bottomSheetFavoriteButton.iconTint = ColorStateList.valueOf(it.animatedValue as Int) } } - currentJob = null + return listOf( + surfaceTransition, primaryTransition, secondaryContainerTransition, + onSecondaryContainerTransition, contrastFaintedTransition, onSurfaceTransition, + lyricTextTransition, lyricHighlightTlTransition, loopTransition, + shuffleTransition, favoriteTransition + ) + } + + private fun applyStaticColors(colors: FullPlayerColorScheme) { postOnAnimation { - setBackgroundColor(backgroundProcessedColor) - bottomSheetFullLyricView.setBackgroundColor(backgroundProcessedColor) - bottomSheetFullTitle.setTextColor( - colorPrimary - ) - bottomSheetFullSubtitle.setTextColor( - colorSecondary - ) - bottomSheetFullControllerButtonBg.imageTintList = - ColorStateList.valueOf(colorSecondaryContainer) - bottomSheetFullControllerButton.iconTint = - ColorStateList.valueOf(colorOnSecondaryContainer) - - bottomSheetFullSlider.thumbTintList = - ColorStateList.valueOf(colorPrimary) - bottomSheetFullSlider.trackActiveTintList = - ColorStateList.valueOf(colorPrimary) - bottomSheetFullSeekBar.progressTintList = - ColorStateList.valueOf(colorPrimary) - bottomSheetFullSeekBar.thumbTintList = - ColorStateList.valueOf(colorPrimary) - bottomSheetFullSlider.trackInactiveTintList = - ColorStateList.valueOf(colorContrastFainted) + setBackgroundColor(colors.backgroundProcessed) + bottomSheetFullLyricView.setBackgroundColor(colors.backgroundProcessed) + bottomSheetFullTitle.setTextColor(colors.primary) + bottomSheetFullSubtitle.setTextColor(colors.secondary) + bottomSheetFullControllerButtonBg?.imageTintList = ColorStateList.valueOf(colors.secondaryContainer) + bottomSheetFullControllerButton.iconTint = ColorStateList.valueOf(colors.onSecondaryContainer) + + bottomSheetFullSlider.thumbTintList = ColorStateList.valueOf(colors.primary) + bottomSheetFullSlider.trackActiveTintList = ColorStateList.valueOf(colors.primary) + bottomSheetFullSeekBar.progressTintList = ColorStateList.valueOf(colors.primary) + bottomSheetFullSeekBar.thumbTintList = ColorStateList.valueOf(colors.primary) + bottomSheetFullSlider.trackInactiveTintList = ColorStateList.valueOf(colors.contrastFainted) TextViewCompat.setCompoundDrawableTintList( - bottomSheetFullQualityDetails, - ColorStateList.valueOf(colorOnSurfaceVariant) - ) - bottomSheetFullQualityDetails.setTextColor( - colorOnSurfaceVariant - ) - bottomSheetFullLyricView.updateTextColor( - lyricsTextColor, - colorPrimary, - lyricsHighlightTlColor, + bottomSheetFullQualityDetails, ColorStateList.valueOf(colors.onSurfaceVariant) ) + bottomSheetFullQualityDetails.setTextColor(colors.onSurfaceVariant) + bottomSheetFullLyricView.updateTextColor(colors.lyricsText, colors.primary, colors.lyricsHighlightTl) - bottomSheetTimerButton.iconTint = - ColorStateList.valueOf(colorOnSurface) - bottomSheetPlaybackSpeedButton.iconTint = - ColorStateList.valueOf(colorOnSurface) - bottomSheetPlaylistButton.iconTint = - ColorStateList.valueOf(colorOnSurface) - bottomSheetShuffleButton.iconTint = - selectorBackground - bottomSheetLoopButton.iconTint = - selectorBackground - bottomSheetLyricButton.iconTint = - ColorStateList.valueOf(colorOnSurface) - bottomSheetFavoriteButton.iconTint = - selectorFavBackground - - bottomSheetFullNextButton.iconTint = - ColorStateList.valueOf(colorOnSurface) - bottomSheetFullPreviousButton.iconTint = - ColorStateList.valueOf(colorOnSurface) - bottomSheetFullSlideUpButton.iconTint = - ColorStateList.valueOf(colorOnSurface) - - bottomSheetFullPosition.setTextColor( - colorOnSurfaceVariant - ) - bottomSheetFullDuration.setTextColor( - colorOnSurfaceVariant - ) + bottomSheetTimerButton.iconTint = ColorStateList.valueOf(colors.onSurface) + bottomSheetPlaybackSpeedButton.iconTint = ColorStateList.valueOf(colors.onSurface) + bottomSheetPlaylistButton.iconTint = ColorStateList.valueOf(colors.onSurface) + bottomSheetShuffleButton.iconTint = colors.selectorBackground + bottomSheetLoopButton.iconTint = colors.selectorBackground + bottomSheetLyricButton.iconTint = ColorStateList.valueOf(colors.onSurface) + bottomSheetFavoriteButton.iconTint = colors.selectorFavBackground + + bottomSheetFullNextButton.iconTint = ColorStateList.valueOf(colors.onSurface) + bottomSheetFullPreviousButton.iconTint = ColorStateList.valueOf(colors.onSurface) + bottomSheetFullSlideUpButton.iconTint = ColorStateList.valueOf(colors.onSurface) + + bottomSheetFullPosition.setTextColor(colors.onSurfaceVariant) + bottomSheetFullDuration.setTextColor(colors.onSurfaceVariant) } } + private suspend fun applyColorScheme(animate: Boolean = true) { + val ctx = wrappedContext ?: context + val colors = resolveThemeColors(ctx) + + if (animate) { + val animators = buildColorAnimators(ctx, colors) + withContext(Dispatchers.Main) { + animators.forEach { it.start() } + animators.forEach { it.awaitEnd() } + } + } + + currentJob = null + applyStaticColors(colors) + } + private suspend fun ValueAnimator.awaitEnd() { if (!isStarted) return @@ -1627,49 +1505,57 @@ class FullBottomSheet override fun onPlaybackStateChanged(playbackState: Int) { if (instance?.isPlaying == true) { - if (bottomSheetFullControllerButton.getTag(R.id.play_next) as Int? != 1) { - bottomSheetFullControllerButton.icon = - AppCompatResources.getDrawable( - wrappedContext ?: context, - R.drawable.play_anim - ) - bottomSheetFullControllerButtonBg.setImageDrawable( - AppCompatResources.getDrawable(context, R.drawable.bg_play_anim) - ) - bottomSheetFullControllerButton.icon.startAnimation() - bottomSheetFullControllerButtonBg.drawable.startAnimation() - bottomSheetFullControllerButton.setTag(R.id.play_next, 1) - } - if (!isUserTracking) { - progressDrawable.animate = true - } - if (!runnableRunning) { - runnableRunning = true - handler.postDelayed(positionRunnable, SLIDER_UPDATE_INTERVAL) - } - bottomSheetFullCover.startRotation() - if (rotateCookieButton) { - startButtonRotation() - } + updatePlayingStateUI() } else if (playbackState != Player.STATE_BUFFERING) { - if (bottomSheetFullControllerButton.getTag(R.id.play_next) as Int? != 2) { - bottomSheetFullControllerButton.icon = - AppCompatResources.getDrawable( - wrappedContext ?: context, - R.drawable.pause_anim - ) - bottomSheetFullControllerButtonBg.setImageDrawable( - AppCompatResources.getDrawable(context, R.drawable.bg_pause_anim) + updatePausedStateUI() + } + } + + private fun updatePlayingStateUI() { + if (bottomSheetFullControllerButton.getTag(R.id.play_next) as Int? != 1) { + bottomSheetFullControllerButton.icon = + AppCompatResources.getDrawable( + wrappedContext ?: context, + R.drawable.play_anim ) - bottomSheetFullControllerButton.icon.startAnimation() - bottomSheetFullControllerButtonBg.drawable.startAnimation() - bottomSheetFullControllerButton.setTag(R.id.play_next, 2) - bottomSheetFullCover.stopRotation() - stopButtonRotation() - } - if (!isUserTracking) { - progressDrawable.animate = false - } + bottomSheetFullControllerButtonBg?.setImageDrawable( + AppCompatResources.getDrawable(context, R.drawable.bg_play_anim) + ) + bottomSheetFullControllerButton.icon.startAnimation() + bottomSheetFullControllerButtonBg?.drawable?.startAnimation() + bottomSheetFullControllerButton.setTag(R.id.play_next, 1) + } + if (!isUserTracking) { + progressDrawable.animate = true + } + if (!runnableRunning) { + runnableRunning = true + handler.postDelayed(positionRunnable, SLIDER_UPDATE_INTERVAL) + } + bottomSheetFullCover.startRotation() + if (rotateCookieButton) { + startButtonRotation() + } + } + + private fun updatePausedStateUI() { + if (bottomSheetFullControllerButton.getTag(R.id.play_next) as Int? != 2) { + bottomSheetFullControllerButton.icon = + AppCompatResources.getDrawable( + wrappedContext ?: context, + R.drawable.pause_anim + ) + bottomSheetFullControllerButtonBg?.setImageDrawable( + AppCompatResources.getDrawable(context, R.drawable.bg_pause_anim) + ) + bottomSheetFullControllerButton.icon.startAnimation() + bottomSheetFullControllerButtonBg?.drawable?.startAnimation() + bottomSheetFullControllerButton.setTag(R.id.play_next, 2) + bottomSheetFullCover.stopRotation() + stopButtonRotation() + } + if (!isUserTracking) { + progressDrawable.animate = false } } diff --git a/app/src/main/res/layout-w600dp-land/full_player.xml b/app/src/main/res/layout-w600dp-land/full_player.xml index bd831d0458..2649746a28 100644 --- a/app/src/main/res/layout-w600dp-land/full_player.xml +++ b/app/src/main/res/layout-w600dp-land/full_player.xml @@ -200,7 +200,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="28dp" - app:layout_constraintBottom_toTopOf="@id/sheet_mid_button" + app:layout_constraintBottom_toTopOf="@id/sheet_mid_button_frame" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/slider" @@ -253,29 +253,43 @@ - + app:layout_constraintTop_toBottomOf="@id/duration_frame"> + + + + + From 945f7a1d84001f1b4f79eb43e9ff4b957f069e4d Mon Sep 17 00:00:00 2001 From: stevezmtstudios Date: Thu, 27 Aug 2026 00:03:07 +0800 Subject: [PATCH 4/4] style(strings): align player gesture string locations with feature branch --- app/src/main/res/values-zh-rCN/strings.xml | 8 ++++---- app/src/main/res/values/strings.xml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 1ec7a629cf..889b419ffc 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -157,14 +157,14 @@ 启用魅族状态栏歌词(仅适用于部分设备) 启用新歌词界面 显示当前播放媒体歌词的小部件 - 滚动到专辑 - 滚动到歌曲 - 关闭 - 跳到上一个 旋转播放按钮 播放时让饼干样式的播放/暂停按钮细微地转动 手势切歌 在播放器封面上左右滑动以切换上一曲/下一曲 + 滚动到专辑 + 滚动到歌曲 + 关闭 + 跳到上一个 位置滑块 实验性界面,显示逐字歌词,具有高级功能 返回 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index df0449eb0e..cc57b01ffd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -239,10 +239,6 @@ Bold title Use bold title for full player - Rotate play button - Subtly rotate the cookie-style play/pause button while playing - Swipe to switch track - Swipe left or right on the album cover to switch tracks Experimental @@ -406,6 +402,10 @@ Always skip to previous track Directly switch to previous track when pressing previous button instead of seeking to start + Rotate play button + Subtly rotate the cookie-style play/pause button while playing + Swipe to switch track + Swipe left or right on the album cover to switch tracks Delete