From 931a9433f353a30c36a222aee4352c6dc4c44b8e Mon Sep 17 00:00:00 2001
From: klaus <klaus@vividnext.co.kr>
Date: Fri, 3 Jan 2025 19:47:10 +0900
Subject: [PATCH] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EC=A7=80?=
 =?UTF-8?q?=EC=9B=90=EC=9E=90=20-=20=EC=98=A4=EB=94=94=EC=98=A4=20?=
 =?UTF-8?q?=EC=9E=AC=EC=83=9D=EC=8B=9C=20=EC=8B=9C=EA=B0=84=EA=B3=BC=20Pro?=
 =?UTF-8?q?gressBar=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../applicant/AuditionApplicantListAdapter.kt | 45 ++++++++++++
 .../AuditionApplicantMediaPlayerManager.kt    | 39 +++++++++--
 .../role/AuditionRoleDetailActivity.kt        | 24 +++++--
 .../kr/co/vividnext/sodalive/common/Utils.kt  | 10 ++-
 .../res/drawable/audition_player_seekbar.xml  | 25 +++++++
 .../res/layout/item_audition_applicant.xml    | 70 ++++++++++++++++---
 6 files changed, 190 insertions(+), 23 deletions(-)
 create mode 100644 app/src/main/res/drawable/audition_player_seekbar.xml

diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantListAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantListAdapter.kt
index aa6be3d..159f30b 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantListAdapter.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantListAdapter.kt
@@ -1,6 +1,8 @@
 package kr.co.vividnext.sodalive.audition.applicant
 
+import android.annotation.SuppressLint
 import android.view.LayoutInflater
+import android.view.View
 import android.view.ViewGroup
 import androidx.recyclerview.widget.DiffUtil
 import androidx.recyclerview.widget.ListAdapter
@@ -8,6 +10,7 @@ import androidx.recyclerview.widget.RecyclerView
 import coil.load
 import coil.transform.CircleCropTransformation
 import kr.co.vividnext.sodalive.R
