본문 바로가기
flutter/Widget of the Week

Flutter[플러터] / Slider 를 사용하여 네이티브로 음량 조절 바 만들기 (소리 바, slider, bar, control, 컨트롤, Kotlin, Native, 패키지 X) Slider (Flutter Widget of the Week)

by ch5c 2025. 6. 14.
반응형

Slider class

Material Design 슬라이더.

다양한 값 중에서 선택하는 데 사용됩니다.

https://youtu.be/ufb4gIPDmEs

 

공식 문서 코드

 

 


앱에서 음량 조절을 하게 만들고 싶을 수 있다. 그럴 때에는 음량 조절을 어떻게 하게 만들어야 할까? 사용자가 직접 값을 입력? 물론 좋다. 하지만 뭐니뭐니 해도 예로부터 음량 조절은 드래그로 값을 조절할 수 있는 '바'로 만드는 것이 국룰이었다.

여기서 Slider 위젯이 그 역할을 아주 간단하게 만들어줄 수 있다. Slider 는 값의 범위를 신속하게 바꾸고 싶고 사용자 경험을 더 만족시켜 줄 수 있는 위젯이다. Slider 를 알아보면서 코틀린과 연결하여 음량 조절 기능도 만들어보자.

하위 속성
속성명 타입 기본값 설명
value double 현재 슬라이더의 값으로 반드시 min과 max 사이여야 함
onChanged ValueChanged<double>? null 값이 변경될 때 호출되는 콜백, null이면 슬라이더가 비활성화됨
onChangeStart ValueChanged<double>? null 사용자가 드래그를 시작할 때 호출되는 콜백
onChangeEnd ValueChanged<double>? null 사용자가 드래그를 마쳤을 때 호출되는 콜백
min double 0.0 선택 가능한 최소 값
max double 1.0 선택 가능한 최대 값
divisions int? null 슬라이더를 나눌 구간 수, null이면 연속형 슬라이더
label String? null 현재 값을 나타내는 레이블, 값 인디케이터로 표시됨
activeColor Color? null 선택된 범위의 트랙 색상
inactiveColor Color? null 선택되지 않은 범위의 트랙 색상
secondaryTrackValue double? null 보조 트랙을 표시할 값, buffering 등을 표현할 때 사용
secondaryActiveColor Color? null 보조 트랙에 사용할 색상
thumbColor Color? null 슬라이더 썸(손잡이)의 색상
overlayColor MaterialStateProperty<Color?>? null hover, focus 상태일 때 오버레이 색상
mouseCursor MouseCursor? null 마우스 커서 스타일
semanticFormatterCallback SemanticFormatterCallback? null 접근성 화면 읽기를 위한 값 포맷터
focusNode FocusNode? null 슬라이더의 포커스를 관리하는 노드
autofocus bool false 초기 빌드시 자동 포커스를 받을지 여부
allowedInteraction SliderInteraction? tapAndSlide 사용자가 슬라이더와 상호작용하는 방식 설정
padding EdgeInsetsGeometry? null 슬라이더 주변 여백을 설정

 

일단 사용하는 방법은 굉장히 쉽다. 조절할 값을 만들어주고 어떻게 조절하는지만 입력하면 된다.

double sliderValue = 0;

일단 가장 먼저 하위 속성인 value 안에 들어갈 double 타입의 값을 만들어줘야 한다. 여기서 입력했던 값은 Slider 의 초기값이 되겠다. (당연하지만 final 로 선언하면 적용 안됨)

Slider(
  value: sliderValue,
  onChanged: (value) {
    setState(() {
      sliderValue = value;
    });
  },
),

이게 끝이다. value 에 만들어둔 값을 넣어주고 이제 onChanged 에서는 value 로 현재 슬라이더의 Thumb 가 위치해 있는 곳의 값을 넘겨주기 때문에 초기값에 대입해 주면 간단하게 바로 슬라이더를 만들 수가 있다. 여기서 많이 쓰는 속성들을 좀 정리해 주겠다.

