Katalog tarayıcı oluşturma

Compose ile daha iyi uygulamalar geliştirin
Android TV OS için Jetpack Compose'u kullanarak minimum kodla güzel kullanıcı arayüzleri oluşturun.

TV'de çalışan bir medya uygulaması, kullanıcıların içerik tekliflerine göz atmasına, seçim yapmasına ve içeriği oynatmaya başlamasına olanak tanımalıdır. İçeriklere göz atma deneyimi basit ve sezgisel olmasının yanı sıra görsel olarak hoş ve ilgi çekici olmalıdır.

Bu kılavuzda, uygulamanızın medya kataloğundaki müziklere veya videolara göz atmak için kullanıcı arayüzü oluşturmak üzere kullanımdan kaldırılan androidx.leanback kitaplığı tarafından sağlanan sınıfların nasıl kullanılacağı açıklanmaktadır.

Not: Burada gösterilen uygulama örneğinde, kullanımdan kaldırılan BrowseFragment sınıfı yerine BrowseSupportFragment sınıfı kullanılmaktadır. BrowseSupportFragment, AndroidX Fragment sınıfını genişletir ve cihazlar ile Android sürümleri arasında tutarlı davranışlar sağlanmasına yardımcı olur.

Uygulamanın ana ekranı

1.şekil Leanback örnek uygulamasının göz atma parçası, video kataloğu verilerini gösterir.

Medya tarama düzeni oluşturma

Leanback kullanıcı arayüzü araç setindeki BrowseSupportFragment sınıfı, minimum kodla kategorilere ve medya öğesi satırlarına 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>

Uygulamanın ana etkinliği bu görünümü ayarlar. Aşağıdaki örnekte gösterildiği gibi:

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. Ayrıca simge ve başlık gibi düzen parametrelerini ve kategori başlıklarının etkinleştirilip etkinleştirilmeyeceğini ayarlar.

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

BrowseSupportFragment yöntemlerini uygulayan uygulamanın alt sınıfı, aşağıdaki örnekte gösterildiği gibi kullanıcı arayüzü öğelerindeki kullanıcı işlemleri için etkinlik dinleyicileri 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, özel yöntem setupUIElements(), medya kataloğu tarayıcısını stilize etmek için çeşitli BrowseSupportFragment yöntemlerini çağırır:

  • setBadgeDrawable() belirtilen çizilebilir kaynağı, 1 ve 2 numaralı şekillerde 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 çizilebilir kaynakla değiştirir. Çizilebilir kaynak 52 dp yüksekliğinde olmalıdır.
  • setTitle() setBadgeDrawable() çağrılmadığı sürece, göz atma parçasının sağ üst köşesindeki başlık dizesini ayarlar.
  • setHeadersState() ve setHeadersTransitionOnBackEnabled() üstbilgileri gizler veya devre dışı bırakır. Daha fazla bilgi için Başlıkları gizleme veya devre dışı bırakma bölümünü inceleyin.
  • setBrandColor() göz atma parçasındaki kullanıcı arayüzü öğelerinin arka plan rengini, özellikle de başlık bölümünün arka plan rengini belirtilen renk değeriyle ayarlar.
  • setSearchAffordanceColor() arama simgesinin rengini belirtilen renk değeriyle ayarlar. Arama simgesi, Şekil 1 ve 2'de gösterildiği gibi göz atma parçasının sol üst köşesinde 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österir. Daha karmaşık bir düzende ek görünümler eklemek için başlığı da özelleştirebilirsiniz. Aşağıdaki bölümlerde, Şekil 2'de gösterildiği gibi kategori adının yanında bir simge gösteren resim görünümünün nasıl ekleneceği açıklanmaktadır.

Uygulamanın ana ekranı

Şekil 2. Göz atma parçasındaki hem simge hem de metin etiketi içeren satır başlıkları.

Satır başlığının düzeni şu şekilde 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>

Presenter kullanın ve görünüm tutucuyu oluşturmak, bağlamak ve bağlantısını kaldırmak için soyut yöntemleri uygulayın. Aşağıdaki örnekte, görünüm tutucunun iki görünümle (ImageView ve TextView) 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
    }
}

