HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-02-21
1 <p><a>#Руководства</a></p>
1 <p><a>#Руководства</a></p>
2 <ul><li>22 окт 2019</li>
2 <ul><li>22 окт 2019</li>
3 <li>0</li>
3 <li>0</li>
4 </ul><p>Продолжаем разбираться с Leanback. Сегодня создадим карточки контента, которые можно использовать, например, в списке элементов.</p>
4 </ul><p>Продолжаем разбираться с Leanback. Сегодня создадим карточки контента, которые можно использовать, например, в списке элементов.</p>
5 <p> vlada_maestro / shutterstock</p>
5 <p> vlada_maestro / shutterstock</p>
6 <p>Android-разработчик в аутсорс-продакшене FINCH. Программирует на Kotlin и Java. Любит кодить и отдыхать на пляже.</p>
6 <p>Android-разработчик в аутсорс-продакшене FINCH. Программирует на Kotlin и Java. Любит кодить и отдыхать на пляже.</p>
7 <p><strong><strong>об авторе</strong></strong></p>
7 <p><strong><strong>об авторе</strong></strong></p>
8 <p>Android-разработчик в аутсорс-продакшне<a>FINCH</a>. Программирует на Kotlin и Java. Любит кодить и отдыхать на пляже.</p>
8 <p>Android-разработчик в аутсорс-продакшне<a>FINCH</a>. Программирует на Kotlin и Java. Любит кодить и отдыхать на пляже.</p>
9 <p>* Мнение автора может не совпадать с мнением редакции</p>
9 <p>* Мнение автора может не совпадать с мнением редакции</p>
10 <p>В предыдущей статье я рассказал о том, как<a>создавать главный экран приложения</a>со списком элементов. Тогда каждый элемент отрисовывался при помощи своей уникальной карточки. В этот раз я расскажу о том, как создаются эти карточки и какие инструменты работы доступны "из коробки".</p>
10 <p>В предыдущей статье я рассказал о том, как<a>создавать главный экран приложения</a>со списком элементов. Тогда каждый элемент отрисовывался при помощи своей уникальной карточки. В этот раз я расскажу о том, как создаются эти карточки и какие инструменты работы доступны "из коробки".</p>
11 <p>Допустим, мы хотим создать раздел приложения, где карточки будут выглядеть следующим образом:</p>
11 <p>Допустим, мы хотим создать раздел приложения, где карточки будут выглядеть следующим образом:</p>
12 <p>В Leanback уже есть готовая карточка, которую можно использовать в своем приложении, - это<strong>ImageCardView</strong>. Эта карточка - подкласс<em>BaseCardView</em>, которая достаточно гибко настраивается. Рассмотрим, из чего она состоит:</p>
12 <p>В Leanback уже есть готовая карточка, которую можно использовать в своем приложении, - это<strong>ImageCardView</strong>. Эта карточка - подкласс<em>BaseCardView</em>, которая достаточно гибко настраивается. Рассмотрим, из чего она состоит:</p>
13 <ol><li><strong>ImageView</strong> - основная картинка.</li>
13 <ol><li><strong>ImageView</strong> - основная картинка.</li>
14 <li><strong>ViewGroup</strong><em>(infoArea)</em>- контейнер для дополнительных элементов, таких как:</li>
14 <li><strong>ViewGroup</strong><em>(infoArea)</em>- контейнер для дополнительных элементов, таких как:</li>
15 </ol><ul><li>ImageView - иконка;</li>
15 </ol><ul><li>ImageView - иконка;</li>
16 <li>TextView - заголовок;</li>
16 <li>TextView - заголовок;</li>
17 <li>TextView - описание.</li>
17 <li>TextView - описание.</li>
18 </ul><p>Попробуем создать нашу карточку. Для начала сделаем элемент контента и поместим в него параметры для отрисовки:</p>
18 </ul><p>Попробуем создать нашу карточку. Для начала сделаем элемент контента и поместим в него параметры для отрисовки:</p>
19 data class LeanbackCardItem( val title: String, val content: String, @DrawableRes val image: Int, @DrawableRes val badgeIcon: Int )<p>Затем создадим<strong>Presenter</strong>, который будет связывать данные из <em>LeanbackCardItem</em>c <em>ImageCardView</em>. Presenter нужен для отрисовки элементов контента, которые были переданы в адаптер - он связывает каждый элемент контента с UI-отображением, то есть карточкой контента.</p>
19 data class LeanbackCardItem( val title: String, val content: String, @DrawableRes val image: Int, @DrawableRes val badgeIcon: Int )<p>Затем создадим<strong>Presenter</strong>, который будет связывать данные из <em>LeanbackCardItem</em>c <em>ImageCardView</em>. Presenter нужен для отрисовки элементов контента, которые были переданы в адаптер - он связывает каждый элемент контента с UI-отображением, то есть карточкой контента.</p>
20 class LeanbackCardPresenter : Presenter() { override fun onCreateViewHolder(viewGroup: ViewGroup): Presenter.ViewHolder = ViewHolder(createView(viewGroup)) private fun createView(viewGroup: ViewGroup) = viewGroup .context .let { context -&gt; ImageCardView(context) .apply { isFocusable = true isFocusableInTouchMode = true setMainImageDimensions( context.getDimensionPixelSizeRes(R.dimen.card_width), context.getDimensionPixelSizeRes(R.dimen.card_height) ) } } override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, item: Any) = (viewHolder as ViewHolder) .bind(item as LeanbackCardItem) override fun onUnbindViewHolder(viewHolder: Presenter.ViewHolder) = (viewHolder as ViewHolder) .unbind() private inner class ViewHolder(view: ImageCardView) : Presenter.ViewHolder(view) { private var requestManager: RequestManager? = null private val imageTarget = ImageCardViewTarget(view) fun bind(item: LeanbackCardItem) = with(view as ImageCardView) { item.run { requestManager = Glide.with(view) requestManager ?.load(image) ?.into(imageTarget) titleText = title contentText = content badgeImage = context.getDrawable(badgeIcon) } } fun unbind() = with(view as ImageCardView) { requestManager?.clear(imageTarget) mainImage = null badgeImage = null titleText = "" contentDescription = "" } } }<p>Каждый Presenter требует реализовать 3 метода:</p>
20 class LeanbackCardPresenter : Presenter() { override fun onCreateViewHolder(viewGroup: ViewGroup): Presenter.ViewHolder = ViewHolder(createView(viewGroup)) private fun createView(viewGroup: ViewGroup) = viewGroup .context .let { context -&gt; ImageCardView(context) .apply { isFocusable = true isFocusableInTouchMode = true setMainImageDimensions( context.getDimensionPixelSizeRes(R.dimen.card_width), context.getDimensionPixelSizeRes(R.dimen.card_height) ) } } override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, item: Any) = (viewHolder as ViewHolder) .bind(item as LeanbackCardItem) override fun onUnbindViewHolder(viewHolder: Presenter.ViewHolder) = (viewHolder as ViewHolder) .unbind() private inner class ViewHolder(view: ImageCardView) : Presenter.ViewHolder(view) { private var requestManager: RequestManager? = null private val imageTarget = ImageCardViewTarget(view) fun bind(item: LeanbackCardItem) = with(view as ImageCardView) { item.run { requestManager = Glide.with(view) requestManager ?.load(image) ?.into(imageTarget) titleText = title contentText = content badgeImage = context.getDrawable(badgeIcon) } } fun unbind() = with(view as ImageCardView) { requestManager?.clear(imageTarget) mainImage = null badgeImage = null titleText = "" contentDescription = "" } } }<p>Каждый Presenter требует реализовать 3 метода:</p>
21 <ol><li><strong>fun onCreateViewHolder (viewGroup: ViewGroup)</strong>: Presenter.ViewHolder - создание ViewHolder, смысл которого - в переиспользовании уже созданной View. Подобное происходит с ViewHolder адаптера RecyclerView.</li>
21 <ol><li><strong>fun onCreateViewHolder (viewGroup: ViewGroup)</strong>: Presenter.ViewHolder - создание ViewHolder, смысл которого - в переиспользовании уже созданной View. Подобное происходит с ViewHolder адаптера RecyclerView.</li>
22 <li><strong>fun onBindViewHolder (viewHolder: Presenter.ViewHolder, item: Any)</strong> - во время вызова этого метода необходимо связать данные определенного элемента с его UI-представлением.</li>
22 <li><strong>fun onBindViewHolder (viewHolder: Presenter.ViewHolder, item: Any)</strong> - во время вызова этого метода необходимо связать данные определенного элемента с его UI-представлением.</li>
23 <li><strong>fun onUnbindViewHolder (viewHolder: Presenter.ViewHolder)</strong> - отвязывание View от его элемента и очистка ресурсов, связанных с данным элементом.</li>
23 <li><strong>fun onUnbindViewHolder (viewHolder: Presenter.ViewHolder)</strong> - отвязывание View от его элемента и очистка ресурсов, связанных с данным элементом.</li>
24 </ol><p>У созданной<strong>ImageCardView</strong>установим два флага<em>isFocusable = true</em>и<em>isFocusableInTouchMode = true</em> - это необходимо для того, чтобы наша View получала фокус от контроллера управления.</p>
24 </ol><p>У созданной<strong>ImageCardView</strong>установим два флага<em>isFocusable = true</em>и<em>isFocusableInTouchMode = true</em> - это необходимо для того, чтобы наша View получала фокус от контроллера управления.</p>
25 <p>С помощью метода<strong>setMainImageDimensions (int width, int height)</strong>можно установить размер основной картинки. Также для упрощения работы, связанной с загрузкой изображений, мы создали таргет для<em>Glide ImageCardViewTarget,</em>который умеет работать с ImageCardView.</p>
25 <p>С помощью метода<strong>setMainImageDimensions (int width, int height)</strong>можно установить размер основной картинки. Также для упрощения работы, связанной с загрузкой изображений, мы создали таргет для<em>Glide ImageCardViewTarget,</em>который умеет работать с ImageCardView.</p>
26 class ImageCardViewTarget(view: ImageCardView) : CustomViewTarget&lt;ImageCardView, Drawable&gt;(view) { override fun onLoadFailed(errorDrawable: Drawable?) { view.mainImage = errorDrawable } override fun onResourceCleared(placeholder: Drawable?) { view.mainImage = placeholder } override fun onResourceReady( resource: Drawable, transition: Transition&lt;in Drawable&gt;? ) { view.mainImage = resource } }<p>Добавим код создания элементов и инициализации адаптеров во фрагмент:</p>
26 class ImageCardViewTarget(view: ImageCardView) : CustomViewTarget&lt;ImageCardView, Drawable&gt;(view) { override fun onLoadFailed(errorDrawable: Drawable?) { view.mainImage = errorDrawable } override fun onResourceCleared(placeholder: Drawable?) { view.mainImage = placeholder } override fun onResourceReady( resource: Drawable, transition: Transition&lt;in Drawable&gt;? ) { view.mainImage = resource } }<p>Добавим код создания элементов и инициализации адаптеров во фрагмент:</p>
27 class MainFragment : RowsSupportFragment() { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) initViewWithLeanbackCard(savedInstanceState) setOnItemViewClickedListener { _, _, _, _ -&gt; } } private fun initViewWithLeanbackCard(savedInstanceState: Bundle?) { val cardAdapter = ArrayObjectAdapter(LeanbackCardPresenter()) cardAdapter.addAll(0, getLeanbackCardItems()) setCardAdapter(cardAdapter) } private fun setCardAdapter(cardAdapter: ArrayObjectAdapter) { val header = HeaderItem("Finch") val rowPresenter = getRowPresenter() val rowsAdapter = ArrayObjectAdapter(rowPresenter) rowsAdapter.add(ListRow(header, cardAdapter)) adapter = rowsAdapter } private fun getRowPresenter() = ListRowPresenter() .apply { context?.let { context -&gt; headerPresenter = TitleHeaderPresenter( context.getDimensionRes(R.dimen.title_text_size), context.getDimensionPixelSizeRes(R.dimen.title_bottom_padding) ) } } private fun getLeanbackCardItems(): List&lt;LeanbackCardItem&gt; = listOf( LeanbackCardItem( title = "title 1", content = "content 1", image = R.drawable.image1, badgeIcon = R.drawable.app_icon_your_company ), LeanbackCardItem( title = "title 2", content = "content 2", image = R.drawable.image2, badgeIcon = R.drawable.app_icon_your_company ), LeanbackCardItem( title = "title 3", content = "content 3", image = R.drawable.image3, badgeIcon = R.drawable.app_icon_your_company ), LeanbackCardItem( title = "title 4", content = "content 4", image = R.drawable.image4, badgeIcon = R.drawable.app_icon_your_company ), LeanbackCardItem( title = "title 5", content = "content 5", image = R.drawable.image5, badgeIcon = R.drawable.app_icon_your_company ), LeanbackCardItem( title = "title 6", content = "content 6", image = R.drawable.image6, badgeIcon = R.drawable.app_icon_your_company ) ) }<p>Посмотрим на результат:</p>
27 class MainFragment : RowsSupportFragment() { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) initViewWithLeanbackCard(savedInstanceState) setOnItemViewClickedListener { _, _, _, _ -&gt; } } private fun initViewWithLeanbackCard(savedInstanceState: Bundle?) { val cardAdapter = ArrayObjectAdapter(LeanbackCardPresenter()) cardAdapter.addAll(0, getLeanbackCardItems()) setCardAdapter(cardAdapter) } private fun setCardAdapter(cardAdapter: ArrayObjectAdapter) { val header = HeaderItem("Finch") val rowPresenter = getRowPresenter() val rowsAdapter = ArrayObjectAdapter(rowPresenter) rowsAdapter.add(ListRow(header, cardAdapter)) adapter = rowsAdapter } private fun getRowPresenter() = ListRowPresenter() .apply { context?.let { context -&gt; headerPresenter = TitleHeaderPresenter( context.getDimensionRes(R.dimen.title_text_size), context.getDimensionPixelSizeRes(R.dimen.title_bottom_padding) ) } } private fun getLeanbackCardItems(): List&lt;LeanbackCardItem&gt; = listOf( LeanbackCardItem( title = "title 1", content = "content 1", image = R.drawable.image1, badgeIcon = R.drawable.app_icon_your_company ), LeanbackCardItem( title = "title 2", content = "content 2", image = R.drawable.image2, badgeIcon = R.drawable.app_icon_your_company ), LeanbackCardItem( title = "title 3", content = "content 3", image = R.drawable.image3, badgeIcon = R.drawable.app_icon_your_company ), LeanbackCardItem( title = "title 4", content = "content 4", image = R.drawable.image4, badgeIcon = R.drawable.app_icon_your_company ), LeanbackCardItem( title = "title 5", content = "content 5", image = R.drawable.image5, badgeIcon = R.drawable.app_icon_your_company ), LeanbackCardItem( title = "title 6", content = "content 6", image = R.drawable.image6, badgeIcon = R.drawable.app_icon_your_company ) ) }<p>Посмотрим на результат:</p>
28 <p>Близко, но нет. Карточки еще нужно доработать: иконка пока находится с правой стороны, а infoArea расположена на картинке и имеет селектор у выделенной карточки.</p>
28 <p>Близко, но нет. Карточки еще нужно доработать: иконка пока находится с правой стороны, а infoArea расположена на картинке и имеет селектор у выделенной карточки.</p>
29 <p>Управлять компонентами ImageCardView можно путем расширения стиля Widget.Leanback.ImageCardViewStyle и установки свойства lbImageCardViewType с одним из следующих поддерживаемых значений: Title, Content, IconOnRight, IconOnLeft, ImageOnly или их комбинацией.</p>
29 <p>Управлять компонентами ImageCardView можно путем расширения стиля Widget.Leanback.ImageCardViewStyle и установки свойства lbImageCardViewType с одним из следующих поддерживаемых значений: Title, Content, IconOnRight, IconOnLeft, ImageOnly или их комбинацией.</p>
30 <p>Создадим стиль ImageCardViewStyle и укажем для свойства lbImageCardViewType следующие значения IconOnLeft|Title|Content. Комбинация этих значений позволит нам разместить иконку с левой стороны от описания:</p>
30 <p>Создадим стиль ImageCardViewStyle и укажем для свойства lbImageCardViewType следующие значения IconOnLeft|Title|Content. Комбинация этих значений позволит нам разместить иконку с левой стороны от описания:</p>
31 &lt;style name="ImageCardViewStyle" parent="Widget.Leanback.ImageCardViewStyle"&gt; &lt;item name="lbImageCardViewType"&gt;IconOnLeft|Title|Content&lt;/item&gt; &lt;/style&gt;<p>Применить этот стиль к ImageCardView можно через основную тему приложения, установив ее в качестве значения imageCardViewStyle. В данном случае этот стиль будет применен ко всем карточкам приложения, но для большей гибкости создадим отдельную тему:</p>
31 &lt;style name="ImageCardViewStyle" parent="Widget.Leanback.ImageCardViewStyle"&gt; &lt;item name="lbImageCardViewType"&gt;IconOnLeft|Title|Content&lt;/item&gt; &lt;/style&gt;<p>Применить этот стиль к ImageCardView можно через основную тему приложения, установив ее в качестве значения imageCardViewStyle. В данном случае этот стиль будет применен ко всем карточкам приложения, но для большей гибкости создадим отдельную тему:</p>
32 &lt;style name="ImageCardTheme" parent="Theme.Leanback"&gt; &lt;item name="imageCardViewStyle"&gt;@style/ImageCardViewStyle&lt;/item&gt; &lt;/style&gt;<p>Затем применим тему к конкретной ImageCardView при помощи<em>ContextThemeWrapper</em>. Такой подход позволяет создавать различные стили под определенный тип карточек приложения.<em>ImageCardView (ContextThemeWrapper (context, R.style.ImageCardTheme))</em></p>
32 &lt;style name="ImageCardTheme" parent="Theme.Leanback"&gt; &lt;item name="imageCardViewStyle"&gt;@style/ImageCardViewStyle&lt;/item&gt; &lt;/style&gt;<p>Затем применим тему к конкретной ImageCardView при помощи<em>ContextThemeWrapper</em>. Такой подход позволяет создавать различные стили под определенный тип карточек приложения.<em>ImageCardView (ContextThemeWrapper (context, R.style.ImageCardTheme))</em></p>
33 <p>Запустим приложение и посмотрим, что получилось:</p>
33 <p>Запустим приложение и посмотрим, что получилось:</p>
34 <p>Бинго! Иконка расположена с левой стороны, а заголовок и описание отчетливо видны. Теперь сделаем селектор и градиентный бэкграунд для infoArea. Для начала создадим градиентную область gradient_background.xml:</p>
34 <p>Бинго! Иконка расположена с левой стороны, а заголовок и описание отчетливо видны. Теперь сделаем селектор и градиентный бэкграунд для infoArea. Для начала создадим градиентную область gradient_background.xml:</p>
35 &lt;shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android"&gt; &lt;gradient android:angle="90" android:startColor="#000000" android:centerColor="#80000000" android:endColor="@android:color/transparent"/&gt; &lt;/shape&gt;<p>Затем создадим селектор info_area_selector.xml:</p>
35 &lt;shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android"&gt; &lt;gradient android:angle="90" android:startColor="#000000" android:centerColor="#80000000" android:endColor="@android:color/transparent"/&gt; &lt;/shape&gt;<p>Затем создадим селектор info_area_selector.xml:</p>
36 &lt;selector xmlns:android="http://schemas.android.com/apk/res/android"&gt; &lt;item android:state_selected="true"&gt; &lt;layer-list&gt; &lt;item android:drawable="@drawable/gradient_background" /&gt; &lt;item android:gravity="bottom"&gt; &lt;shape android:shape="rectangle"&gt; &lt;size android:height="2dp"/&gt; &lt;solid android:color="#FFFF"/&gt; &lt;/shape&gt; &lt;/item&gt; &lt;/layer-list&gt; &lt;/item&gt; &lt;item android:drawable="@drawable/gradient_background" /&gt; &lt;/selector&gt;<p>Зададим селектор как бэкграунд infoArea нашей карточки. Это можно сделать через стиль imageCardViewStyle, установив его в качестве значения свойства infoAreaBackground. Стиль карточки теперь выглядит следующим образом:</p>
36 &lt;selector xmlns:android="http://schemas.android.com/apk/res/android"&gt; &lt;item android:state_selected="true"&gt; &lt;layer-list&gt; &lt;item android:drawable="@drawable/gradient_background" /&gt; &lt;item android:gravity="bottom"&gt; &lt;shape android:shape="rectangle"&gt; &lt;size android:height="2dp"/&gt; &lt;solid android:color="#FFFF"/&gt; &lt;/shape&gt; &lt;/item&gt; &lt;/layer-list&gt; &lt;/item&gt; &lt;item android:drawable="@drawable/gradient_background" /&gt; &lt;/selector&gt;<p>Зададим селектор как бэкграунд infoArea нашей карточки. Это можно сделать через стиль imageCardViewStyle, установив его в качестве значения свойства infoAreaBackground. Стиль карточки теперь выглядит следующим образом:</p>
37 &lt;style name="ImageCardViewStyle" parent="Widget.Leanback.ImageCardViewStyle"&gt; &lt;item name="lbImageCardViewType"&gt;IconOnLeft|Title|Content&lt;/item&gt; &lt;item name="infoAreaBackground"&gt;@drawable/info_area_selector&lt;/item&gt; &lt;/style&gt;<p>Теперь осталось разместить infoArea поверх основной картинки.</p>
37 &lt;style name="ImageCardViewStyle" parent="Widget.Leanback.ImageCardViewStyle"&gt; &lt;item name="lbImageCardViewType"&gt;IconOnLeft|Title|Content&lt;/item&gt; &lt;item name="infoAreaBackground"&gt;@drawable/info_area_selector&lt;/item&gt; &lt;/style&gt;<p>Теперь осталось разместить infoArea поверх основной картинки.</p>
38 <p>У BaseCardView есть такие параметры, как cardType и infoVisibility, они также позволяют управлять элементами карточки. cardType управляет расположением областей и может принимать одно из следующих значений:</p>
38 <p>У BaseCardView есть такие параметры, как cardType и infoVisibility, они также позволяют управлять элементами карточки. cardType управляет расположением областей и может принимать одно из следующих значений:</p>
39 <ul><li>CARD_TYPE_MAIN_ONLY - видимой будет только основная область (у ImageCardView это основная картинка, т.е. это значение эквивалентно ImageOnly);</li>
39 <ul><li>CARD_TYPE_MAIN_ONLY - видимой будет только основная область (у ImageCardView это основная картинка, т.е. это значение эквивалентно ImageOnly);</li>
40 <li>CARD_TYPE_INFO_OVER - информационная область находится поверх основной (infoArea будет размещена поверх основной картинки);</li>
40 <li>CARD_TYPE_INFO_OVER - информационная область находится поверх основной (infoArea будет размещена поверх основной картинки);</li>
41 <li>CARD_TYPE_INFO_UNDER - информационная область находится под основной (infoArea будет размещена под основной картинкой);</li>
41 <li>CARD_TYPE_INFO_UNDER - информационная область находится под основной (infoArea будет размещена под основной картинкой);</li>
42 <li>CARD_TYPE_INFO_UNDER_WITH_EXTRA - поддерживает третью дополнительную область, которая будет расположена под основной областью, но над информационной. Этой областью может быть view, которая добавлена в карточку с помощью метода addView (view: View) (относительно ImageCardView дополнительная область будет расположена между основной картинкой и infoArea).</li>
42 <li>CARD_TYPE_INFO_UNDER_WITH_EXTRA - поддерживает третью дополнительную область, которая будет расположена под основной областью, но над информационной. Этой областью может быть view, которая добавлена в карточку с помощью метода addView (view: View) (относительно ImageCardView дополнительная область будет расположена между основной картинкой и infoArea).</li>
43 </ul><p>infoVisibility управляет видимостью информационной области (у ImageCardView это infoArea) и может принимать одно из следующих значений:</p>
43 </ul><p>infoVisibility управляет видимостью информационной области (у ImageCardView это infoArea) и может принимать одно из следующих значений:</p>
44 <ul><li>CARD_REGION_VISIBLE_ALWAYS;</li>
44 <ul><li>CARD_REGION_VISIBLE_ALWAYS;</li>
45 <li>CARD_REGION_VISIBLE_ACTIVATED;</li>
45 <li>CARD_REGION_VISIBLE_ACTIVATED;</li>
46 <li>CARD_REGION_VISIBLE_SELECTED.</li>
46 <li>CARD_REGION_VISIBLE_SELECTED.</li>
47 </ul><p>Для того чтобы разместить infoArea поверх основной картинки, установим для нашей ImageCardView cardType со значением CARD_TYPE_INFO_OVER:</p>
47 </ul><p>Для того чтобы разместить infoArea поверх основной картинки, установим для нашей ImageCardView cardType со значением CARD_TYPE_INFO_OVER:</p>
48 cardType = BaseCardView.CARD_TYPE_INFO_OVER.<p>Посмотрим на результат. Получилось то, что и планировалось.</p>
48 cardType = BaseCardView.CARD_TYPE_INFO_OVER.<p>Посмотрим на результат. Получилось то, что и планировалось.</p>
49 <p>С помощью стандартных инструментов Leanback можно создавать много разных вариантов карточек контента ImageCardView, но все же варианты "из коробки" ограничены. Иногда нужны совсем нестандартные варианты.</p>
49 <p>С помощью стандартных инструментов Leanback можно создавать много разных вариантов карточек контента ImageCardView, но все же варианты "из коробки" ограничены. Иногда нужны совсем нестандартные варианты.</p>
50 <p>Реализуем раздел меню для нашего приложения, который должен выглядеть следующим образом:</p>
50 <p>Реализуем раздел меню для нашего приложения, который должен выглядеть следующим образом:</p>
51 <p>Для начала создадим элемент меню, который мы хотим отрисовать. Он содержит заголовок и иконку:</p>
51 <p>Для начала создадим элемент меню, который мы хотим отрисовать. Он содержит заголовок и иконку:</p>
52 data class MenuItem( val title: String, @DrawableRes val icon: Int )<p>Попробуем его реализовать при помощи ImageCardView. Создадим презентер для связывания элементов меню в ImageCardView:</p>
52 data class MenuItem( val title: String, @DrawableRes val icon: Int )<p>Попробуем его реализовать при помощи ImageCardView. Создадим презентер для связывания элементов меню в ImageCardView:</p>
53 class MenuPresenter : Presenter() { override fun onCreateViewHolder(viewGroup: ViewGroup): Presenter.ViewHolder = ViewHolder(createView(viewGroup)) private fun createView(viewGroup: ViewGroup) = viewGroup .context .let { context -&gt; ImageCardView(context) .apply { isFocusable = true isFocusableInTouchMode = true setMainImageDimensions( context.getDimensionPixelSizeRes(R.dimen.menu_item_width), context.getDimensionPixelSizeRes(R.dimen.menu_item_height) ) } } override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, item: Any) = (viewHolder as ViewHolder) .bind(item as MenuItem) override fun onUnbindViewHolder(viewHolder: Presenter.ViewHolder) = (viewHolder as ViewHolder) .unbind() private inner class ViewHolder(view: ImageCardView) : Presenter.ViewHolder(view) { fun bind(item: MenuItem) = with(view as ImageCardView) { item.run { mainImage = view.context.getDrawable(icon) titleText = title } } fun unbind() = with(view as ImageCardView) { mainImage = null badgeImage = null titleText = "" contentDescription = "" } } }<p>Добавим код создания элементов меню и инициализации адаптеров в наш фрагмент:</p>
53 class MenuPresenter : Presenter() { override fun onCreateViewHolder(viewGroup: ViewGroup): Presenter.ViewHolder = ViewHolder(createView(viewGroup)) private fun createView(viewGroup: ViewGroup) = viewGroup .context .let { context -&gt; ImageCardView(context) .apply { isFocusable = true isFocusableInTouchMode = true setMainImageDimensions( context.getDimensionPixelSizeRes(R.dimen.menu_item_width), context.getDimensionPixelSizeRes(R.dimen.menu_item_height) ) } } override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, item: Any) = (viewHolder as ViewHolder) .bind(item as MenuItem) override fun onUnbindViewHolder(viewHolder: Presenter.ViewHolder) = (viewHolder as ViewHolder) .unbind() private inner class ViewHolder(view: ImageCardView) : Presenter.ViewHolder(view) { fun bind(item: MenuItem) = with(view as ImageCardView) { item.run { mainImage = view.context.getDrawable(icon) titleText = title } } fun unbind() = with(view as ImageCardView) { mainImage = null badgeImage = null titleText = "" contentDescription = "" } } }<p>Добавим код создания элементов меню и инициализации адаптеров в наш фрагмент:</p>
54 private fun initViewWithMenu(savedInstanceState: Bundle?) { val cardAdapter = ArrayObjectAdapter(MenuPresenter()) cardAdapter.addAll(0, getMenuCardItems()) setCardAdapter(cardAdapter) } private fun getMenuItems(): List&lt;MenuItem&gt; = listOf( MenuItem( title = "profile", icon = R.drawable.ic_tag_faces_black_24dp ), MenuItem( title = "subscriptions", icon = R.drawable.ic_subscriptions_black_24dp ), MenuItem( title = "history", icon = R.drawable.ic_history_black_24dp ), MenuItem( title = "settings", icon = R.drawable.ic_settings_black_24dp ) )<p>Запустим и посмотрим, что получилось:</p>
54 private fun initViewWithMenu(savedInstanceState: Bundle?) { val cardAdapter = ArrayObjectAdapter(MenuPresenter()) cardAdapter.addAll(0, getMenuCardItems()) setCardAdapter(cardAdapter) } private fun getMenuItems(): List&lt;MenuItem&gt; = listOf( MenuItem( title = "profile", icon = R.drawable.ic_tag_faces_black_24dp ), MenuItem( title = "subscriptions", icon = R.drawable.ic_subscriptions_black_24dp ), MenuItem( title = "history", icon = R.drawable.ic_history_black_24dp ), MenuItem( title = "settings", icon = R.drawable.ic_settings_black_24dp ) )<p>Запустим и посмотрим, что получилось:</p>
55 <p>Неплохо, но далеко от идеала. Иконки растягиваются на всю область основного изображения, а изменение ее размеров приводит к изменению размеров самой карточки. Для решения этой задачи нам необходимо создать собственную карточку для отрисовки элементов меню. Назовем ее MenuView.</p>
55 <p>Неплохо, но далеко от идеала. Иконки растягиваются на всю область основного изображения, а изменение ее размеров приводит к изменению размеров самой карточки. Для решения этой задачи нам необходимо создать собственную карточку для отрисовки элементов меню. Назовем ее MenuView.</p>
56 <p>Для начала создадим ее layout, view_menu.xml:</p>
56 <p>Для начала создадим ее layout, view_menu.xml:</p>
57 &lt;merge xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" android:gravity="center" tools:parentTag="android.widget.FrameLayout"&gt; &lt;LinearLayout android:layout_width="@dimen/menu_item_width" android:layout_height="@dimen/menu_item_height" android:orientation="vertical" android:gravity="center" tools:parentTag="android.widget.LinearLayout"&gt; &lt;androidx.appcompat.widget.AppCompatImageView android:id="@+id/vIcon" android:layout_width="100dp" android:layout_height="100dp"/&gt; &lt;TextView android:id="@+id/vTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="24sp" android:textColor="@android:color/black" android:layout_marginStart="16dp" android:layout_marginEnd="16dp"/&gt; &lt;/LinearLayout&gt; &lt;/merge&gt;<p>Затем создадим MenuView и заинфлейтим в нее наш layout:</p>
57 &lt;merge xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" android:gravity="center" tools:parentTag="android.widget.FrameLayout"&gt; &lt;LinearLayout android:layout_width="@dimen/menu_item_width" android:layout_height="@dimen/menu_item_height" android:orientation="vertical" android:gravity="center" tools:parentTag="android.widget.LinearLayout"&gt; &lt;androidx.appcompat.widget.AppCompatImageView android:id="@+id/vIcon" android:layout_width="100dp" android:layout_height="100dp"/&gt; &lt;TextView android:id="@+id/vTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="24sp" android:textColor="@android:color/black" android:layout_marginStart="16dp" android:layout_marginEnd="16dp"/&gt; &lt;/LinearLayout&gt; &lt;/merge&gt;<p>Затем создадим MenuView и заинфлейтим в нее наш layout:</p>
58 class MenuView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { var titleText: String = "" set(value) { field = value vTitle.text = value } @DrawableRes var iconId: Int = 0 set(value) { field = value vIcon.setImageResource(value) } init { View.inflate(context, R.layout.view_menu, this) isFocusable = true isFocusableInTouchMode = true } }<p>И, наконец, перепишем презентер. Теперь он будет связывать данные из элемента меню в MenuView:</p>
58 class MenuView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { var titleText: String = "" set(value) { field = value vTitle.text = value } @DrawableRes var iconId: Int = 0 set(value) { field = value vIcon.setImageResource(value) } init { View.inflate(context, R.layout.view_menu, this) isFocusable = true isFocusableInTouchMode = true } }<p>И, наконец, перепишем презентер. Теперь он будет связывать данные из элемента меню в MenuView:</p>
59 class MenuPresenter : Presenter() { override fun onCreateViewHolder(viewGroup: ViewGroup): Presenter.ViewHolder = ViewHolder(createView(viewGroup)) private fun createView(viewGroup: ViewGroup) = viewGroup .context .let { context -&gt; MenuView(context) .apply { setBackgroundColor(context.getColorRes(R.color.menu_item_background)) } } override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, item: Any) = (viewHolder as ViewHolder) .bind(item as MenuItem) override fun onUnbindViewHolder(viewHolder: Presenter.ViewHolder) = (viewHolder as ViewHolder) .unbind() private inner class ViewHolder(view: MenuView) : Presenter.ViewHolder(view) { fun bind(item: MenuItem) = with(view as MenuView) { item.run { titleText = title iconId = icon } } fun unbind() = with(view as MenuView) { titleText = "" iconId = 0 } } }<p>Похоже, но при нажатии на карточку отсутствует ripple-эффект. Создадим ripple drawable:</p>
59 class MenuPresenter : Presenter() { override fun onCreateViewHolder(viewGroup: ViewGroup): Presenter.ViewHolder = ViewHolder(createView(viewGroup)) private fun createView(viewGroup: ViewGroup) = viewGroup .context .let { context -&gt; MenuView(context) .apply { setBackgroundColor(context.getColorRes(R.color.menu_item_background)) } } override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, item: Any) = (viewHolder as ViewHolder) .bind(item as MenuItem) override fun onUnbindViewHolder(viewHolder: Presenter.ViewHolder) = (viewHolder as ViewHolder) .unbind() private inner class ViewHolder(view: MenuView) : Presenter.ViewHolder(view) { fun bind(item: MenuItem) = with(view as MenuView) { item.run { titleText = title iconId = icon } } fun unbind() = with(view as MenuView) { titleText = "" iconId = 0 } } }<p>Похоже, но при нажатии на карточку отсутствует ripple-эффект. Создадим ripple drawable:</p>
60 &lt;ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="?android:attr/colorControlHighlight" &gt; &lt;/ripple&gt;<p>И установим его как foreground MenuView:</p>
60 &lt;ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="?android:attr/colorControlHighlight" &gt; &lt;/ripple&gt;<p>И установим его как foreground MenuView:</p>
61 foreground = context.getDrawableRes(R.drawable.menu_card_ripple)<p>Теперь при нажатии на карточку можно увидеть ripple-эффект:</p>
61 foreground = context.getDrawableRes(R.drawable.menu_card_ripple)<p>Теперь при нажатии на карточку можно увидеть ripple-эффект:</p>
62 <p>Появилась другая проблема. RippleDrawable обрабатывает состояние focused и накладывает на view полупрозрачный бэкграунд. Чтобы этот эффект было лучше видно, изменим цвет карточки на темный.</p>
62 <p>Появилась другая проблема. RippleDrawable обрабатывает состояние focused и накладывает на view полупрозрачный бэкграунд. Чтобы этот эффект было лучше видно, изменим цвет карточки на темный.</p>
63 <p>Как видно, выделенная карточка становится светлее.Для решения этой проблемы отфильтруем состояние focused у MenuView, переопределив следующий метод:</p>
63 <p>Как видно, выделенная карточка становится светлее.Для решения этой проблемы отфильтруем состояние focused у MenuView, переопределив следующий метод:</p>
64 override fun onCreateDrawableState(extraSpace: Int): IntArray { val states = super.onCreateDrawableState(extraSpace) return states.filter { it != android.R.attr.state_focused }.toIntArray() }<p>Мы избавились от искажения цвета карточки в выделенном состоянии и сохранили ripple.</p>
64 override fun onCreateDrawableState(extraSpace: Int): IntArray { val states = super.onCreateDrawableState(extraSpace) return states.filter { it != android.R.attr.state_focused }.toIntArray() }<p>Мы избавились от искажения цвета карточки в выделенном состоянии и сохранили ripple.</p>
65 <p>Таким образом можно создавать карточки контента, которые нельзя сделать при помощи ImageCardView. Но это еще не все: я бы хотел улучшить получившуюся карточку и сделать у нее поддержку функционала, который предоставляет базовая реализация карточек BaseCardView. Давайте добавим на нашу карточку небольшое описание, которое будет появляться при переходе в состояние selected. Для этого создадим еще один layout:</p>
65 <p>Таким образом можно создавать карточки контента, которые нельзя сделать при помощи ImageCardView. Но это еще не все: я бы хотел улучшить получившуюся карточку и сделать у нее поддержку функционала, который предоставляет базовая реализация карточек BaseCardView. Давайте добавим на нашу карточку небольшое описание, которое будет появляться при переходе в состояние selected. Для этого создадим еще один layout:</p>
66 &lt;merge xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:lb="http://schemas.android.com/apk/res-auto" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" android:gravity="center" tools:parentTag="android.widget.FrameLayout"&gt; &lt;LinearLayout android:layout_width="@dimen/menu_item_width" android:layout_height="@dimen/menu_item_height" android:orientation="vertical" android:gravity="center" tools:parentTag="android.widget.LinearLayout" lb:layout_viewType="main"&gt; &lt;androidx.appcompat.widget.AppCompatImageView android:id="@+id/vIcon" android:layout_width="100dp" android:layout_height="100dp"/&gt; &lt;TextView android:id="@+id/vTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="24sp" android:textColor="@android:color/black" android:layout_marginStart="16dp" android:layout_marginEnd="16dp"/&gt; &lt;/LinearLayout&gt; &lt;TextView android:id="@+id/vDescription" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/menu_desc_background" android:paddingStart="16dp" android:paddingBottom="16dp" android:paddingTop="16dp" android:paddingEnd="16dp" lb:layout_viewType="info"/&gt; &lt;/merge&gt;<p>Этот layout похож на предыдущий, за исключением того что здесь добавили дополнительный TextView для вывода описания и новый атрибут lb: layout_viewType. Это атрибут BaseCardView, с помощью которого он находит элементы, которыми умеет управлять. У тега существует 3 значения: main - основная область карточки, info - информационная область и extra - дополнительная область.</p>
66 &lt;merge xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:lb="http://schemas.android.com/apk/res-auto" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" android:gravity="center" tools:parentTag="android.widget.FrameLayout"&gt; &lt;LinearLayout android:layout_width="@dimen/menu_item_width" android:layout_height="@dimen/menu_item_height" android:orientation="vertical" android:gravity="center" tools:parentTag="android.widget.LinearLayout" lb:layout_viewType="main"&gt; &lt;androidx.appcompat.widget.AppCompatImageView android:id="@+id/vIcon" android:layout_width="100dp" android:layout_height="100dp"/&gt; &lt;TextView android:id="@+id/vTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="24sp" android:textColor="@android:color/black" android:layout_marginStart="16dp" android:layout_marginEnd="16dp"/&gt; &lt;/LinearLayout&gt; &lt;TextView android:id="@+id/vDescription" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/menu_desc_background" android:paddingStart="16dp" android:paddingBottom="16dp" android:paddingTop="16dp" android:paddingEnd="16dp" lb:layout_viewType="info"/&gt; &lt;/merge&gt;<p>Этот layout похож на предыдущий, за исключением того что здесь добавили дополнительный TextView для вывода описания и новый атрибут lb: layout_viewType. Это атрибут BaseCardView, с помощью которого он находит элементы, которыми умеет управлять. У тега существует 3 значения: main - основная область карточки, info - информационная область и extra - дополнительная область.</p>
67 <p>Создадим новую view, которая будет наследником BaseCardView. Так мы добавим базовые функции карточек:</p>
67 <p>Создадим новую view, которая будет наследником BaseCardView. Так мы добавим базовые функции карточек:</p>
68 class MenuCardView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : BaseCardView(ContextThemeWrapper(context, R.style.MenuCardViewStyle), attrs, defStyleAttr) { var titleText: String = "" set(value) { field = value vTitle.text = value } var descriptionText: String = "" set(value) { field = value vDescription.text = value } @DrawableRes var iconId: Int = 0 set(value) { field = value vIcon.setImageResource(value) } init { View.inflate(context, R.layout.view_menu_card, this) isFocusable = true isFocusableInTouchMode = true } }<p>Теперь наследуем тему для view. Установим для свойства cardType значение infoOver - это означает, что информационная область (TextView с описанием) будет находиться поверх основной. А для свойства infoVisibility установим значение selected - это означает, что информационная область будет видна только в момент, когда карточка находится в состоянии selected.</p>
68 class MenuCardView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : BaseCardView(ContextThemeWrapper(context, R.style.MenuCardViewStyle), attrs, defStyleAttr) { var titleText: String = "" set(value) { field = value vTitle.text = value } var descriptionText: String = "" set(value) { field = value vDescription.text = value } @DrawableRes var iconId: Int = 0 set(value) { field = value vIcon.setImageResource(value) } init { View.inflate(context, R.layout.view_menu_card, this) isFocusable = true isFocusableInTouchMode = true } }<p>Теперь наследуем тему для view. Установим для свойства cardType значение infoOver - это означает, что информационная область (TextView с описанием) будет находиться поверх основной. А для свойства infoVisibility установим значение selected - это означает, что информационная область будет видна только в момент, когда карточка находится в состоянии selected.</p>
69 &lt;style name="MenuCardViewStyle" parent="Widget.Leanback.BaseCardViewStyle"&gt; &lt;item name="cardType"&gt;infoOver&lt;/item&gt; &lt;item name="infoVisibility"&gt;selected&lt;/item&gt; &lt;/style&gt;<p>Так мы получили кастомную карточку, поддерживающую базовые фичи карточек из Leanback.</p>
69 &lt;style name="MenuCardViewStyle" parent="Widget.Leanback.BaseCardViewStyle"&gt; &lt;item name="cardType"&gt;infoOver&lt;/item&gt; &lt;item name="infoVisibility"&gt;selected&lt;/item&gt; &lt;/style&gt;<p>Так мы получили кастомную карточку, поддерживающую базовые фичи карточек из Leanback.</p>
70 <p>Leanback предоставляет достаточно гибко настраиваемый вариант карточки, а именно ImageCardView. При необходимости можно достаточно просто создать свою карточку контента, а для того чтобы она имела базовый функционал карточек, ее родителем должна выступать BaseCardView.</p>
70 <p>Leanback предоставляет достаточно гибко настраиваемый вариант карточки, а именно ImageCardView. При необходимости можно достаточно просто создать свою карточку контента, а для того чтобы она имела базовый функционал карточек, ее родителем должна выступать BaseCardView.</p>
71 <p>Если вам понадобится проект, который создавался в статье, то его можно<a>скачать на GitHub</a>.</p>
71 <p>Если вам понадобится проект, который создавался в статье, то его можно<a>скачать на GitHub</a>.</p>
72 <a><b>Бесплатный курс по Python ➞</b>Мини-курс для новичков и для опытных кодеров. 4 крутых проекта в портфолио, живое общение со спикером. Кликните и узнайте, чему можно научиться на курсе. Смотреть программу</a>
72 <a><b>Бесплатный курс по Python ➞</b>Мини-курс для новичков и для опытных кодеров. 4 крутых проекта в портфолио, живое общение со спикером. Кликните и узнайте, чему можно научиться на курсе. Смотреть программу</a>