먼저 가장 많이 쓰는 하위 속성을 알아보겠다. (사실 바로 밑에 건 반필수 이긴 하다.)

Slider(
  min: 0, // 최소
  max: 100, // 최대
  label: sliderValue.round().toString(),
  value: sliderValue,
  onChanged: (value) {
    setState(() {
      sliderValue = value;
    });
  },
),

그냥 보자마자 알겠지만 말 그대로 최솟값과 최댓값을 설정해 줄 수 있다. 이렇게 하면 정해진 범위 내에서 슬라이더가 동작하게 된다.

그다음은 label 이다. 이 값은 Thumb 가 현재 위치해 있는 곳의 값을 UI 로 표시해 준다. 다만 단독으로는 사용 못 하고 지점을 나눌 수 있게 해주는 divisions 를 사용해줘야 한다.

Slider(
  divisions: 10, // max / 10 해서 현코드에서는 10 단위
  min: 0,
  max: 100, // divisions 사용 시 필수 값
  label: sliderValue.round().toString(), // round() 로 소수점 출력제한
  value: sliderValue,
  onChanged: (value) {
    setState(() {
      sliderValue = value;
    });
  },
),

솔직히 이것들만 알면 다 안거다. 바로 한번 코틀린과 연동시켜서 음량 조절기능을 구현해 보자.

기본적으로 효과음 크기를 조절한다고 해보자.

(전체코드)

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(MaterialApp(home: Test()));

class Test extends StatefulWidget {
  const Test({super.key});

  @override
  State<Test> createState() => _TestState();
}

class _TestState extends State<Test> {
  double volume = 50.0;

  Future<void> setVolume(double volume) async {
    await MethodChannel('com.example.test').invokeMethod('setVolume', {"volume": volume});
  }

  Future<void> playSound(String fileName) async {
    await MethodChannel('com.example.test').invokeMethod('playSound', fileName);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            RotatedBox(
              quarterTurns: -1,
              child: Slider(
                value: volume,
                min: 0,
                max: 100,
                onChanged: (value) {
                  setState(() {
                    volume = value;
                    setVolume(volume);
                  });
                },
              ),
            ),
            Text(volume.round().toString()),
            ElevatedButton(
              onPressed: () => playSound("mario_death"),
              child: Text("Play"),
            ),
          ],
        ),
      ),
    );
  }
}

 

일단 먼저 슬라이더 부분인데 뭐 다를 것 없다.

double volume = 50.0;

RotatedBox(
  quarterTurns: -1, // 왼쪽으로 90도 회전
  child: Slider(
    value: volume,
    min: 0,
    max: 100,
    onChanged: (value) {
      setState(() {
        volume = value;
        setVolume(volume); // 코틀린에 현재 값 전달
      });
    },
  ),
),

초기 값 생성해 주고~ 최소/최댓값 지정해 주고~ 슬라이더에 넣어주고~  이렇게 하면 금방 만들 수 있다.

그리고 효과음 조절 기능이기도 하고 심심해서 RotatedBox 사용해서 세로로 돌려줬다.

Future<void> setVolume(double volume) async {
  await MethodChannel('com.example.test').invokeMethod('setVolume', {"volume": volume});
}

Future<void> playSound(String fileName) async {
  await MethodChannel('com.example.test').invokeMethod('playSound', fileName);
}

이제 요 친구들은 둘 다 MethodChannel 사용해서 네이티브와 통신을 해줬다. setVolumn 은 현재 디바이더 값을 넘겨줘서 소리 크기를 조절할 수 있게 해 주고 playSound 는 말 그대로 효과음 재생시켜 주게 한다.

이제 핵심인 코틀린 코드다.

(전체코드)

package com.example.test

