Хабрахабр

[Из песочницы] Android Support Library 28. Что нового?

По давней традиции вместе с новой версией Android выходит обновление Support Library. Пока библиотека вышла в стадии альфа, но список изменений уже намного интереснее, чем такой же список у Android P. Google несправедливо мало рассказал и написал об основных нововведениях главной библиотеки для Android. Приходится читать исходники и разбираться, в чем особенности новых фич и зачем они нужны. Восстановлю справедливость и расскажу, чем нас порадовал Google:

  • RecyclerView selection — выбор элементов теперь из коробки;
  • Slices — новый способ отображать контент другого приложения;
  • новые элементы дизайна: BottomAppBar, ChipGroup и другие;
  • мелкие изменения одной строкой.

RecyclerView selection

В 2014 году, вместе с релизом Lollipop, Google добавила в support новый элемент — RecyclerView, как замену устаревшему ListView. Все было хорошо с ним, да не хватало одного метода из ListView — setSelectionMode(). Спустя 4 года этот метод косвенно был реализован в RecyclerView в виде целой библиотеки.

Что же волшебного в selection? Selection mode — режим, которой инициализируется долгим нажатием по элементу списка. Далее можем выбрать несколько других элементов и сделать общее действие на ними. Пример: в Google Photos selection mode значительно облегчает жизнь.

Давайте разберемся на практике, как обстоит дело в support.

Добавим в gradle зависимости. Интересно, что Google выделила selection в отдельный репозиторий.

dependencies { implementation "com.android.support:recyclerview-selection:28.0.0-alpha1"
}

Напишем стандартный адаптер для RecyclerView.

class WordAdapter(private val items: List<Word>) : RecyclerView.Adapter<WordViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = WordViewHolder( LayoutInflater .from(parent.context) .inflate(R.layout.item_word, parent, false) ) override fun getItemCount() = items.size override fun onBindViewHolder(holder: WordViewHolder, position: Int) { val item = items[position] holder.bind(item) } class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val text: TextView = itemView.findViewById(R.id.item_word_text) fun changeText(word: Word) { text.text = word.text } } }

Модель Word используем в качестве данных.

@Parcelize
data class Word(val id: Int, val text: String) : Parcelable

Фундамент есть, приступим к реализации выбора. Сперва нужно определиться, что будет идентифицировать элемент списка. Google предлагает на выбор три варианта: Long, String, Parcelable. Для этой цели у нас уже сформирован Word, не хватает только реализации Parcelable. Реализацию добавим аннотацией @Parcelize, которая доступная в экспериментальной версии Kotlin. В Android Studio 3.2 пока есть проблемы со сборкой проекта с экспериментальным Kotlin, но никто не отменял студийные шаблоны.

SelectionTracker — главный класс библиотеки. Объект этого класса обладает информацией про выбранные пользователем элементы и позволяет из кода изменять этот список. Чтобы инициализировать данный класс, понадобятся реализации двух абстрактных классов: ItemKeyProvider и ItemDetailsLookup. Первый нужен для двусторонней связи позиции элемента в коллекции и ключа.

// В конструкторе ItemKeyProvider мы выбираем метод предоставления доступа к данным:
// SCOPE_MAPPED - ко всем данным. Позволяет реализовать функционал, требующий наличие всех элементов в памяти
// SCOPE_CACHED - к данным, которые были недавно или сейчас на экране. Экономит память
class WordKeyProvider(private val items: List<Word>) : ItemKeyProvider<Word>(ItemKeyProvider.SCOPE_CACHED) { override fun getKey(position: Int) = items.getOrNull(position) override fun getPosition(key: Word) = items.indexOf(key)
}

ItemDetailsLookup нужен для получения позиции элемента и его ключа по координатам x и y.

class WordLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup<Word>() { override fun getItemDetails(e: MotionEvent) = recyclerView.findChildViewUnder(e.x, e.y) ?.let { (recyclerView.getChildViewHolder(it) as? ViewHolderWithDetails<Word>)?.getItemDetail() } }

