Katalog tarayıcı oluşturma

TV'de çalışan bir medya uygulamasının, kullanıcıların içerik tekliflerine göz atmasına, seçim yapmasına ve içerik oynatmaya başlamasına izin vermesi gerekir. İçerik tarama deneyimi basit ve sezgisel olmanın yanı sıra görsel açıdan ilgi çekici ve ilgi çekici olmalıdır.

Bu kılavuzda, uygulamanızın medya kataloğundaki müziklere veya videolara göz atmak için bir kullanıcı arayüzü uygulamak üzere Leanback androidx kitaplığı tarafından sağlanan sınıfların nasıl kullanılacağı anlatılmaktadır.

Not: Burada gösterilen uygulama örneğinde, kullanımdan kaldırılan BrowseFragment sınıfı yerine BrowseSupportFragment kullanılmaktadır. BrowseSupportFragment, AndroidX Fragment sınıfını genişleterek farklı cihazlarda ve Android sürümlerinde tutarlı davranış sağlamaya yardımcı olur.

Uygulama ana ekranı

Şekil 1. Leanback örnek uygulamasının göz atma parçası, video katalog verilerini görüntüler.

Medyaya göz atma düzeni oluşturma

Leanback kitaplığındaki BrowseSupportFragment sınıfı, minimum kod kullanarak medya öğesi satırlarına ve kategorilerine göz atmak için birincil düzen oluşturmanıza olanak tanır. Aşağıdaki örnekte, BrowseSupportFragment nesnesi içeren bir düzenin nasıl oluşturulacağı gösterilmektedir:

<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>

Bu görünümü, aşağıdaki örnekte gösterildiği gibi, uygulamanın ana etkinliği ayarlar:

Kotlin

class MainActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main)
    }
...

Java

public class MainActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
...

BrowseSupportFragment yöntemleri, görünümü video verileri ve kullanıcı arayüzü öğeleriyle doldurur ve simge, başlık gibi düzen parametrelerini ve kategori başlıklarının etkin olup olmadığını belirler.

Kullanıcı arayüzü öğelerini ayarlama hakkında daha fazla bilgi için Kullanıcı arayüzü öğeleri ayarlama bölümüne bakın. Üstbilgileri gizleme hakkında daha fazla bilgi için Üstbilgileri gizleme veya devre dışı bırakma bölümüne bakın.

Uygulamanın BrowseSupportFragment yöntemlerini uygulayan alt sınıfı, aşağıdaki örnekte gösterildiği gibi, kullanıcı arayüzü öğelerindeki kullanıcı işlemleri için etkinlik işleyiciler de ayarlar ve arka plan yöneticisini hazırlar:

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

Java

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

Kullanıcı arayüzü öğelerini ayarlama

Önceki örnekte, setupUIElements() gizli yöntemi medya kataloğu tarayıcısını biçimlendirmek için birkaç BrowseSupportFragment yöntemini çağırır:

  • setBadgeDrawable(), belirtilen çekilebilir kaynağı şekil 1 ve 2'de gösterildiği gibi göz atma parçasının sağ üst köşesine yerleştirir. Bu yöntem, setTitle() de çağrılırsa başlık dizesini çekilebilir kaynakla değiştirir. Çekilebilir kaynağın yüksekliği 52 dp olmalıdır.
  • setTitle(), setBadgeDrawable() çağrılmadığı sürece başlık dizesini göz atma parçasının sağ üst köşesinde ayarlar.
  • setHeadersState() ve setHeadersTransitionOnBackEnabled(), üst bilgileri gizler veya devre dışı bırakır. Daha fazla bilgi için Üstbilgileri gizleme veya devre dışı bırakma bölümüne bakın.
  • setBrandColor(), göz atma parçasındaki kullanıcı arayüzü öğelerinin arka plan rengini, özellikle de başlık bölümü arka plan rengini belirtilen renk değeriyle ayarlar.
  • setSearchAffordanceColor(), arama simgesinin rengini belirtilen renk değeriyle belirler. Şekil 1 ve 2'de gösterildiği gibi, göz atma parçasının sol üst köşesinde arama simgesi görünür.