import android.media.MediaPlayer
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity : FlutterActivity() {

    private val CHANNEL = "com.example.test"
    private var volume: Float = 1.0f
    private var mediaPlayer: MediaPlayer? = null

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
            .setMethodCallHandler { call, result ->
                when (call.method) {
                    "setVolume" -> {
                        val volumeDouble = call.argument<Double>("volume") ?: 100.0
                        volume = (volumeDouble.coerceIn(0.0, 100.0) / 100.0).toFloat()
                        result.success(null)
                    }

                    "playSound" -> {
                        val fileName = call.arguments as String
                        playSound(fileName)
                        result.success(null)
                    }

                    else -> result.notImplemented()
                }
            }
    }

    private fun playSound(fileName: String) {
        val resId = getResId(fileName)
        if (resId != 0) {
            mediaPlayer?.release()
            mediaPlayer = MediaPlayer.create(this, resId)
            mediaPlayer?.setVolume(volume, volume)
            mediaPlayer?.setOnCompletionListener {
                it.release()
                mediaPlayer = null
            }
            mediaPlayer?.start()
        }
    }

    private fun getResId(fileName: String): Int {
        return when (fileName) {
            "mario_death" -> R.raw.mario_death
            else -> 0
        }
    }

    override fun onDestroy() {
        mediaPlayer?.release()
        super.onDestroy()
    }
}

 

최대한 쉽게 만들었다. 그리고 패키지 이름은 꼭 자기 것으로 수정해 주길 바란다. (com.example.test -> 본인 패키지 이름)

private var volume: Float = 1.0f
private var mediaPlayer: MediaPlayer? = null

효과음을 담당해 줄 volume 과 그 효과음 재생용 mediaPlayer 를 써줬다.

그리고 이제 MethodChannel 을 썼다면 필연적으로 나오는 '그' 코드를 써준다.

// Flutter 엔진이 구성될 때 호출됨
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)

    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
        .setMethodCallHandler { call, result ->
            when (call.method) {
                "setVolume" -> { // 볼륨 설정 요청
                    val volumeDouble = call.argument<Double>("volume") ?: 100.0
                    volume = (volumeDouble.coerceIn(0.0, 100.0) / 100.0).toFloat()
                    result.success(null)
                }

                "playSound" -> { // 효과음 재생 요청
                    val fileName = call.arguments as String
                    playSound(fileName)
                    result.success(null)
                }

                else -> result.notImplemented() // 오류 처리
            }
        }
}

 

아까 Flutter 에서 보내준 MethodChannel 을 전달받고 메서드들을 실행시켜 준다.

이제 마지막으로 그 메소드들를 봐보자면

private fun playSound(fileName: String) { // 전달된 파일 이름으로 효과음 재생
    val resId = getResId(fileName) // 파일 이름으로 리소스 ID 찾기
    if (resId != 0) {
        mediaPlayer?.release()
        mediaPlayer = MediaPlayer.create(this, resId)
        mediaPlayer?.setVolume(volume, volume) // 현재 볼륨 적용
        mediaPlayer?.setOnCompletionListener {
            it.release()
            mediaPlayer = null
        }
        mediaPlayer?.start() // 재생 시작
    }
}

 

당연한 건지 모르겠지만 일단 파일하나를 디렉터리에 위치시켜 놔야 한다.

경로는 이렇다.

android/app/src/main/res/raw/mario_death.mp3

raw 디렉토리가 없다면 만들면 된다. 그리고 재생할 파일을 넣어주자.

mario_death.mp3
0.08MB

 

 

마리오 죽는 소리인데 난 이걸로 했다.

암튼 이제 방금 위에서 호출했던 getResId 메서드를 만들어주면 된다.

private fun getResId(fileName: String): Int { // 파일 이름을 통해 raw 리소스의 ID를 반환
    return when (fileName) {
        "mario_death" -> R.raw.mario_death // "mario_death" 파일 이름 매칭
        else -> 0
    }
}

override fun onDestroy() { // 액티비티 종료 시 리소스 정리
    mediaPlayer?.release()
    super.onDestroy()
}

 

이렇게 해서 실행해 보면 굉장히 잘 된다는 것을 알 수 있다.

안타깝지만 gif 는 소리를 못 넣기 때문에 직접 만들어서 시험해 보길 바란다. 도움이 되었길 바라며 마치겠다.

반응형