TV에서 실행되는 미디어 앱은 사용자가 콘텐츠 서비스를 탐색하고 콘텐츠 재생을 시작합니다. 콘텐츠 탐색 환경 시각적으로 즐겁고 눈길을 끌 수 있을 뿐만 아니라 단순하고 직관적이어야 합니다.
이 가이드에서는 androidx.leanback 라이브러리에서 제공하는 클래스를 사용하는 방법을 설명합니다. 앱의 미디어 카탈로그에서 음악이나 동영상을 탐색하기 위한 사용자 인터페이스를 구현할 수 있습니다.
참고: 여기에 표시된 구현 예에서는
<ph type="x-smartling-placeholder">BrowseSupportFragment
</ph>
지원 중단된 BrowseFragment
가 아님
클래스에 대해 자세히 알아보세요. BrowseSupportFragment
는 AndroidX를 확장합니다.
Fragment
클래스,
여러 기기와 Android 버전에서 일관된 동작을 보장하도록 지원합니다.
![앱 기본 화면](https://developer.android.google.cn/static/images/tv/app-browse.png?authuser=0&hl=ko)
그림 1. Leanback 샘플 앱의 탐색 프래그먼트는 동영상 카탈로그 데이터를 표시합니다.
미디어 탐색 레이아웃 만들기
BrowseSupportFragment
클래스에 대해 자세히 알아보세요.
를 사용하면 미디어 항목의 카테고리와 행을 탐색하기 위한 기본 레이아웃을 만들 수 있습니다.
배포할 수 있습니다 다음 예는 요소가 포함된 레이아웃을 만드는 방법을 보여줍니다.
BrowseSupportFragment
객체:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/main_frame" android:layout_width="match_parent" android:layout_height="match_parent"> <fragment android:name="com.example.android.tvleanback.ui.MainFragment" android:id="@+id/main_browse_fragment" android:layout_width="match_parent" android:layout_height="match_parent" /> </FrameLayout>
애플리케이션의 기본 활동에서는 다음 예와 같이 이 뷰를 설정합니다.
Kotlin
class MainActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main) } ...
자바
public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } ...
BrowseSupportFragment
메서드는
동영상 데이터와 UI 요소를 추가하고 아이콘, 제목 및 UI 요소 등의 레이아웃 매개변수를
카테고리 헤더 사용 설정 여부입니다.
BrowseSupportFragment
를 구현하는 애플리케이션의 서브클래스
메서드는 또한 UI 요소에서 사용자 작업에 대한 이벤트 리스너를 설정하고
백그라운드 관리자에 위치 정보를 추가할 수 있습니다.
Kotlin
class MainFragment : BrowseSupportFragment(), LoaderManager.LoaderCallbacks<HashMap<String, List<Movie>>> { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) loadVideoData() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) prepareBackgroundManager() setupUIElements() setupEventListeners() } ... private fun prepareBackgroundManager() { backgroundManager = BackgroundManager.getInstance(activity).apply { attach(activity?.window) } defaultBackground = resources.getDrawable(R.drawable.default_background) metrics = DisplayMetrics() activity?.windowManager?.defaultDisplay?.getMetrics(metrics) } private fun setupUIElements() { badgeDrawable = resources.getDrawable(R.drawable.videos_by_google_banner) // Badge, when set, takes precedent over title title = getString(R.string.browse_title) headersState = BrowseSupportFragment.HEADERS_ENABLED isHeadersTransitionOnBackEnabled = true // Set header background color brandColor = ContextCompat.getColor(requireContext(), R.color.fastlane_background) // Set search icon color searchAffordanceColor = ContextCompat.getColor(requireContext(), R.color.search_opaque) } private fun loadVideoData() { VideoProvider.setContext(activity) videosUrl = getString(R.string.catalog_url) loaderManager.initLoader(0, null, this) } private fun setupEventListeners() { setOnSearchClickedListener { Intent(activity, SearchActivity::class.java).also { intent -> startActivity(intent) } } onItemViewClickedListener = ItemViewClickedListener() onItemViewSelectedListener = ItemViewSelectedListener() } ...
자바
public class MainFragment extends BrowseSupportFragment implements LoaderManager.LoaderCallbacks<HashMap<String, List<Movie>>> { } ... @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); loadVideoData(); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); prepareBackgroundManager(); setupUIElements(); setupEventListeners(); } ... private void prepareBackgroundManager() { backgroundManager = BackgroundManager.getInstance(getActivity()); backgroundManager.attach(getActivity().getWindow()); defaultBackground = getResources() .getDrawable(R.drawable.default_background); metrics = new DisplayMetrics(); getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics); } private void setupUIElements() { setBadgeDrawable(getActivity().getResources() .getDrawable(R.drawable.videos_by_google_banner)); // Badge, when set, takes precedent over title setTitle(getString(R.string.browse_title)); setHeadersState(HEADERS_ENABLED); setHeadersTransitionOnBackEnabled(true); // Set header background color setBrandColor(ContextCompat.getColor(requireContext(), R.color.fastlane_background)); // Set search icon color setSearchAffordanceColor(ContextCompat.getColor(requireContext(), R.color.search_opaque)); } private void loadVideoData() { VideoProvider.setContext(getActivity()); videosUrl = getString(R.string.catalog_url); getLoaderManager().initLoader(0, null, this); } private void setupEventListeners() { setOnSearchClickedListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(getActivity(), SearchActivity.class); startActivity(intent); } }); setOnItemViewClickedListener(new ItemViewClickedListener()); setOnItemViewSelectedListener(new ItemViewSelectedListener()); } ...
UI 요소 설정
이전 샘플에서 비공개 메서드 setupUIElements()
는
BrowseSupportFragment
메서드를 사용하여 미디어 카탈로그 브라우저의 스타일을 지정합니다.
setBadgeDrawable()
는 지정된 드로어블 리소스를 탐색 프래그먼트의 오른쪽 위 모서리에 배치합니다. 더 높은 수준의 성능을 제공할 수 있습니다 이 메서드는 제목 문자열을 드로어블 리소스(setTitle()
도 호출되는 경우) 드로어블 리소스는 52dp여야 합니다. 있습니다.setTitle()
가 탐색 프래그먼트 오른쪽 상단에 제목 문자열을 설정합니다.setBadgeDrawable()
가 호출됩니다.setHeadersState()
및setHeadersTransitionOnBackEnabled()
는 헤더를 숨기거나 사용 중지합니다. 자세한 내용은 헤더 숨기기 또는 사용 중지 섹션을 참고하세요.setBrandColor()
탐색 프래그먼트에서 UI 요소, 특히 헤더의 배경 색상을 설정합니다. 섹션 배경 색상(지정된 색상 값 사용)setSearchAffordanceColor()
- 지정된 색상 값으로 검색 아이콘의 색상을 설정합니다. 검색 아이콘 는 그림 1 및 그림 2와 같이 탐색 프래그먼트의 왼쪽 상단에 표시됩니다.
헤더 뷰 맞춤설정
그림 1의 찾아보기 프래그먼트는 동영상 데이터베이스의 행 헤더입니다. 또한 헤더를 사용하여 더 복잡한 레이아웃에 추가 뷰를 포함합니다. 다음 섹션에서는 그림 2와 같이 카테고리 이름 옆에 아이콘을 표시하는 이미지 보기를 포함해야 합니다.
![앱 기본 화면](https://developer.android.google.cn/static/images/tv/custom-head.png?authuser=0&hl=ko)
그림 2. 아이콘이 모두 있는 탐색 프래그먼트의 행 헤더 텍스트 라벨이 있습니다
행 헤더의 레이아웃은 다음과 같이 정의됩니다.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/header_icon" android:layout_width="32dp" android:layout_height="32dp" /> <TextView android:id="@+id/header_label" android:layout_marginTop="6dp" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
Presenter
를 사용하여
뷰 홀더를 만들고 바인딩하고 바인딩 해제하는 추상 메서드를 지원합니다. 다음
예를 들어 뷰 홀더를 두 개의 뷰, 즉
ImageView
및 TextView
Kotlin
class IconHeaderItemPresenter : Presenter() { override fun onCreateViewHolder(viewGroup: ViewGroup): Presenter.ViewHolder { val view = LayoutInflater.from(viewGroup.context).run { inflate(R.layout.icon_header_item, null) } return Presenter.ViewHolder(view) } override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, o: Any) { val headerItem = (o as ListRow).headerItem val rootView = viewHolder.view rootView.findViewById<ImageView>(R.id.header_icon).apply { rootView.resources.getDrawable(R.drawable.ic_action_video, null).also { icon -> setImageDrawable(icon) } } rootView.findViewById<TextView>(R.id.header_label).apply { text = headerItem.name } } override fun onUnbindViewHolder(viewHolder: Presenter.ViewHolder) { // no-op } }
자바
public class IconHeaderItemPresenter extends Presenter { @Override public ViewHolder onCreateViewHolder(ViewGroup viewGroup) { LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); View view = inflater.inflate(R.layout.icon_header_item, null); return new ViewHolder(view); } @Override public void onBindViewHolder(ViewHolder viewHolder, Object o) { HeaderItem headerItem = ((ListRow) o).getHeaderItem(); View rootView = viewHolder.view; ImageView iconView = (ImageView) rootView.findViewById(R.id.header_icon); Drawable icon = rootView.getResources().getDrawable(R.drawable.ic_action_video, null); iconView.setImageDrawable(icon); TextView label = (TextView) rootView.findViewById(R.id.header_label); label.setText(headerItem.getName()); } @Override public void onUnbindViewHolder(ViewHolder viewHolder) { // no-op } }
D패드를 사용하여 다음 작업을 수행할 수 있도록 헤더는 포커스가 가능해야 합니다. 스크롤하면 됩니다. 이를 관리하는 방법에는 두 가지가 있습니다.
onBindViewHolder()
에서 뷰를 포커스 가능하게 설정합니다.Kotlin
override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, o: Any) { val headerItem = (o as ListRow).headerItem val rootView = viewHolder.view rootView.focusable = View.FOCUSABLE // ... }
자바
@Override public void onBindViewHolder(ViewHolder viewHolder, Object o) { HeaderItem headerItem = ((ListRow) o).getHeaderItem(); View rootView = viewHolder.view; rootView.setFocusable(View.FOCUSABLE) // Allows the D-Pad to navigate to this header item // ... }
- 레이아웃을 포커스 지정 가능하게 설정합니다.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" ... android:focusable="true">
마지막으로 BrowseSupportFragment
구현에서
카탈로그 브라우저에서 setHeaderPresenterSelector()
를 사용합니다.
메서드를 사용하여 행 헤더의 프레젠터를 설정합니다.
Kotlin
setHeaderPresenterSelector(object : PresenterSelector() { override fun getPresenter(o: Any): Presenter { return IconHeaderItemPresenter() } })
자바
setHeaderPresenterSelector(new PresenterSelector() { @Override public Presenter getPresenter(Object o) { return new IconHeaderItemPresenter(); } });
전체 예는 다음을 참조하세요. <ph type="x-smartling-placeholder"></ph> Leanback 샘플 앱 에서 자세한 내용을 확인하실 수 있습니다.
헤더 숨기기 또는 사용 중지
스크롤 가능한 목록을 필요로 합니다. BrowseSupportFragment.setHeadersState()
호출
프래그먼트의 onActivityCreated()
도중 메서드를 호출합니다.
메서드를 사용하여 행 헤더를 숨기거나 사용 중지합니다. setHeadersState()
메서드는 다음 중 하나가 지정된 경우 탐색 프래그먼트에서 헤더의 초기 상태를 설정합니다.
상수를 매개변수로 사용하세요.
HEADERS_ENABLED
: 탐색 프래그먼트 활동이 만들어질 때 헤더가 사용 설정되고 기본값입니다. 헤더는 이 페이지의 그림 1 및 그림 2와 같이 표시됩니다.HEADERS_HIDDEN
: 탐색 프래그먼트 활동이 만들어지는 경우 헤더가 기본적으로 사용 설정되고 숨겨집니다. 화면의 헤더 섹션은 카드 뷰 제공에 수치를 표시합니다. 이 사용자는 접힌 헤더 섹션을 선택하여 펼칠 수 있습니다.HEADERS_DISABLED
: 탐색 프래그먼트 활동이 만들어질 때 헤더는 기본적으로 사용 중지되며 표시되지 않습니다.
HEADERS_ENABLED
또는 HEADERS_HIDDEN
가 설정된 경우 다음을 호출할 수 있습니다.
<ph type="x-smartling-placeholder">setHeadersTransitionOnBackEnabled()
</ph>
행의 선택된 콘텐츠 항목에서 행 헤더로 다시 이동하도록 지원합니다. 사용 설정:
메서드를 호출하지 않으면 기본값입니다. 등 동작을 직접 처리하려면
setHeadersTransitionOnBackEnabled()
에 false
전달
자체 백 스택 처리를 구현합니다.
미디어 목록 표시
BrowseSupportFragment
클래스를 사용하면
탐색 가능한 미디어 콘텐츠 카테고리와
어댑터와 프레젠터로 미디어 카탈로그 만들기 어댑터로 연결
로컬 또는 온라인 데이터 소스에 추가할 수 있습니다.
어댑터는 프레젠터를 사용하여 뷰를 만들고
화면에 항목을 표시합니다.
다음 코드 예는 문자열 데이터를 표시하기 위한 Presenter
의 구현을 보여줍니다.
Kotlin
private const val TAG = "StringPresenter" class StringPresenter : Presenter() { override fun onCreateViewHolder(parent: ViewGroup): Presenter.ViewHolder { val textView = TextView(parent.context).apply { isFocusable = true isFocusableInTouchMode = true background = parent.resources.getDrawable(R.drawable.text_bg) } return Presenter.ViewHolder(textView) } override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, item: Any) { (viewHolder.view as TextView).text = item.toString() } override fun onUnbindViewHolder(viewHolder: Presenter.ViewHolder) { // no op } }
자바
public class StringPresenter extends Presenter { private static final String TAG = "StringPresenter"; public ViewHolder onCreateViewHolder(ViewGroup parent) { TextView textView = new TextView(parent.getContext()); textView.setFocusable(true); textView.setFocusableInTouchMode(true); textView.setBackground( parent.getResources().getDrawable(R.drawable.text_bg)); return new ViewHolder(textView); } public void onBindViewHolder(ViewHolder viewHolder, Object item) { ((TextView) viewHolder.view).setText(item.toString()); } public void onUnbindViewHolder(ViewHolder viewHolder) { // no op } }
미디어 항목에 대한 프레젠터 클래스를 구성한 후에는
어댑터를 BrowseSupportFragment
에 연결
사용자가 탐색할 수 있도록 해당 항목을 화면에 표시합니다. 다음 예를 참고하세요.
카테고리와 항목을 표시하는 어댑터를 구성하는 방법을 보여주는 코드
StringPresenter
클래스를 사용하여 이러한 카테고리에서
이전 코드 예:
Kotlin
private const val NUM_ROWS = 4 ... private lateinit var rowsAdapter: ArrayObjectAdapter override fun onCreate(savedInstanceState: Bundle?) { ... buildRowsAdapter() } private fun buildRowsAdapter() { rowsAdapter = ArrayObjectAdapter(ListRowPresenter()) for (i in 0 until NUM_ROWS) { val listRowAdapter = ArrayObjectAdapter(StringPresenter()).apply { add("Media Item 1") add("Media Item 2") add("Media Item 3") } HeaderItem(i.toLong(), "Category $i").also { header -> rowsAdapter.add(ListRow(header, listRowAdapter)) } } browseSupportFragment.adapter = rowsAdapter }
자바
private ArrayObjectAdapter rowsAdapter; private static final int NUM_ROWS = 4; @Override protected void onCreate(Bundle savedInstanceState) { ... buildRowsAdapter(); } private void buildRowsAdapter() { rowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); for (int i = 0; i < NUM_ROWS; ++i) { ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter( new StringPresenter()); listRowAdapter.add("Media Item 1"); listRowAdapter.add("Media Item 2"); listRowAdapter.add("Media Item 3"); HeaderItem header = new HeaderItem(i, "Category " + i); rowsAdapter.add(new ListRow(header, listRowAdapter)); } browseSupportFragment.setAdapter(rowsAdapter); }
이 예시는 어댑터의 정적 구현을 보여줍니다. 일반적인 미디어 탐색 애플리케이션 온라인 데이터베이스 또는 웹 서비스의 데이터를 사용 예를 들어, 웹에서 검색한 데이터를 사용하는 경우 <ph type="x-smartling-placeholder"></ph> Leanback 샘플 앱 에서 자세한 내용을 확인하실 수 있습니다.
백그라운드 업데이트
TV의 미디어 탐색 앱에 시각적 흥미를 더하려면 배경을 업데이트하면 됩니다. 이미지 광고를 게재할 수 있습니다. 이 기법을 사용하면 앱과의 상호작용을 개선할 수 있습니다. 즐길 수 있습니다.
Leanback UI 도구 키트는 BackgroundManager
클래스를 사용합니다. 다음 예는 다음을 실행하는 방법을 보여줍니다.
TV 앱 활동 내에서 백그라운드를 업데이트하는 간단한 메서드를 만듭니다.
Kotlin
protected fun updateBackground(drawable: Drawable) { BackgroundManager.getInstance(this).drawable = drawable }
자바
protected void updateBackground(Drawable drawable) { BackgroundManager.getInstance(this).setDrawable(drawable); }
많은 미디어 브라우징 앱은 사용자가 탐색할 때 백그라운드를 자동으로 업데이트합니다.
찾을 수 있습니다. 이렇게 하려면 선택 리스너를 설정하여
사용자의 현재 선택에 따라 배경을 업데이트합니다. 다음 예는
다음과 같이 OnItemViewSelectedListener
클래스를 설정합니다.
선택 이벤트를 포착하고 백그라운드를 업데이트합니다.
Kotlin
protected fun clearBackground() { BackgroundManager.getInstance(this).drawable = defaultBackground } protected fun getDefaultItemViewSelectedListener(): OnItemViewSelectedListener = OnItemViewSelectedListener { _, item, _, _ -> if (item is Movie) { item.getBackdropDrawable().also { background -> updateBackground(background) } } else { clearBackground() } }
자바
protected void clearBackground() { BackgroundManager.getInstance(this).setDrawable(defaultBackground); } protected OnItemViewSelectedListener getDefaultItemViewSelectedListener() { return new OnItemViewSelectedListener() { @Override public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { if (item instanceof Movie ) { Drawable background = ((Movie)item).getBackdropDrawable(); updateBackground(background); } else { clearBackground(); } } }; }
참고: 이전 구현은 간단한 예로 삽화 자체 앱에서 이 함수를 만들 때는 다음을 실행합니다. 백그라운드 업데이트 작업을 별도로 실행해야 합니다. 또한 사용자가 항목을 스크롤하는 것에 대한 응답으로 백그라운드를 업데이트하고, 사용자가 상품에 착수할 때까지 배경 이미지 업데이트를 지연시키는 시간. 이 기법을 사용하면 과도한 배경 이미지 업데이트.