company_banner

Лекция Яндекса: Advanced UI, часть первая

    Мы опубликуем несколько лекций Школы мобильной разработки 2017 года. Эта школа — часть проекта Яндекса «Мобилизация». Здесь можно найти видеокурсы, составленные по итогам «Мобилизации» прошлого года.

    Лекцию «Advanced UI» прочитал Дмитрий Свирихин — разработчик из команды мобильной Яндекс.Почты. Дмитрий объясняет, как при разработке интерфейса Android-приложения решать самые распространённые проблемы.


    Давайте резюмируем все вышесказанное про темы. Единообразные UI-элементы мы должны стилизовать с помощью тем, при этом нам не нужно писать всю тему с нуля, мы можем унаследоваться от какой-то системной темы и первоначально задавать некоторое подмножество атрибутов для этой темы.

    — Вы уже месяц обучаетесь в школе «Мобилизации» и много всего за этот месяц узнали — в том числе и про UI. Сегодня я попробую раскрыть какие-то вещи, которые вы уже могли узнать, а также попробую рассказать что-то новое. Надеюсь, будет интересно.

    Моя лекция будет состоять из двух частей, и в ней мы будем сначала ставить какую-то проблему, а затем искать пути её решения.


    Давайте посмотрим, какие проблемы мы будем сегодня исследовать.

    Во-первых, мы рассмотрим проблему неконсистентности UI в нашем приложении и средства решения этой неконсистентности.

    Во-вторых, одна из довольно крупных проблем Android-разработки — это взаимодействие с клавиатурой. Что делать, когда у нас на экране появляется клавиатура, как с этим жить.

    В-третьих, мы исследуем то, как мы можем эффективно применять кастомные view. Вы уже в одной из лекций проходили и примерно представляете себе, как мы должны создавать кастомные view, какие для этого есть правила. Но сегодня мы немножечко раскроем эту тему и логически ее продолжим. Также мы немножечко исследуем потерю состояния, когда она возможна. Вы уже тоже, конечно, знаете, как нужно себя вести, когда у нас возможна потеря состояния при пересоздании activity. Нужно сохранять какие-то вещи в bundle и затем после пересоздания activity их каким-то образом использовать.

    Но тем не менее, все равно у вас иногда могут возникать проблемы, и мы все эти проблемы сегодня попробуем учесть.

    Еще мы рассмотрим проблему недостаточной интерактивности нашего UI, посмотрим, как сделать его более интерактивным и понятно реагировать на какие-то действия, которые делает пользователь.

    Начнем с самого начала — про неконсистентность.

    Неконсистентность UI может быть нескольких типов. Во-первых, у нас может быть неконсистентность UI на различных девайсах в Android. Например, одни и те же view у нас на разных девайсах могут выглядеть по-разному. Все вы знаете, что Android обладает довольно обширным парком девайсов, и из-за этого у нас довольно высокая фрагментация по версиям Android. Соответственно, на Android 4 UI может выглядеть одним образом, на Android 5 — другим. Это вызывает неконсистентность UI на разных устройствах.

    Но это, наверное, не самая главная проблема, потому что также неконсистентность UI может у нас быть и в приложении, когда на разных экранах у нас тулбар может быть разного цвета, либо при этом тулбар еще может не сочетаться с цветом, который у нас находится в Navigation Drawer, который выезжает в левой части экрана. И есть некоторый способ, с помощью которого мы можем если не решить эту проблему, то как минимум значительно упростить ее решение.

    Это темы и стили.


    Начнем мы с тем. И давайте для начала определим, что такое тема.

    Тема — это некая глобальная конфигурация всего нашего UI в Android. Атрибуты темы могут включать в себя, например, какие-то window-лаги, относительно которых строится весь наш UI, какие-то глобальные цвета и изображения, которые также у нас используются для построения UI, и даже целые стили для отдельных view и подтемы, например, для диалогов.

    Атрибутов в теме около 300, и даже больше, но когда мы определяем свою тему, нам не нужно переопределять все 300 атрибутов, мы просто не должны этого делать. Для этого в платформе предусмотрен такой способ, что мы должны наследоваться от какой-то уже готовой системной темы и переопределить то, что нам нужно.

    Давайте посмотрим, от каких тем мы можем наследоваться.

    В самом начале лучше всего подключить библиотеку AppCompat, которая входит в состав support library, и унаследовать нашу собственную тему от какой-то из тех, которые уже предусмотрены в этой библиотеке. Соответственно, Theme.AppCompat вам может понадобиться для того, чтобы сделать темный UI, и Theme.AppCompat.Light для светлого. И после того, как вы это сделаете — унаследуете свою собственную тему от одной из данных предопределенных тем, — у вас уже будет решена проблема неконсистентности UI на разных девайсах. У вас на четвертом, пятом и более высоком Android UI, может быть, будет и не идентичный, но очень похожий, и проблема неконсистентности на разных девайсах уже будет решена.

    Итак, мы создали свою тему, унаследовались от какой-то системной. Что нам нужно сделать дальше?


    Особенность атрибутов темы состоит в том, что все внутренние темы и стили так или иначе завязаны друг на друге, и первоначально нам нужно определить некоторое подмножество атрибутов, некоторые основные атрибуты. На слайде они представлены. Давайте разберемся с каждым из них.

    Во-первых, windowBackground, на слайде вы можете видеть, как будет изменяться вид нашего приложения с заданными различными бэкграундами. windowBackground Как раз и задает основной фон для нашего приложения, задавая тот цвет или тот drawable, который мы передали в параметр windowBackground в качестве фона windows, в котором рисуются все наши view.

    Также есть такие параметры, как colorPrimary и colorPrimaryDark. По их названию можно подумать, что это какие-то очень важные параметры, но, на самом деле, единственное, для чего они нужны — это для того, чтобы просто задать некоторые цвета для нашего тул-бара и статус-бара. Еще они могут использоваться в том же тул-баре, когда он у нас переходит в Action Mode. С помощью данных атрибутов окрашивается разделитель между тулбаром и контентом.

    Поэтому мы переходим к атрибуту поинтереснее — colorAccent. Как можно понять из названия, он нужен для того, чтобы акцентировать внимание пользователя на какие-то важные части UI. Что это может быть? Например, это может быть FloatingActionButton, это может быть edittext, который в данный момент редактирует пользователь, это может быть выбранный чекбокс и т. д.

    colorControlNormal определяет цвет UI-элементов в неактивном состоянии. Под неактивным состоянием мы будем понимать несфокусированный edittext, к примеру, либо невыбранный чекбокс и т. д.

    colorControlActivated, напротив, определяет цвет сфокусированного edittext, выбранного чекбокс и т. д. Как правило, данный атрибут нам переопределять не надо, потому что он завязан на colorAccent, по умолчанию он использует тот же самый ресурс, что и colorAccent, и трогать нам его не нужно, но, тем не менее, полезно все-таки о нем знать.

    colorControlHighlight определяет цвет, которым наша view будет подсвечиваться, когда мы на нее тапаем. По сути, это тот самый цвет ripple, который у нас появляется, когда мы тапаем на view и ожидаем некоторое время.

    Стоит отметить, что у данного цвета должен быть задан альфа-канал, чтобы у нас получился такой красивый эффект.

    Есть также некоторый перечень атрибутов, который нужен для стилизации, например, кнопок — это colorButtonNormal. Кнопки, они неподвластны атрибуту colorControlNormal, который мы рассматривали ранее, потому что оп гайдам они все-таки несколько отличаются, и для него придумали отдельный атрибут.

    Также, например, есть подобный атрибут для Switch. Поэтому, когда вы задаете какие-то атрибуты вроде colorControlNormal, если что-то у вас не окрашивается — не беда, скорее всего, есть какие-то атрибуты, которые помогут вам также решить создавшуюся проблему.

    И немножко поговорим про параметры, про текст.

    Есть такой параметр в системе, как textColorPrimary (основной цвет текста приложения) и textColorHighlight, который определяет цвет выделения текста. Вообще, параметров, связанных с текстом, их довольно огромное количество, и не всегда сразу можно угадать, как заданный параметр, атрибут цвета текста, повлияет на многие view в нашей системе, поэтому здесь есть два варианта. Если параметр textColorPrimary не подействовал, вы можете определить либо опытным путем, какой атрибут с текстом вам нужен, либо залезть во внутренности библиотеки AppCompat и найти, от какого же цвета ваша view в итоге зависит.

    Давайте теперь рассмотрим, как же вообще происходит такое, что мы задаем цвет в тему, а при этом у нас окрашиваются вообще бэкграунды всех view в нашем layout. На самом деле, это происходит следующим образом, и никакой магии здесь нет.

    Изначально в системе содержится некоторая битмапа, как правило, черного цвета, либо на новых Android она задается с помощью вектора, и привязанный к ней селектор в виде tint для этой view. Например, для чекбокса может быть привязан вот такой селектор, как представлен на слайде, который показывает, что в состоянии Checked мы должны окрашивать бэкграунд нашей view в цвет, который задан в атрибуте colorControlActivated. Соответственно, если наша view — в данном случае чекбокс — принимает атрибут Checked, то он будет окрашиваться именно в этот цвет. Вот так будет выглядеть наш чекбокс, если под colorControlActivated у нас будет задан желтый цвет.


    И еще один важный момент, который стоит отметить про атрибуты — мы можем ссылаться на них прямо в наших layout. Вот самый распространенный способ представлен на слайде, когда мы хотим сделать эффект triple для какого-нибудь layout в нашей иерархии. Мы можем просто задать ему бэкграунд selectableItemBackground, и тогда, тапая на данный frame layout, пользователь увидит ripple. Это будет на Android 5 и выше, а на Android 4 у него просто по тапу изменится бэкграунд.

    Давайте резюмируем все вышесказанное про темы. Единообразные UI-элементы мы должны стилизовать с помощью тем, при этом нам не нужно писать всю тему с нуля, мы можем унаследоваться от какой-то системной темы и первоначально задавать некоторое подмножество атрибутов для этой темы.

    Затем, если нам этого не хватает, мы можем точечно менять еще какие-то атрибуты, чтобы добиться желаемого результата. И также при необходимости вы можете использовать атрибуты прямо в ваших layout.

    Итак, мы разобрались с тем, как написать тему. Давайте посмотрим, где мы можем ее использовать. Мы можем прямо в манифесте задать тему для нашего приложения и activity с помощью атрибута android:theme. Когда мы каждый из этих случаев можем использовать? В application мы можем задавать, например, светлую тему. Довольно часто сейчас все приложения выглядят со светлой темой. И при этом довольно модно сейчас делать галереи, которые, напротив, имеют темный цвет. Таким образом мы можем задать основную тему и затем отдельно в activity, в котором мы хотим видеть свою галерею, сделать, например, темную тему.

    Но бывают также и другие ситуации.

    Вот у нас есть некоторый layout, так он выглядит у нас с темами по умолчанию. В целом все неплохо, но нам он не нравится. Мы или наш дизайнер хочет тему несколько иную — он хочет, чтобы тулбар у нас был таким темным. Получается, что в основной части приложения у нас используется светлая тема — это светлый бэкграунд с темным текстом, а вот тулбар внезапно стал с темной темой и со светлым текстом.

    Для этого в Android предусмотрели возможность задавать темы на уровне view. Что происходит, когда мы задаем тему на уровне view? Те атрибуты, которые указаны в этой теме, которые мы задали во view, они, соответственно, будут переопределять основные атрибуты той activity, в которой эта view у нас отображается.

    И для этого в системе предусмотрен также так называемый ThemeOverlay, от которого мы должны наследовать наши собственные темы, которые мы собираемся передавать во view, и также с помощью библиотеки AppCompat у нас уже есть некоторый перечень тех тем, которые мы можем использовать, от которых мы можем наследоваться. Соответственно, ThemeOverlay.AppCompat.Light предназначен для того, чтобы переопределять какую-то часть иерархии с темной темы на светлую, а ThemeOverlay.AppCompat.Dark — напротив, со светлой на темную. Есть также уже подготовленные Overlay для ActionBar и для DarkActionBar. Последний нам нужно использовать в нашем примере, который мы рассмотрели ранее.

    Теперь давайте посмотрим такую ситуацию. Чем отличаются темы и стили, которые мы задаем на уровне view? Думаю, все вы знаете, что такое стили, уже не раз ими пользовались, и, допустим, мы задаем некоторые стили для view B в этой иерархии, которая сейчас представлена на слайде. Что произойдет в таком случае? Все атрибуты, которые находятся внутри этого стиля, будут действовать только view B.

    Если же мы хотим задать тему для view B, что произойдет? Все глобальные атрибуты из этой темы в свою очередь будут действовать на всю иерархию: не только на B, но и на всех ее child'ов (в данном случае D и E). Но если бы у D и E были еще другие childs, тема также на них бы распространялась.

    Что же произойдет, если мы зададим и стиль, и тему для view B? Все в целом довольно понятно. Стиль будет действовать также для этой view B, и тема будет распространяться на B и на всех ее childs.

    Теперь давайте формально определим, в чем состоит разница стиля и темы, которая может задаваться на уровне view. Стиль предназначен для того, чтобы задавать какое-то подмножество атрибутов, которое адекватно только для данного view, в котором мы задаем этот стиль. Например, в стиле может содержаться, если мы задаем его для TextView, атрибут TextAppearance или Background. Это нормальный пример темы. И вообще, изначально темы предназначались для того, чтобы мы могли устранять дублирование для однотипных view. Если же мы задаем тему для какой-то view, она должна предназначаться для того, чтобы переопределять какие-то глобальные атрибуты для всей иерархии, в которой мы задаем эту тему.

    В теме могут содержаться такие атрибуты, как, например, colorControlNormal, который мы рассмотрели ранее.

    Также существует еще один вид стилей под названием TextAppearance, который предназначен для определения множества атрибутов, связанных исключительно с текстом. Это может быть как цвет, размер, шрифт, даже тень от текста. И используются они в основном для таких view, как EditText и TextView, но в целом есть еще некоторый набор view, где они используются. Главная отличительная черта, что эти стили должны передаваться в те атрибуты, в названии которых присутствует словосочетание TextAppearance. Довольно просто запомнить.

    Вот такие TextAppearance у нас с помощью библиотеки AppCompat существуют в платформе. Вы можете их использовать. Вы можете даже сказать дизайнерам своим, чтобы они с помощью таких стилей выдавали вам какие-то макеты.

    И вообще тема типографии в Android довольно обширна. На них можно потратить целую лекцию, но я, пожалуй, оставлю это на самостоятельное изучение. Внизу есть ссылочка, где можно прочитать различные подробности про текст в Android, а сам перейдут на следующую тему, связанную непосредственно с разработкой.

    И последнее, что я хотел сказать по поводу стилей — это про явное и неявное наследование стилей. Давайте посмотрим, у нас есть некоторый layout. Это обычная форма логина. Соответственно, здесь есть поле ввода e-mail и пароля. Кажется, что эти поля выглядят очень похоже, но, тем не менее, у них есть какая-то общая часть и различия стиля. Давайте попробуем выстроить иерархию этих стилей.


    Общую часть мы выделим с помощью стиля под названием AuthField, а затем отдельно для логина и для пароля сделаем стили, которые уже будут наследоваться знакомым нам образом. Мы точно также наследовали темы. Это делается с помощью атрибута parent, и в целом здесь все понятно и просто. Но мы можем поступить иначе. Мы можем сделать нагляднее, способ более наглядный, который позволит нам следить за иерархией стилей.


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

    Для собственных иерархий данный способ кажется более приемлемым, потому что он более наглядный и очевидный, и в целом используйте такой способ.

    Про стили и темы я сказал все, что хотел. (Сессию вопросов и ответов см. в видео. — прим. ред.)

    Раз уж мы заговорили про текстовые поля, разберемся, какая может быть главная проблема, когда у нас появляются текстовые поля. Данная проблема называется клавиатурой, которая может перекрывать у нас какую-то часть layout.

    И не всегда это хорошо для пользователя, поэтому давайте разберемся с некоторыми частями, которые связаны с клавиатурой, в секции «Взаимодействие с клавиатурой».

    Итак, если в какой-то момент, когда вы разрабатываете ваше приложение, вам захочется определить размер клавиатуры, то в 99% случаев делать этого не нужно, просто не нужно. Вам это может пригодиться, только если вы разрабатываете чат, например, а во всех остальных случаях это не нужно. Намного проще следить за изменением размера layout и уже в зависимости от этого каким-то образом подстраивать все, что у вас находится на экране, под тот размер, который у вас остается после появления клавиатуры.

    Также важно понимать, что клавиатура — это отдельное приложение, которое мы не можем никак настроить, мы можем только смириться с тем, что она есть, поэтому на всяких маленьких девайсах клавиатура может занимать и 2/3 экрана.

    И что нам нужно делать, если она будет перекрывать довольно большую часть нашего layout? Мы можем просто поместить этот layout в ScrollView, и пользователь увидит такую картину, когда клавиатура перекрывает 2/3 всего экрана, может проскроллить и найти нужную ему кнопочку где-то там в недрах нашего layout.


    Итак, как мы можем реагировать на изменение нашего layout? С помощью такого нетривиального способа OnLayoutChangeListener. Нам необходимо подписать его на наш ScrollView либо другой скролящийся контейнер, где находятся наши view, и в зависимости от изменения размера, если он изменился, мы можем каким-то образом обновить его для новой высоты, которая нам доступна.

    Что мы можем сделать? Мы можем, например, убрать логотип, и тогда у нас все останется видно, или мы можем переместить вообще кнопку для логина в тулбар — тоже вариант. И вообще отступы можем убрать, много чего можем сделать. Все ограничивается только вашей фантазией и фантазией вашего дизайнера.

    Еще один момент, который связан со взаимодействием с клавиатурой. Пользователи любят, когда мы не заставляем их делать каких-то лишних действий. Соответственно, при вводе e-mail мы можем поместить на клавиатуре кнопочку собаки (@) прямо рядом с его пальцами. То есть он будет вводить какой-то текст, название своего ящика, затем сразу же нажмет на кнопочку e-mail. Замечательно.

    Также мы можем ему помочь следующим образом. Когда пользователь ввел e-mail, ему не хочется лишний раз тянуться на текстовое поле ввода пароля, ему хочется, чтобы все было под рукой. И мы опять ему можем с этим помочь, сделав кнопку «Далее» на клавиатуре. Если user нажмет на кнопку «Далее», он, соответственно, перейдет на поле пароля и сможет ввести пароль. И это не все. Мы, опять-таки, можем еще раз помочь пользователю, не заставлять ему тянуться на кнопку Sign In, которая у нас находится в тулбаре, и сделать кнопку Done, которая появляется в нижней правой части layout, более функциональной. По нажатию на нее пользователь войдет к основной функциональности приложения.

    За все эти действия отвечает два параметра: inputType и imeOptions. inputType нам нужен, чтобы определить какое-то поведение поля. Это во-первых. То есть если мы сделаем inputType Text Password, то все символы пароля, которые пользователь будет вводить, они будут скрываться. Это понятно, почему.

    И также с помощью параметра inputType мы можем настроить какое-то дополнительное поведение на клавиатуре, например, как у нас было с textEmailAddress, что у нас просто символ «собаки» появлялся сразу же на клавиатуре.

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

    И также еще параметр imeOptions, с помощью которого мы можем задавать действия, которые у нас будут появляться в нижнем правом углу клавиатуры, как правило. Это также может зависеть от клавиатуры. Это могут быть действия, как нажать кнопку «Далее», «Поиск», «Готово», и там еще могут быть несколько.

    Давайте сначала посмотрим, как клавиатуры у нас могут выглядеть с заданным inputType. Уже знакомый нам textEmailAddress, когда у нас есть символ «собаки» и точка — довольно частый символ, который нам нужен для ввода e-mail.

    Также inputType для ввода каких-то цифровых значений. Например, мы можем с помощью данного типа клавиатуры ввести какую-то сумму заказа или что-то прочее.

    И также inputType=”phone”, с помощью которого мы можем ввести номер телефона. Если присмотреться, то можно увидеть, что там есть и спецсимволы — такие, как плюс, звездочка, решетка и т. д.


    А с помощью такого listener мы можем переопределять какие-то действия, которые пользователь нажимает на клавиатуре. В данном случае вставлен код для реагирования на действие Done. Как я говорил о нашем приложении, по которому мы можем позволить пользователю сразу же перейти к основной части приложения. И главное здесь — не забыть еще и скрыть клавиатуру.

    Actiondone, по умолчанию единственное, что он делает — он скрывает клавиатуру.

    И вообще, у многих экшенов есть какое-то поведение по умолчанию. Например, Actionnext сам по умолчанию может просто переместить вас на следующее поле для ввода, и для него переопределять экшены никакие не нужно. Но, тем не менее, все в ваших рука чем- можете переопределять любые экшены, какие захотите.

    И также еще одна ситуация, которая у вас может возникнуть с клавиатурой. Если вы разрабатывали-разрабатывали ваше приложение, затем перевернули его в landscape, и что-то пошло не так. Весь наш layout перекрылся такой странной штукой. Это называется fullscreen-клавиатура, и в целом это довольно полезная вещь, когда вам нужно вводить какие-то длинные текстовые штуки в вашем приложении. Она появляется тогда, когда система решает, что осталось довольно мало места для того, чтобы вы что-то могли разглядеть, и такой layout вставляет. Это поведение по умолчанию. Тем не менее, если это вам не нравится, если вы считаете: «Больше одной строчки я здесь все равно не введу» (если это e-mail), вы можете просто использовать флаг NoExtractUi в качестве imeOptions, его также можно использовать с Pipe, то есть вы можете несколько флагов в imeOptions указывать, и вы можете action основной задать и этот флаг. Так что пользуйтесь, и все будет хорошо.

    Подведем некоторые промежуточные итоги, связанные с взаимодействием с клавиатурой. Вы должны реагировать на изменение размеров layout, а не на сам факт появления или отсутствия клавиатуры на вашем экране. Это намного проще.

    Во-вторых, все UI-компоненты на тех экранах, где возможно появление клавиатуры, должны находиться в скроллящемся контейнере, например, ScrollView. И всегда, когда у вас есть текстовые поля на экране, вам практически обязательно понадобятся атрибуты inputType и imeOptions, которые вы можете задать в XML, и в коде вы тоже их можете задать. Поэтому пользуйтесь данными параметрами, вы можете очень сильно облегчить жизнь пользователям. (Сессию вопросов и ответов см. в видео. — прим. ред.)

    Существует такая тенденция, особенно у начинающих разработчиков, когда разработчик сталкивается с какой-то проблемой, он хочет все это решить с помощью кастомной view.

    К примеру, разработчик встретился с проблемой, связанной с отображением клавиатуры на каком-нибудь frame layout. Давайте сделаем кастомную view с помощью этого frame layout, который нам будет все решать.

    Потом такая же проблема возникла на каком-нибудь linear layout. Давайте сделаем еще одну кастомную view, которая нам опять все проблемы решит.

    И на каждую проблему разработчик может создавать какие-то кастомные view. И в итоге получается что?

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

    Во-вторых, когда начинающий разработчик создает view, он видит конструкторы и не понимает, а зачем нам четыре конструктора, какие из них нам реально нужны. И может доходить иногда вплоть до крэшей.

    С этой темой мы обязательно разберемся в нашей следующей теме «Эффективное применение custom view».

    Давайте сначала разберемся с мотивацией, когда нам нужно создавать кастомные view.

    Наверное, самая замечательная и лучшая мотивация — это если мы хотим написать view с какой-то совершенно новой функциональностью, которой в платформе вообще нет. Например, насколько я помню, в платформе не существует view для выбора цвета, некоторой палитры. Это замечательная мотивация, чтобы создать кастомную view. Во-вторых, кастомная view не решает какой-то нашей проблемы, недостает в какой-то существующей системной view функционала. Например, мы хотим, чтобы по нажатию на наш button он считался нажатым по двойному тапу. В таком случае нам нужно заэкстендить button и каким-то образом это прописать. Тоже неплохая мотивация.

    Третье. К сожалению, до сих пор существуют view с багами, особенно они часто встречаются в составе Support Library, и нам, чтобы как-то продолжать с ними работать, приходится их экстендить и костылить, чтобы они хоть каким-то образом заработали.

    Других мотиваций для создания кастомных view я больше не вижу. Если вы создаете какое-то кастомное view, подумайте в какой-то момент: «Возможно, я смогу это решить, просто добавив какой-то listener для этой view, и больше ничего не нужно будет». Например, проблема с клавиатурой, которую мы решали — там не нужно никаких кастомных view.

    Давайте также рассмотрим мотивацию для создания compound view. Что такое compound view? Для начала разберемся. Compound view известен также с compound components (можно встретить в разных гайдах). Это тот случай, когда мы экстендим view group или производные от него.

    Когда мы можем это делать? Во-первых, если у нас есть какой-то layout, который постоянно используется на разных экранах, то есть на одном экране, на каком-нибудь другом экране с просмотром чего-то. У нас он очень часто используется. Тогда мы можем создать кастомный compound view, тем самым заэкстендив какой-нибудь linear layout, frame layout и т. д. Другая мотивация, если мы хотим повысить performance нашего приложения. Например, наша задача какая-то решается тремя вложенными relative layout. Наверное, это не самое лучшее решение. У вас оно может даже тормозить, особенно если оно в списках. Можно создать кастомную view, которая экстендит view group, и прописать свой алгоритм расстановки view по экрану.

    На самом деле данный вариант уже становится не настолько актуальным, потому что Android обрастает все бо́льшим количеством всяких модерновых view, таких, как ConstraintLayout, CoordinatorLayout, даже FlexboxLayout скоро выйдет из альфы или беты (не помню, в котором он сейчас состоянии). И этих layout, кажется, достаточно, чтобы можно было реализовать какие-то даже самые сложные layout.

    Теперь рассмотрим конструкторы для кастомных view. Сначала рассмотрим самые простые первые два.

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

    Второй конструктор, ведь котором параметром фигурирует AttributeSet, также можно догадаться, что он нужен нам для того, чтобы создавать view из XML. Данный конструктор использует LayoutInflater.

    А что же с остальными двумя? Чтобы понять, для чего нам нужен конструктор с тремя параметрами, давайте заглянем в исходный код андроидовской кнопки, класс Button, а именно заглянем в конструктор, состоящий из двух параметров. Вот так он выглядит, и он каскадно вызывает конструктор из трех параметров.

    В качестве третьего параметра используется атрибут buttonStyle. Что это вообще такое? Это тот самый атрибут, который у нас находится в той самой тем, которую мы обсуждали в самом начале лекции, и тем самым кнопка сообщает, что по умолчанию для данной view должен использоваться стиль, который находится по данному атрибуту.

    Что же по поводу конструктора из четырех параметров? Данный конструктор появился только в Android 5, поэтому сейчас он еще не особо в ходу. Но, тем не менее, мы на будущее рассмотрим, зачем он может быть нужен.

    Опять-таки, представим, как выглядела бы реализация конструктора Button из трех параметров.

    Там бы у нас мог вызываться каскадный конструктор из четырех параметров, и в качестве последнего атрибута у нас не была бы непосредственная ссылка на стиль, с которым данная view должна была бы стилизоваться. То есть последний конструктор из четырех параметров привязывает нашу view не только к какому-то атрибуту в теме, но и непосредственно к какому-то стилю.


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


    А нужны они только для вызова одного-единственного метода obtainStyledAttributes, который резолвит различные атрибуты из XML, которые вы задали в XML. Три параметра attrs, defStyleAttr и defStyleRes — это те самые параметры из конструктора. А вот что такое параметр styleable? Это ссылка на XML, который у нас находится также в ресурсах, и с помощью тега declare-styleable определяет, какие вообще у этой view могут быть атрибуты. Соответственно, метод obtainStyledAttributes эти атрибуты с помощью всех входных параметров резолвит.

    На выходе из этого метода мы получаем экземпляр класса TypedArray, из него мы можем достать все, что нам необходимо, соответственно, значение всех атрибутов из XML, затем мы будем их как-то использовать. Но очень важно не забыть вызвать у объекта TypedArray метод Recycle, что снова закинет его в пул TypedArray, и он сможет использоваться для других view.


    А теперь давайте решим, сколько нам вообще в итоге конструкторов нужно, учитывая все вышесказанное. В целом, можно сказать, что нам, как правило, в 90% случаев достаточно двух конструкторов, самых первых. Третий параметр с атрибутом кажется излишний для наших костюмных view, потому что, как правило, они используются не очень часто, и выделять отдельный атрибут для темы кажется излишним. И в качестве данного параметра в obtainStyledAttributes мы можем передавать просто 0.

    А в качестве четвертого параметра мы можем просто передавать тему. Все-таки это наша view, в нашем проекте мы всегда сможем ее изменить. Поэтому сразу передаем тему, которая будет содержать непосредственно какие-то значения для тех атрибутов, которые у нас находятся в styleable.CustomView.

    Итак, подведем очередной промежуточный итог. Перед созданием кастомной view обязательно подумайте, действительно ли она вам нужна. Может, вы можете обойтись и без нее, может быть, она у вас уже даже была реализована, либо вы можете использовать какой-то хитрый listener в вашей view. Обязательно задавайте себе этот вопрос.

    Также в связи с тем, что кастомные view, которые мы создаем, у нас, как правило, очень узконаправлены, обычно нам достаточно всего лишь двух конструкторов.

    И также для повышения переиспользования вы можете использовать атрибуты, которые вы можете задавать в XML, и наша view от этого только выиграет, вы также ее сможете чаще использовать и переиспользовать.

    Когда мы используем различные view, в том числе и свои кастомные view, создавая свои layout, у нас может возникать некоторая проблема, связанная с состоянием.

    Например, вот я сделал приложение, в котором, нажимая на какую-то из частей, вы посмотрите краткое содержание моей лекции, которую я сейчас рассказываю. И вот я сделал это приложение, хочу посмотреть, что я буду рассказывать во второй части. Тапаю на этот элемент, у меня все красивенько открывается. Кстати, тут спойлеры — не смотрите пока. И затем поворачиваю экран, и у меня все теряется. Как с этим быть? Думаю, вы отлично все знаете, что мы можем в Bundle просто в методе OnSaveInstanceState сохранить некоторое состояние о том, что вторая часть у нас была открыта, и затем восстановить, и проблема действительно решится.

    Возможны такие ситуации, что у нас, опять-таки, есть приложение, где мы заполняем анкету сведений о себе, и мы все заполнили, повернули девайс, а у нас каким-то волшебным образом все само собой сохранилось. Давайте определим, где эта грань, когда у нас все само собой сохраняется и когда не сохраняется ничего и хочется сломать компьютер, прописывая очередные изящества с Bundle.

    Давайте посмотрим это в секции про восстановление состояния.

    Давайте для начала вспомним, как вообще работает сохранение и восстановление state. Перед тем, как у нас уничтожится activity, а это может произойти в таких случаях, как, например, поворот экрана — у нас activity уничтожается и пересоздается в новом виде, — либо когда у системы остается мало памяти и она хочет очистить какие-то ресурсы, убивает нашу activity, система может у нашего activity восстановить state. Вызвать, например, у activity метод OnSaveInstanceState. Там, соответственно, какой-то state записывается, затем, условно, после поворота экрана у нас перед самой первой отрисовкой этой иерархии state восстановится.

    Какие контроллеры сами по себе умеют сохранять state? Это activity, fragment, где мы сами вручную можем схоронить, накидать то, что нам понадобится. И также view, в них мы также можем что-то сохранить. Но и view, которые используются напрямую в системе, они также умеют сохранять свой state.

    Что именно система сама умеет из этого сохранять?

    Например, если вы введете какой-то текст в EditText, система, естественно, все это сохранит. Также может сохраняться страница ViewPager, до которой вы дошли, может сохраниться выделенный элемент в Spinner и даже состояние скролла в RecyclerView. Все это view умеют сами по себе сохранять.

    Что объединяет все эти параметры? То, что они связаны с непосредственной реализацией именно этих view.

    А такие параметры, как состояние иерархии, если вы динамически добавляли view в иерархию, либо удаляли. И общие параметры, такие, как видимость, например — это именно наш случай, когда мы сделали видимым одну view, затем повернули экран, а видимость осталась в дефолтном состоянии, — также не восстановится масштаб, поворот. К сожалению, все это нам нужно записывать в Bundle и сохранять своими силами, если это действительно для нас в нашем приложении является важным.

    Также не восстанавливается все, что связано с drawable. Просто drawable мы не смогли поместить в Bundle и в Parcelable, потому что тот Parcelable, который мы можем сохранить, он ограничивается на последней версии Android 700 килобайтами, насколько я помню. И очень большая вероятность, что все наши drawable, которые мы бы хотели сохранить, не поместятся.

    Задачу с drawable решают такие условные библиотеки Glide и Picasso. Они умеют все кэшировать, так что с drawable обычно проблем не возникает.

    А как же быть со списками? Ранее говорил, что view восстанавливается перед первой отрисовкой, но если вспомнить, то мы обычно в наших activity асинхронно грузим какие-то данные, которые будут отображаться именно в списке, то есть мы из базы можем грузить данные, из файла. И есть очень высокая вероятность, что к отрисовке первого кадра мы можем не успеть загрузить те данные, которые у нас будут отображаться в списке. В результате мы потеряем состояние.

    Думаю, у каждого такое было, что мы сделали список, проскроллили куда-то, затем повернули экран и оказались в самом начале списка.

    Вообще про списки нужно знать некоторый нюанс, как они работают. Обычные view, не списки, восстанавливают состояние перед самой первой отрисовкой иерархии. Это я уже говорил. А вот view умеет восстанавливать свое состояние отложенно. Не сразу же перед первой отрисовкой иерархии, а перед первой отрисовкой непосредственно самого списка. Это будет верно и для RecyclerView, и для ListView. Первая отрисовка списка может происходить тогда, когда наш список становится видимым.


    Что из этого следует? Мы можем в самом начале, когда у нас создается, например, фрагмент, сделать список невидимым. И затем, когда мы уже получили какие-то данные из базы или из файла — неважно, — в тот момент, когда мы задаем для адаптера какие-то данные, в этот же момент сделать список видимым, и состояние скролла у нас магическим образом само собой восстановится.

    Также мы можем это время ожидания, в которое у нас список появляется, скрасить ProgressBar.

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

    И также я хотел вам сказать еще кое-что про фрагменты.

    Все вы, наверное, знаете, что мы с помощью метода OnSaveInstanceState можем сохранить какие-то данные о состоянии фрагмента. Но не все, наверное, знают, что Fragment Manager, который добавляет все эти наши фрагменты, которые должны использоваться в layout, точно также умеет сохранять все свое состояние, включая Back Stack, состояние видимости фрагментов и т. д. Activity, в котором содержится Fragment Manager, все это умеет делать. То есть если мы динамически, скажем, добавим фрагмент в какой-то контейнер, затем повернем экран, он у нас окажется на этом же самом месте. И довольно часто случается, что здесь разработчики теряют свое состояние. Казалось бы, состояние восстанавливается, а мы его теряем. Как это происходит?


    Если мы динамически добавляем фрагменты таким образом, например, в onCreate, мы каждый раз добавляем фрагмент, то после сохранения состояния фрагмента, который у нас был таким образом добавлен, после поворота экрана и при создании activity старый фрагмент с состоянием просто заменится новым, который мы, опять же, с помощью этого кода создаем, и, соответственно, состояние просто-напросто потеряется.

    Как решать такую ситуацию, как мы можем из нее выкрутиться?


    Мы можем просто, например, добавлять фрагменты с помощью тегов. Вот таким образом. И затем после пересоздания activity проверять, а существует ли во фрагмент-менеджере фрагмент с таким тегом. Если он уже существует, значит, нам делать ничего не надо, он сам без каких-либо проблем восстановит свое состояние. Если же его нет, то мы его, соответственно, и добавляем.

    Это то, что всегда необходимо помнить про фрагменты: что Fragment Manager сам умеет сохранять свое состояние.

    И вы, должно быть, читали в различных документациях, что вам для того, чтобы показать диалог, нужно обязательно использовать DialogFragment. Ничего иного. И в этом, кстати, имеется смысл, потому что DialogFragment — это единственный способ создать диалог, который у вас после поворота экрана сам по себе восстановится. Даже это фрагменты умеют сами восстанавливать.

    Я бы хотел завершить первую часть нашей лекции. И давайте подведем некоторые итоги.

    Во-первых, чтобы избавиться от проблемы неконсистентности UI внутри вашего приложения, вам нужно использовать темы и стили.

    Во-вторых, когда у вас есть layout, в котором есть текстовые поля, вам для начала нужно подготовить layout, чтобы он хорошо смотрелся, когда у вас поверх него появится клавиатура, а также подготовить все текстовые поля, которые будут инициировать появление клавиатуры.

    В-третьих, кастомные view вам нужно создавать с умом. Перед этим вам нужно подумать — а нет ли у меня такой же view? Или я смогу поправить какие-то XML для этой view, чтобы она также могла использовать какие-то новые параметры и мы могли ее переиспользовать?

    Что касается состояния — не нужно делать то, что система умеет делать за нас. Не нужно писать разные костыли для восстановления состояния списка. Нужно просто использовать некоторые, возможно, не очень очевидные правила, связанные с восстановлением состояния.

    На этом я бы хотел закончить первую часть. Большое всем спасибо за внимание.
    • +29
    • 5,9k
    • 4
    Яндекс 549,36
    Как мы делаем Яндекс
    Поделиться публикацией
    Похожие публикации
    Комментарии 4
    • 0

      Видео интересное, но статья лучше imho

      • +1

        Ну очень большое спасибо за статью ;)

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое