Post

Leanback tv앱 개발(1)

Leanback tv앱 개발(1)

TV 앱 개발에 입문하게됐다.

TV 앱 개발을 그냥 일반 앱 개발할 때 처럼 해도 보여지는 것엔 큰 문제가 없지만, TV는 터치스크린이 없고, 리모컨을 통한 제어가 주된 환경이라 신경 쓸 게 조금 많아진다. 리모컨의 방향키와 선택 버튼(DPAD)이 주 입력 수단이다. 음성 검색이나 게임 컨트롤러를 지원하지만, 입력은 제한적이며 배터리 제약이 없지만 기기 자체의 성능이 높지 않기 때문에 최적화는 필수다.

모바일 개발과, TV개발의 차이는 아래와 같다.

 안드로이드 TV 개발안드로이드 모바일 개발
UI 설계큰 화면, 거리에서 보기에 최적화, Leanback 라이브러리 사용작은 화면, 터치 기반, 표준 레이아웃 사용
입력 처리리모콘 키 이벤트 (DPAD, 선택 버튼)터치 이벤트 (탭, 스와이프)
내비게이션포커스 기반, 선형/그리드 탐색터치 기반, 자유로운 내비게이션
하드웨어 고려사항터치 스크린 없음, GPS/카메라와 같은 기능 제한 가능다양한 하드웨어 기능 지원 (터치, GPS, 카메라 등)
자원 관리더 큰 화면에 맞춘 이미지/텍스트 최적화작은 화면과 배터리 효율성 고려

지금은 Compose로 TV앱을 개발하는 게 권장사항이지만 내가 유지보수 해야하는 앱은 Leanback 툴킷을 사용하고 있기 때문에, 튜토리얼을 천천히 진행해보기로 했다.

android-tv-application-hands-on-tutorial

위 링크에서 @corochann님이 만들어두신 튜토리얼을 Kotlin으로 진행한다.

초기 설정

진짜 처음부터 하는 거라서, TV탭 -> No Acitivty로 시작한다. No Acitivty로 만들면 진짜 아무것도 없다.

의존성 추가해주고

1
2
3
4
5
6
[versions]
leanback = "1.0.0"


[libraries]
androidx-leanback = { group = "androidx.leanback", name = "leanback", version.ref = "leanback" }
1
implementation(libs.androidx.leanback)

Theme도 설정해준다.

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Theme.TV" parent="@style/Theme.Leanback" />
</resources>

Theme을 설정해주지 않으면 Leanback 컴포넌트를 빌드할 때 xml단에서 오류가 발생한다. Leanback의 Theme을 기본 테마로 넣고 이걸 manifest에서 지정해줘야 오류가 나지않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-feature
        android:name="android.hardware.touchscreen"
        android:required="false" />
    <uses-feature
        android:name="android.software.leanback"
        android:required="true" />

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

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

</manifest>

매니페스트를 작성해준다. 물론 그 전에 MainAcitivity를 만들어두고 레이아웃 파일까지는 만들어놓자.

1
2
3
4
5
6
    <uses-feature
        android:name="android.software.leanback"
        android:required="true" />
    <uses-feature
        android:name="android.hardware.touchscreen"
        android:required="false" />

이 두 옵션의 의미가 있다.

앱이 Leanback 라이브러리(안드로이드 TV용 UI 프레임워크)를 지원하는 장치를 대상으로 한다는 의미가 첫번째 것이다. android.software.leanback은 안드로이드 TV 환경을 말한다. 두번째는 터치스크린을 지원하는 기기인지 설정하는 것이다.

img

배너또한 중요하다. 배너의 역할은 앱을 홈화면에서 노출시켜줄 수 있는 것 인데, 앱 로고라고 생각하면 편하다. 위 사진과 같이 노출되려면 배너를 꼭 넣어줘야한다.

img

이제 화면을 구성해보자.

1
2
3
4
5
6
7
8
9
10
import android.os.Bundle
import android.view.View
import androidx.leanback.app.BrowseSupportFragment

class MainFragment: BrowseSupportFragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

    }
}

MainActivity에 넣을 MainFragment로 BrowseSupportFragment를 사용해주면 된다. MainFragment는 그냥 간단하게, FragmentContaierView에 name으로 지정해주면 끝난다.

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:name="com.kimmandoo.tv.MainFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

</androidx.fragment.app.FragmentContainerView>

이대로 실행시키면 잘 동작할까?

터진다. MainActivity를 템플릿으로 생성하면 AppCompatActivity로 생성되는데, 이건 Theme을 AppCompat으로 해줘야 올바르게 동작한다. 지금 우리는 TV앱을 빌드하기 위해 Leanback을 사용하기로 했기 때문에, Theme이 어긋나서 올바른 리소스를 찾지 못하는 것이다.

1
2
3
4
5
6
class MainActivity : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

그래서 FragmentActivity로 만들어야한다. tv에서 실행할꺼니까 edgeToEdge 날리고, view padding 세팅 다 날리고 FragmentActivity로 바꿔주면 된다.

img 그러면 위와 같이 잘 나온다.

BrowseSupportFragment는 아래 사진과 같은 구성으로 되어있다.

img

BrowseSupportFragment는 Headers와 Rows로 나뉘어져있다. Headers에서는 Drawer처럼 메뉴가 있고, Rows는 Detail화면이라고 보면 될 것 같다.

튜토리얼을 그대로 따라가기 위해 테마 색상도 지정해보자.

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="fastlane_background">#0096e6</color>
    <color name="search_opaque">#ffaa3f</color>
</resources>

fastlane이라는 단어의 의미가 있다. Leanback 라이브러리에서 Fastlane은 홈 화면의 왼쪽에 위치한 수직 탐색 메뉴, 카테고리 선택 UI를 의미한다. 사용자가 리모컨으로 빠르게 탐색할 수 있는 영역으로 보통 TV 앱에서 앱의 메인 메뉴 또는 사이드바다.

색상을 적용하기 위해 MainFragment를 수정해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MainFragment: BrowseSupportFragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setUpUIElements()
    }

    private fun setUpUIElements(){
        title = "Hello Android TV"
        headersState = HEADERS_ENABLED
        isHeadersTransitionOnBackEnabled = true
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            brandColor = resources.getColor(R.color.fastlane_background, null)
            searchAffordanceColor = resources.getColor(R.color.search_opaque, null)
        }
    }
}

img

brandColor로 색상을 넣어주면, 사이드바에 색이 들어간다.

1
2
3
4
5
6
7
8
private fun setUpUIElements(){
    title = "Hello Android TV"
    badgeDrawable = ResourcesCompat.getDrawable(resources, R.drawable.banner, null)
    headersState = HEADERS_ENABLED
    isHeadersTransitionOnBackEnabled = true
    brandColor = resources.getColor(R.color.fastlane_background, null)
    searchAffordanceColor = resources.getColor(R.color.search_opaque, null)
}

title 대신에 badge로 이미지를 넣을 수 있는데, title, badge 둘 다 넣으면 badge만 보인다.

This post is licensed under CC BY 4.0 by the author.