Напишем также интерфейс для получение данных из ViewHolder и реализуем его.

interface ViewHolderWithDetails<TItem> { fun getItemDetail(): ItemDetails<TItem> } class WordDetails(private val adapterPosition: Int, private val selectedKey: Word?) : ItemDetails<Word>() { override fun getSelectionKey() = selectedKey override fun getPosition() = adapterPosition } inner class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), ViewHolderWithDetails<Word> { override fun getItemDetail() = WordDetails(adapterPosition, items.getOrNull(adapterPosition)) }

Везде стандартный код. Удивительно, почему разработчики support library не добавили классическую реализацию сами.

Сформируем трекер в Activity.

val tracker = SelectionTracker .Builder<Word>( // идентифицируем трекер в контексте "someId", recyclerView, // для Long ItemKeyProvider реализован в виде StableIdKeyProvider WordKeyProvider(items), WordLookup(recyclerView), // существуют аналогичные реализации для Long и String StorageStrategy.createParcelableStorage(Word::class.java) ).build()

Поправим ViewHolder, добавим реакцию на изменение состояния выбора.

fun setActivatedState(isActivated: Boolean) { itemView.isActivated = isActivated
}

Добавим трекер в адаптер, переопределим onBindViewHolder с payload. Если изменения касаются только состояния выбора, то в payloads будет находиться константа SelectionTracker.SELECTION_CHANGED_MARKER.

override fun onBindViewHolder(holder: WordViewHolder, position: Int, payloads: List<Any>) { holder.setActivatedState(tracker.isSelected(items[position])) if (SelectionTracker.SELECTION_CHANGED_MARKER !in payloads) { holder.changeText(items[position]) } }

Tracker готов и работает, как часы. Добавим немного красоты и смысла. Пусть AppBar меняет цвет, заголовок начнет отображать количество выбранных элементов и добавляется кнопка Clear в меню, когда пользователь что-нибудь выбирает. Для этого есть ActionMode и поддержка его в AppCombatActivity.

Первым делом напишем реализацию ActionMode.Callback.

class ActionModeController( private val tracker: SelectionTracker<*>
) : ActionMode.Callback { override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { mode.menuInflater.inflate(R.menu.action_menu, menu) return true } override fun onDestroyActionMode(mode: ActionMode) { tracker.clearSelection() } override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean = true override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean = when (item.itemId) { R.id.action_clear -> { mode.finish() true } else -> false } }

Добавим observer к SelectionTracker и свяжем изменения в трекере с ActionMode в Activity.

tracker.addObserver(object : SelectionTracker.SelectionObserver<Any>() { override fun onSelectionChanged() { super.onSelectionChanged() if (tracker.hasSelection() && actionMode == null) { actionMode = startSupportActionMode(ActionModeController(tracker)) setSelectedTitle(tracker.selection.size()) } else if (!tracker.hasSelection()) { actionMode?.finish() actionMode = null } else { setSelectedTitle(tracker.selection.size()) } } }) } private fun setSelectedTitle(selected: Int) { actionMode?.title = "Selected: $selected" }

Теперь точно все. Наслаждаемся простотой и красотой.

Мы сделали стандартный вариант. Кратко отмечу, что в Builder много методов для кастомизации процесса выбора. Например, с помощью метода withSelectionPredicate(predicate: SelectionPredicate) можно ограничить количество выбранных элементов или запретить выбор особых элементов. Также в Builder предусмотрены методы по добавлению поведения, которое может конфликтовать с selection при традиционном способе добавления. Например, при помощи withOnDragInitiatedListener(listener: OnDragInitiatedListener) можно настроить Drag&Drop.

Slices

Самой странной новинкой оказался Slice. Google посвятила очень мало времени объяснениям сообществу, что это за диковина. Есть только код и документации к половине классов. Давайте разбираться.

