I am trying to make a custom view for playing audio in my app. This custom view should be used anywhere like activity, fragment, or list items. The given code is working but I want to optimize it to avoid memory leaks by using the best practices. In the layout, there is a button and slider. I am using google mdc for the slider .
Issue with the MDC slider : Values got from the exoplayer are of type Long. But slider accepts only float values.When converting the float values to long to show the progress, toFloat() giving -ve values.So i m using .toInt().toFloat().How to optimize it?
Issue with runOnUiTThread : To update the slider progress i am using runOnuithread in which it takes current duration from exoplayer instance to show the progress. I need to optimise it because i am not sure how to kill this runOnuithread once the view is not visible.I have tried using .post{} and postDelayed{} but the code inside them just worked only for one time.
Please help to optimize the given code.
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) {
}
}
})
}
}
layout used in custom view
<?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>
Is there any way to avoid using runOnuithread? Because if this custom view is used in any item of recycler view or a viewpager this thread will continue to work till its activity ends?
Please help to optimise it with the best practice.
First, quick suggestions:
-If you're gonna use handler.postDelayed recursively, you should call mHandler.removeCallbacksAndMessages(null)
when view is destroyed/player is stopped
-I think 10 milliseconds interval for updating the slider is unnecessarily too short, it will have a cost on performance. Consider choosing a more reasonable interval.
Other than that, I would suggest implementing LifecycleObserver to make this custom view lifecycle-aware. As a media player needs to react to callbacks like onPause/onStop/onDestroy, this could facilitate dealing with those states. Besides, when you implement this, you can also access to the lifecycleScope of the lifecycleOwner that you're observing(activity or fragment). You'd update the slider inside this scope, and it would be automatically cancelled when the host fragment/activity is destroyed. You can also pass the lifecycleScope to the custom view without implementing LifecycleObserver, but I think in your case, it is worth implementing it.
For more information about passing the lifecyclescope to the custom view, check out this question
And with coroutines, your runnable who updates the slider could turn into something like this:
lifecycleScope.launchWhenResumed {
while((slider.value < slider.valueTo)){
//Update UI
delay(yourDelayInterval)
}
//Stop player etc
}