크리에이터 커뮤니티 게시글 등록

- 오디오 녹음 기능 추가
This commit is contained in:
klaus 2024-08-02 17:53:34 +09:00
parent 76aaaddb5a
commit e6e3df701d
7 changed files with 576 additions and 1 deletions

View File

@ -22,6 +22,7 @@ interface CreatorCommunityApi {
@POST("/creator-community")
@Multipart
fun createCommunityPost(
@Part audioFile: MultipartBody.Part?,
@Part postImage: MultipartBody.Part?,
@Part("request") request: RequestBody,
@Header("Authorization") authHeader: String

View File

@ -75,10 +75,12 @@ class CreatorCommunityRepository(private val api: CreatorCommunityApi) {
)
fun createCommunityPost(
audioFile: MultipartBody.Part?,
postImage: MultipartBody.Part?,
request: RequestBody,
token: String
) = api.createCommunityPost(
audioFile = audioFile,
postImage = postImage,
request = request,
authHeader = token

View File

@ -24,10 +24,11 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityCreatorCommunityWriteBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
import java.io.File
class CreatorCommunityWriteActivity : BaseActivity<ActivityCreatorCommunityWriteBinding>(
ActivityCreatorCommunityWriteBinding::inflate
) {
), RecordingVoiceFragment.OnAudioRecordedListener {
private val viewModel: CreatorCommunityWriteViewModel by inject()
@ -51,7 +52,10 @@ class CreatorCommunityWriteActivity : BaseActivity<ActivityCreatorCommunityWrite
}
viewModel.setImageUri(fileUri)
binding.llRecordAudio.visibility = View.VISIBLE
} else {
binding.llRecordAudio.visibility = View.GONE
Toast.makeText(
this,
"잘못된 파일입니다.\n다시 선택해 주세요.",
@ -59,6 +63,7 @@ class CreatorCommunityWriteActivity : BaseActivity<ActivityCreatorCommunityWrite
).show()
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
binding.llRecordAudio.visibility = View.GONE
Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
}
}
@ -74,12 +79,28 @@ class CreatorCommunityWriteActivity : BaseActivity<ActivityCreatorCommunityWrite
bindData()
}
override fun onDestroy() {
deleteAudioFile()
super.onDestroy()
}
private fun deleteAudioFile() {
if (viewModel.audioFile != null && viewModel.audioFile!!.exists()) {
viewModel.audioFile?.delete()
}
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "게시글 등록"
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.tvRecordAudio.setOnClickListener {
val fragment = RecordingVoiceFragment()
fragment.show(supportFragmentManager, fragment.tag)
}
binding.ivPhotoPicker.setOnClickListener {
ImagePicker.with(this)
.crop()
@ -332,4 +353,10 @@ class CreatorCommunityWriteActivity : BaseActivity<ActivityCreatorCommunityWrite
R.drawable.bg_round_corner_6_7_13181b
)
}
override fun onAudioRecorded(file: File) {
deleteAudioFile()
binding.tvRecordAudio.text = file.name
viewModel.audioFile = file
}
}

View File

