Skip to content

k906506/AOS-Record

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

19 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

ํ‚ค์›Œ๋“œ

  • Request Runtime Permissions
  • CustomView
  • MediaRecorder

๊ตฌํ˜„ ๋ชฉ๋ก

  • ๋งˆ์ดํฌ๋ฅผ ํ†ตํ•œ ์Œ์„ฑ ๋…น์Œ
  • ๋…น์Œํ•œ ์Œ์„ฑ ์žฌ์ƒ
  • ๋…น์Œ ์ค‘์ธ ์Œ์„ฑ ์‹œ๊ฐํ™”

๊ฐœ๋ฐœ ๊ณผ์ • (๋…ธ์…˜์—์„œ ํ™•์ธ)

1. ๊ธฐ๋ณธ UI ์„ค์ •ํ•˜๊ธฐ

State Class

package com.example.record

enum class State {
    BEFORE_RECORDING,
    ON_RECORDING,
    AFTER_RECORDING,
    ON_PALYING
}

์ƒํƒœ์— ๋”ฐ๋ผ ๋‹ค๋ฅธ UI๋ฅผ ๋ณด์—ฌ์ค˜์•ผํ•˜๋Š”๋ฐ ์ด๋ฅผ Enum ์œผ๋กœ ๋ฏธ๋ฆฌ ์ •์˜ํ•ด์คฌ๋‹ค.

RecordButton Class

๋ฒ„ํŠผ์˜ ์ƒํƒœ๊ด€๋ฆฌ๋ฅผ ๋ณด๋‹ค ํŽธํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด์„œ ImageButton ์„ ์ƒ์†ํ•˜๋Š” RecordButton ํด๋ž˜์Šค๋ฅผ ์ •์˜ํ•ด์คฌ๋‹ค.

package com.example.record

import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageButton

class RecordButton(
    context: Context,
    attrs: AttributeSet
) : AppCompatImageButton(context, attrs) {

    fun updateIconWithState(state: State) {
        when (state) {
            State.BEFORE_RECORDING ->
                setImageResource(R.drawable.ic_record)
            State.ON_RECORDING ->
                setImageResource(R.drawable.ic_stop)
            State.AFTER_RECORDING ->
                setImageResource(R.drawable.ic_play)
            State.ON_PALYING ->
                setImageResource(R.drawable.ic_stop)
        }
    }
}

์ง์ ‘ ๋งŒ๋“ค์–ด์ค€ UI์ด๋ฏ€๋กœ ํ•˜์œ„ ๋ฒ„์ ผ์˜ ์•ˆ๋“œ๋กœ์ด๋“œ API์—์„œ๋Š” ์ ์šฉ์ด ์•ˆ๋  ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด AppCompat ํ‚ค์›Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค๊ณ  ์•Œ๋ ค์ค€๋‹ค. ๋˜ํ•œ Record ํด๋ž˜์Šค ๋‚ด๋ถ€์— ์ƒํƒœ์— ๋”ฐ๋ผ ๋ฒ„ํŠผ์˜ ๋ชจ์–‘์„ ๋ณ€๊ฒฝํ•ด์ฃผ๋Š” updateIconWithState ๋ฉ”์†Œ๋“œ๋ฅผ ์ •์˜ํ•ด์คฌ๋‹ค.

image

๋”ฐ๋กœ ์ •์˜ํ•ด์ค€ RecordButton ํด๋ž˜์Šค ๋ฅผ ํ†ตํ•ด ๋ฒ„ํŠผ์„ ๊ตฌํ˜„ํ–ˆ๋‹ค. ์ดˆ๊ธฐ ์ƒํƒœ๋ฅผ ์ง€์ •ํ•ด์ฃผ์ง€ ์•Š์•˜๊ธฐ์— ์ฒซ ๋ฒˆ์งธ ์‚ฌ์ง„์„ ๋ณด๋ฉด ๋นˆ์นธ์œผ๋กœ ๋œจ๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. ์ดˆ๊ธฐ ์ƒํƒœ๋ฅผ ์ง€์ •ํ•ด์ฃผ๊ธฐ ์œ„ํ•ด MainActivity ์—์„œ RecordButton ๊ณผ ์—ฐ๊ฒฐ๋œ ๋ณ€์ˆ˜๋ฅผ ์„ ์–ธํ•˜๊ณ  ํ•ด๋‹น ํด๋ž˜์Šค์—์„œ ์ •์˜ํ•ด์ค€ updateIconWIthState ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธ ํ•ด์คฌ๋‹ค. ๋‘ ๋ฒˆ์งธ ์‚ฌ์ง„์—์„œ๋Š” ic_record ์˜ Vector Asset์œผ๋กœ ๋˜์–ด ์žˆ๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

