|
|
@@ -0,0 +1,324 @@
|
|
|
+package com.adealink.weparty.module.playmate.widget
|
|
|
+
|
|
|
+import android.annotation.SuppressLint
|
|
|
+import android.content.Context
|
|
|
+import android.media.MediaPlayer
|
|
|
+import android.os.Handler
|
|
|
+import android.os.Looper
|
|
|
+import android.os.Message
|
|
|
+import android.util.AttributeSet
|
|
|
+import android.view.LayoutInflater
|
|
|
+import android.view.animation.Animation
|
|
|
+import android.view.animation.LinearInterpolator
|
|
|
+import android.view.animation.RotateAnimation
|
|
|
+import androidx.constraintlayout.widget.ConstraintLayout
|
|
|
+import androidx.core.content.withStyledAttributes
|
|
|
+import com.adealink.frame.base.IError
|
|
|
+import com.adealink.frame.download.listener.TaskListener
|
|
|
+import com.adealink.frame.download.task.Task
|
|
|
+import com.adealink.frame.log.Log
|
|
|
+import com.adealink.frame.oss.data.UploadFile
|
|
|
+import com.adealink.frame.storageService
|
|
|
+import com.adealink.frame.util.md5
|
|
|
+import com.adealink.frame.util.onClick
|
|
|
+import com.adealink.frame.util.runOnUiThread
|
|
|
+import com.adealink.weparty.App
|
|
|
+import com.adealink.weparty.R
|
|
|
+import com.adealink.weparty.databinding.LayoutSoundViewBinding
|
|
|
+import com.adealink.weparty.storage.file.FilePath.audioPath
|
|
|
+import com.adealink.weparty.util.getFilePathBy
|
|
|
+import com.tencent.qcloud.tuikit.tuichat.component.audio.AudioPlayer
|
|
|
+import kotlin.math.max
|
|
|
+
|
|
|
+@SuppressLint("SetTextI18n")
|
|
|
+class SoundView @JvmOverloads constructor(
|
|
|
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
|
|
+) : ConstraintLayout(context, attrs, defStyleAttr) {
|
|
|
+
|
|
|
+ companion object {
|
|
|
+ const val TAG = "SoundView"
|
|
|
+
|
|
|
+ const val MSG_UPDATE_DURATION = 1
|
|
|
+
|
|
|
+ const val MSG_UPDATE_PLAY_DURATION = 2
|
|
|
+ }
|
|
|
+
|
|
|
+ data class LoadSoundTask(
|
|
|
+ val url: String
|
|
|
+ )
|
|
|
+
|
|
|
+ private val binding = LayoutSoundViewBinding.inflate(LayoutInflater.from(context), this)
|
|
|
+
|
|
|
+ private var durationTextSize = 12f
|
|
|
+
|
|
|
+ private var soundUrl: String? = null
|
|
|
+ private var soundPath: String? = null
|
|
|
+ private var soundDuration: Long? = null
|
|
|
+
|
|
|
+ private var loadingAnimation: Animation? = null
|
|
|
+
|
|
|
+ private val handler = object : Handler(Looper.getMainLooper()) {
|
|
|
+ override fun handleMessage(msg: Message) {
|
|
|
+ super.handleMessage(msg)
|
|
|
+ when (msg.what) {
|
|
|
+ MSG_UPDATE_DURATION -> {
|
|
|
+ binding.tvDuration.text = "${max(((soundDuration ?: 0) / 1000).toInt(), 1)}\""
|
|
|
+ }
|
|
|
+
|
|
|
+ MSG_UPDATE_PLAY_DURATION -> {
|
|
|
+ binding.tvDuration.text =
|
|
|
+ "${max(((AudioPlayer.getInstance().playPosition ?: 0) / 1000), 1)}\""
|
|
|
+ if (AudioPlayer.getInstance().isPlaying && AudioPlayer.getInstance().path == soundPath) {
|
|
|
+ sendEmptyMessageDelayed(MSG_UPDATE_PLAY_DURATION, 1000L)
|
|
|
+ } else {
|
|
|
+ sendEmptyMessage(MSG_UPDATE_DURATION)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ init {
|
|
|
+ binding.root.setBackgroundResource(R.drawable.common_playmate_sound_widget_bg)
|
|
|
+ if (isInEditMode) {
|
|
|
+ binding.tvDuration.text = "1000\""
|
|
|
+ }
|
|
|
+
|
|
|
+ initAttr(context)
|
|
|
+ binding.root.onClick {
|
|
|
+ playOrStop()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun initAttr(context: Context, attrs: AttributeSet? = null) {
|
|
|
+ if (isInEditMode) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ context.withStyledAttributes(
|
|
|
+ attrs,
|
|
|
+ R.styleable.SoundView
|
|
|
+ ) {
|
|
|
+ durationTextSize = getFloat(
|
|
|
+ R.styleable.SoundView_sound_duration_text_size,
|
|
|
+ 12f
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ binding.tvDuration.textSize = durationTextSize
|
|
|
+ }
|
|
|
+
|
|
|
+ fun setSoundUrl(url: String?) {
|
|
|
+ this.soundUrl = url
|
|
|
+ this.soundPath = null
|
|
|
+ this.soundDuration = null
|
|
|
+ stopPlay()
|
|
|
+ loadSound()
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun loadSound() {
|
|
|
+ val url = soundUrl
|
|
|
+ if (url.isNullOrEmpty()) {
|
|
|
+ Log.e(TAG, "playSound fail, url is null")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ downloadSound(
|
|
|
+ onFinish = { soundPath ->
|
|
|
+ Log.d(TAG, "loadSound, path:$soundPath")
|
|
|
+ val loadSoundTask = LoadSoundTask(url)
|
|
|
+ MediaPlayer().apply {
|
|
|
+ try {
|
|
|
+ setDataSource(soundPath)
|
|
|
+
|
|
|
+ // 设置准备完成监听器
|
|
|
+ setOnPreparedListener { mp ->
|
|
|
+ Log.e(TAG, "loadSound, onPrepared")
|
|
|
+ if (loadSoundTask.url == soundUrl) {
|
|
|
+ soundDuration = mp.duration.toLong()
|
|
|
+ Log.e(TAG, "loadSound, url:$soundUrl, duration:$soundDuration")
|
|
|
+ handler.sendEmptyMessage(MSG_UPDATE_DURATION)
|
|
|
+ }
|
|
|
+
|
|
|
+ //释放
|
|
|
+ release()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置错误监听器
|
|
|
+ setOnErrorListener { _, what, extra ->
|
|
|
+ Log.e(TAG, "loadSound error")
|
|
|
+ if (loadSoundTask.url == soundUrl) {
|
|
|
+ soundDuration = 0
|
|
|
+ handler.sendEmptyMessage(MSG_UPDATE_DURATION)
|
|
|
+ }
|
|
|
+ false
|
|
|
+ }
|
|
|
+
|
|
|
+ setOnCompletionListener {
|
|
|
+ release()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 异步准备音频
|
|
|
+ prepareAsync()
|
|
|
+ } catch (e: Exception) {
|
|
|
+ Log.d(TAG, "loadSound fail, for ${e.message}", e)
|
|
|
+ release()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun downloadSound(
|
|
|
+ onStartDownload: (() -> Unit)? = null,
|
|
|
+ onDownloadProgress: (() -> Unit)? = null,
|
|
|
+ onFinish: ((soundPath: String) -> Unit)? = null,
|
|
|
+ onError: (() -> Unit)? = null
|
|
|
+ ) {
|
|
|
+ val url = soundUrl
|
|
|
+ if (url.isNullOrEmpty()) {
|
|
|
+ Log.e(TAG, "downloadSound fail, url is null")
|
|
|
+ return
|
|
|
+ }
|
|
|
+ val soundPath = url.getFilePathBy(audioPath, UploadFile.FileType.MP3)
|
|
|
+ if (soundPath.isNullOrEmpty()) {
|
|
|
+ Log.e(TAG, "downloadSound fail, can not getFilePath for $url")
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.soundPath = soundPath
|
|
|
+ val soundFile = storageService.file.createWeFile(soundPath)
|
|
|
+ if (soundFile.exists()) {
|
|
|
+ Log.d(TAG, "downloadSound return, for sound exists, path:$soundPath")
|
|
|
+ onFinish?.invoke(soundPath)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ Log.d(TAG, "downloadSound, url:$url, path:$soundPath")
|
|
|
+ val taskId = url.md5()
|
|
|
+ val task = Task(taskId, url, soundPath)
|
|
|
+ task.listeners.add(object : TaskListener {
|
|
|
+ override fun onFinished(task: Task) {
|
|
|
+ if (isAttachedToWindow && task.path == url) {
|
|
|
+ runOnUiThread {
|
|
|
+ onStartDownload?.invoke()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onProgress(
|
|
|
+ task: Task,
|
|
|
+ progress: Int?,
|
|
|
+ currentSize: Long,
|
|
|
+ totalSize: Long
|
|
|
+ ) {
|
|
|
+ if (isAttachedToWindow && task.path == url) {
|
|
|
+ runOnUiThread {
|
|
|
+ onDownloadProgress?.invoke()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onError(task: Task, error: IError) {
|
|
|
+ if (isAttachedToWindow && task.path == url) {
|
|
|
+ runOnUiThread {
|
|
|
+ onError?.invoke()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ Log.d(
|
|
|
+ TAG,
|
|
|
+ "downloadSound, start download, url:$url, task:$task"
|
|
|
+ )
|
|
|
+ App.instance.downloadService.addTask(task)
|
|
|
+ }
|
|
|
+
|
|
|
+ fun playOrStop() {
|
|
|
+ downloadSound(
|
|
|
+ onFinish = { soundPath ->
|
|
|
+ if (AudioPlayer.getInstance().isPlaying && AudioPlayer.getInstance().path == soundPath) {
|
|
|
+ stopPlay()
|
|
|
+ } else {
|
|
|
+ startPlay(soundPath)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onStartDownload = {
|
|
|
+ playLoadingAnim()
|
|
|
+ },
|
|
|
+ onDownloadProgress = {
|
|
|
+ playLoadingAnim()
|
|
|
+ },
|
|
|
+ onError = {
|
|
|
+ stopLoadingAnim()
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun playLoadingAnim() {
|
|
|
+ binding.btnPlay.setImageResource(R.drawable.common_playmate_sound_play_ic)
|
|
|
+ if (loadingAnimation != null) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ loadingAnimation?.cancel()
|
|
|
+ loadingAnimation = RotateAnimation(
|
|
|
+ 0f,
|
|
|
+ 360f,
|
|
|
+ Animation.RELATIVE_TO_SELF,
|
|
|
+ 0.5f,
|
|
|
+ Animation.RELATIVE_TO_SELF,
|
|
|
+ 0.5f
|
|
|
+ ).apply {
|
|
|
+ duration = 1000
|
|
|
+ repeatMode = Animation.RESTART
|
|
|
+ repeatCount = Animation.INFINITE
|
|
|
+ interpolator = LinearInterpolator()
|
|
|
+ }
|
|
|
+ binding.btnPlay.animation = loadingAnimation
|
|
|
+ loadingAnimation?.start()
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun stopLoadingAnim() {
|
|
|
+ loadingAnimation?.cancel()
|
|
|
+ loadingAnimation = null
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun startPlay(soundPath: String) {
|
|
|
+ AudioPlayer.getInstance().stopPlay()
|
|
|
+ AudioPlayer.getInstance().startPlay(soundPath, object : AudioPlayer.Callback {
|
|
|
+ override fun onCompletion(success: Boolean?) {
|
|
|
+ if (isAttachedToWindow && soundPath == this@SoundView.soundPath) {
|
|
|
+ runOnUiThread {
|
|
|
+ onStopPlay()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ onStartPlay()
|
|
|
+ }
|
|
|
+
|
|
|
+ fun stopPlay() {
|
|
|
+ AudioPlayer.getInstance().stopPlay()
|
|
|
+ onStopPlay()
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun onStartPlay() {
|
|
|
+ binding.btnPlay.setImageResource(R.drawable.common_playmate_sound_pause_ic)
|
|
|
+ handler.sendEmptyMessage(MSG_UPDATE_PLAY_DURATION)
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun onStopPlay() {
|
|
|
+ stopLoadingAnim()
|
|
|
+ binding.btnPlay.setImageResource(R.drawable.common_playmate_sound_play_ic)
|
|
|
+ handler.sendEmptyMessage(MSG_UPDATE_DURATION)
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onAttachedToWindow() {
|
|
|
+ super.onAttachedToWindow()
|
|
|
+ stopPlay()
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onDetachedFromWindow() {
|
|
|
+ super.onDetachedFromWindow()
|
|
|
+ stopPlay()
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+}
|