更新有声书界面

This commit is contained in:
HapeLee
2025-09-05 03:38:27 +08:00
parent 081e3e2311
commit 03ec56de59
17 changed files with 709 additions and 280 deletions
@@ -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<AudioPlayService>(IntentAction.resume)
)
} else {
builder.addAction(
R.drawable.ic_pause_24dp,
R.drawable.ic_pause,
getString(R.string.pause),
servicePendingIntent<AudioPlayService>(IntentAction.pause)
)
@@ -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<AudioPlayViewModel>()
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<Int>(EventBus.AUDIO_STATE) {
AudioPlay.status = it
if (it == Status.PLAY) {
binding.fabPlayStop.setImageResource(R.drawable.ic_pause_24dp)
observeEventSticky<Int>(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<String>(EventBus.AUDIO_SUB_TITLE) {
binding.tvSubTitle.text = it
binding.ivSkipPrevious.isEnabled = AudioPlay.durChapterIndex > 0
@@ -321,7 +449,7 @@ class AudioPlayActivity :
//
// }
observeEventSticky<Int>(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<Float>(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<Int>(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()
}
}
@@ -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)
}
}
@@ -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)
}
}
fun Drawable.startAnimation() {
when (this) {
is AnimatedVectorDrawable -> start()
is AnimatedVectorDrawableCompat -> start()
is Animatable -> start()
else -> {}
}
}