package com.example.record

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {

    private val recordButton : RecordButton by lazy {
        findViewById(R.id.recordButton)
    }

    private var state = State.BEFORE_RECORDING

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initViews()
    }

    fun initViews() {
        recordButton.updateIconWithState(state)
    }
}

2. ๊ถŒํ•œ ์š”์ฒญํ•˜๊ธฐ

์ง€๋‚œ๋ฒˆ์— ๊ฐค๋Ÿฌ๋ฆฌ ์•ฑ์„ ์ œ์ž‘ํ•  ๋•Œ์ฒ˜๋Ÿผ ์ด๋ฒˆ์—๋„ ์‚ฌ์šฉ์ž์˜ ๋งˆ์ดํฌ์— ์ ‘๊ทผํ•ด์•ผ ํ•˜๋ฏ€๋กœ ๊ถŒํ•œ ์„ ์š”์ฒญํ•ด์•ผํ•œ๋‹ค. AndroidManifest.xml ์—์„œ ์šฐ์„  ๊ถŒํ•œ์„ ์š”์ฒญํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="<http://schemas.android.com/apk/res/android>"
    package="com.example.record">
    
    <!--๊ถŒํ•œ ์š”์ฒญ-->
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Record">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
	override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initViews()
        requestAudioPermission()
    }

์ด๋ฒˆ ๋…น์Œ๊ธฐ ์•ฑ์€ ์•ฑ์„ ์‹คํ–‰ํ•˜๋ฉด ๋™์‹œ์— ๊ถŒํ•œ์„ ์š”์ฒญํ•˜๋„๋ก ํ–ˆ๋‹ค. ๋”ฐ๋ผ์„œ onCreate ๋ฉ”์†Œ๋“œ์— ๊ถŒํ•œ์„ ์š”์ฒญํ•˜๋Š” ๋ฉ”์†Œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜๋ ค๊ณ  ํ•œ๋‹ค.

	private val requiredPermissions = arrayOf(Manifest.permission.RECORD_AUDIO) (์ฒซ ๋ฒˆ์งธ ์ธ์ž)

	private fun requestAudioPermission() {
        requestPermissions(requiredPermissions, REQUEST_RECORD_AUDIO_PERMISSION)
	}

// ... ์ค‘๋žต
// ์ •์ ๋ณ€์ˆ˜๋กœ ๋งŒ๋“ค์–ด์ฃผ๊ธฐ ์œ„ํ•ด companion ๊ฐ์ฒด ์‚ฌ์šฉ (๋‘๋ฒˆ์งธ ์ธ์ž)
    companion object {
        private const val REQUEST_RECORD_AUDIO_PERMISSION = 201
   }

requestPermissons ๋ฉ”์†Œ๋“œ๋Š” ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋กœ ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•  ๋ชฉ๋ก(๋ฐฐ์—ด)์„ ๋‘ ๋ฒˆ์งธ ์ธ์ž๋กœ ์‘๋‹ต์ฝ”๋“œ๋ฅผ ๋ฐ›๋Š”๋‹ค. ๋ชฉ๋ก์— ์žˆ๋Š” ๊ถŒํ•œ๋“ค์„ ์‚ฌ์šฉ์ž์—๊ฒŒ ์š”์ฒญํ•œ๋‹ค. ๊ถŒํ•œ์ด ์ˆ˜๋ฝ๋˜๋ฉด ๋‘ ๋ฒˆ์งธ ์ธ์ž๋กœ ๋„ฃ์–ด์ค€ ๊ฐ’์„ ์‘๋‹ต์ฝ”๋“œ๋กœ ๊ฐ€์ง„๋‹ค.

	override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        val audioRecordPermissionGranted =
            requestCode == REQUEST_RECORD_AUDIO_PERMISSION && grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED

        if (!audioRecordPermissionGranted) {
            finish()
        }
    }

