Андроид — кастомные View или ToggleButton 4×4
Рано или поздно любого начинающего андроид-разработчика перестает удовлетворять стандартный набор элементов управления. При этом имеется в виду как внешний вид, так и функциональность. И если с внешним видом все более или менее понятно, все достаточно легко кастомизируется, то функционала часто не хватает.
Когда (уже достаточно давно) в одном из своих проектов я столкнулся с необходимостью выбора в настройках одного из трех параметров, решение было очевидно — RadioButton. Но по ряду причин, типа экономии места на экране и некоторых других, возникло желание использовать нечто вроде ToggleButton. Поскольку стандартный Toggle имеет лишь два состояния, был использован костыль в виде программной обработки некоей циклично меняющейся переменной, в зависимости от которой менялись свойства стандартного элемента вроде обычной Button или ImageButton – уже даже не помню. Способ вполне работоспособный, однако не без греха. Первый и самый главный – нарушается Генеральная линия партии, призывающая к раздельному хранению ресурсов и программного кода. Ну и при большом количестве подобных элементов управления код теряет всю свою изящность и привлекательность. Инкапсуляция, опять же, жутко страдает. Посему было решено создать кастомный элемент.
Представленный пример не представляет из себя ничего сложного для опытного разработчика, однако я в свое время был бы весьма рад найти подобное в одном месте, а не собирать по кусочкам. Давайте попытаемся создать кастомный ToggleButton, имеющий произвольное количество (в данном случае четыре) циклически переключающихся состояний. В зависимости от состояния в приведенном примере меняется надпись на кнопке и значок состояния слева (DrawableLeft у кнопки). Однако все настолько просто и прозрачно, что легко применить изменение состояния к любым свойствам контрола.
Первое, что нам необходимо – описать нужные нам свойства в файле res/values/attrs.xml. Как мы уже решили, это будут четыре строки и четыре изображения для различных состояний элемента управления:
Теперь можем перейти непосредственно к коду нового класса. Отнаследуем его от обычной Button.
Я опустил импорты для экономии, если что – Эклипс подскажет.
Теперь, когда новый класс создан, мы можем добавить его на разметку и канонично указать свойства в xml-файле, не забыв добавить свое пространство имен:
Как говорится, profit.
Мы получили скелет, с которым дальше можем извращаться как угодно. Как варианты: вместо константы STATE_COUNT добавить соответствующие атрибут и метод типа setStateCount(), что позволит программно динамически ограничивать количество доступных состояний. Полный простор для фантазии.
На этом собственно все. Спасибо за внимание.
Источник
Полный список
— создаем свой адаптер на основе BaseAdapter
Предоставляемые нам адаптеры универсальны и полезны, но иногда их возможностей не хватает для реализации задуманного. Тогда возникает необходимость написать свой адаптер. Попробуем и мы. Создавать будем не с нуля, а используя BaseAdapter.
Сделаем подобие интернет магазина. Будем выводить список товаров. Каждый пункт списка будет содержать название товара, цену и изображение. Также будет возможность отметить пункт галкой, поместив его тем самым в корзину.
Внизу списка сделаем кнопку, которая будет отображать содержимое корзины. В настоящем интернет-магазине мы повесили бы на нее, например, переход к созданию заказа.
Project name: P0541_CustomAdapter
Build Target: Android 2.3.3
Application name: CustomAdapter
Package name: ru.startandroid.develop.p0541customadapter
Create Activity: MainActivity
В файл strings.xml добавим текстовый параметр для названия кнопки.
layout для пункта списка – item.xml:
Чекбокс, пара текстовых полей и картинка.
Теперь пишем код. Можно все написать в MainActivity.java, но тогда он получится достаточно большим и неудобным для чтения. Я раскидаю весь код по трем классам.
Product.java – класс, описывающий товар:
Тут все просто – только конструктор и элементы класса. Не заморачиваюсь с доступом и методами Set/Get, чтобы не усложнять код.
BoxAdapter.java – созданный адаптер, который будем использовать
На всякий случай напомню общий принцип действия адаптера: он получает данные и выдает View для отображения пункта списка.
Смотрим код. В конструкторе мы заполняем наши внутренние переменные и получаем LayoutInflater для работы с layout-ресурсами. В objects у нас теперь хранится список товаров, которые надо отобразить в списке.
Методы, отмеченные аннотацией @Override, мы обязаны реализовать при наследовании BaseAdapter. Эти методы используются списком и должны работать корректно.
Метод getCount должен возвращать кол-во элементов. Мы возвращаем кол-во товаров.
Метод getItem должен возвращать элемент по указанной позиции. Используя позицию, получаем конкретный элемент из objects.
Метод getItemId должен возвращать id элемента. Здесь не заморачиваемся и возвращаем позицию. Кстати, также сделано в некоторых адаптерах. Поэтому мы и видели в обработчиках, что >
Метод getView должен возвращать View пункта списка. Для этого мы создавали layout-ресурс R.layout.item. В этом методе мы должны из R.layout.item создать View, заполнить его данными и отдать списку. Но перед тем как создавать, мы пробуем использовать convertView, который идет на вход метода. Это уже созданное ранее View, но неиспользуемое в данный момент. Например, при прокрутке списка, часть пунктов уходит за экран и их уже не надо прорисовывать. View из этих «невидимых» пунктов используются для новых пунктов. Нам остается только заполнить их данными. Это значительно ускоряет работу приложения, т.к. не надо прогонять inflate лишний раз.
Если же convertView в этот раз нам не дали (null), то создаем сами view. Далее заполняем наименования, цену и картинку из данных по товарам. Для чекбокса мы присваиваем обработчик, сохраняем в Tag позицию элемента и ставим галку, если товар уже в корзине.
Tag – это некое Object-хранилище у каждого View, куда вы можете поместить нужные вам данные. В нашем случае я для каждого чекбокса помещаю в его Tag номер позиции пункта списка. Далее в обработчике чекбокса я смогу этот номер позиции извлечь и определить, в каком пункте списка был нажат чекбокс.
В итоге, метод getView возвращает списку полностью заполненное view, и список его отобразит как очередной пункт.
Далее идет пара методов, которые не обязательно было создавать при наследовании BaseAdapter. Я их создал для удобства.
Метод getProduct – это аналог getItem, но он сразу конвертирует Object в Product. Он используется всего пару раз. И в принципе, можно было бы и без него обойтись.
Метод getBox проверяет, какие товары отмечены галками и формирует из них коллекцию-корзину.
myCheckChangeList – обработчик для чекбоксов. Когда мы нажимаем на чекбокс в списке, он срабатывает, читает из Tag позицию пункта списка и помечает соответствующий товар, как положенный в корзину.
Тут важно понимать, что без этого обработчика не работало бы помещение товаров в корзину. Да и на экране — значения чекбоксов в списке терялись бы при прокрутке. Потому что пункты списка пересоздаются, если они уйдут «за экран» и снова появятся. Это пересоздание обеспечивает метод getView, а он для заполнения View берет данные из товаров. Значит при нажатии на чекбокс, обязательно надо сохранить в данных о товаре то, что он теперь в корзине.
Остается накодить MainActivity.java:
Тут кода совсем мало.
В onCreate создаем адаптер и список.
В методе fillData генерируем данные для адаптера. В качестве картинки используем стандартную для всех пунктов. В идеале, для каждого товара своя картинка.
Метод showResult получает из адаптера список товаров корзины и выводит их наименования. Этот метод вызывается по нажатию кнопки на экране, т.к. прописан в ее свойстве onClick.
Все сохраняем и запускаем. Отмечаем товары и жмем кнопку для просмотра содержимого корзины.
Достаточно непростой получился пример из-за чекбокса.
Вполне может быть, что есть другой способ реализации этого примера. Но смысл был в том, чтобы показать создание своего адаптера. Для закрепления темы посмотрите еще этот гугловский пример.
На следующем уроке:
— используем Header и Footer в списках
— разбираемся, как и где используется HeaderViewListAdapter
Присоединяйтесь к нам в Telegram:
— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование
— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня
— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме
Источник
Android курс в Технополисе 2019
В этом уроке мы научимся создавать собственные View .
Custom View
Обычно термин Custom View обозначает View , которого нет в sdk Android. Или другими словами — это View которое мы сделали сами.
Когда может понадобиться реализация собственного View :
- специфичная отрисовка;
- специфичная обработка жестов;
- оптимизация существующих элементов;
- правка багов в существующем элементе.
Как правило, создание custom view можно избежать используя темы, различные параметры View , а иногда и лисенеры. Но, если все таки вам действительно нужно сделать что-то особенное, давайте разберемся как же это сделать.
Для начала, давайте вспомним о том, как выглядит иерархия базовых компонентов:
Все ui компоненты наследуются от View , а лейауты от ViewGroup . В свою очередь ViewGroup наследуется от View .
Прежде чем наследоваться от базового класса View посмотрите, может быть вам ближе функциональность уже какого-то существующего элемента. Например Button , это не написанный с нуля компонент, а наследник TextView .
Жизненный цикл View
Первостепенно давайте разберемся с жизненным циклом View .
Constructor
Каждый элемент начинает свое существование с конструктора. У View их целых четыре штуки:
Создание View из кода:
Создание View из XML:
Создание View из XML со стилем из темы:
Создание View из XML со стилем из темы и/или с ресурсом стиля:
Последний конструктор добавлен в sdk версии 21. Каждый из конструктор каскадно вызывает следующий.
onAttachedToWindow
После того как родитель View вызовет метод addView(View) , наш View будет прикреплён к окну. На этой стадии наш View-компонент попадает в иерархию родителя.
onMeasure
Этот метод означает, что наш View находится на стадии определения собственного размера. Для того что бы понять как распределить элементы на экране и сколько они занимают место нужно получить от каждого View его размер. В методе measure как раз и происходят расчеты.
Давайте посмотрим на сам метод:
Метод onMeasure() принимает 2 аргумента: widthMeasureSpec и heightMeasureSpec . Это значения, которые содержат в себе информацию о том, каким размером хочет видеть ваше View родительский элемент.
Каждое из значений на самом деле содержит 2 параметра:
- mode . Указывает на то, какие правила применяются ко второму параметру size;
- size . Непосредственно размер View .
Получить эти параметры можно при помощи методов класса MeasureSpec :
mode может принимать следующие значения:
- MeasureSpec.EXACTLY . Означает, что размер задан жёстко. Независимо от размера вашего View , вы должны установить определённую ширину и высоту;
- MeasureSpec.AT_MOST . Означает что View может быть любого размера, которого пожелает, но, не больше чем размер родителя. Это значение match_parent ;
- MeasureSpec.UNSPECIFIED . Означение что View может само решить какой размер ему нужен не взирая ни на какие ограничения. Это значение wrap_content .
В коде это можно описать следующим образом:
где wrapWidth , это наша желаемая ширина. Аналогичный подход применяется и к высоте View .
Конечно же не нужно каждый раз писать эту конструкцию из условий. Для упрощения работы у View есть метод
который уже включает в себя все необходимые условия.
После того как мы выполнили все расчеты, необходимо установить рассчитанные размеры при помощи метода:
Расчет размера можно разделить на 4 стадии:
- Родитель узнает “пожелания”, каким размером View хочет быть, определение LayoutParams наследника. Это может быть сделано как через xml, так и кодом:
- Родитель начинает измерять свои дочерние View и просит рассчитать их размеры.
- Дочерняя View рассчитывает свои размеры и устанавливает значение.
- Родитель сообщает о том, что расчет закончен и можно получить финальные значения.
onLayout
Этот метод позволяет присваивать позицию и размер дочерним элементам ViewGroup . В случае, если мы наследовались от View , нам не нужно переопределять этот метод.
onDraw
Это основной метод при разработки собственной View . В onDraw вы можете рисовать все что вам нужно. Метод имеет следующую сигнатуру:
На полученном Canvas вам требуется непосредственно изобразить саму View . Рисование на Canvas происходит при мощи объекта Paint . Paint отвечает за то, как именно будет отрисован контент вашего View и имеет множество параметров.
Стоит обратить внимание, что onDraw вызывается не один раз и может занимать много времени. Поэтому стоит максимально аккуратно работать с отрисовкой, не аллоцировать никаких объектов и не делать лишних операций.
Обновление View
Из диаграммы жизненного цикла видно, что существуют два метода, которые заставляют View перерисовываться:
invalidate() . Используется когда нужно только перерисовать ваш элемент. Когда изменился цвет или текст или нужно сделать какие-то еще визуальные изменения;
requestLayout() . Используется когда нужно изменить размеры вашего View . Вызов requestLayout не только заставит View заново измериться, но и перерисует элемент.
Иерархия
Вызовы всех методов View проходят от базового View к потомкам, сверху вниз.
Во время расчета размера View потомок принимает “пожелания” от родителя, рассчитывает свои размеры, а также размеры своих потомков. (Measure pass)
После того как размеры известны, родитель проставляет размеры и расположение своим потомкам. (Layout pass)
Последним этапом является отрисовка. Она также происходит от родителя к потомку
Источник