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"