๋˜ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ๊ถŒํ•œ์„ ๊ฑฐ๋ถ€ํ•œ ๊ฒฝ์šฐ ์•ฑ์„ ์ข…๋ฃŒ์‹œํ‚ค๊ธฐ ์œ„ํ•ด onRequestPermissionsResult ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉ ํ•ด์คฌ๋‹ค. ๊ถŒํ•œ์„ ๋ถ€์—ฌ ๋ฐ›์€ ๊ฒฝ์šฐ ์‘๋‹ต ์ฝ”๋“œ์—๋Š” ์ •์  ๋ณ€์ˆ˜ ๋กœ ์„ ์–ธํ•ด์ค€ REQUEST_RECORD_AUDIO_PERMISSION ๊ฐ€ ์ €์žฅ๋˜์–ด ์žˆ๋‹ค. ๋˜ํ•œ ์‹คํ–‰ ๊ฒฐ๊ณผ๊ฐ€ grantResult ์— ๋ฐฐ์—ด๋กœ ์ €์žฅ๋˜์–ด ์žˆ๋Š”๋ฐ ํ˜„์žฌ ์˜ค๋””์˜ค์— ๋Œ€ํ•œ ๊ถŒํ•œ ๋งŒ ์š”์ฒญํ–ˆ์œผ๋ฏ€๋กœ 1๊ฐœ๋งŒ ๋Œ๋ ค๋ฐ›๋Š”๋‹ค. ๋”ฐ๋ผ์„œ firstOfNull ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด ์‹คํ–‰ ๊ฒฐ๊ณผ๋ฅผ ๋น„๊ตํ–ˆ๋‹ค.

3. ๋…น์Œ ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ

startRecording

๋…น์Œ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” MediaRecorder ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ํ•˜์ง€๋งŒ ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜๋Š” ์—†๊ณ  ์•„๋ž˜ ๊ทธ๋ฆผ๊ณผ ๊ฐ™์€ ์ƒํƒœ๋„ ๋ฅผ ๊ฐ€์ง„๋‹ค.

์ˆœ์„œ๋ฅผ ๋‚˜์—ดํ•ด๋ณด์ž.

  1. setAudioSource ๋กœ ๋งˆ์ดํฌ์— ์ ‘๊ทผํ•œ๋‹ค.

  2. setOutputFormat ์œผ๋กœ ํฌ๋งท ๋ฐฉ์‹์„ ์ง€์ •ํ•œ๋‹ค.

  3. setAudioEncorder ๋ฅผ ํ†ตํ•ด ์ธ์ฝ”๋” ๋ฐฉ์‹์„ ์ง€์ •ํ•œ๋‹ค.

    ์ธ์ฝ”๋” ๋ฐฉ์‹์„ ์ง€์ •ํ•˜๋Š” ์ด์œ ๋Š” ๋…น์Œ ํŒŒ์ผ์˜ ํฌ๊ธฐ๋ฅผ ์ค„์ด๊ธฐ ์œ„ํ•จ์ด๋‹ค.

  4. setOutputFile ์„ ํ†ตํ•ด ํŒŒ์ผ์ด ์ €์žฅ๋  ๊ฒฝ๋กœ๋ฅผ ์ง€์ •ํ•œ๋‹ค.

  5. prepare ์„ ํ†ตํ•ด ๋ชจ๋“  ์ค€๋น„๋ฅผ ์™„๋ฃŒํ•œ๋‹ค.

์ด ์ˆœ์„œ์— ๋”ฐ๋ผ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด ๋˜๋Š”๋ฐ ์ฃผ์˜ํ•  ์ ์ด ์žˆ๋‹ค. ๋ฐ”๋กœ ๋…น์Œ ํŒŒ์ผ์˜ ํฌ๊ธฐ์ด๋‹ค. ๋…น์Œ ํŒŒ์ผ์˜ ๊ฒฝ์šฐ ์งง๊ฒŒ๋Š” ๋ช‡ ์ดˆ, ๊ธธ๊ฒŒ๋Š” ๋ช‡ ์‹œ๊ฐ„์ด ๋  ์ˆ˜๋„ ์žˆ๋‹ค. ์ด๋Ÿฐ ์ƒํ™ฉ์— ๋งŒ์•ฝ ์•ฑ์˜ ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅํ•˜๊ฒŒ ๋œ๋‹ค๋ฉด ์•ฑ ์šฉ๋Ÿ‰์ด ์ƒ๋‹นํžˆ ์ปค์งˆ ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ํœด๋Œ€ํฐ์˜ ์บ์‹œ ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅํ•˜๊ณ  ์‚ญ์ œํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์•ฑ์„ ์ œ์ž‘ํ•˜๋ ค๊ณ  ํ•œ๋‹ค.