Başlık görünümlerini özelleştirme

Şekil 1'de gösterilen göz atma parçası, video veritabanındaki satır başlıkları olan video kategorisi adlarını metin görünümlerinde göstermektedir. Ayrıca başlığı, daha karmaşık bir düzende ek görünümler içerecek şekilde özelleştirebilirsiniz. Aşağıdaki bölümlerde, Şekil 2'de gösterildiği gibi, kategori adının yanında bir simge bulunan bir resim görünümünün nasıl ekleneceği gösterilmektedir.

Uygulama ana ekranı

2. Şekil. Göz atma parçasındaki hem bir simge hem de metin etiketine sahip satır başlıkları.

Satır başlığının düzeni aşağıdaki gibi tanımlanır:

<?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>

Görünüm sahibini oluşturmak, bağlamak ve bağlantısını kaldırmak için bir Presenter kullanın ve soyut yöntemleri uygulayın. Aşağıdaki örnekte, görüntüleme sahibinin ImageView ve TextView olmak üzere iki görünümle nasıl bağlanacağı gösterilmektedir.

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
    }
}

Java

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-pad'in gezinmek için kullanılabilmesi için başlıklarınıza odaklanabilirsiniz. Bunu yönetmenin iki yolu vardır:

  • onBindViewHolder() ürününde görünümünüzü odaklanılabilir şekilde ayarlayın:

    Kotlin

    override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, o: Any) {
        val headerItem = (o as ListRow).headerItem
        val rootView = viewHolder.view
    
        rootView.focusable = View.FOCUSABLE
        // ...
    }
    

    Java

    @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
        // ...
    }
    
  • Düzeninizi odaklanılabilir olacak şekilde ayarlayın:
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       ...
       android:focusable="true">

Son olarak, katalog tarayıcısını görüntüleyen BrowseSupportFragment uygulamasında, aşağıdaki örnekte gösterildiği gibi, satır başlığı için sunucuyu ayarlamak üzere setHeaderPresenterSelector() yöntemini kullanın.

Kotlin

setHeaderPresenterSelector(object : PresenterSelector() {
    override fun getPresenter(o: Any): Presenter {
        return IconHeaderItemPresenter()
    }
})

Java

setHeaderPresenterSelector(new PresenterSelector() {
    @Override
    public Presenter getPresenter(Object o) {
        return new IconHeaderItemPresenter();
    }
});

Tam bir örnek için Leanback örnek uygulamasına bakın.

Üstbilgileri gizleme veya devre dışı bırakma

Kaydırılabilir bir liste gerektirecek kadar kategorinin olmaması gibi bazı durumlarda satır başlıklarının görünmesini istemezsiniz. Satır üst bilgilerini gizlemek veya devre dışı bırakmak için parçanın onActivityCreated() yöntemi sırasında BrowseSupportFragment.setHeadersState() yöntemini çağırın. setHeadersState() yöntemi, aşağıdaki sabitlerden biri parametre olarak verildiğinde göz atma parçasındaki başlıkların başlangıç durumunu ayarlar:

  • HEADERS_ENABLED: Göz atma parçası etkinliği oluşturulduğunda başlıklar etkinleştirilir ve varsayılan olarak gösterilir. Başlıklar, bu sayfadaki şekil 1 ve 2'de gösterildiği gibi görünür.
  • HEADERS_HIDDEN: Göz atma parçası etkinliği oluşturulduğunda başlıklar varsayılan olarak etkinleştirilir ve gizlenir. Ekranın başlık bölümü Kart görünümü sağlama bölümündeki şekilde gösterildiği gibi daraltılmış durumdadır. Kullanıcı, daraltılmış başlık bölümünü seçerek genişletebilir.
  • HEADERS_DISABLED: Göz atma parçası etkinliği oluşturulduğunda başlıklar varsayılan olarak devre dışı bırakılır ve hiçbir zaman gösterilmez.