+import kr.co.vividnext.sodalive.common.Utils
 import kr.co.vividnext.sodalive.databinding.ItemAuditionApplicantBinding
 
 class AuditionApplicantListAdapter(
@@ -16,6 +19,8 @@ class AuditionApplicantListAdapter(
 ) : ListAdapter<GetAuditionRoleApplicantItem, AuditionApplicantListAdapter.ViewHolder>(DiffCallback()) {
 
     private var currentPlayingIndex: Int = -1
+    private var currentTotalDuration: Int = 0
+    private var currentTime: Int = 0
     private var isPlaying: Boolean = false
 
     class DiffCallback : DiffUtil.ItemCallback<GetAuditionRoleApplicantItem>() {
@@ -37,9 +42,14 @@ class AuditionApplicantListAdapter(
     inner class ViewHolder(
         private val binding: ItemAuditionApplicantBinding
     ) : RecyclerView.ViewHolder(binding.root) {
+        @SuppressLint("SetTextI18n")
         fun bind(item: GetAuditionRoleApplicantItem) {
             binding.llVote.setOnClickListener { onClickVote(bindingAdapterPosition) }
             binding.ivProfile.setOnClickListener {
+                if (currentPlayingIndex != bindingAdapterPosition) {
+                    currentTime = 0
+                    currentTotalDuration = 0
+                }
                 onClickPlayOrPause(bindingAdapterPosition, item.applicantId, item.voiceUrl)
             }
 
@@ -58,6 +68,31 @@ class AuditionApplicantListAdapter(
                     R.drawable.ic_audition_play
                 }
             )
+
+            if (bindingAdapterPosition == currentPlayingIndex) {
+                binding.tvTotalDuration.visibility = View.VISIBLE
+                binding.tvCurrentTime.visibility = View.VISIBLE
+                binding.sbProgress.visibility = View.VISIBLE
+
+                binding.sbProgress.max = currentTotalDuration
+                binding.sbProgress.progress = currentTime
+
+                binding.tvTotalDuration.text =
+                    "/${
+                        Utils.convertDurationToString(
+                            currentTotalDuration,
+                            showHours = false
+                        )
+                    }"
+                binding.tvCurrentTime.text = Utils.convertDurationToString(
+                    currentTime,
+                    showHours = false
+                )
+            } else {
+                binding.tvTotalDuration.visibility = View.GONE
+                binding.tvCurrentTime.visibility = View.GONE
+                binding.sbProgress.visibility = View.GONE
+            }
         }
     }
 
@@ -81,4 +116,14 @@ class AuditionApplicantListAdapter(
         notifyItemChanged(previousIndex)
         notifyItemChanged(currentPlayingIndex)
     }
+
+    fun updateTotalDuration(totalDuration: Int) {
+        currentTotalDuration = totalDuration
+        notifyItemChanged(currentPlayingIndex)
+    }
+
+    fun updateCurrentTime(currentTime: Int) {
+        this.currentTime = currentTime
+        notifyItemChanged(currentPlayingIndex)
+    }
 }
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantMediaPlayerManager.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantMediaPlayerManager.kt
index 11ccd13..ce1b75f 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantMediaPlayerManager.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantMediaPlayerManager.kt
@@ -5,29 +5,46 @@ import android.content.Intent
 import android.media.AudioAttributes
 import android.media.MediaPlayer
 import android.net.Uri
+import android.os.Handler
+import android.os.Looper
 import android.widget.Toast
-import com.orhanobut.logger.Logger
+import androidx.annotation.OptIn
+import androidx.media3.common.util.UnstableApi
 import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
 import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService
 import java.io.IOException
 
+@OptIn(UnstableApi::class)
 class AuditionApplicantMediaPlayerManager(
     private val context: Context,
-    private val updateUI: (Int, Boolean) -> Unit
+    private val updatePlayPauseState: (Int, Boolean) -> Unit,
+    private val updateTotalDuration: (Int) -> Unit,
+    private val updateCurrentTime: (Int) -> Unit,
+    private val showLoadingDialog: (Boolean) -> Unit
 ) {
     private var mediaPlayer: MediaPlayer? = null
     private var currentPlayingApplicantId: Long = -1
     private var currentPlayingPosition: Int = -1
 
+    private val handler = Handler(Looper.getMainLooper())
+    private var changeMediaPlayerPositionRunnable = object : Runnable {
+        override fun run() {
+            if (mediaPlayer != null) {
+                updateCurrentTime(mediaPlayer!!.currentPosition)
+            }
+            handler.postDelayed(this, 1000)
+        }
+    }
+
     fun pauseContent() {
         mediaPlayer?.pause()
-        updateUI(currentPlayingPosition, false)
+        updatePlayPauseState(currentPlayingPosition, false)
     }
 
     private fun resumeContent() {
         pauseAudioContentService()
         mediaPlayer?.start()
-        updateUI(currentPlayingPosition, true)
+        updatePlayPauseState(currentPlayingPosition, true)
     }
 
     fun stopContent() {
@@ -38,22 +55,27 @@ class AuditionApplicantMediaPlayerManager(
         }
         currentPlayingApplicantId = -1
         currentPlayingPosition = -1
-        updateUI(currentPlayingPosition, false)
+        updatePlayPauseState(currentPlayingPosition, false)
+        handler.removeCallbacks(changeMediaPlayerPositionRunnable)
     }
 
     fun toggleContent(position: Int, applicantId: Long, voiceUrl: String) {
         if (currentPlayingApplicantId == applicantId && currentPlayingPosition == position) {
             if (mediaPlayer?.isPlaying == true) {
                 pauseContent()
+                handler.removeCallbacks(changeMediaPlayerPositionRunnable)
             } else {
                 resumeContent()
+                handler.postDelayed(changeMediaPlayerPositionRunnable, 1000)
             }
         } else {
             playContent(position, applicantId, voiceUrl)
+            handler.postDelayed(changeMediaPlayerPositionRunnable, 1000)
         }
     }
 
     private fun playContent(position: Int, applicantId: Long, voiceUrl: String) {
+        showLoadingDialog(true)
         pauseAudioContentService()
         stopContent()
 
@@ -73,15 +95,18 @@ class AuditionApplicantMediaPlayerManager(
                 prepareAsync() // 비동기적으로 준비
                 setOnPreparedListener {
                     start()
-                    updateUI(currentPlayingPosition, true)
+                    updateTotalDuration(duration)
+                    updatePlayPauseState(currentPlayingPosition, true)
+                    showLoadingDialog(false)
                 }
             } catch (e: IOException) {
                 e.printStackTrace()
                 Toast.makeText(context, "콘텐츠를 재생하지 못했습니다.\n다시 시도해 주세요", Toast.LENGTH_SHORT).show()
+                showLoadingDialog(false)
             }
 
             setOnCompletionListener {
-                updateUI(currentPlayingPosition, false)
+                updatePlayPauseState(currentPlayingPosition, false)
                 currentPlayingApplicantId = -1
             }
         }
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/role/AuditionRoleDetailActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/role/AuditionRoleDetailActivity.kt
index f716b2e..0b03286 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/audition/role/AuditionRoleDetailActivity.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/role/AuditionRoleDetailActivity.kt
@@ -91,10 +91,24 @@ class AuditionRoleDetailActivity : BaseActivity<ActivityAuditionRoleDetailBindin
         super.onCreate(savedInstanceState)
 
         mediaPlayerManager = AuditionApplicantMediaPlayerManager(
-            this
-        ) { position, isPlaying ->
-            adapter.updatePlayingIndex(position, isPlaying)
-        }
+            context = this,
+            updatePlayPauseState = { position, isPlaying ->
+                adapter.updatePlayingIndex(position, isPlaying)
+            },
+            updateTotalDuration = { duration ->
+                adapter.updateTotalDuration(totalDuration = duration)
+            },
+            updateCurrentTime = { currentTime ->
+                adapter.updateCurrentTime(currentTime)
+            },
+            showLoadingDialog = {
+                if (it) {
+                    loadingDialog.show(screenWidth, "")
+                } else {
+                    loadingDialog.dismiss()
+                }
+            }
+        )
 
         bindData()
         viewModel.getAuditionRoleDetail(auditionRoleId = auditionRoleId)
@@ -197,6 +211,8 @@ class AuditionRoleDetailActivity : BaseActivity<ActivityAuditionRoleDetailBindin
         )
 
         val recyclerView = binding.rvApplicant