์‹ค์ œ๋กœ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ๋ณด๋ฉด ์•ฑ์˜ ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€๊ฐ€ ์ถฉ๋ถ„ํ•œ ๊ณต๊ฐ„์„ ์ œ๊ณตํ•˜์ง€ ์•Š๋Š” ๋‹ค๋ฉด ์™ธ๋ถ€ ์Šคํ† ๋ฆฌ์ง€๋ฅผ ์‚ฌ์šฉํ•˜๋ผ๊ณ  ๋‚˜์™€์žˆ๋‹ค.

	private var recorder: MediaRecorder? = null
	// ... ์ค‘๋žต
	private fun startRecording() {
        recorder = MediaRecorder().apply {
            setAudioSource(MediaRecorder.AudioSource.MIC) // ๋งˆ์ดํฌ์— ์ ‘๊ทผ
            setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) // ํฌ๋ฉง ์ง€์ •
            setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) // ์ธ์ฝ”๋” ๋ฐฉ์‹ ์ง€์ •
            setOutputFile(recordingFilePath) // ์ง€์ •ํ•ด์ค€ ๊ฒฝ๋กœ์— ์ €์žฅ
            prepare()
        }
        recorder?.start()
        state = State.ON_RECORDING
    }

์šฐ์„  MediaRecorder ๊ฐ์ฒด๋ฅผ Nullable ๋กœ ๋งŒ๋“ค์–ด์ฃผ๊ณ  startRecording ๋ฉ”์†Œ๋“œ๋ฅผ ๋งŒ๋“ค์–ด์คฌ๋‹ค. ์œ„์—์„œ ๋‚˜์—ดํ•œ ์ˆœ์„œ์— ๋”ฐ๋ผ Prepare ์ƒํƒœ๊นŒ์ง€ ์ง„ํ–‰ํ•˜๊ณ  ๋…น์Œ์„ ์‹œ์ž‘ํ•œ๋‹ค. ์ดํ›„ ์ƒํƒœ๋ฅผ ON_RECORDING ์œผ๋กœ ๋ฐ”๊ฟ”์คฌ๋‹ค.

	private var state = State.BEFORE_RECORDING
        set(value) {
            field = value
            recordButton.updateIconWithState(value)
        }

์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋Š” ๊ฒฝ์šฐ ์‹ค์ œ ํ™”๋ฉด์—์„œ์˜ ์•„์ด์ฝ˜๋„ ๋ณ€๊ฒฝ๋˜์•ผํ•˜๋ฏ€๋กœ setter ๋ฅผ ํ†ตํ•ด ๊ฐ’์„ ๋ณ€๊ฒฝ์‹œ์ผœ์คฌ๋‹ค.

stopRecording

๋…น์Œ์„ ์ค‘์ง€ํ•˜๋Š” ๋ฉ”์†Œ๋“œ์ด๋‹ค. ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„ํ–ˆ๋‹ค.

	private fun stopRecording() {
        recorder?.run {
            stop()
            release() // ๋ฉ”๋ชจ๋ฆฌ ํ•ด์ œ
        }
        recorder = null
        state = State.AFTER_RECORDING
    }

๋…น์Œ์ด ์ง„ํ–‰๋˜๋ฉด recorder ๊ฐ์ฒด์—๋Š” null์ด ์•„๋‹ˆ๊ฒŒ ๋œ๋‹ค. ๋”ฐ๋ผ์„œ run์„ ์‹คํ–‰ํ•˜๊ฒŒ ๋˜๊ณ  ์ด๋•Œ stop ์„ ์‹คํ–‰ํ•˜๊ณ  release ๋ฅผ ํ†ตํ•ด ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ํ•ด์ œํ•œ๋‹ค. ์ดํ›„ recorder ๊ฐ์ฒด๋ฅผ null๋กœ ๋งŒ๋“ค์–ด์ฃผ๊ณ  ์ƒํƒœ๋ฅผ AFTER_RECORDING ์œผ๋กœ ๋ฐ”๊ฟ”์คฌ๋‹ค.

