Skils/Android

[Android] 영상 편집 UI 구현

재한 2024. 6. 23. 21:06

프로젝트 관련 글

[Android] 네이버 간편 로그인(Kotlin)

[Android] 자동 로그인 with DataStore(Kotlin)

[Android] 네이버 로그인 프로필 가져오기

[Android] DataSource 적용 및 분리

[Android] - Retrofit으로 에러 메시지 처리하기

 

영상을 다루는 프로젝트를 진행함에 있어서 영상 편집을 한번 시도해보고 싶었습니다.

그러기 위해서 Android 기본 어플인 갤러리의 UI를 많이 참고했습니다.

시중에 많은 동영상 편집 어플은 있지만, 실제로 블로그에 다룬 사람들은 많이 없었기에, 적절한 UI 컴포넌트를 찾는데 정말 오래 걸린 것 같습니다.

 

UI 컴포넌트

 

후보는 여러 가지가 있는데요,

SeekBar ❌

https://developer.android.com/reference/android/widget/SeekBar

 

SeekBar  |  Android Developers

 

developer.android.com

현재 Material에서는 SeekBar가 아닌 Slider로 사용되기 때문에 후보에서 제외했습니다.

 

Slider ❌

https://m3.material.io/components/sliders/overview

 

Sliders – Material Design 3

Sliders allow users to make selections from a range of values.

m3.material.io

 

처음 양방향 스크롤을 구현하기 위해 Slider를 두 개 배치해서, 각각 왼쪽과 오른쪽을 담당하려고 했었습니다.
UI 특성상 왼쪽과 오른쪽의 위치 역전이 발생하면 안 되고, xml에서 width 값을 모두 match_parent로 지정할 경우
한쪽의 Slider 터치 이벤트가 묻혀버리기 때문에 양방향 스크롤을 구현하기 위해서는 Slider는 적합하지 않다고 판단했습니다.

RangeSlider ✅

Slider에서 파생된 컴포넌트입니다.

Slider의 추가적인 구현을 통해 양방향 스크롤이 가능한 컴포넌트였고, 왼쪽과 오른쪽의 역전을 방지하는 것이 내부 구현되어 있기 때문에 적합하다고 판단했습니다.

 

비디오 타임라인 기능을 구현하기 위해서는 영상에 대한 시간 표시가 필요했는데, 내부적으로 라벨링에 대한 기능도 제공하기 때문에 더욱 적절했습니다.

 

이제 RandeSlider 컴포넌트를 하나씩 뜯어보겠습니다.

 

RangeSlider

 

1️⃣ Value

Slider의 현재 지점을 뜻한다. Slider는 0.0~100.0까지의 범위를 가지고 있는데, 상대적인 위치가 표시됩니다.

RangeSlider는 Slider와 다르게 2가지 value를 가지고 있습니다.

fromValue[최소값], toValue[최대값]으로 구성되어 있는데, 쉽게 생각하면 from이 왼쪽, to가 오른쪽이라고 이해하면 됩니다.

Value는 위 그림처럼 숫자로 표현되는 것이 일반적이지만, UI 쓰임에 맞게 변형이 가능합니다.

Float값인 Value에 대해 더 의미 있는 라벨링을 해주고 싶다면 setLabelFormatter를 통해 구현할 수 있습니다.

저는 Value에 대해 영상의 타임라인을 표시해야 했기에 아래와 같은 구현을 했다.

sliderVideoTime.setLabelFormatter { value->
   converterValueToTimeLine(value) // 반환타입은 반드시 string으로!
}

여기서 주의할 점은 반드시 반환타입이 string으로 지정해줘야 합니다.

만약 함수를 사용하지 않는 내부 구현이라면 다음과 같이 쓸 수 있습니다.

sliderVideoTime.setLabelFormatter { value ->
   val line = "현재 값은 ${value*100}원입니다."
   line
}

 

위와 같은 labelFormatter를 통해 float인 value를 원하는 문자열로 변환해서 적용할 수 있습니다.

 

2️⃣ 6️⃣ Indicator

휴대폰을 사용할 경우 슬라이더의 대해 이동할 수 있는 정해진 구간이 있는 UI를 경험해 본 적 있을 것이라고 생각합니다.

자유로운 Slider 이동이 아닌 고정적으로 단계를 정해주고 싶을 때, 그 단계가 Indicator입니다.

Slider의 step size를 지정하면 step size만큼의 indicator가 증가합니다.

지속적인 스크롤이 아닌 뚝뚝 끊긴 느낌의 UI를 제공하는데 내가 원했던 스크롤은 끊기지 않고 부드러운 스크롤이기 때문에 적용하지 않았습니다.

 

3️⃣ 5️⃣ Active/InActivte

3번과 5번은 각각 활성/비활성화된 영역을 의미합니다.
슬라이더가 지나간 부분은 활성화이고, 지나가지 못한 부분은 비활성화 영역이다.
Slider의 속성에서 ActiveColor와 InActiveColor를 통해서 UI 차이점을 두곤 합니다.

 

삼성 갤러리앱의 UI입니다.

클립이 지나간 부분과 지나가지 않은 부분의 영역 처리를 통해 영상의 길이를 확실하게 사용자에게 보여주고 있는 것을 알 수 있습니다.
이렇게 RangeSlider의 활성/비활성화 영역 구분은 반드시 필요합니다.

 

4️⃣ Handle

명칭은 Handle이지만 코드에서는 Thumb라고 불립니다. 슬라이더를 움직이게 하는 막대기인데, 갤러리앱에서 클립 같이 생긴 것이 Thumb역할을 합니다. 실제 구현에서는 Thumb의 Custom Drawable을 입혀 그려내는 것처럼 Custom이 가능합니다.

 