За основу возьму код отсюда, потому что они придумали, как обходить баги с Permission в Android P DP1. Хочу отметить, что Slices не является новинкой support library. Фича появилась в Android SDK 28, а в support ареал обитания расширен до 24 версии SDK. На этом можно завершить рассказ и продолжить его через несколько лет. Пока minSdkVersion может быть максимум 19, поговорим в общем об идее этой технологии и о том, зачем она вообще нужна.

Slices — библиотека, которая позволит запрашивать из одного приложения (клиент или хост) часть или статичный кусочек другого приложения (отправитель или провайдер). Очень похоже на описание RemoteViews, которое часто используется для программирования кастомных виджетов и уведомлений.

Slice — это данные в каркасе без дизайна и интерактивности, как HTML без CSS и Js. Дизайн будет подстраиваться под тему приложения-хоста. Пример слайса.

Отправитель — это ContentProvider, которому нужно реализовать простой метод onBindSlice(sliceUri: Uri): Slice и внутри метода сформировать Slice. У нас провайдер будет отсылать время и количество вызовов.

class SliceContentProvider : SliceProvider() { private var counter = 0 override fun onBindSlice(sliceUri: Uri): Slice { return when (sliceUri.path) { "/time" -> createTimeSlice(sliceUri) else -> throw IllegalArgumentException("Bad url") } } override fun onCreateSliceProvider(): Boolean { Toast.makeText(context, "Slice content provider is launched", Toast.LENGTH_LONG).show() return true } private fun createTimeSlice(sliceUri: Uri): Slice = ListBuilder(context, sliceUri) .apply { counter++ setHeader( ListBuilder.HeaderBuilder(this) .setTitle("What's the time now?") ) addRow( ListBuilder.RowBuilder(this) .setTitle("It is ${SimpleDateFormat("HH:mm").format(Calendar.getInstance().time)}") ) addRow( ListBuilder.RowBuilder(this) .setTitle("Slice has called $counter times") ) } .build() }

Клиенту нужно сделать запрос по URI к провайдеру, запросить через него slice, получить и передать его в SliceView. Все действия производятся через SliceManager. Важно не забыть про permission.