startPlaying

๋…น์Œ๋œ ํŒŒ์ผ์„ ๋“ฃ๊ธฐ ์œ„ํ•ด์„œ๋Š” MediaPlayer ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ์—ญ์‹œ MediaRecorder ์ฒ˜๋Ÿผ ์ƒํƒœ ๊ฐ’์„ ๊ฐ–๋Š”๋‹ค. ๊ณต์‹ ๋ฌธ์„œ์— ๋‚˜์™€์žˆ๋Š” ์ƒํƒœ๋„๋ฅผ ์‚ดํŽด๋ณด์ž.

MediaPlayer State diagram

MediaRecorder๋ณด๋‹ค Prepare๊นŒ์ง€์˜ ๊ณผ์ •์ด ์ข€ ๋” ๊ฐ„๊ฒฐํ•˜๋‹ค.

  1. setDataSource ๋กœ ํŒŒ์ผ์„ ๋ถˆ๋Ÿฌ์˜จ๋‹ค.
  2. prepare ์„ ํ†ตํ•ด ๋ชจ๋“  ์ค€๋น„๋ฅผ ์™„๋ฃŒํ•œ๋‹ค.

๋‘ ๊ณผ์ •์ด ๋์ด๋‹ค. ์‹ค์ œ ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด์ž.

	private var player: MediaPlayer? = null
	// ... ์ค‘๋žต
	private fun startPlaying() {
        player = MediaPlayer().apply {
            setDataSource(recordingFilePath)
            prepare()
        }
        player?.start()
        state = State.ON_PALYING
    }

player ๊ฐ์ฒด๊ฐ€ null์ด ์•„๋‹Œ ๊ฒฝ์šฐ start ๋ฅผ ์‹คํ–‰ํ•œ๋‹ค. ์ดํ›„ ON_PLAYING ์ƒํƒœ๋กœ ๋ฐ”๊ฟ”์ค€๋‹ค.

stopPlaying

stopRecording ๊ณผ ์œ ์‚ฌํ•˜๋‹ค.

	private fun stopPlaying() {
        player?.release()
        player = null
        state = State.AFTER_RECORDING
    }

์ด๋ ‡๊ฒŒ ๋…น์Œ ์‹œ์ž‘, ๋…น์Œ ์ค‘์ง€, ๋…น์Œ ํŒŒ์ผ ์žฌ์ƒ, ๋…น์Œ ํŒŒ์ผ ์žฌ์ƒ ์ค‘์ง€ ์ด 4๊ฐ€์ง€์— ์ƒํƒœ์— ๋”ฐ๋ฅธ ๋ฉ”์†Œ๋“œ๋ฅผ ๋ชจ๋‘ ๋งŒ๋“ค์—ˆ๊ณ  ๋…น์Œ ๋ฒ„ํŠผ๊ณผ ์—ฐ๊ฒฐํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

bindViews

setOnClickListener ๋ฅผ ํ†ตํ•ด ๋ฒ„ํŠผ ํด๋ฆญ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ ํ˜„์žฌ ๋ฒ„ํŠผ์˜ ์ƒํƒœ์— ๋”ฐ๋ผ ํŠน์ • ๋ฉ”์†Œ๋“œ๋ฅผ ์‹คํ–‰ํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ๋‹ค.

	private fun bindViews() {
        recordButton.setOnClickListener {
            when (state) {
                State.BEFORE_RECORDING -> startRecording()
                State.ON_RECORDING -> stopRecording()
                State.AFTER_RECORDING -> startPlaying()
                State.ON_PALYING -> stopPlaying()
            }
        }
    }

image

4. ์˜ค๋””์˜ค ์‹œ๊ฐํ™”ํ•˜๊ธฐ