+        recyclerView.setHasFixedSize(true)
+        recyclerView.itemAnimator = null
         recyclerView.layoutManager = LinearLayoutManager(
             applicationContext,
             LinearLayoutManager.VERTICAL,
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/Utils.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/Utils.kt
index cd22ea3..bd638f7 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/common/Utils.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/common/Utils.kt
@@ -1,13 +1,17 @@
 package kr.co.vividnext.sodalive.common
 
 object Utils {
-    fun convertDurationToString(duration: Int): String {
+    fun convertDurationToString(duration: Int, showHours: Boolean = true): String {
         val durationSeconds = duration / 1000
         val hours = (durationSeconds / 3600)
-        val minutes = ((durationSeconds % 3600) / 60)
+        val minutes = if (showHours) (durationSeconds % 3600) / 60 else durationSeconds / 60
         val seconds = (durationSeconds % 60)
 
-        return "%02d:%02d:%02d".format(hours, minutes, seconds)
+        return if (showHours) {
+            "%02d:%02d:%02d".format(hours, minutes, seconds)
+        } else {
+            "%02d:%02d".format(minutes, seconds)
+        }
     }
 
     fun convertStringToDuration(timeString: String): Long {
diff --git a/app/src/main/res/drawable/audition_player_seekbar.xml b/app/src/main/res/drawable/audition_player_seekbar.xml
new file mode 100644
index 0000000..93ad777
--- /dev/null
+++ b/app/src/main/res/drawable/audition_player_seekbar.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@android:id/background">
+        <shape android:shape="rectangle">
+            <corners android:radius="6.7dp" />
+            <solid android:color="@color/color_cc979797" />
+        </shape>
+    </item>
+    <item android:id="@android:id/secondaryProgress">
+        <clip>
+            <shape android:shape="rectangle">
+                <corners android:radius="6.7dp" />
+                <solid android:color="@color/color_cc979797" />
+            </shape>
+        </clip>
+    </item>
+    <item android:id="@android:id/progress">
+        <clip>
+            <shape android:shape="rectangle">
+                <corners android:radius="6.7dp" />
+                <solid android:color="@color/color_3bb9f1" />
+            </shape>
+        </clip>
+    </item>
+</layer-list>
diff --git a/app/src/main/res/layout/item_audition_applicant.xml b/app/src/main/res/layout/item_audition_applicant.xml
index 5f1d358..d48139d 100644
--- a/app/src/main/res/layout/item_audition_applicant.xml
+++ b/app/src/main/res/layout/item_audition_applicant.xml
@@ -27,18 +27,70 @@
         app:layout_constraintStart_toStartOf="@+id/iv_profile"
         app:layout_constraintTop_toTopOf="@+id/iv_profile" />
 
-    <TextView
-        android:id="@+id/tv_nickname"
-        android:layout_width="wrap_content"
+    <LinearLayout
+        android:layout_width="0dp"
         android:layout_height="wrap_content"
-        android:layout_marginStart="13.3dp"
-        android:fontFamily="@font/gmarket_sans_medium"
-        android:textColor="@color/white"
-        android:textSize="12sp"
+        android:layout_marginHorizontal="13.3dp"
+        android:gravity="center_vertical"
+        android:orientation="vertical"
         app:layout_constraintBottom_toBottomOf="@+id/iv_profile"
+        app:layout_constraintEnd_toStartOf="@+id/ll_vote"
         app:layout_constraintStart_toEndOf="@+id/iv_profile"
-        app:layout_constraintTop_toTopOf="@+id/iv_profile"
-        tools:text="닉네임" />
+        app:layout_constraintTop_toTopOf="@+id/iv_profile">
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <TextView
+                android:id="@+id/tv_nickname"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_centerVertical="true"
+                android:ellipsize="end"
+                android:fontFamily="@font/gmarket_sans_medium"
+                android:maxEms="9"
+                android:maxLines="1"
+                android:textColor="@color/white"
+                android:textSize="12sp"
+                tools:text="닉네임닉네임닉네임닉네임닉네임" />
+
+            <TextView
+                android:id="@+id/tv_current_time"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_centerVertical="true"
+                android:layout_toStartOf="@+id/tv_total_duration"
+                android:layout_toEndOf="@+id/tv_nickname"
+                android:fontFamily="@font/gmarket_sans_medium"
+                android:gravity="end"
+                android:textColor="@color/color_777777"
+                android:textSize="12sp"
+                tools:text="00:10:00" />
+
+            <TextView
+                android:id="@+id/tv_total_duration"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentEnd="true"
+                android:layout_centerVertical="true"
+                android:fontFamily="@font/gmarket_sans_medium"
+                android:textColor="@color/color_777777"
+                android:textSize="12sp"
+                tools:text="/30:00:00" />
+        </RelativeLayout>
+
+        <SeekBar
+            android:id="@+id/sb_progress"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="2.3dp"
+            android:paddingStart="0dp"
+            android:paddingEnd="0dp"
+            android:progressDrawable="@drawable/audition_player_seekbar"
+            android:thumb="@null"
+            android:visibility="gone" />
+    </LinearLayout>
 
     <LinearLayout
         android:id="@+id/ll_vote"