【问题标题】:Android Custom View - Audio Player using ExoplayerAndroid 自定义视图 - 使用 Exoplayer 的音频播放器
【发布时间】:2021-09-16 01:52:10
【问题描述】:

我正在尝试制作自定义视图以在我的应用中播放音频。此自定义视图应在活动、片段或列表项等任何地方使用。给定的代码正在运行,但我想通过使用最佳实践对其进行优化以避免内存泄漏。在布局中,有一个按钮和滑块。我正在使用 google mdc 作为滑块。

MDC 滑块的问题:从 exoplayer 获得的值是 Long 类型。但是slider只接受float值。将float值转换为long以显示进度时,toFloat()给出-ve值。所以我使用.toInt().toFloat()。如何优化它?

runOnUiTThread 的问题:为了更新滑块进度,我正在使用 runOnuithread,它从 exoplayer 实例获取当前持续时间来显示进度。我需要优化它,因为一旦视图不可见,我不确定如何杀死这个 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,此线程将继续工作直到其活动结束?

请帮助使用最佳实践对其进行优化。

【问题讨论】:

  • 你在这个项目中使用的是 kotlin 协程吗?
  • 是的,我正在使用协程@OyaCanli

标签: android multithreading kotlin android-custom-view exoplayer2.x


【解决方案1】:

首先,快速建议:
- 如果你要递归使用 handler.postDelayed,你应该在视图被销毁/播放器停止时调用 mHandler.removeCallbacksAndMessages(null)
- 我认为更新滑块的 10 毫秒间隔不必要地太短了,这会影响性能。考虑选择一个更合理的间隔。

除此之外,我建议实施LifecycleObserver 以使此自定义视图具有生命周期感知能力。由于媒体播放器需要对 onPause/onStop/onDestroy 之类的回调做出反应,这有助于处理这些状态。此外,当您实现这一点时,您还可以访问您正在观察的生命周期所有者的生命周期范围(活动或片段)。您将更新此范围内的滑块,当主机片段/活动被销毁时,它将自动取消。您也可以在不实现 LifecycleObserver 的情况下将生命周期范围传递给自定义视图,但我认为在您的情况下,值得实现它。

有关将生命周期范围传递给自定义视图的更多信息,请查看this question

使用协程,更新滑块的可运行对象可能会变成这样:

lifecycleScope.launchWhenResumed {
       while((slider.value < slider.valueTo)){
          //Update UI
          delay(yourDelayInterval)
       }
       //Stop player etc
}

【讨论】:

    猜你喜欢
    • 2017-02-18
    • 1970-01-01
    • 1970-01-01
    • 2020-02-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多