์šฐ์„  View ๋ฅผ ์ƒ์†ํ•˜๋Š” SoundVisualizerView ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด์คฌ๋‹ค. ์•ˆ๋“œ๋กœ์ด๋“œ์˜ onDraw ๋ฉ”์†Œ๋“œ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•ด์„œ ์˜ค๋””์˜ค๋ฅผ ์‹œ๊ฐํ™”ํ•œ๋‹ค. onDraw์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋Š” Canvas ํด๋ž˜์Šค์ด๋‹ค. Canvas ๊ฐ์ฒด๋Š” ๋ทฐ์—์„œ ํ…์ŠคํŠธ, ์„ , ๋น„ํŠธ๋งต ๋“ฑ ๋‹ค์–‘ํ•œ ๊ทธ๋ž˜ํ”ฝ์„ ๊ทธ๋ฆฌ๊ธฐ ์œ„ํ•œ ๋ฉ”์†Œ๋“œ๋ฅผ ์ •์˜ํ•œ๋‹ค. ์ด ๋ถ€๋ถ„์€ ์•ˆ๋“œ๋กœ์ด๋“œ ๊ณต์‹ ๋ฌธ์„œ์— ์ž˜ ๋‚˜์™€์žˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ทธ๋ฆฌ๊ธฐ ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๊ธฐ ์ „์— Paint ๊ฐ์ฒด๋ฅผ ๋จผ์ € ๋งŒ๋“ค์–ด์•ผํ•œ๋‹ค. Canvas ํด๋ž˜์Šค๊ฐ€ ๊ทธ๋ฆฌ๋Š” ๋‚ด์šฉ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ๋‹ค๋ฉด, Paint ํด๋ž˜์Šค๋Š” ๊ทธ๋ฆฌ๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด Canvas์—๋Š” ์ง์‚ฌ๊ฐํ˜•์„ ๊ทธ๋ฆฌ๋Š” ๋ฉ”์†Œ๋“œ๊ฐ€ ์žˆ๊ณ  Paint์—๋Š” ์ง์‚ฌ๊ฐํ˜•์„ ๋ฌด์Šจ ์ƒ‰์œผ๋กœ ์ฑ„์šฐ๋Š”์ง€์— ๋Œ€ํ•œ ๋ฉ”์†Œ๋“œ๊ฐ€ ์žˆ๋‹ค.

	companion object {
        private const val LINE_WIDTH = 10F
        private const val LINE_SPACE = 15F
        private const val MAX_AMPLITUDE = Short.MAX_VALUE.toFloat()
        private const val ACTION_INTERVAL = 20L
    }

	// ... ์ค‘๋žต

	private val amplitudePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = context.getColor(R.color.purple_500)
        strokeWidth = LINE_WIDTH
        strokeCap = Paint.Cap.ROUND
    }

์šฐ์„  Paint ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ ๋‹ค. ANTI_ALIAS_FLAG ์ƒ์„ฑ์ž๋ฅผ ํ†ตํ•ด ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค๊ณ  apply ๋กœ ์†์„ฑ ๊ฐ’์„ ์ •ํ•ด์ค€๋‹ค. ์ดํ›„์— onDraw ๋ฉ”์†Œ๋“œ๋กœ View๋ฅผ ๋งŒ๋“ค์–ด์ฃผ๋ฉด ๋œ๋‹ค. ํ•˜์ง€๋งŒ ๊ทธ ์ „์— ์•Œ์•„์•ผํ•  ๊ฒƒ์ด ์žˆ๋‹ค. View์˜ ํฌ๊ธฐ์ด๋‹ค.

	private var drawingWidth: Int = 0
  private var drawingHeight: Int = 0	
	override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        drawingWidth = w
        drawingHeight = h
    }

onSizeChanged ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•ด์„œ View์˜ ํฌ๊ธฐ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค. ์ด์ œ ์šฐ๋ฆฌ๊ฐ€ ์˜ค๋””์˜ค๋ฅผ ์‹œ๊ฐํ™”ํ•  ์˜์—ญ์˜ ํฌ๊ธฐ๋ฅผ ์•Œ์•„๋ƒˆ๋‹ค.

onDraw

	private var drawingAmplitudes: List<Int> = emptyList()	
	override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)

        canvas ?: return

        val centerY = drawingHeight / 2f
        var offsetX = drawingWidth.toFloat()

        drawingAmplitudes
            .let { amplitudes ->
                if(isReplaying) {
                    amplitudes.takeLast(replayingPosition)
                }
                else {
                    amplitudes
                }
            }
            .forEach { amplitude ->
            val lineLength = amplitude / MAX_AMPLITUDE * drawingHeight * 0.8F
            offsetX -= LINE_SPACE
            if (offsetX < 0) return@forEach

            canvas.drawLine(
                offsetX,
                centerY - lineLength / 2F,
                offsetX,
                centerY + lineLength / 2F,
                amplitudePaint
            )
        }
    }