@ -49,6 +49,7 @@ class CreatorCommunityWriteViewModel(
var price = 0
var content = ""
var audioFile: File? = null
private var imageUri: Uri? = null
fun setAdult(isAdult: Boolean) {
@ -110,8 +111,37 @@ class CreatorCommunityWriteViewModel(
null
}
val multipartAudioFile = if (audioFile != null) {
MultipartBody.Part.createFormData(
"audioFile",
audioFile!!.name,
body = object : RequestBody() {
override fun contentType(): MediaType {
return "audio/*".toMediaType()
}
override fun writeTo(sink: BufferedSink) {
audioFile!!.inputStream().use { inputStream ->
val buffer = ByteArray(512)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
sink.write(buffer, 0, bytesRead)
}
}
}
override fun contentLength(): Long {
return audioFile!!.length()
}
}
)
} else {
null
}
compositeDisposable.add(
repository.createCommunityPost(
audioFile = multipartAudioFile,
postImage = postImage,
request = requestJson.toRequestBody("text/plain".toMediaType()),
token = "Bearer ${SharedPreferenceManager.token}"

View File

@ -0,0 +1,300 @@
package kr.co.vividnext.sodalive.explorer.profile.creator_community.write
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.media.MediaPlayer
import android.media.MediaRecorder
import android.os.Build
import android.os.Bundle
import android.os.CountDownTimer
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Toast
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.FragmentRecordingVoiceBinding
import java.io.File
import java.io.IOException
import java.util.Locale
class RecordingVoiceFragment : BottomSheetDialogFragment() {
private var listener: OnAudioRecordedListener? = null
private var countDownTimer: CountDownTimer? = null
private var mediaRecorder: MediaRecorder? = null
private var mediaPlayer: MediaPlayer? = null
private var fileNameMedia = ""
private var second = -1
private var minute = 0
private var hour = 0
private lateinit var binding: FragmentRecordingVoiceBinding
interface OnAudioRecordedListener {
fun onAudioRecorded(file: File)
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is OnAudioRecordedListener) {
listener = context
} else {
throw RuntimeException("$context must implement OnAudioRecordedListener")
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.setOnShowListener {
val d = it as BottomSheetDialog
val bottomSheet = d.findViewById<FrameLayout>(
com.google.android.material.R.id.design_bottom_sheet
)
if (bottomSheet != null) {
BottomSheetBehavior.from(bottomSheet).state = BottomSheetBehavior.STATE_EXPANDED
}
}
return dialog
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentRecordingVoiceBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.ivClose.setOnClickListener { dismiss() }
binding.ivRecordStart.setOnClickListener {
fileNameMedia = requireActivity().filesDir.path +
"/record_community_voice_${System.currentTimeMillis()}.m4a"
val fileMedia = File(fileNameMedia)
if (!fileMedia.exists()) {
try {
fileMedia.createNewFile()
startRecording()
} catch (e: IOException) {
Toast.makeText(requireActivity(), R.string.retry, Toast.LENGTH_LONG).show()
e.printStackTrace()
}
}
}
binding.ivRecordStop.setOnClickListener { stopRecording() }
binding.ivRecordPlay.setOnClickListener { startPlaying() }
binding.ivRecordPause.setOnClickListener { stopPlaying() }
binding.tvDelete.setOnClickListener {
deleteAudioFile()
binding.ivRecordStart.visibility = View.VISIBLE
binding.llRetryOrComplete.visibility = View.GONE
binding.rlRecordPlay.visibility = View.GONE
binding.soundVisualizer.visibility = View.GONE
}
binding.tvRetryRecord.setOnClickListener {
deleteAudioFile()
binding.ivRecordStart.visibility = View.VISIBLE
binding.llRetryOrComplete.visibility = View.GONE
binding.rlRecordPlay.visibility = View.GONE
binding.soundVisualizer.visibility = View.GONE
}
binding.tvComplete.setOnClickListener {
listener?.onAudioRecorded(file = File(fileNameMedia))
dismiss()
}
}
override fun onDetach() {
super.onDetach()
listener = null
}
override fun onDestroy() {
releaseMediaPlayer()
releaseMediaRecorder()
deleteAudioFile()
super.onDestroy()
}
private fun deleteAudioFile() {
if (fileNameMedia.isNotBlank()) {
val fileMedia = File(fileNameMedia)
if (fileMedia.exists()) {
fileMedia.delete()
}
fileNameMedia = ""
}
}
private fun startRecording() {
releaseMediaRecorder()
// safety check, don't start a new recording if one is already going
mediaRecorder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaRecorder(requireContext())
} else {
MediaRecorder()
}.apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFile(fileNameMedia)
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
setAudioEncodingBitRate(192000)
setAudioSamplingRate(48000)
}
try {
mediaRecorder!!.prepare()
} catch (e: Exception) {
Toast.makeText(requireActivity(), R.string.retry, Toast.LENGTH_LONG).show()
return
}
mediaRecorder!!.start()
binding.ivRecordStart.visibility = View.GONE
binding.ivRecordStop.visibility = View.VISIBLE
startCountDownTimer()
}
private fun releaseMediaRecorder() {
if (mediaRecorder != null) {
// stop recording and free up resources
mediaRecorder!!.stop()
mediaRecorder!!.reset()
mediaRecorder!!.release()
mediaRecorder = null
}
}
private fun stopRecording() {
releaseMediaRecorder()
stopCountDownTimer()
binding.ivRecordStop.visibility = View.GONE
binding.rlRecordPlay.visibility = View.VISIBLE
binding.llRetryOrComplete.visibility = View.VISIBLE
}
private fun startPlaying() {
if (mediaPlayer == null) {
mediaPlayer = MediaPlayer()
mediaPlayer!!.reset()
mediaPlayer!!.setOnCompletionListener {
releaseMediaPlayer()
stopCountDownTimer()
binding.tvDelete.visibility = View.VISIBLE
binding.ivRecordPlay.visibility = View.VISIBLE
binding.llRetryOrComplete.visibility = View.VISIBLE
binding.ivRecordPause.visibility = View.GONE
binding.soundVisualizer.visibility = View.GONE
}
mediaPlayer!!.setOnPreparedListener {
binding.soundVisualizer.visibility = View.VISIBLE
binding.soundVisualizer.setAudioSessionId(mediaPlayer!!.audioSessionId)
it.start()
startCountDownTimer()
}
try {
mediaPlayer!!.setDataSource(fileNameMedia)
mediaPlayer!!.prepare()
} catch (e: Exception) {
Toast.makeText(requireActivity(), R.string.retry, Toast.LENGTH_LONG).show()
return
}
binding.tvDelete.visibility = View.GONE
binding.ivRecordPlay.visibility = View.GONE
binding.llRetryOrComplete.visibility = View.GONE
binding.ivRecordPause.visibility = View.VISIBLE
}
}
private fun stopPlaying() {
releaseMediaPlayer()
stopCountDownTimer()
binding.tvDelete.visibility = View.VISIBLE
binding.ivRecordPlay.visibility = View.VISIBLE
binding.llRetryOrComplete.visibility = View.VISIBLE
binding.ivRecordPause.visibility = View.GONE
binding.soundVisualizer.visibility = View.GONE
}
private fun releaseMediaPlayer() {
if (mediaPlayer != null) {
mediaPlayer!!.release()
mediaPlayer = null
}
}
private fun startCountDownTimer() {
countDownTimer = object : CountDownTimer(Long.MAX_VALUE, 1000) {
override fun onTick(p0: Long) {
second += 1
binding.tvTimer.text = recordingTime()
if (second >= 180) {
stopRecording()
}
}
override fun onFinish() {
}
}
countDownTimer!!.start()
}
private fun recordingTime(): String {
if (second == 60) {
minute += 1
second = 0
}
if (minute == 60) {
hour += 1
minute = 0
}
return String.format(Locale.getDefault(), "%02d:%02d:%02d", hour, minute, second)
}
@SuppressLint("SetTextI18n")
private fun stopCountDownTimer() {
if (countDownTimer != null) {
countDownTimer!!.cancel()
countDownTimer = null
}
binding.tvTimer.text = "00:00:00"
second = -1
minute = 0
hour = 0
}
}

View File

@ -94,6 +94,63 @@
android:textSize="13.3sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_record_audio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginTop="24dp"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="13.3dp"
android:fontFamily="@font/gmarket_sans_bold"
android:text="오디오 녹음"
android:textColor="@color/color_eeeeee"
android:textSize="16.7sp" />
<TextView
android:id="@+id/tv_record_audio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="13.3dp"
android:layout_marginTop="13.3dp"
android:background="@drawable/bg_round_corner_5_3_13181b_3bb9f1"
android:fontFamily="@font/gmarket_sans_medium"
android:gravity="center"
android:paddingVertical="8dp"
android:text="녹음"
android:textColor="@color/color_80d8ff"
android:textSize="16.7sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:layout_marginTop="13.3dp"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_medium"
android:text="※ "
android:textColor="@color/color_777777"
android:textSize="13.3sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_medium"
android:text="오디오 녹음은 최대 3분입니다"
android:textColor="@color/color_777777"
android:textSize="13.3sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@ -0,0 +1,158 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/color_222222">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_bold"
android:paddingHorizontal="26.7dp"
android:paddingTop="26.7dp"
android:text="음성녹음"
android:textColor="@color/white"
android:textSize="18.3sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:paddingHorizontal="26.7dp"
android:paddingTop="26.7dp"
android:src="@drawable/ic_close_white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="81dp"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iv_close">
<TextView
android:id="@+id/tv_timer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_light"
android:text="00:00:00"
android:textColor="@color/white"
android:textSize="33.3sp" />
<com.gauravk.audiovisualizer.visualizer.WaveVisualizer
android:id="@+id/sound_visualizer"
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_marginHorizontal="13.3dp"
android:visibility="gone"
app:avColor="@color/av_deep_orange"
app:avDensity="0.8"
app:avSpeed="normal"
app:avType="fill" />
<ImageView
android:id="@+id/iv_record_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="52.3dp"
android:contentDescription="@null"
android:src="@drawable/ic_record" />
<ImageView
android:id="@+id/iv_record_stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="52.3dp"
android:contentDescription="@null"
android:src="@drawable/ic_record_stop"
android:visibility="gone" />
<RelativeLayout
android:id="@+id/rl_record_play"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
<ImageView
android:id="@+id/iv_record_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginTop="90dp"
android:contentDescription="@null"
android:src="@drawable/ic_record_play" />
<ImageView
android:id="@+id/iv_record_pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginTop="90dp"
android:contentDescription="@null"
android:src="@drawable/ic_record_pause"
android:visibility="gone" />
<TextView
android:id="@+id/tv_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="60dp"
android:layout_toEndOf="@+id/iv_record_play"
android:fontFamily="@font/gmarket_sans_medium"
android:text="삭제"
android:textColor="@color/color_bbbbbb"
android:textSize="15.3sp" />
</RelativeLayout>
<LinearLayout
android:id="@+id/ll_retry_or_complete"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="13.3dp"
android:layout_marginTop="26.7dp"
android:layout_marginBottom="13.3dp"
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:id="@+id/tv_retry_record"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:background="@drawable/bg_round_corner_10_13181b_3bb9f1"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"
android:text="다시 녹음"
android:textColor="@color/color_3bb9f1"
android:textSize="18.3sp" />
<TextView
android:id="@+id/tv_complete"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginStart="13.3dp"
android:layout_weight="2"
android:background="@drawable/bg_round_corner_10_3bb9f1"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"
android:text="녹음 완료"
android:textColor="@color/white"
android:textSize="18.3sp" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>