private val baseSliceUri: Uri = Uri.parse("content://ru.touchin.provider/") private val timeSliceUri = baseSliceUri.buildUpon().appendPath("time").build() private lateinit var sliceManager: SliceManager override fun onCreate(savedInstanceState: Bundle?) { // стандартные процедуры инициализации View sliceManager = SliceManager.getInstance(this) findViewById<View>(R.id.get_slice).setOnClickListener { tryShowingSlice(timeSliceUri) } } override fun onStart() { super.onStart() if (providerAppNotInstalled(packageManager, baseSliceUri.authority)) { showMissingProviderDialog(this, { finish() }, baseSliceUri) return } } private fun tryShowingSlice(sliceUri: Uri) { if (sliceManager.missingPermission(sliceUri, appName = getString(R.string.app_name))) { // запрашиваем permission сложным образом из-за Android P DP1 } } else { getSliceAndBind(sliceUri) } } private fun getSliceAndBind(sliceUri: Uri) { sliceView.setSlice(sliceManager.bindSlice(sliceUri)) }

SliceManager предоставляет возможность подписаться с помощью SliceLiveData на изменения Slice в провайдере и внутри подписки обновлять SliceView. К сожалению, оно сейчас не работает. Мы использовали менее реактивный вариант.

Запускаем провайдер, запускаем приложение. Наблюдаем результат работы. Все круто. Забавно, что счетчик инкрементируется два раза.

В большинстве случаев RemoteView используется для виджетов и уведомлений. Slices плохо подходят под эти цели, они мало кастомизируемые и, как я уже писал, подстраиваются под дизайн приложения. Идеально подходят под приложения, которые используют данные других приложений. Под категорию всеобъемлющих подходят голосовые ассистенты — Google Assistant, Алиса и так далее. Как было замечено в блоге компании Novada, с помощью конструктора slice можно собирать слайсы, очень похожие на ответы для Google Assistant.

И тут самое время для теории.

Возьмем за основу то, что Slice сделан для программирования ответов в Google Assistant — стратегически важный продукт для компании. Очевидно, что мы живем во времена, когда графический интерфейс постепенно вытесняется голосовым: растет популярность домашних ассистентов и есть прогресс в разработке голосового искусственного интеллекта посредством ИИ, нейронный сетей и других хайповых технологий.

Для Google самым логичным вариантом было бы развивать и наращивать Google Assistant, чтобы за год-два он стал мощным инструментом. Slice — теоретически отличный инструмент для накачки дополнениями от сторонних разработчиков. Так ассистент станет мощнее, все действия можно проводить через него и отпадет надобность в рабочих столах и иконках. Тогда Google Assistant станет основой для Android.

На данный момент нам ничего не рассказали толком про Slice: ни целей, ни преимуществ над RemoteView. Хотя по количеству кода в новой версии support Slice занимает чуть ли не первое место. Поэтому я думаю, на ближайшей I/O нам будут подробно рассказывать про Slice. И возможно расскажут о планах эволюции ОС или даже представят версию Android с голосовым интерфейсом для разработчиков.

Но все это спекуляция и желание автора раскрыть теорию заговора и добраться к истине. Единственное, что можно сказать на сто процентов, на Google I/O нас ждет развязка истории.

Новые элементы:

MaterialCardView и MaterialButton

MaterialCardView наследуется от CardView и практически ничем не отличается от него. Добавлена только возможность задавать границы карточки и в качестве background используется другой drawable. Найдите 10 отличий.
MaterialButton является наследником AppCombatButton и тут различия заметны. Разработчики сюда добавили больше способов кастомизировать кнопку: цвет ripple эффекта, разные радиусы кнопок, границы, как у MaterialCardView.

Chip и ChipGroup

Тут и слова лишние.

BottomAppBar

Самый интересный и неожиданный виджет в данной подборке, хотя идея очень проста, AppBar разместить внизу. Пользователю с маленькими руками и большими экранами неудобно дотягиваться до кнопки вызова меню или просто кнопки на AppBar вверху. Но никакой другой пользы в этом элементе нет.

Меню на BottomAppBar нужно добавлять искусственно, для этого есть метод replaceMenu(@MenuRes int newMenu).

Дизайнеры классно придумали, как сочетать FloatingActionButton и BottomAppBar. Но без кнопки BottomAppBar смотрится лишним. Убирается вырез, остается подбородок с кнопками меню с одной стороны. Проблему с меню на больших экранах можно было бы решить интересней, например по длинному нажатию на FloatingActionButton трансформировать ее в меню внизу экрана.

Список коротких нововведений:

  • Android KTX, который анонсировали ранее. Куча open-source extension-ов на Kotlin. Очень полезно.
  • HEIF Writer. Новый формат кодирования одного или последовательности изображений дошел до Android через год после анонса на ios. Здесь не идёт речь о полной замене форматов, как у Apple. Просто библиотечка с конвертацией.
  • Browser Actions — протокол для кастомизации контекстного меню браузера под определенный url. Кастомизация ограничивается добавлением нескольких MenuItem-ов со своими иконкой, текстом и Intent-ом. Протокол подразумевает реализацию логики также со стороны браузера. В Chrome пока не реализовано.

Для тех, кто хочет поковыряться:

  1. Используйте Android studio 3.1 и выше. Эти версии пока не в релизе, но работают стабильно, я работал с 3.2.
  2. Немного пошаманить в build.gradle с версиями. Ну и, естественно, нужно добавить нужные зависимости.
    android { compileSdkVersion 'android-P' defaultConfig { targetSdkVersion 'P' // или 28 }
    }
    
  3. Пока код, который использовал support 28, запускался только на эмуляторе с Android P. Все, что старее, ругалось и выдавало кучу ошибок при попытке запуска.

Список новых фич не окончательный. Если анализировать changelog библиотеки за предыдущие 2-3 года и экстраполировать данные на этот год, то в мае нас ожидает ещё много-много интересного. Ждём.

Полезные ссылки:

Теги
Показать больше

Похожие статьи

Кнопка «Наверх»
Закрыть