Başlıklarınız, D-pad ile kaydırılabilmesi için odaklanılabilir olmalıdır. Bunu yönetmenin iki yolu vardır:

  • Görünümünüzü onBindViewHolder() içinde odaklanılabilir olarak 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 ş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, BrowseSupportFragment katalog tarayıcısını gösteren uygulamada, aşağıdaki örnekte gösterildiği gibi setHeaderPresenterSelector() yöntemini kullanarak satır başlığı için sunan kişiyi ayarlayı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.

Başlıkları gizleme veya devre dışı bırakma

Bazen satır başlıklarının görünmesini istemeyebilirsiniz. Örneğin, kaydırılabilir bir liste gerektirecek kadar kategori yoktur. Satır başlıklarını gizlemek veya devre dışı bırakmak için parçanın BrowseSupportFragment.setHeadersState() yöntemi sırasında onActivityCreated() yöntemini çağırın. setHeadersState() yöntemi, parametre olarak aşağıdaki sabitlerden biri 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 varsayılan olarak etkinleştirilir ve gösterilir. Üstbilgiler, 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 üstbilgi bölümü, Kart görünümü sağlama bölümündeki şekilde gösterildiği gibi daraltılı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() işlevini çağırabilirsiniz. Yöntemi çağırmazsanız bu özellik varsayılan olarak etkindir. Geri hareketini kendiniz yönetmek için false öğesini setHeadersTransitionOnBackEnabled() öğesine iletin ve kendi geri yığın işleme mantığınızı uygulayın.

Görüntülü reklam medya listeleri

BrowseSupportFragment sınıfı, uyarlayıcılar ve sunucular kullanarak bir medya kataloğundaki göz atılabilir medya içeriği kategorilerini ve medya öğelerini tanımlayıp görüntülemenize olanak tanır. Adaptörler, medya kataloğu bilgilerinizi içeren yerel veya online veri kaynaklarına bağlanmanıza olanak tanır. Adaptörler, görünümler oluşturmak ve ekranda bir öğe göstermek için verileri bu görünümlere bağlamak üzere sunucuları kullanır.

Aşağıdaki örnek kodda, dize verilerini görüntülemek için Presenter uygulaması gösterilmektedir:

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 bir sunucu sınıfı oluşturduktan sonra, bir bağdaştırıcı oluşturup BrowseSupportFragment öğesine ekleyerek bu öğeleri ekranda kullanıcı tarafından göz atılacak şekilde görüntüleyebilirsiniz. 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 bir bağdaştırıcı oluşturma işlemi 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 bir uygulaması gösterilmektedir. Tipik bir medya tarama uygulaması, online bir veri tabanından veya web hizmetinden alınan verileri kullanır. Web'den alınan verileri kullanan bir göz atma uygulaması örneği için Leanback örnek uygulamasına bakın.

Arka planı güncelleme

TV'de medya tarama uygulamasına görsel ilgi çekicilik katmak için kullanıcılar içeriklere göz atarken arka plan resmini güncelleyebilirsiniz. Bu teknik, uygulamanızla etkileşimi daha sinematik ve keyifli hale getirebilir.

Leanback kullanıcı arayüzü araç seti, TV uygulaması etkinliğinizin arka planını değiştirmek için BackgroundManager sınıfı sağlar. Aşağıdaki örnekte, TV uygulaması etkinliğinizdeki 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);
}

Medya tarama uygulamalarının çoğu, kullanıcı medya listelerinde gezinirken arka planı otomatik olarak günceller. Bunu yapmak için, kullanıcının mevcut seçimine göre arka planı otomatik olarak güncellemek üzere bir seçim dinleyicisi ayarlayabilirsiniz. Aşağıdaki örnekte, seçim etkinliklerini yakalamak ve arka planı güncellemek için OnItemViewSelectedListener sınıfının nasıl ayarlanacağı 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, yalnızca örnek 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, kullanıcıların öğeler arasında gezinmesine yanıt olarak arka planı güncellemeyi planlıyorsanız kullanıcı bir öğeyi seçene kadar arka plan resmi güncellemesini geciktirecek bir süre ekleyin. Bu teknik, arka plan resimlerinin aşırı güncellenmesini önler.