์šฐ์„  ์ธ์ฝ”๋”ฉ๋œ ์Œ์„ฑ์„ ์ €์žฅํ•  ๋ฆฌ์ŠคํŠธ๋ฅผ ์„ ์–ธํ•ด์คฌ๋‹ค. ์ดํ›„ ๋‘ ๊ฐœ์˜ ๊ธฐ์ค€์ ์„ ๋งŒ๋“ค์–ด์คฌ๊ณ  forEach ๋ฅผ ํ†ตํ•ด ์ €์žฅ๋œ ์Œ์›์„ ํ•˜๋‚˜์”ฉ ํ™”๋ฉด์— ํ‘œ์‹œํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ง„ํ–‰ํ–ˆ๋‹ค. ์ผ๋‹จ ์ขŒํ‘œ์ฃฝ ๊ฐœ๋…๋ถ€ํ„ฐ ์ง‘๊ณ  ๋„˜์–ด๊ฐ€์ž.

image

์ด๋ ‡๊ฒŒ ์ขŒํ‘œ๊ฐ€ ์„ค์ •๋˜๋Š”๋ฐ ์ฝ”๋“œ์—์„œ offsetX ๊ฐ€ View์˜ ๋„ˆ๋น„๋กœ ์ง€์ •๋˜์–ด ์žˆ๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. ์ฆ‰, ๊ฐ€์žฅ ์šฐ์ธก๋ถ€ํ„ฐ ๋ฆฌ์ŠคํŠธ์˜ ์›์†Œ๋ฅผ ์ถœ๋ ฅํ•˜๊ฒŒ ๋œ๋‹ค.

visualizeRepeatAction

Runnable ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ด์šฉํ•ด์„œ ๊ตฌํ˜„ํ–ˆ๋‹ค.

	var onRequestCurrentAmplitude: (() -> Int)? = null
	private val visualizeRepeatAction: Runnable = object : Runnable {
        override fun run() {
            if (!isReplaying) {
                val currentAmplitude = onRequestCurrentAmplitude?.invoke() ?: 0
                drawingAmplitudes = listOf(currentAmplitude) + drawingAmplitudes
            } else {
                replayingPosition++
            }
            invalidate()
            handler?.postDelayed(this, ACTION_INTERVAL)
        }
    }

onRequestCurrentAmplitude ๋ฅผ ์ •์˜ํ•ด์„œ ํ•จ์ˆ˜ ๊ฐ’์ด ๋ฐ˜ํ™˜๋˜๋„๋ก ๊ตฌํ˜„ํ–ˆ๋Š”๋ฐ ์ด๋ฅผ handler ๋ฅผ ํ†ตํ•ด 20milliseconds ๋งˆ๋‹ค ์‹คํ–‰๋˜๋„๋ก ํ–ˆ๋‹ค. ์ฆ‰ 20milliseconds ๋งˆ๋‹ค ํ˜„์žฌ ์Œ์„ฑ์˜ maxAmplitude ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค. ์ดํ›„ ๊ธฐ์กด์˜ ์Œ์„ฑ์ด ๋‹ด๊ฒจ์žˆ๋Š” ๋ฆฌ์ŠคํŠธ์˜ ๋งจ ์•ž์— ์ƒˆ๋กœ์šด ์Œ์„ฑ์„ ์ถ”๊ฐ€ํ•ด์คฌ๋‹ค. ์žฌ์ƒ ๋ฒ„ํŠผ ์œ„์— ์žˆ๋Š” ๋…น์Œ ์‹œ๊ฐ„ ์ถœ๋ ฅ ์—ญ์‹œ ๋™์ผํ•˜๊ฒŒ Runnable ์„ ์‚ฌ์šฉํ•ด์„œ ๊ตฌํ˜„ํ–ˆ๋‹ค.

image

image

About

๐Ÿ›  MediaRecorder์™€ Player, Runnable๊ณผ Thread, CustomView๋ฅผ ํ™œ์šฉํ•œ ๋…น์Œ๊ธฐ

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages