Katalog tarayıcı oluşturma

Compose ile daha iyi hizmet verin
Android TV OS için Jetpack Compose'u kullanarak minimum kodla etkileyici kullanıcı arayüzleri oluşturun.

TV'de çalışan bir medya uygulamasının, kullanıcıların sunduğu içeriklere göz atmasına, seçim yapmasına ve içeriği oynatmaya başlamasına izin vermesi gerekir. İçeriğe göz atma 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 amacıyla androidx.leanback kitaplığı tarafından sağlanan sınıfların nasıl kullanılacağı anlatılmaktadır.

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

Uygulama ana ekranı

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

Medyaya göz atma düzeni oluştur

Leanback kullanıcı arayüzü araç setindeki BrowseSupportFragment sınıfı, minimum düzeyde 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, aşağıdaki örnekte gösterildiği gibi bu görünümü 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. Ayrıca simge, başlık ve kategori başlıklarının etkinleştirilip etkinleştirilmediği gibi düzen parametrelerini 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. Ü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ın stilini belirlemek 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, göz atma parçasının sağ üst köşesindeki başlık dizesini 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ğerine ayarlar.
  • setSearchAffordanceColor(), arama simgesinin rengini, belirtilen renk değeriyle ayarlar. Ş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 bölümü, metin görünümlerinde, video veritabanındaki satır başlıkları olan video kategorisi adlarını görüntüler. 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 görüntüleyen resim görünümünün nasıl ekleneceği gösterilmektedir.

Uygulama ana ekranı

Şekil 2. Göz atma parçasındaki, hem simge hem de metin etiketiyle birlikte yer alan 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>

Görünüm sahibini oluşturmak, bağlamak ve bağlantısını kaldırmak için Presenter kullanın ve soyut yöntemleri uygulayın. Aşağıdaki örnekte, iki görünümle (ImageView ve TextView) görünüm sahibini nasıl bağlayacağınız 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 kaydırarak aralarından geçiş yapabilmesi için başlıklarınızın odaklanılabilir olması gerekir. Bunu yönetmenin iki yolu vardır:

  • onBindViewHolder() ürününde görünümünüzü odaklanılabilir olacak ş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österen 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 göz atın.

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

Kaydırılabilir bir liste gerektirecek kadar kategori olmaması gibi bazı durumlarda satır başlıklarının görünmesini istemezsiniz. Satır başlıklarını 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, parametre olarak aşağıdaki sabit değerlerden biri verildiğinde göz atma parçasındaki üstbilgilerin ilk durumunu ayarlar:

  • HEADERS_ENABLED: Göz atma parçası etkinliği oluşturulduğunda üst bilgiler etkinleştirilir ve varsayılan olarak gösterilir. Üstbilgiler, bu sayfadaki şekil 1 ve 2'de gösterildiği gibidir.
  • 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 bir şekilde gösterildiği gibi daraltılmıştı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 üst bilgiler varsayılan olarak devre dışı bırakılır ve hiçbir zaman görüntülenmez.

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

Medya listelerini görüntüle

BrowseSupportFragment sınıfı, bağdaştırıcıları ve sunucuları kullanarak bir medya kataloğundaki göz atılabilir medya içeriği kategorilerini ve medya öğelerini tanımlamanıza ve 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. Bağdaştırıcılar, görünüm oluşturmak ve ekranda bir öğe görüntülemek amacıyla 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 bir bağdaştırıcı oluşturabilir ve kullanıcının göz atabilmesi için bu öğeleri ekranda görüntülemek üzere bağdaştırıcıyı BrowseSupportFragment özelliğine bağlayabilirsiniz. Aşağıdaki örnek kod, önceki kod örneğinde gösterilen StringPresenter sınıfını kullanarak bu kategorilerdeki kategorileri ve öğeleri görüntülemek için bağdaştırıcının nasıl oluşturulacağını göstermektedir:

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ı, online 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 göz atın.

Arka planı güncelle

TV'deki bir medya tarama uygulamasına görsel ilgi eklemek için kullanıcılar içeriklere göz atarken arka plan resmini güncelleyebilirsiniz. Bu teknik, uygulamanızla etkileşimi daha sinematik ve eğlenceli 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ğ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 o anki 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 nasıl OnItemViewSelectedListener sınıfı 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. Kullanıcıların öğeler arasında gezinmesine yanıt olarak arka planı güncellemeyi planlıyorsanız, kullanıcı bir öğeye karar verene kadar arka plan resminin güncellenmesini geciktirecek bir süre ekleyin. Bu teknik, arka plan resminin çok fazla güncellenmesini önler.