Android 自定义视图 - 使用 Exoplayer 的音频播放器
Android Custom View - Audio Player using Exoplayer
我正在尝试为在我的应用程序中播放音频制作自定义视图。此自定义视图应在任何地方使用,例如 activity、片段或列表项。给定的代码正在运行,但我想通过使用最佳实践对其进行优化以避免内存泄漏。在布局中,有一个按钮和滑块。我正在为滑块使用 google mdc。
MDC 滑块问题:从 exoplayer 获得的值是 Long 类型。但是滑块只接受 float values.When 将 float 值转换为 long 以显示进度,toFloat() 给出 -ve values.So 我正在使用 .toInt().toFloat()。如何优化它?
runOnUiTThread 问题:要更新滑块进度,我正在使用 runOnuithread,它需要 exoplayer 实例的当前持续时间来显示进度。我需要对其进行优化,因为我不确定如何在视图不是 visible.I 时终止此 runOnuithread 已尝试使用 .post{} 和 postDelayed{} 但其中的代码只工作了一次。
请帮助优化给定的代码。
class AudioPlayer(context: Context, attrs: AttributeSet) : ConstraintLayout(context, attrs) {
val actionButton: AppCompatImageView
val slider: Slider
var playerState = AudioPlayerState.Stop
var media: String = ""
var player: SimpleExoPlayer? = null
var mediaItem: MediaItem? = null
val mHandler = Handler()
enum class AudioPlayerState {
Played, Stop
}
init {
inflate(context, R.layout.layout_audio_player, this)
actionButton = findViewById(R.id.iv_play)
slider = findViewById(R.id.seek)
initPlayer()
actionButton.setOnClickListener {
if (playerState == AudioPlayerState.Stop) {
playMedia()
} else if (playerState == AudioPlayerState.Played) {
stopMedia(false)
}
}
}
override fun onDetachedFromWindow() {
stopMedia(true)
release()
super.onDetachedFromWindow()
}
private fun initPlayer() {
player?.addListener(object : Player.EventListener {
override fun onPlayerError(error: ExoPlaybackException) {
super.onPlayerError(error)
Log.e("hhp Player error", "Error $error")
}
override fun onPlaybackStateChanged(state: Int) {
super.onPlaybackStateChanged(state)
if (state == ExoPlayer.STATE_ENDED) {
stopMedia(true)
}
}
})
}
private fun stopMedia(reset: Boolean) {
actionButton.loadImageWithResId(R.drawable.ic_play)
playerState = AudioPlayerState.Stop
player?.playWhenReady = false
}
private fun playMedia() {
actionButton.loadImageWithResId(R.drawable.ic_baseline_pause_24)
playerState = AudioPlayerState.Played
player?.playWhenReady = true
}
fun release() {
player?.stop()
player?.release()
Log.e("hhp player", "released")
}
fun setMediaUrl(url: String) {
media = url
player = SimpleExoPlayer.Builder(context).build()
player?.setMediaItem(MediaItem.fromUri(url))
player?.playWhenReady = false
player?.prepare()
slider.value = 0F
slider.addOnChangeListener { slider, value, fromUser ->
if (fromUser) {
player?.seekTo(value.toInt().toLong())
}
}
(context as? Activity)?.runOnUiThread(object : Runnable {
override fun run() {
try {
slider.valueTo = player?.duration?.toInt()?.toFloat() ?: 100F
val mCurrentPosition = player?.currentPosition?.toInt()?.toFloat()
if (mCurrentPosition != null) {
if (mCurrentPosition >= slider.valueFrom && mCurrentPosition <= slider.valueTo)
slider.value = mCurrentPosition
if (slider.value == slider.valueTo) {
player?.stop()
stopMedia(true)
}
}
mHandler.postDelayed(this, 10)
} catch (e: Exception) {
}
}
})
}
}
自定义视图中使用的布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/exoplayerView"
android:layout_width="0dp"
android:visibility="gone"
android:layout_height="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.card.MaterialCardView
android:id="@+id/cv_audio"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:cardCornerRadius="12dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:id="@+id/ll_audio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#234B92"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_play"
android:layout_width="28dp"
android:layout_height="20dp"
android:layout_gravity="center"
android:layout_marginStart="18dp"
android:layout_marginTop="18dp"
android:layout_marginBottom="18dp"
app:srcCompat="@drawable/ic_play"
app:tint="@color/white" />
<com.google.android.material.slider.Slider
android:id="@+id/seek"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:value="0.0"
android:valueFrom="0.0"
android:valueTo="100.0"
app:labelBehavior="gone"
app:thumbColor="@color/white"
app:trackColorActive="@color/white"
app:trackColorInactive="@color/ash" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>
有什么办法可以避免使用runOnuithread吗?因为如果此自定义视图用于回收站视图或 viewpager 的任何项目,此线程将继续工作直到其 activity 结束?
请根据最佳实践帮助优化它。
首先,快速建议:
-如果你要递归地使用handler.postDelayed,你应该在视图destroyed/player停止时调用mHandler.removeCallbacksAndMessages(null)
- 我认为更新滑块的 10 毫秒间隔太短了,这会降低性能。考虑选择更合理的区间。
除此之外,我建议实施 LifecycleObserver 以使此自定义视图具有生命周期感知能力。由于媒体播放器需要对 onPause/onStop/onDestroy 之类的回调做出反应,因此这有助于处理这些状态。此外,当您实现它时,您还可以访问您正在观察的 lifecycleOwner 的 lifecycleScope(activity 或片段)。您将在此范围内更新滑块,当主机 fragment/activity 被销毁时,它会自动取消。您也可以在不实现 LifecycleObserver 的情况下将 lifecycleScope 传递给自定义视图,但我认为在您的情况下,值得实现它。
有关将生命周期范围传递给自定义视图的详细信息,请查看this question
使用协同程序,更新滑块的可运行对象可能会变成这样:
lifecycleScope.launchWhenResumed {
while((slider.value < slider.valueTo)){
//Update UI
delay(yourDelayInterval)
}
//Stop player etc
}
我正在尝试为在我的应用程序中播放音频制作自定义视图。此自定义视图应在任何地方使用,例如 activity、片段或列表项。给定的代码正在运行,但我想通过使用最佳实践对其进行优化以避免内存泄漏。在布局中,有一个按钮和滑块。我正在为滑块使用 google mdc。
MDC 滑块问题:从 exoplayer 获得的值是 Long 类型。但是滑块只接受 float values.When 将 float 值转换为 long 以显示进度,toFloat() 给出 -ve values.So 我正在使用 .toInt().toFloat()。如何优化它?
runOnUiTThread 问题:要更新滑块进度,我正在使用 runOnuithread,它需要 exoplayer 实例的当前持续时间来显示进度。我需要对其进行优化,因为我不确定如何在视图不是 visible.I 时终止此 runOnuithread 已尝试使用 .post{} 和 postDelayed{} 但其中的代码只工作了一次。
请帮助优化给定的代码。
class AudioPlayer(context: Context, attrs: AttributeSet) : ConstraintLayout(context, attrs) {
val actionButton: AppCompatImageView
val slider: Slider
var playerState = AudioPlayerState.Stop
var media: String = ""
var player: SimpleExoPlayer? = null
var mediaItem: MediaItem? = null
val mHandler = Handler()
enum class AudioPlayerState {
Played, Stop
}
init {
inflate(context, R.layout.layout_audio_player, this)
actionButton = findViewById(R.id.iv_play)
slider = findViewById(R.id.seek)
initPlayer()
actionButton.setOnClickListener {
if (playerState == AudioPlayerState.Stop) {
playMedia()
} else if (playerState == AudioPlayerState.Played) {
stopMedia(false)
}
}
}
override fun onDetachedFromWindow() {
stopMedia(true)
release()
super.onDetachedFromWindow()
}
private fun initPlayer() {
player?.addListener(object : Player.EventListener {
override fun onPlayerError(error: ExoPlaybackException) {
super.onPlayerError(error)
Log.e("hhp Player error", "Error $error")
}
override fun onPlaybackStateChanged(state: Int) {
super.onPlaybackStateChanged(state)
if (state == ExoPlayer.STATE_ENDED) {
stopMedia(true)
}
}
})
}
private fun stopMedia(reset: Boolean) {
actionButton.loadImageWithResId(R.drawable.ic_play)
playerState = AudioPlayerState.Stop
player?.playWhenReady = false
}
private fun playMedia() {
actionButton.loadImageWithResId(R.drawable.ic_baseline_pause_24)
playerState = AudioPlayerState.Played
player?.playWhenReady = true
}
fun release() {
player?.stop()
player?.release()
Log.e("hhp player", "released")
}
fun setMediaUrl(url: String) {
media = url
player = SimpleExoPlayer.Builder(context).build()
player?.setMediaItem(MediaItem.fromUri(url))
player?.playWhenReady = false
player?.prepare()
slider.value = 0F
slider.addOnChangeListener { slider, value, fromUser ->
if (fromUser) {
player?.seekTo(value.toInt().toLong())
}
}
(context as? Activity)?.runOnUiThread(object : Runnable {
override fun run() {
try {
slider.valueTo = player?.duration?.toInt()?.toFloat() ?: 100F
val mCurrentPosition = player?.currentPosition?.toInt()?.toFloat()
if (mCurrentPosition != null) {
if (mCurrentPosition >= slider.valueFrom && mCurrentPosition <= slider.valueTo)
slider.value = mCurrentPosition
if (slider.value == slider.valueTo) {
player?.stop()
stopMedia(true)
}
}
mHandler.postDelayed(this, 10)
} catch (e: Exception) {
}
}
})
}
}
自定义视图中使用的布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/exoplayerView"
android:layout_width="0dp"
android:visibility="gone"
android:layout_height="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.card.MaterialCardView
android:id="@+id/cv_audio"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:cardCornerRadius="12dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:id="@+id/ll_audio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#234B92"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_play"
android:layout_width="28dp"
android:layout_height="20dp"
android:layout_gravity="center"
android:layout_marginStart="18dp"
android:layout_marginTop="18dp"
android:layout_marginBottom="18dp"
app:srcCompat="@drawable/ic_play"
app:tint="@color/white" />
<com.google.android.material.slider.Slider
android:id="@+id/seek"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:value="0.0"
android:valueFrom="0.0"
android:valueTo="100.0"
app:labelBehavior="gone"
app:thumbColor="@color/white"
app:trackColorActive="@color/white"
app:trackColorInactive="@color/ash" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>
有什么办法可以避免使用runOnuithread吗?因为如果此自定义视图用于回收站视图或 viewpager 的任何项目,此线程将继续工作直到其 activity 结束?
请根据最佳实践帮助优化它。
首先,快速建议:
-如果你要递归地使用handler.postDelayed,你应该在视图destroyed/player停止时调用mHandler.removeCallbacksAndMessages(null)
- 我认为更新滑块的 10 毫秒间隔太短了,这会降低性能。考虑选择更合理的区间。
除此之外,我建议实施 LifecycleObserver 以使此自定义视图具有生命周期感知能力。由于媒体播放器需要对 onPause/onStop/onDestroy 之类的回调做出反应,因此这有助于处理这些状态。此外,当您实现它时,您还可以访问您正在观察的 lifecycleOwner 的 lifecycleScope(activity 或片段)。您将在此范围内更新滑块,当主机 fragment/activity 被销毁时,它会自动取消。您也可以在不实现 LifecycleObserver 的情况下将 lifecycleScope 传递给自定义视图,但我认为在您的情况下,值得实现它。
有关将生命周期范围传递给自定义视图的详细信息,请查看this question
使用协同程序,更新滑块的可运行对象可能会变成这样:
lifecycleScope.launchWhenResumed {
while((slider.value < slider.valueTo)){
//Update UI
delay(yourDelayInterval)
}
//Stop player etc
}