카탈로그 브라우저 만들기

Compose를 사용하여 더 효과적으로 빌드
Android TV OS용 Jetpack Compose를 사용하여 최소한의 코드로 멋진 UI를 만드세요.
<ph type="x-smartling-placeholder"></ph> TV용 Compose → 를 통해 개인정보처리방침을 정의할 수 있습니다.

TV에서 실행되는 미디어 앱은 사용자가 콘텐츠 서비스를 탐색하고 콘텐츠 재생을 시작합니다. 콘텐츠 탐색 환경 시각적으로 즐겁고 눈길을 끌 수 있을 뿐만 아니라 단순하고 직관적이어야 합니다.

이 가이드에서는 androidx.leanback 라이브러리에서 제공하는 클래스를 사용하는 방법을 설명합니다. 앱의 미디어 카탈로그에서 음악이나 동영상을 탐색하기 위한 사용자 인터페이스를 구현할 수 있습니다.

참고: 여기에 표시된 구현 예에서는 <ph type="x-smartling-placeholder">BrowseSupportFragment</ph> 지원 중단된 BrowseFragment가 아님 클래스에 대해 자세히 알아보세요. BrowseSupportFragmentAndroidX를 확장합니다. Fragment 클래스, 여러 기기와 Android 버전에서 일관된 동작을 보장하도록 지원합니다.

앱 기본 화면

그림 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 요소 등의 레이아웃 매개변수를 카테고리 헤더 사용 설정 여부입니다.

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와 같이 카테고리 이름 옆에 아이콘을 표시하는 이미지 보기를 포함해야 합니다.

앱 기본 화면

그림 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를 사용하여 뷰 홀더를 만들고 바인딩하고 바인딩 해제하는 추상 메서드를 지원합니다. 다음 예를 들어 뷰 홀더를 두 개의 뷰, 즉 ImageViewTextView

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();
            }
        }
    };
}

참고: 이전 구현은 간단한 예로 삽화 자체 앱에서 이 함수를 만들 때는 다음을 실행합니다. 백그라운드 업데이트 작업을 별도로 실행해야 합니다. 또한 사용자가 항목을 스크롤하는 것에 대한 응답으로 백그라운드를 업데이트하고, 사용자가 상품에 착수할 때까지 배경 이미지 업데이트를 지연시키는 시간. 이 기법을 사용하면 과도한 배경 이미지 업데이트.