HEADERS_ENABLED veya HEADERS_HIDDEN ayarlanmışsa satırdaki seçili bir içerik öğesinden satır başlığına geri dönmeyi desteklemek için setHeadersTransitionOnBackEnabled() çağrısını çağırabilirsiniz. Yöntemi çağırmazsanız bu özellik varsayılan olarak etkinleştirilir. Geri hareketini kendiniz yönetmek için false öğesini setHeadersTransitionOnBackEnabled() bölümüne iletin ve kendi arka yığın işlemenizi uygulayın.

Görüntülü reklam medya listeleri

BrowseSupportFragment sınıfı, bağdaştırıcıları ve sunucuları kullanarak bir medya kataloğundan göz atılabilir medya içeriği kategorilerini ve medya öğelerini tanımlayıp görüntülemenize olanak tanır. Bağdaştırıcılar, medya katalog bilgilerinizi içeren yerel veya çevrimiçi veri kaynaklarına bağlanabilmenizi sağlar. Bağdaştırıcılar, görünümler oluşturmak ve bir öğeyi ekranda görüntülemek üzere verileri bu görünümlere bağlamak için sunucuları kullanır.

Aşağıdaki örnek kod, dize verilerini görüntülemek için bir Presenter uygulamasını göstermektedir:

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
    }
}

Java

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
    }
}

Medya öğeleriniz için sunucu sınıfı oluşturduktan sonra bağdaştırıcı oluşturup bu öğeleri ekranda kullanıcının göz atması için görüntülemek üzere BrowseSupportFragment öğesine ekleyebilirsiniz. Aşağıdaki örnek kodda, önceki kod örneğinde gösterilen StringPresenter sınıfını kullanarak kategorileri ve bu kategorilerdeki öğeleri görüntülemek için nasıl bağdaştırıcı oluşturulacağı gösterilmektedir:

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
}

Java

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

Bu örnekte, bağdaştırıcıların statik uygulaması gösterilmektedir. Tipik bir medya tarama uygulaması, çevrimiçi bir veritabanından veya web hizmetinden alınan verileri kullanır. Web'den alınan verileri kullanan bir tarama uygulaması örneği için Leanback örnek uygulamasına bakın.

Arka planı güncelle

TV'deki bir medya tarama uygulamasına görsel açıdan ilgi eklemek için kullanıcılar içeriğe göz atarken arka plan resmini güncelleyebilirsiniz. Bu teknik, uygulamanızla etkileşimi daha sinematik ve keyifli hale getirebilir.

Leanback destek kitaplığı, TV uygulaması etkinliğinizin arka planını değiştirmek için bir BackgroundManager sınıfı sağlar. Aşağıdaki örnekte, TV uygulama etkinliğinizde arka planı güncellemek için nasıl basit bir yöntem oluşturulacağı gösterilmektedir:

Kotlin

protected fun updateBackground(drawable: Drawable) {
    BackgroundManager.getInstance(this).drawable = drawable
}

Java

protected void updateBackground(Drawable drawable) {
    BackgroundManager.getInstance(this).setDrawable(drawable);
}

Birçok medya tarama uygulaması, kullanıcı medya listelerinde gezinirken arka planı otomatik olarak günceller. Bunu yapmak için arka planı kullanıcının geçerli seçimine göre otomatik olarak güncelleyecek bir seçim işleyici ayarlayabilirsiniz. Aşağıdaki örnekte, seçim etkinliklerini yakalamak ve arka planı güncellemek için OnItemViewSelectedListener sınıfının nasıl oluşturulacağı gösterilmektedir:

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

Java

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

Not: Önceki uygulama, açıklama amaçlı basit bir örnektir. Bu işlevi kendi uygulamanızda oluştururken daha iyi performans için arka plan güncelleme işlemini ayrı bir iş parçacığında çalıştırın. Ayrıca, öğeler arasında gezinen kullanıcılara yanıt olarak arka planı güncellemeyi planlıyorsanız arka plan resmi güncellemesini kullanıcı bir öğe üzerinde çalışmaya başlayana kadar ertelemek için bir süre ekleyin. Bu teknik, aşırı sayıda arka plan resmi güncellemesinin önüne geçer.