sound 播放逻辑
This commit is contained in:
parent
8f171d3214
commit
1eb8b02ef0
|
|
@ -6,14 +6,17 @@ import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ChatSound(
|
data class ChatSound(
|
||||||
val ttsId: Long,
|
val ttsId: Long = 0,
|
||||||
val language: Int,
|
val language: Int = 0,
|
||||||
val desLanguage: String,
|
val desLanguage: String = "",
|
||||||
val gender: Int,
|
val gender: Int = 1,
|
||||||
val rules: Int,
|
val rules: Int = 0,
|
||||||
val nameLanguage: String,
|
val nameLanguage: String = "",
|
||||||
val headPortrait: String,
|
val headPortrait: String = "",
|
||||||
var isSelected: Boolean = false
|
var isSelected: Boolean = false,
|
||||||
) : Parcelable {
|
|
||||||
|
// Other needed
|
||||||
|
var sampleUrl: String = ""
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,33 @@ class ChatSettingView @JvmOverloads constructor(
|
||||||
|
|
||||||
|
|
||||||
fun initSoundSelectorView() {
|
fun initSoundSelectorView() {
|
||||||
|
val items = listOf(
|
||||||
|
ChatSound(
|
||||||
|
ttsId = 1,
|
||||||
|
gender = 2,
|
||||||
|
nameLanguage = "名称-1"
|
||||||
|
),
|
||||||
|
ChatSound(
|
||||||
|
ttsId = 1,
|
||||||
|
gender = 2,
|
||||||
|
nameLanguage = "名称-2"
|
||||||
|
),
|
||||||
|
ChatSound(
|
||||||
|
ttsId = 1,
|
||||||
|
gender = 1,
|
||||||
|
nameLanguage = "名称-3"
|
||||||
|
),
|
||||||
|
ChatSound(
|
||||||
|
ttsId = 1,
|
||||||
|
gender = 1,
|
||||||
|
nameLanguage = "名称-4"
|
||||||
|
),
|
||||||
|
ChatSound(
|
||||||
|
ttsId = 1,
|
||||||
|
gender = 2,
|
||||||
|
nameLanguage = "名称-5"
|
||||||
|
),
|
||||||
|
)
|
||||||
/*val items = listOf(
|
/*val items = listOf(
|
||||||
ChatSound(
|
ChatSound(
|
||||||
id = 1L,
|
id = 1L,
|
||||||
|
|
@ -214,7 +241,7 @@ class ChatSettingView @JvmOverloads constructor(
|
||||||
)
|
)
|
||||||
*/
|
*/
|
||||||
with(mBinding.soundActorSelector) {
|
with(mBinding.soundActorSelector) {
|
||||||
//setItems(items)
|
setItems(items)
|
||||||
setEventListener(
|
setEventListener(
|
||||||
object : ExpandSoundSelectView.IEventListener {
|
object : ExpandSoundSelectView.IEventListener {
|
||||||
override fun onItemSelected(
|
override fun onItemSelected(
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,27 @@ import android.animation.Animator
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Environment
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.Toast
|
||||||
import com.remax.visualnovel.R
|
import com.remax.visualnovel.R
|
||||||
|
import com.remax.visualnovel.configs.NovelApplication
|
||||||
import com.remax.visualnovel.databinding.LayoutExpandSelectViewBinding
|
import com.remax.visualnovel.databinding.LayoutExpandSelectViewBinding
|
||||||
import com.remax.visualnovel.entity.response.ChatSound
|
import com.remax.visualnovel.entity.response.ChatSound
|
||||||
|
import com.remax.visualnovel.utils.media.AudioPlayableView
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
class ExpandSoundSelectView @JvmOverloads constructor(
|
class ExpandSoundSelectView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyleAttr: Int = 0
|
defStyleAttr: Int = 0
|
||||||
) : android.widget.LinearLayout(context, attrs, defStyleAttr) {
|
) : AudioPlayableView(context, attrs, defStyleAttr) {
|
||||||
private lateinit var mBinding: LayoutExpandSelectViewBinding
|
private lateinit var mBinding: LayoutExpandSelectViewBinding
|
||||||
private lateinit var mExpandView : ExpandSoundSubView
|
private lateinit var mExpandView : ExpandSoundSubView
|
||||||
private var isExpanded = false
|
private var isExpanded = false
|
||||||
|
|
@ -56,6 +62,10 @@ class ExpandSoundSelectView @JvmOverloads constructor(
|
||||||
override fun onFilterChanged(sexValue: Int) {
|
override fun onFilterChanged(sexValue: Int) {
|
||||||
mEventListener.onFiltersChanged(sexValue)
|
mEventListener.onFiltersChanged(sexValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPlaySample(soundItem: ChatSound) {
|
||||||
|
playSoundSample(soundItem)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -150,5 +160,88 @@ class ExpandSoundSelectView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun playSoundSample(chatSound: ChatSound) {
|
||||||
|
if (audioPlayer.isPlaying()) {
|
||||||
|
pauseAudio()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chatSound.sampleUrl.isEmpty()) {
|
||||||
|
playAudio( chatSound.sampleUrl)
|
||||||
|
} else {
|
||||||
|
playAudio(NovelApplication.appContext().getExternalFilesDir(null)?.path + "/ringtone.mp3")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun onAudioStarted() {
|
||||||
|
post {
|
||||||
|
// 更新UI为暂停状态
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAudioPaused() {
|
||||||
|
post {
|
||||||
|
// 更新UI为播放状态
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAudioError(errorMessage: String) {
|
||||||
|
post {
|
||||||
|
Toast.makeText(context, "播放失败: $errorMessage", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
// 在Activity中使用
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var audioPlayer: AudioPlayerManager
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
|
audioPlayer = AudioPlayerManager.getInstance(this)
|
||||||
|
|
||||||
|
setupAudioViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupAudioViews() {
|
||||||
|
// 按钮1播放网络音频
|
||||||
|
findViewById<View>(R.id.btnPlay1).setOnClickListener {
|
||||||
|
audioPlayer.play("https://example.com/audio1.mp3", object : AudioPlayListener {
|
||||||
|
override fun onStateChanged(state: AudioPlayState, source: String) {
|
||||||
|
// 只监听这个音频源的状态
|
||||||
|
updateButton1State(state)
|
||||||
|
}
|
||||||
|
override fun onProgressChanged(position: Int, duration: Int, source: String) {}
|
||||||
|
override fun onBufferingUpdate(percent: Int, source: String) {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按钮2播放本地文件
|
||||||
|
findViewById<View>(R.id.btnPlay2).setOnClickListener {
|
||||||
|
val file = File(getExternalFilesDir("audio"), "local.mp3")
|
||||||
|
audioPlayer.play(file, object : AudioPlayListener {
|
||||||
|
override fun onStateChanged(state: AudioPlayState, source: String) {
|
||||||
|
updateButton2State(state)
|
||||||
|
}
|
||||||
|
override fun onProgressChanged(position: Int, duration: Int, source: String) {}
|
||||||
|
override fun onBufferingUpdate(percent: Int, source: String) {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
// 根据需求决定是否释放单例
|
||||||
|
// AudioPlayerManager.releaseInstance()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -36,6 +36,7 @@ class ExpandSoundSubView @JvmOverloads constructor(
|
||||||
interface IEventListener {
|
interface IEventListener {
|
||||||
fun onSoundSelected(sound: ChatSound)
|
fun onSoundSelected(sound: ChatSound)
|
||||||
fun onFilterChanged(setValue: Int)
|
fun onFilterChanged(setValue: Int)
|
||||||
|
fun onPlaySample(soundItem: ChatSound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -68,10 +69,6 @@ class ExpandSoundSubView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun playActorSound(bubble : ChatSound) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun setEventListener(eventListener: IEventListener) {
|
fun setEventListener(eventListener: IEventListener) {
|
||||||
mEventListener = eventListener
|
mEventListener = eventListener
|
||||||
|
|
@ -86,8 +83,8 @@ class ExpandSoundSubView @JvmOverloads constructor(
|
||||||
addType<ChatSound>(R.layout.layout_item_setting_sound)
|
addType<ChatSound>(R.layout.layout_item_setting_sound)
|
||||||
|
|
||||||
onClick(R.id.left_container) {
|
onClick(R.id.left_container) {
|
||||||
val bubble = getModel<ChatSound>()
|
val sound = getModel<ChatSound>()
|
||||||
playActorSound(bubble)
|
mEventListener.onPlaySample(sound)
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick(R.id.item_root) {
|
onClick(R.id.item_root) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
package com.remax.visualnovel.utils.media
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
|
abstract class AudioPlayableView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : FrameLayout(context, attrs, defStyleAttr), AudioPlayListener {
|
||||||
|
|
||||||
|
protected val audioPlayer = GlobalAudioPlayerManager.getInstance()
|
||||||
|
protected var currentAudioSource: String = ""
|
||||||
|
private var lifecycleObserver: LifecycleEventObserver? = null
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fun playAudio(source: Any, autoStopOnDetach: Boolean = true) {
|
||||||
|
currentAudioSource = when (source) {
|
||||||
|
is String -> source
|
||||||
|
is Uri -> source.toString()
|
||||||
|
is File -> source.absolutePath
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just listen current source
|
||||||
|
audioPlayer.addListener(currentAudioSource, this, "view_${hashCode()}")
|
||||||
|
if (!audioPlayer.play(source, this, "view_${hashCode()}")) {
|
||||||
|
onPlayError("Play Error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* bind lifecycle from fragment or activity
|
||||||
|
*/
|
||||||
|
fun bindLifecycle(lifecycleOwner: LifecycleOwner) {
|
||||||
|
lifecycleObserver = LifecycleEventObserver { _, event ->
|
||||||
|
when (event) {
|
||||||
|
Lifecycle.Event.ON_PAUSE -> pauseAudio()
|
||||||
|
Lifecycle.Event.ON_RESUME -> { /* 可选的恢复逻辑 */ }
|
||||||
|
Lifecycle.Event.ON_DESTROY -> releaseAudio()
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleOwner.lifecycle.addObserver(lifecycleObserver!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* pause
|
||||||
|
*/
|
||||||
|
fun pauseAudio() {
|
||||||
|
if (audioPlayer.getCurrentSource() == currentAudioSource) {
|
||||||
|
audioPlayer.pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* stop
|
||||||
|
*/
|
||||||
|
fun stopAudio() {
|
||||||
|
if (audioPlayer.getCurrentSource() == currentAudioSource) {
|
||||||
|
audioPlayer.stop()
|
||||||
|
}
|
||||||
|
audioPlayer.removeListener(currentAudioSource, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* stop automatically while view detach
|
||||||
|
*/
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
stopAudio()
|
||||||
|
lifecycleObserver?.let { observer ->
|
||||||
|
(context as? LifecycleOwner)?.lifecycle?.removeObserver(observer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* release
|
||||||
|
*/
|
||||||
|
fun releaseAudio() {
|
||||||
|
stopAudio()
|
||||||
|
currentAudioSource = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Specified source state changed callback
|
||||||
|
override fun onStateChanged(state: AudioPlayState, source: String) {
|
||||||
|
if (source != currentAudioSource) return
|
||||||
|
|
||||||
|
when (state) {
|
||||||
|
is AudioPlayState.Playing -> onAudioStarted()
|
||||||
|
is AudioPlayState.Paused -> onAudioPaused()
|
||||||
|
is AudioPlayState.Completed -> onAudioCompleted()
|
||||||
|
is AudioPlayState.Error -> onAudioError(state.errorMessage)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onProgressChanged(position: Int, duration: Int, source: String) {
|
||||||
|
if (source == currentAudioSource) {
|
||||||
|
onAudioProgressChanged(position, duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBufferingUpdate(percent: Int, source: String) {
|
||||||
|
if (source == currentAudioSource) {
|
||||||
|
onAudioBufferingUpdate(percent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Sub class could override methods
|
||||||
|
open fun onPlayError(message: String) {}
|
||||||
|
open fun onAudioStarted() {}
|
||||||
|
open fun onAudioPaused() {}
|
||||||
|
open fun onAudioCompleted() {}
|
||||||
|
open fun onAudioError(errorMessage: String) {}
|
||||||
|
open fun onAudioProgressChanged(position: Int, duration: Int) {}
|
||||||
|
open fun onAudioBufferingUpdate(percent: Int) {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,651 @@
|
||||||
|
package com.remax.visualnovel.utils.media
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.AudioAttributes
|
||||||
|
import android.media.AudioFocusRequest
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.media.MediaPlayer
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import com.remax.visualnovel.configs.NovelApplication
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
|
// Player State
|
||||||
|
sealed class AudioPlayState {
|
||||||
|
object Idle : AudioPlayState()
|
||||||
|
object Preparing : AudioPlayState()
|
||||||
|
object Playing : AudioPlayState()
|
||||||
|
object Paused : AudioPlayState()
|
||||||
|
object Stopped : AudioPlayState()
|
||||||
|
data class Error(val errorCode: Int, val errorMessage: String, val source: String) : AudioPlayState()
|
||||||
|
object Completed : AudioPlayState()
|
||||||
|
}
|
||||||
|
|
||||||
|
// error code
|
||||||
|
object AudioErrorCode {
|
||||||
|
const val UNKNOWN_ERROR = 1000
|
||||||
|
const val NETWORK_ERROR = 1001
|
||||||
|
const val FILE_NOT_FOUND = 1002
|
||||||
|
const val FORMAT_NOT_SUPPORTED = 1003
|
||||||
|
const val PERMISSION_DENIED = 1004
|
||||||
|
const val AUDIO_FOCUS_LOST = 1005
|
||||||
|
const val PREPARE_FAILED = 1006
|
||||||
|
const val AUDIO_FOCUS_REQUEST_FAILED = 1007
|
||||||
|
const val INVALID_DATA_SOURCE = 1008
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player config
|
||||||
|
data class AudioPlayerConfig(
|
||||||
|
val streamType: Int = AudioManager.STREAM_MUSIC,
|
||||||
|
val usage: Int = AudioAttributes.USAGE_MEDIA,
|
||||||
|
val contentType: Int = AudioAttributes.CONTENT_TYPE_MUSIC,
|
||||||
|
val focusGain: Int = AudioManager.AUDIOFOCUS_GAIN,
|
||||||
|
val autoPlay: Boolean = true,
|
||||||
|
val loop: Boolean = false,
|
||||||
|
val timeoutMs: Long = 30000,
|
||||||
|
val retryCount: Int = 2,
|
||||||
|
val duckOnFocusLoss: Boolean = true
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// Player state listener
|
||||||
|
interface AudioPlayListener {
|
||||||
|
fun onStateChanged(state: AudioPlayState, source: String)
|
||||||
|
fun onProgressChanged(position: Int, duration: Int, source: String)
|
||||||
|
fun onBufferingUpdate(percent: Int, source: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Single instance player manager
|
||||||
|
class GlobalAudioPlayerManager private constructor(
|
||||||
|
private val config: AudioPlayerConfig = AudioPlayerConfig()
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Volatile
|
||||||
|
private var instance: GlobalAudioPlayerManager? = null
|
||||||
|
|
||||||
|
private val mAppContext: Context = NovelApplication.appContext()
|
||||||
|
|
||||||
|
fun getInstance(config: AudioPlayerConfig = AudioPlayerConfig()): GlobalAudioPlayerManager {
|
||||||
|
return instance ?: synchronized(this) {
|
||||||
|
instance ?: GlobalAudioPlayerManager(config).also { instance = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun releaseInstance() {
|
||||||
|
instance?.release()
|
||||||
|
instance = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Global audio focus manager
|
||||||
|
private val audioManager: AudioManager by lazy {
|
||||||
|
mAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private var mediaPlayer: MediaPlayer? = null
|
||||||
|
private var currentState: AudioPlayState = AudioPlayState.Idle
|
||||||
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
|
||||||
|
private var currentSource: String = ""
|
||||||
|
private var previousSource: String = ""
|
||||||
|
private var isAudioFocusGranted = false
|
||||||
|
private var audioFocusRequest: AudioFocusRequest? = null
|
||||||
|
private var wasPlayingBeforeFocusLoss = false
|
||||||
|
|
||||||
|
|
||||||
|
// Listeners, just notify events about specified file(Map, key)
|
||||||
|
private val sourceListeners = ConcurrentHashMap<String, CopyOnWriteArrayList<AudioPlayListener>>()
|
||||||
|
|
||||||
|
// progress runnable
|
||||||
|
private var progressUpdateRunnable: Runnable? = null
|
||||||
|
private var isReleased = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
initializeMediaPlayer()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeMediaPlayer() {
|
||||||
|
if (isReleased) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
mediaPlayer = MediaPlayer().apply {
|
||||||
|
setAudioAttributes(
|
||||||
|
AudioAttributes.Builder()
|
||||||
|
.setUsage(config.usage)
|
||||||
|
.setContentType(config.contentType)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
setOnPreparedListener(::onPrepared)
|
||||||
|
setOnCompletionListener(::onCompletion)
|
||||||
|
setOnErrorListener(::onError)
|
||||||
|
setOnBufferingUpdateListener(::onBufferingUpdate)
|
||||||
|
isLooping = config.loop
|
||||||
|
}
|
||||||
|
setState(AudioPlayState.Idle, currentSource)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
handleError(AudioErrorCode.UNKNOWN_ERROR, "MediaPlayer init failed: ${e.message}", currentSource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* play source, also pause previous playing source.
|
||||||
|
*/
|
||||||
|
fun play(source: Any, listener: AudioPlayListener? = null, tag: String = ""): Boolean {
|
||||||
|
if (isReleased) {
|
||||||
|
Log.w("AudioPlayer", "Player released, play failed")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val sourceString = convertSourceToString(source)
|
||||||
|
if (sourceString.isBlank()) {
|
||||||
|
handleError(AudioErrorCode.INVALID_DATA_SOURCE, "Invalid source", sourceString)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止上一个播放(如果是不同的source)
|
||||||
|
if (currentSource.isNotEmpty() && currentSource != sourceString) {
|
||||||
|
stopCurrentPlayback()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册监听器
|
||||||
|
listener?.let { addListener(sourceString, it, tag) }
|
||||||
|
|
||||||
|
currentSource = sourceString
|
||||||
|
return startPlayback(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为特定source添加监听器
|
||||||
|
*/
|
||||||
|
fun addListener(source: Any, listener: AudioPlayListener, tag: String = "") {
|
||||||
|
val sourceString = convertSourceToString(source)
|
||||||
|
if (sourceString.isBlank()) return
|
||||||
|
|
||||||
|
sourceListeners.getOrPut(sourceString) { CopyOnWriteArrayList() }.add(listener)
|
||||||
|
Log.d("AudioPlayer", "为source[$sourceString]添加监听器, 当前监听器数: ${sourceListeners[sourceString]?.size}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除特定source的监听器
|
||||||
|
*/
|
||||||
|
fun removeListener(source: Any, listener: AudioPlayListener) {
|
||||||
|
val sourceString = convertSourceToString(source)
|
||||||
|
sourceListeners[sourceString]?.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除特定tag的所有监听器
|
||||||
|
*/
|
||||||
|
fun removeListenersByTag(tag: String, source: Any? = null) {
|
||||||
|
if (source != null) {
|
||||||
|
val sourceString = convertSourceToString(source)
|
||||||
|
sourceListeners[sourceString]?.removeAll { listener ->
|
||||||
|
// 如果监听器有tag属性,可以根据需要实现
|
||||||
|
true // 这里需要根据实际tag实现过滤逻辑
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sourceListeners.values.forEach { listeners ->
|
||||||
|
listeners.removeAll { true } // 简化实现,实际需要根据tag过滤
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 暂停播放
|
||||||
|
*/
|
||||||
|
fun pause(): Boolean {
|
||||||
|
if (isReleased || currentState != AudioPlayState.Playing) return false
|
||||||
|
|
||||||
|
return try {
|
||||||
|
mediaPlayer?.pause()
|
||||||
|
setState(AudioPlayState.Paused, currentSource)
|
||||||
|
stopProgressUpdates()
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
handleError(AudioErrorCode.UNKNOWN_ERROR, "暂停失败: ${e.message}", currentSource)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复播放
|
||||||
|
*/
|
||||||
|
fun resume(): Boolean {
|
||||||
|
if (isReleased || currentState != AudioPlayState.Paused) return false
|
||||||
|
|
||||||
|
if (!requestAudioFocus()) {
|
||||||
|
handleError(AudioErrorCode.AUDIO_FOCUS_REQUEST_FAILED, "无法获取音频焦点", currentSource)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
mediaPlayer?.start()
|
||||||
|
setState(AudioPlayState.Playing, currentSource)
|
||||||
|
startProgressUpdates()
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
handleError(AudioErrorCode.UNKNOWN_ERROR, "恢复播放失败: ${e.message}", currentSource)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止播放
|
||||||
|
*/
|
||||||
|
fun stop(): Boolean {
|
||||||
|
if (isReleased) return false
|
||||||
|
|
||||||
|
stopCurrentPlayback()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到指定位置
|
||||||
|
*/
|
||||||
|
fun seekTo(position: Int): Boolean {
|
||||||
|
if (isReleased) return false
|
||||||
|
|
||||||
|
return try {
|
||||||
|
mediaPlayer?.takeIf { it.duration > 0 }?.let { player ->
|
||||||
|
val safePosition = position.coerceIn(0, player.duration)
|
||||||
|
player.seekTo(safePosition)
|
||||||
|
true
|
||||||
|
} ?: false
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前播放的source
|
||||||
|
*/
|
||||||
|
fun getCurrentSource(): String = currentSource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否正在播放
|
||||||
|
*/
|
||||||
|
fun isPlaying(): Boolean = currentState == AudioPlayState.Playing && !isReleased
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前状态
|
||||||
|
*/
|
||||||
|
fun getCurrentState(): AudioPlayState = currentState
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region 播放控制内部实现
|
||||||
|
|
||||||
|
private fun startPlayback(source: Any): Boolean {
|
||||||
|
if (!requestAudioFocus()) {
|
||||||
|
handleError(AudioErrorCode.AUDIO_FOCUS_REQUEST_FAILED, "无法获取音频焦点", currentSource)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
setState(AudioPlayState.Preparing, currentSource)
|
||||||
|
|
||||||
|
mediaPlayer?.let { player ->
|
||||||
|
player.reset()
|
||||||
|
|
||||||
|
when (source) {
|
||||||
|
is String -> setupDataSourceFromString(source)
|
||||||
|
is Uri -> player.setDataSource(mAppContext, source)
|
||||||
|
is File -> {
|
||||||
|
if (!source.exists()) {
|
||||||
|
throw IOException("File not exist: ${source.absolutePath}")
|
||||||
|
}
|
||||||
|
player.setDataSource(source.absolutePath)
|
||||||
|
}
|
||||||
|
else -> throw IllegalArgumentException("Not support media type")
|
||||||
|
}
|
||||||
|
|
||||||
|
player.prepareAsync()
|
||||||
|
true
|
||||||
|
} ?: false
|
||||||
|
} catch (e: Exception) {
|
||||||
|
handlePlayException(e, "Play prepare failed")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupDataSourceFromString(source: String) {
|
||||||
|
mediaPlayer?.let { player ->
|
||||||
|
when {
|
||||||
|
source.startsWith("http://") || source.startsWith("https://") -> {
|
||||||
|
player.setDataSource(source)
|
||||||
|
}
|
||||||
|
source.startsWith("file://") -> {
|
||||||
|
player.setDataSource(source)
|
||||||
|
}
|
||||||
|
source.startsWith("content://") -> {
|
||||||
|
player.setDataSource(mAppContext, Uri.parse(source))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// 尝试作为文件路径处理
|
||||||
|
val file = File(source)
|
||||||
|
if (file.exists()) {
|
||||||
|
player.setDataSource(source)
|
||||||
|
} else {
|
||||||
|
throw IOException("File not exist: $source")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopCurrentPlayback() {
|
||||||
|
try {
|
||||||
|
mediaPlayer?.let { player ->
|
||||||
|
if (player.isPlaying) {
|
||||||
|
player.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(AudioPlayState.Stopped, currentSource)
|
||||||
|
stopProgressUpdates()
|
||||||
|
abandonAudioFocus()
|
||||||
|
|
||||||
|
// 通知上一个source的监听器停止
|
||||||
|
if (previousSource.isNotEmpty()) {
|
||||||
|
notifyStateChanged(AudioPlayState.Stopped, previousSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
previousSource = currentSource
|
||||||
|
currentSource = ""
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AudioPlayer", "Stop playing failed: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// region 音频焦点管理
|
||||||
|
private fun requestAudioFocus(): Boolean {
|
||||||
|
if (isAudioFocusGranted) return true
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
audioFocusRequest = AudioFocusRequest.Builder(config.focusGain).apply {
|
||||||
|
setAudioAttributes(
|
||||||
|
AudioAttributes.Builder()
|
||||||
|
.setUsage(config.usage)
|
||||||
|
.setContentType(config.contentType)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
setOnAudioFocusChangeListener(::onAudioFocusChange)
|
||||||
|
setWillPauseWhenDucked(config.duckOnFocusLoss)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
audioManager.requestAudioFocus(audioFocusRequest!!)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
audioManager.requestAudioFocus(
|
||||||
|
::onAudioFocusChange,
|
||||||
|
config.streamType,
|
||||||
|
config.focusGain
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
isAudioFocusGranted = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
||||||
|
isAudioFocusGranted
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AudioPlayer", "Request audio focus failed: ${e.message}")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun abandonAudioFocus() {
|
||||||
|
try {
|
||||||
|
if (isAudioFocusGranted) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
audioFocusRequest?.let { audioManager.abandonAudioFocusRequest(it) }
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
audioManager.abandonAudioFocus(::onAudioFocusChange)
|
||||||
|
}
|
||||||
|
isAudioFocusGranted = false
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AudioPlayer", "Release audio focus failed: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onAudioFocusChange(focusChange: Int) {
|
||||||
|
mainHandler.post {
|
||||||
|
when (focusChange) {
|
||||||
|
AudioManager.AUDIOFOCUS_GAIN -> {
|
||||||
|
onAudioFocusGained()
|
||||||
|
}
|
||||||
|
AudioManager.AUDIOFOCUS_LOSS -> {
|
||||||
|
onAudioFocusLost()
|
||||||
|
}
|
||||||
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
||||||
|
onAudioFocusLostTransient()
|
||||||
|
}
|
||||||
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||||
|
onAudioFocusDuck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onAudioFocusGained() {
|
||||||
|
isAudioFocusGranted = true
|
||||||
|
if (wasPlayingBeforeFocusLoss && currentState == AudioPlayState.Paused) {
|
||||||
|
resume()
|
||||||
|
}
|
||||||
|
wasPlayingBeforeFocusLoss = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onAudioFocusLost() {
|
||||||
|
wasPlayingBeforeFocusLoss = currentState == AudioPlayState.Playing
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onAudioFocusLostTransient() {
|
||||||
|
wasPlayingBeforeFocusLoss = currentState == AudioPlayState.Playing
|
||||||
|
if (wasPlayingBeforeFocusLoss) {
|
||||||
|
pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onAudioFocusDuck() {
|
||||||
|
if (config.duckOnFocusLoss) {
|
||||||
|
mediaPlayer?.setVolume(0.2f, 0.2f)
|
||||||
|
} else {
|
||||||
|
wasPlayingBeforeFocusLoss = currentState == AudioPlayState.Playing
|
||||||
|
if (wasPlayingBeforeFocusLoss) {
|
||||||
|
pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// region MediaPlayer callback
|
||||||
|
private fun onPrepared(mp: MediaPlayer) {
|
||||||
|
mainHandler.post {
|
||||||
|
try {
|
||||||
|
if (config.autoPlay) {
|
||||||
|
mp.start()
|
||||||
|
setState(AudioPlayState.Playing, currentSource)
|
||||||
|
startProgressUpdates()
|
||||||
|
} else {
|
||||||
|
setState(AudioPlayState.Paused, currentSource)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
handleError(AudioErrorCode.PREPARE_FAILED, "Player prepare failed: ${e.message}", currentSource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onCompletion(mp: MediaPlayer) {
|
||||||
|
mainHandler.post {
|
||||||
|
setState(AudioPlayState.Completed, currentSource)
|
||||||
|
stopProgressUpdates()
|
||||||
|
abandonAudioFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
||||||
|
mainHandler.post {
|
||||||
|
val errorMessage = when (what) {
|
||||||
|
MediaPlayer.MEDIA_ERROR_UNKNOWN -> "Unknown error"
|
||||||
|
MediaPlayer.MEDIA_ERROR_SERVER_DIED -> "Media server error"
|
||||||
|
MediaPlayer.MEDIA_ERROR_IO -> "I/O ERROR"
|
||||||
|
else -> "Media Error: what=$what, extra=$extra"
|
||||||
|
}
|
||||||
|
handleError(AudioErrorCode.PREPARE_FAILED, errorMessage, currentSource)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onBufferingUpdate(mp: MediaPlayer, percent: Int) {
|
||||||
|
mainHandler.post {
|
||||||
|
notifyBufferingUpdate(percent, currentSource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
|
||||||
|
// region progress
|
||||||
|
private fun startProgressUpdates() {
|
||||||
|
stopProgressUpdates()
|
||||||
|
|
||||||
|
progressUpdateRunnable = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
if (currentState == AudioPlayState.Playing && !isReleased) {
|
||||||
|
try {
|
||||||
|
val position = mediaPlayer?.currentPosition ?: 0
|
||||||
|
val duration = mediaPlayer?.duration ?: 0
|
||||||
|
notifyProgressChanged(position, duration, currentSource)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mainHandler.postDelayed(this, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mainHandler.post(progressUpdateRunnable!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopProgressUpdates() {
|
||||||
|
progressUpdateRunnable?.let {
|
||||||
|
mainHandler.removeCallbacks(it)
|
||||||
|
progressUpdateRunnable = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Various state notifications
|
||||||
|
private fun setState(state: AudioPlayState, source: String) {
|
||||||
|
if (currentState != state) {
|
||||||
|
currentState = state
|
||||||
|
notifyStateChanged(state, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyStateChanged(state: AudioPlayState, source: String) {
|
||||||
|
mainHandler.post {
|
||||||
|
sourceListeners[source]?.forEach { listener ->
|
||||||
|
try {
|
||||||
|
listener.onStateChanged(state, source)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AudioPlayer", "onStateChanged exception: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyProgressChanged(position: Int, duration: Int, source: String) {
|
||||||
|
mainHandler.post {
|
||||||
|
sourceListeners[source]?.forEach { listener ->
|
||||||
|
try {
|
||||||
|
listener.onProgressChanged(position, duration, source)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AudioPlayer", "onProgressChanged Exception: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyBufferingUpdate(percent: Int, source: String) {
|
||||||
|
mainHandler.post {
|
||||||
|
sourceListeners[source]?.forEach { listener ->
|
||||||
|
try {
|
||||||
|
listener.onBufferingUpdate(percent, source)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AudioPlayer", "onBufferingUpdate exception: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// region 工具方法
|
||||||
|
private fun convertSourceToString(source: Any): String {
|
||||||
|
return when (source) {
|
||||||
|
is String -> source
|
||||||
|
is Uri -> source.toString()
|
||||||
|
is File -> source.absolutePath
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePlayException(e: Exception, message: String) {
|
||||||
|
val errorCode = when (e) {
|
||||||
|
is IOException -> AudioErrorCode.NETWORK_ERROR
|
||||||
|
is SecurityException -> AudioErrorCode.PERMISSION_DENIED
|
||||||
|
is IllegalArgumentException -> AudioErrorCode.FORMAT_NOT_SUPPORTED
|
||||||
|
else -> AudioErrorCode.UNKNOWN_ERROR
|
||||||
|
}
|
||||||
|
handleError(errorCode, "$message: ${e.message}", currentSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleError(errorCode: Int, errorMessage: String, source: String) {
|
||||||
|
Log.e("AudioPlayer", "Player Error[$errorCode]: $errorMessage, source: $source")
|
||||||
|
setState(AudioPlayState.Error(errorCode, errorMessage, source), source)
|
||||||
|
stopProgressUpdates()
|
||||||
|
abandonAudioFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region release related
|
||||||
|
fun release() {
|
||||||
|
if (isReleased) return
|
||||||
|
|
||||||
|
isReleased = true
|
||||||
|
stopCurrentPlayback()
|
||||||
|
stopProgressUpdates()
|
||||||
|
sourceListeners.clear()
|
||||||
|
abandonAudioFocus()
|
||||||
|
|
||||||
|
try {
|
||||||
|
mediaPlayer?.release()
|
||||||
|
mediaPlayer = null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AudioPlayer", "Release player failed: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
}
|
||||||
|
|
@ -64,17 +64,17 @@ open class IconFontTextView @JvmOverloads constructor(context: Context, attrs: A
|
||||||
var endDrawable: IconFontDrawable? = null
|
var endDrawable: IconFontDrawable? = null
|
||||||
var topDrawable: IconFontDrawable? = null
|
var topDrawable: IconFontDrawable? = null
|
||||||
var bottomDrawable: IconFontDrawable? = null
|
var bottomDrawable: IconFontDrawable? = null
|
||||||
if (!startIconFont.isNullOrEmpty()) {
|
if (!startIconFont.isNullOrBlank()) {
|
||||||
startDrawable = IconFontDrawable(context, startIconFont).color(getFontColor(iconColorToken)).sizePx(getFontSize(iconSize, isDp))
|
startDrawable = IconFontDrawable(context, startIconFont).color(getFontColor(iconColorToken)).sizePx(getFontSize(iconSize, isDp))
|
||||||
}
|
}
|
||||||
if (!endIconFont.isNullOrEmpty()) {
|
if (!endIconFont.isNullOrBlank()) {
|
||||||
endDrawable =
|
endDrawable =
|
||||||
IconFontDrawable(context, endIconFont).color(getFontColor(iconColorToken)).sizePx(getFontSize(iconSize, isDp))
|
IconFontDrawable(context, endIconFont).color(getFontColor(iconColorToken)).sizePx(getFontSize(iconSize, isDp))
|
||||||
}
|
}
|
||||||
if (!topIconFont.isNullOrEmpty()) {
|
if (!topIconFont.isNullOrBlank()) {
|
||||||
topDrawable = IconFontDrawable(context, topIconFont).color(getFontColor(iconColorToken)).sizePx(getFontSize(iconSize, isDp))
|
topDrawable = IconFontDrawable(context, topIconFont).color(getFontColor(iconColorToken)).sizePx(getFontSize(iconSize, isDp))
|
||||||
}
|
}
|
||||||
if (!bottomIconFont.isNullOrEmpty()) {
|
if (!bottomIconFont.isNullOrBlank()) {
|
||||||
bottomDrawable = IconFontDrawable(context, bottomIconFont).color(getFontColor(iconColorToken)).sizePx(getFontSize(iconSize, isDp))
|
bottomDrawable = IconFontDrawable(context, bottomIconFont).color(getFontColor(iconColorToken)).sizePx(getFontSize(iconSize, isDp))
|
||||||
}
|
}
|
||||||
if (startIconFont == null && topIconFont == null && endIconFont == null && bottomIconFont == null) {
|
if (startIconFont == null && topIconFont == null && endIconFont == null && bottomIconFont == null) {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
app:advRadius="@dimen/dp_28" />
|
app:advRadius="@dimen/dp_28" />
|
||||||
|
|
||||||
<com.remax.visualnovel.widget.uitoken.view.UITokenImageView
|
<com.remax.visualnovel.widget.uitoken.view.UITokenImageView
|
||||||
|
android:id="@+id/iv_play_sample"
|
||||||
android:layout_width="@dimen/dp_20"
|
android:layout_width="@dimen/dp_20"
|
||||||
android:layout_height="@dimen/dp_20"
|
android:layout_height="@dimen/dp_20"
|
||||||
android:layout_gravity="end|top"
|
android:layout_gravity="end|top"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue