From 03ec56de5914530947ca65454edfad0611f15559 Mon Sep 17 00:00:00 2001 From: HapeLee <1321903405@qq.com> Date: Thu, 4 Sep 2025 14:04:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=9C=89=E5=A3=B0=E4=B9=A6?= =?UTF-8?q?=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../legato/kazusa/service/AudioPlayService.kt | 9 +- .../kazusa/ui/book/audio/AudioPlayActivity.kt | 191 ++++++-- .../kazusa/ui/book/audio/TimerSliderPopup.kt | 53 -- .../io/legato/kazusa/utils/DrawableUtils.kt | 14 +- app/src/main/res/drawable/bg_pause_anim.xml | 43 ++ app/src/main/res/drawable/bg_play_anim.xml | 43 ++ .../main/res/drawable/ic_media_pause_anim.xml | 44 ++ .../main/res/drawable/ic_media_play_anim.xml | 44 ++ app/src/main/res/drawable/ic_pause.xml | 10 + app/src/main/res/drawable/ic_pause_filled.xml | 11 + app/src/main/res/drawable/ic_play_filled.xml | 12 + app/src/main/res/drawable/ic_speed.xml | 11 + .../res/drawable/selector_btn_accent_bg.xml | 5 - .../res/drawable/selector_circle_btn_bg.xml | 4 - .../main/res/layout/activity_audio_play.xml | 459 +++++++++++------- app/src/main/res/layout/popup_seek_bar.xml | 21 - app/src/main/res/values/drawables.xml | 15 + 17 files changed, 709 insertions(+), 280 deletions(-) delete mode 100644 app/src/main/java/io/legato/kazusa/ui/book/audio/TimerSliderPopup.kt create mode 100644 app/src/main/res/drawable/bg_pause_anim.xml create mode 100644 app/src/main/res/drawable/bg_play_anim.xml create mode 100644 app/src/main/res/drawable/ic_media_pause_anim.xml create mode 100644 app/src/main/res/drawable/ic_media_play_anim.xml create mode 100644 app/src/main/res/drawable/ic_pause.xml create mode 100644 app/src/main/res/drawable/ic_pause_filled.xml create mode 100644 app/src/main/res/drawable/ic_play_filled.xml create mode 100644 app/src/main/res/drawable/ic_speed.xml delete mode 100644 app/src/main/res/drawable/selector_btn_accent_bg.xml delete mode 100644 app/src/main/res/drawable/selector_circle_btn_bg.xml delete mode 100644 app/src/main/res/layout/popup_seek_bar.xml create mode 100644 app/src/main/res/values/drawables.xml diff --git a/app/src/main/java/io/legato/kazusa/service/AudioPlayService.kt b/app/src/main/java/io/legato/kazusa/service/AudioPlayService.kt index 98a0fd3c9..4df7aec77 100644 --- a/app/src/main/java/io/legato/kazusa/service/AudioPlayService.kt +++ b/app/src/main/java/io/legato/kazusa/service/AudioPlayService.kt @@ -306,11 +306,10 @@ class AudioPlayService : BaseService(), * 调节速度 */ @SuppressLint(value = ["ObsoleteSdkInt"]) - private fun upSpeed(adjust: Float) { + private fun upSpeed(speed: Float) { kotlin.runCatching { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - playSpeed += adjust + playSpeed = speed exoPlayer.setPlaybackSpeed(playSpeed) postEvent(EventBus.AUDIO_SPEED, playSpeed) } @@ -580,13 +579,13 @@ class AudioPlayService : BaseService(), builder.setLargeIcon(cover) if (pause) { builder.addAction( - R.drawable.ic_play_24dp, + R.drawable.ic_play, getString(R.string.resume), servicePendingIntent(IntentAction.resume) ) } else { builder.addAction( - R.drawable.ic_pause_24dp, + R.drawable.ic_pause, getString(R.string.pause), servicePendingIntent(IntentAction.pause) ) diff --git a/app/src/main/java/io/legato/kazusa/ui/book/audio/AudioPlayActivity.kt b/app/src/main/java/io/legato/kazusa/ui/book/audio/AudioPlayActivity.kt index 7e8ffa9f6..5be28286e 100644 --- a/app/src/main/java/io/legato/kazusa/ui/book/audio/AudioPlayActivity.kt +++ b/app/src/main/java/io/legato/kazusa/ui/book/audio/AudioPlayActivity.kt @@ -1,14 +1,19 @@ package io.legato.kazusa.ui.book.audio import android.annotation.SuppressLint +import android.graphics.RenderEffect +import android.graphics.Shader import android.icu.text.SimpleDateFormat import android.os.Build import android.os.Bundle -import android.view.Gravity +import android.view.HapticFeedbackConstants import android.view.Menu import android.view.MenuItem import androidx.activity.viewModels +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope +import androidx.transition.TransitionManager import com.google.android.material.slider.Slider import io.legato.kazusa.R import io.legato.kazusa.base.VMBaseActivity @@ -35,14 +40,14 @@ import io.legato.kazusa.ui.book.toc.TocActivityResult import io.legato.kazusa.ui.login.SourceLoginActivity import io.legato.kazusa.utils.StartActivityContract import io.legato.kazusa.utils.applyNavigationBarPadding -import io.legato.kazusa.utils.dpToPx -import io.legato.kazusa.utils.invisible +import io.legato.kazusa.utils.gone import io.legato.kazusa.utils.observeEvent import io.legato.kazusa.utils.observeEventSticky import io.legato.kazusa.utils.sendToClip import io.legato.kazusa.utils.showDialogFragment import io.legato.kazusa.utils.startActivity import io.legato.kazusa.utils.startActivityForBook +import io.legato.kazusa.utils.startAnimation import io.legato.kazusa.utils.viewbindingdelegate.viewBinding import io.legato.kazusa.utils.visible import kotlinx.coroutines.Dispatchers.IO @@ -62,9 +67,9 @@ class AudioPlayActivity : override val binding by viewBinding(ActivityAudioPlayBinding::inflate) override val viewModel by viewModels() - private val timerSliderPopup by lazy { TimerSliderPopup(this) } private var adjustProgress = false private var playMode = AudioPlay.PlayMode.LIST_END_STOP + private var playSpeed = 1f private val progressTimeFormat by lazy { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { @@ -92,9 +97,10 @@ class AudioPlayActivity : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding.titleBar.setBackgroundResource(R.color.transparent) + binding.titleBar.title = "" AudioPlay.register(this) viewModel.titleData.observe(this) { - binding.titleBar.title = it + binding.tvTitle.text = it } viewModel.coverData.observe(this) { upCover(it) @@ -141,6 +147,9 @@ class AudioPlayActivity : } private fun initView() { + binding.ivTimer.isEnabled = false + binding.ivFastForward.isEnabled = false + binding.ivPlayMode.setOnClickListener { AudioPlay.changePlayMode() } @@ -195,35 +204,138 @@ class AudioPlayActivity : } }) + binding.playerProgress.setLabelFormatter { value -> + progressTimeFormat.format(value.toLong()) + } + binding.ivChapter.setOnClickListener { AudioPlay.book?.let { tocActivityResult.launch(it.bookUrl) } } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - binding.ivFastRewind.invisible() - binding.ivFastForward.invisible() + + binding.ivFastForward.setOnClickListener { toggleFastForward() } + binding.ivTimer.setOnClickListener { toggleTimer() } + + binding.llSubMenu.applyNavigationBarPadding() + } + + + private fun toggleFastForward() { + TransitionManager.beginDelayedTransition(binding.root) + if (binding.llSet.isVisible) { + if (binding.ivTimer.isChecked) { + initSlider(true) + } else { + binding.llSet.gone() + } + binding.ivTimer.isChecked = false + } else { + initSlider(true) + binding.llSet.visible() } - binding.ivFastForward.setOnClickListener { - AudioPlay.adjustSpeed(0.1f) + } + + private fun toggleTimer() { + TransitionManager.beginDelayedTransition(binding.root) + if (binding.llSet.isVisible) { + if (binding.ivFastForward.isChecked) { + initSlider(false) + } else { + binding.llSet.gone() + } + binding.ivFastForward.isChecked = false + } else { + initSlider(false) + binding.llSet.visible() } - binding.ivFastRewind.setOnClickListener { - AudioPlay.adjustSpeed(-0.1f) + } + + private fun initSlider(isAudioPlay: Boolean) { + binding.settingSlider.clearOnChangeListeners() + binding.settingSlider.clearOnSliderTouchListeners() + binding.btnReset.clearOnCheckedChangeListeners() + + if (isAudioPlay) { + binding.settingSlider.isEnabled = AudioPlay.status != Status.STOP + binding.btnReset.setOnClickListener { + binding.settingSlider.value = 1f + AudioPlay.adjustSpeed(1f) + } + binding.settingSlider.apply { + valueFrom = 0.5f + valueTo = 5.0f + stepSize = 0.1f + value = playSpeed + + addOnChangeListener { _, newValue, fromUser -> + if (fromUser) { + AudioPlay.adjustSpeed(newValue) + } + } + + addOnSliderTouchListener(object : Slider.OnSliderTouchListener { + override fun onStartTrackingTouch(slider: Slider) { + slider.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) + } + + override fun onStopTrackingTouch(slider: Slider) {} + }) + + setLabelFormatter { value -> String.format(Locale.ROOT, "%.1fX", value) } + } + } else { + binding.settingSlider.isEnabled = true + binding.btnReset.setOnClickListener { + binding.settingSlider.value = 0f + AudioPlay.setTimer(0) + } + binding.settingSlider.apply { + valueFrom = 0f + valueTo = 180f + stepSize = 1f + value = AudioPlayService.timeMinute.toFloat() + + addOnChangeListener { _, newValue, fromUser -> + if (fromUser) AudioPlay.setTimer(newValue.toInt()) + } + + addOnSliderTouchListener(object : Slider.OnSliderTouchListener { + override fun onStartTrackingTouch(slider: Slider) { + slider.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) + } + + override fun onStopTrackingTouch(slider: Slider) {} + }) + + setLabelFormatter { v -> "${v.toInt()}m" } + } } - binding.ivTimer.setOnClickListener { - timerSliderPopup.showAsDropDown(it, 0, (-100).dpToPx(), Gravity.TOP) - } - binding.llPlayMenu.applyNavigationBarPadding() } private fun updatePlayModeIcon() { - binding.ivPlayMode.setImageResource(playMode.iconRes) + binding.ivPlayMode.setIconResource(playMode.iconRes) } private fun upCover(path: String?) { BookCover.load(this, path, sourceOrigin = AudioPlay.bookSource?.bookSourceUrl) { - BookCover.loadBlur(this, path, sourceOrigin = AudioPlay.bookSource?.bookSourceUrl) - .into(binding.ivBg) + if (!AppConfig.isEInkMode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + BookCover.load(this, path, sourceOrigin = AudioPlay.bookSource?.bookSourceUrl) + .into(binding.ivBg) + val blurEffect = + RenderEffect.createBlurEffect(120f, 120f, Shader.TileMode.CLAMP) + binding.ivBg.setRenderEffect(blurEffect) + } else { + // 低版本直接加载模糊图 + BookCover.loadBlur( + this, + path, + sourceOrigin = AudioPlay.bookSource?.bookSourceUrl + ) + .into(binding.ivBg) + } + } }.into(binding.ivCover) } @@ -294,14 +406,30 @@ class AudioPlayActivity : playButton() } } - observeEventSticky(EventBus.AUDIO_STATE) { - AudioPlay.status = it - if (it == Status.PLAY) { - binding.fabPlayStop.setImageResource(R.drawable.ic_pause_24dp) + observeEventSticky(EventBus.AUDIO_STATE) { state -> + AudioPlay.status = state + + val iconDrawable = if (state == Status.PLAY) { + binding.ivTimer.isEnabled = true + binding.ivFastForward.isEnabled = true + AppCompatResources.getDrawable(this, R.drawable.play_anim) } else { - binding.fabPlayStop.setImageResource(R.drawable.ic_play_24dp) + AppCompatResources.getDrawable(this, R.drawable.pause_anim) } + + val bgDrawable = if (state == Status.PLAY) { + AppCompatResources.getDrawable(this, R.drawable.bg_play_anim) + } else { + AppCompatResources.getDrawable(this, R.drawable.bg_pause_anim) + } + + binding.fabPlayStop.icon = iconDrawable + binding.fabPlayStop.background = bgDrawable + + iconDrawable?.startAnimation() + bgDrawable?.startAnimation() } + observeEventSticky(EventBus.AUDIO_SUB_TITLE) { binding.tvSubTitle.text = it binding.ivSkipPrevious.isEnabled = AudioPlay.durChapterIndex > 0 @@ -321,7 +449,7 @@ class AudioPlayActivity : // // } observeEventSticky(EventBus.AUDIO_SIZE) { - binding.playerProgress.valueTo = it.toFloat() + binding.playerProgress.valueTo = maxOf(1f, it.toFloat()) binding.tvAllTime.text = progressTimeFormat.format(it.toLong()) } @@ -335,12 +463,21 @@ class AudioPlayActivity : } observeEventSticky(EventBus.AUDIO_SPEED) { + TransitionManager.beginDelayedTransition(binding.root) + playSpeed = it binding.tvSpeed.text = String.format(Locale.ROOT, "%.1fX", it) - binding.tvSpeed.visible() + if (it != 1f) { + binding.cdSpeed.visible() + } else + binding.cdSpeed.gone() } observeEventSticky(EventBus.AUDIO_DS) { + TransitionManager.beginDelayedTransition(binding.root) binding.tvTimer.text = "${it}m" - binding.tvTimer.visible(it > 0) + if (it > 0) { + binding.cdTimer.visible() + } else + binding.cdTimer.gone() } } diff --git a/app/src/main/java/io/legato/kazusa/ui/book/audio/TimerSliderPopup.kt b/app/src/main/java/io/legato/kazusa/ui/book/audio/TimerSliderPopup.kt deleted file mode 100644 index f0699d563..000000000 --- a/app/src/main/java/io/legato/kazusa/ui/book/audio/TimerSliderPopup.kt +++ /dev/null @@ -1,53 +0,0 @@ -package io.legato.kazusa.ui.book.audio - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.PopupWindow -import io.legato.kazusa.R -import io.legato.kazusa.databinding.PopupSeekBarBinding -import io.legato.kazusa.model.AudioPlay -import io.legato.kazusa.service.AudioPlayService - -class TimerSliderPopup(private val context: Context) : - PopupWindow(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) { - - private val binding = PopupSeekBarBinding.inflate(LayoutInflater.from(context)) - - init { - contentView = binding.root - - isTouchable = true - isOutsideTouchable = false - isFocusable = true - - // 初始化 slider 值 - binding.slider.valueFrom = 1f - binding.slider.valueTo = 180f - binding.slider.stepSize = 1f - binding.slider.value = AudioPlayService.timeMinute.toFloat() - setSliderTextValue(AudioPlayService.timeMinute) - - binding.slider.addOnChangeListener { _, value, fromUser -> - setSliderTextValue(value.toInt()) - if (fromUser) { - AudioPlay.setTimer(value.toInt()) - } - } - } - - override fun showAsDropDown(anchor: View?, xoff: Int, yoff: Int, gravity: Int) { - super.showAsDropDown(anchor, xoff, yoff, gravity) - binding.slider.value = AudioPlayService.timeMinute.toFloat() - } - - override fun showAtLocation(parent: View?, gravity: Int, x: Int, y: Int) { - super.showAtLocation(parent, gravity, x, y) - binding.slider.value = AudioPlayService.timeMinute.toFloat() - } - - private fun setSliderTextValue(value: Int) { - binding.tvSeekValue.text = context.getString(R.string.timer_m, value) - } -} diff --git a/app/src/main/java/io/legato/kazusa/utils/DrawableUtils.kt b/app/src/main/java/io/legato/kazusa/utils/DrawableUtils.kt index 64ba15116..5cc238923 100644 --- a/app/src/main/java/io/legato/kazusa/utils/DrawableUtils.kt +++ b/app/src/main/java/io/legato/kazusa/utils/DrawableUtils.kt @@ -4,11 +4,14 @@ package io.legato.kazusa.utils import android.content.res.ColorStateList import android.graphics.PorterDuff +import android.graphics.drawable.Animatable +import android.graphics.drawable.AnimatedVectorDrawable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.TransitionDrawable import androidx.annotation.ColorInt import androidx.core.graphics.drawable.DrawableCompat +import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat /** * @author Karim Abou Zeid (kabouzeid) @@ -52,4 +55,13 @@ fun Drawable.setTintMutate( wrappedDrawable.mutate() DrawableCompat.setTintMode(wrappedDrawable, tintMode) DrawableCompat.setTint(wrappedDrawable, tint) -} \ No newline at end of file +} + +fun Drawable.startAnimation() { + when (this) { + is AnimatedVectorDrawable -> start() + is AnimatedVectorDrawableCompat -> start() + is Animatable -> start() + else -> {} + } +} diff --git a/app/src/main/res/drawable/bg_pause_anim.xml b/app/src/main/res/drawable/bg_pause_anim.xml new file mode 100644 index 000000000..a51e09ff4 --- /dev/null +++ b/app/src/main/res/drawable/bg_pause_anim.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_play_anim.xml b/app/src/main/res/drawable/bg_play_anim.xml new file mode 100644 index 000000000..6c432fb25 --- /dev/null +++ b/app/src/main/res/drawable/bg_play_anim.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_media_pause_anim.xml b/app/src/main/res/drawable/ic_media_pause_anim.xml new file mode 100644 index 000000000..b28ba2919 --- /dev/null +++ b/app/src/main/res/drawable/ic_media_pause_anim.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_media_play_anim.xml b/app/src/main/res/drawable/ic_media_play_anim.xml new file mode 100644 index 000000000..b972064e9 --- /dev/null +++ b/app/src/main/res/drawable/ic_media_play_anim.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_pause.xml b/app/src/main/res/drawable/ic_pause.xml new file mode 100644 index 000000000..b85dab628 --- /dev/null +++ b/app/src/main/res/drawable/ic_pause.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_pause_filled.xml b/app/src/main/res/drawable/ic_pause_filled.xml new file mode 100644 index 000000000..efdfa4131 --- /dev/null +++ b/app/src/main/res/drawable/ic_pause_filled.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_play_filled.xml b/app/src/main/res/drawable/ic_play_filled.xml new file mode 100644 index 000000000..c3c093505 --- /dev/null +++ b/app/src/main/res/drawable/ic_play_filled.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_speed.xml b/app/src/main/res/drawable/ic_speed.xml new file mode 100644 index 000000000..b11659d41 --- /dev/null +++ b/app/src/main/res/drawable/ic_speed.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_btn_accent_bg.xml b/app/src/main/res/drawable/selector_btn_accent_bg.xml deleted file mode 100644 index 80a399cbc..000000000 --- a/app/src/main/res/drawable/selector_btn_accent_bg.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_circle_btn_bg.xml b/app/src/main/res/drawable/selector_circle_btn_bg.xml deleted file mode 100644 index 6144b889b..000000000 --- a/app/src/main/res/drawable/selector_circle_btn_bg.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_audio_play.xml b/app/src/main/res/layout/activity_audio_play.xml index e42977a25..200493993 100644 --- a/app/src/main/res/layout/activity_audio_play.xml +++ b/app/src/main/res/layout/activity_audio_play.xml @@ -17,8 +17,14 @@ + + + android:alpha="0.80" + android:background="?attr/colorSurfaceContainer" /> - + app:cardElevation="16dp" + app:layout_constraintBottom_toTopOf="@+id/tv_title" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/title_bar" + app:strokeWidth="0dp"> - + + + + android:layout_margin="8dp" + android:visibility="gone" + app:cardBackgroundColor="?attr/colorSurfaceContainer" + app:cardCornerRadius="4dp" + app:cardElevation="16dp" + app:layout_constraintStart_toStartOf="@id/cd_cover" + app:layout_constraintTop_toTopOf="@id/cd_cover" + app:strokeWidth="0dp"> - + + + + + + + + + + + + + + + + + android:scrollbars="none" + android:textColor="?attr/colorOnSurfaceVariant" + android:textSize="17sp" + android:textStyle="bold" + app:layout_constraintBottom_toTopOf="@+id/player_progress" + tools:text="@string/text" /> - + + + android:layout_gravity="center_vertical" + android:layout_marginHorizontal="14dp" + android:value="0" + android:valueFrom="0" + android:valueTo="1" + app:layout_constraintBottom_toTopOf="@id/tv_all_time" + app:thumbElevation="0dp" + app:thumbHeight="20dp" + app:thumbWidth="4dp" + app:trackHeight="5dp" /> - - - - - - - + + app:layout_constraintBottom_toTopOf="@id/ll_sub_menu"> - - - - - - - + + - + app:layout_constraintTop_toTopOf="parent" /> - - + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - diff --git a/app/src/main/res/layout/popup_seek_bar.xml b/app/src/main/res/layout/popup_seek_bar.xml deleted file mode 100644 index fc5e4aef7..000000000 --- a/app/src/main/res/layout/popup_seek_bar.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/drawables.xml b/app/src/main/res/values/drawables.xml new file mode 100644 index 000000000..d7c60b63d --- /dev/null +++ b/app/src/main/res/values/drawables.xml @@ -0,0 +1,15 @@ + + + + @drawable/ic_groups + @drawable/ic_media_pause_anim + @drawable/ic_media_play_anim + + + + @drawable/ic_play_filled + + + @drawable/ic_pause_filled + + \ No newline at end of file