타임라인 구현


갤러리앱을 잘 살펴보면 양 끝의 클립을 조절해 움직일 수 있는 부분과 가운데 동영상 타임라인을 조절할 수 있는 막대기가 있는 것을 확인할 수 있습니다.
영상 편집앱에 대한 레퍼런스와 UI를 참고할 만한 코드가 없어서 굉장히 고민을 많이 했습니다.
움직일 수 있는 Handle은 양 클립 가운데 타임라인이기에 Slider를 2개 사용하는 것이라고 판단했습니다.

따라서 저는 RangeSlider와 Slider를 겹쳐서 구현하기로 했습니다.
RangeSlider : 동영상 클립

Slider : 동영상 재생 타임라인

도식화하면 아래와 같은데요,

이렇게 할 경우 예상되는 문제점은 클릭 이벤트가 겹친다는 것이었습니다.
즉 반드시 하나의 터치 이벤트는 소실될 수밖에 없었고, 동적으로 Slider의 너비를 바꿀 수 없기 때문에 반드시 xml 단계에서 크기를 할당해야 했습니다. 이렇게 영역이 겹치지만, 영역에 따른 개별적인 이벤트 처리가 필요했습니다.

 

개별적인 이벤트 처리

  1. 동영상 클립 스크롤로 썸네일 영역을 활성/비활성화해야 합니다.
    • 이는 위에 언급했던 Activity Color와 InActivity Color를 통해 해결할 수 있습니다.
  2. 동영상 타임 라인의 Handle이 동영상 클립의 Handle 범위를 벗어나면 안 됩니다.
    • 갤러리앱을 사용해 보면, 클립을 이동시키지 않는다면 타임 라인의 Handle은 클립의 범위를 절대로 벗어날 수 없습니다.
    • 즉 동영상 타임 라인의 Handle 범위는 동영상 클립의 최솟값과 최댓값 사이에서 움직여야 합니다.
    • 따라서 왼쪽 클립이 이동할 경우 타임 라인의 Handle도 같이 움직여야 합니다.

 

1번은 xml 속성 영역으로 해결할 수 있지만, 2번 같은 경우는 코드로 구현해야 하고, 처리가 어려웠습니다.

 

Slider의 값을 처리하기 위해선 Listener가 필요합니다.

text도 onTextChagned가 있듯이 Slider도 다음과 같은 리스너가 있습니다.

public abstract void onValueChange (Slider slider, 
                float value, 
                boolean fromUser)
slider.addOnChangeListenr{ slider, value, fromUser ->	
		//이벤트 처리
}

각 파라미터는 다음과 같은 의미입니다.

  • slider : 슬라이더 객체
  • value : 슬라이더의 현재 값
  • fromUser : 변수명으로 추측해 보면 user에 의해 변화한 것인지, 아닌지

처음엔 fromUser의 존재 이유를 몰랐었는데 구현을 하면서 존재 이유를 명확하게 깨달을 수 있었습니다.

 

영상 썸네일 스크롤 이벤트 처리

영상  썸네일 스크롤의 이벤트 처리는 다음과 같이 했습니다.

sliderVideoThumbnail.addOnChangedListener { _, value, _ -> //slider, value, fromUser
   sliderVideoTime.value = value
}

영상 클립 조절 시 영상 타임 라인의 Handle도 따라 이동해야 하기 때문에 다음과 같은 코드를 작성했습니다.

slider와 fromUser는 사용하지 않기 때문에 백틱 처리해 줬습니다.

 

영상 타임라인 이벤트 처리

다음은 영상  타임라인 이벤트 처리입니다.

sliderVideoTime.addOnChangeListener { slider, value, _ ->
   val minRangeValue = sliderVideoThumbnail.values[0]
   val maxRangeValue = sliderVideoThumbnail.values[1]
	 if (value < minRangeValue) {
          slider.value = minRangeValue
          sliderVideoThumbnail.values = mutableListOf(value, maxRangeValue)
   } else if (value > maxRangeValue) {
          slider.value = maxRangeValue
          sliderVideoThumbnail.values = mutableListOf(minRangeValue, value)
   }
}

타임라인의 경우 클립의 범위를 벗어나면 안 되는 것이 첫 번째 지켜야 할 점입니다.

따라서 썸네일의 최솟값과 최댓값을 가져와서 타임라인의 값이 변화할 때마다 비교해 줍니다.

RangeSlider의 value는 [fromValue,toValue]로 지정할 수 있는데, 이를 통해  만약 타임라인의 값이 범위를 벗어난다면 범위를 바꿔줬습니다.

 

최종적인 결과물입니다.

 

 


생각보다 디테일한 점을 신경 쓰느라 오래 걸렸던 것 같습니다.
타임라인의 범위가 벗어날 경우 고정시켜야 할지, 범위를 늘려야 할지 고민하다가 제가 사용한다면 이게 더 편할 것 같다~로 느낀 방향으로 구현하기는 했습니다.

구현을 하고 보니 Slider의 Track 크기가 알맞지 않아 활성화된 영역 UI가 굉장히 불편하기는 한데, 이래서 동영상 편집 어플이 다 검은색으로 UI를 구현하는 건가?라는 의심이 들긴 했습니다.

 

참고

https://m3.material.io/components/sliders/overview

 

Sliders – Material Design 3

Sliders allow users to make selections from a range of values.

m3.material.io

https://github.com/material-components/material-components-android/blob/master/docs/components/Slider.md

 

material-components-android/docs/components/Slider.md at master · material-components/material-components-android

Modular and customizable Material Design UI components for Android - material-components/material-components-android

github.com