- Android курс в Технополисе 2019
- Custom View
- Жизненный цикл View
- Constructor
- onAttachedToWindow
- onMeasure
- onLayout
- onDraw
- Обновление View
- Иерархия
- Введение
- Специфические задачи, специфические view
- Создание custom view
- Добавление логики отображения
- Использование полученного view
- Добавление xml атрибутов
- Custom View, скроллинг и жесты в Android на примере простого вьювера картинок
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)
Последним этапом является отрисовка. Она также происходит от родителя к потомку
Источник
Введение
Это будет серия постов на тему создания пользовательского view компонента для Android. Я покажу как нарисовать содержимое компонента, как работают layouting и measuring, как реализовать view groups и как анимировать содержимое компонента. В этом посте я объясню, как расширить стандартный view, как его использовать и как создать свои собственные xml атрибуты.
Специфические задачи, специфические view
Стандартные view компоненты, которые предоставляет Android, могут использоваться для многих задач и ситуаций. Тем не менее, в наших приложениях наибольшую часть кода часто занимает конфигурация этих view для специфических задач. Этот конфигурационный код часто находится в activity вашего приложения, поэтому их сложно содержать в чистоте. А при наличии большого количества различного кода возникают сложности и с его разделением в отдельные классы.
Предположим, вы работаете над приложением, которое содержит статистику тренировок пользователя. Например, общая дистанция, общее время и так далее, в зависимости от типа тренинга. Для того чтобы отображать данные в удобном виде, вы решили их адаптировать, основываясь на их продолжительности. Если это 2378 секунд, вы хотите отображать это время как «40 минут», а если это 18550 секунд, то лучше было бы отображать на дисплей «5 часов 9 минут».
Создание custom view
Один из способов решения этой задачи — создать специализированный view, который будет должным образом обрабатывать ваш текст. Давайте начнем с создания нашего собственного класса DurationTextView, который будет расширять класс TextView.
TextView, как и все view классы, имеет три различных конструктора: первый просто принимает контекст (Context), второй принимает контекст и набор атрибутов (AttributeSet), а третий еще дополнительно принимает стиль «по умолчанию». В большинстве случаев вам достаточно реализовать второй конструктор, который показан выше.
Теперь мы создали view и чтобы его использовать, мы добавим его в файл разметки:
Обратите внимание, вам необходимо полностью указывать имя вашего класса, который вы реализовали.
В данный момент этот класс практически идентичен стандартному TextView, поэтому давайте добавим ему некоторую функциональность.
Добавление логики отображения
Поскольку этот view будет отображать длительность, давайте добавим метод setDuration().
Что делает этот метод? Он получает значение длительности, переводит его в текстовую строку в определенном формате, используя некую логику, а затем выводит с помощью метода setText(). В данном случае логика заключается в переводе секунд в минуты и дальнейшем разделении на часы и минуты. Также в методе присутствует логика для корректного отображения одной минуты: «1 minute» вместо «1 minute(s)».
В конце метода отформатированное время преобразуется в строку с помощью шаблона.
private static final String TEMPLATE = «Duration: %s«;
Шаблон начинается со слова «Duration», а далее жирными буквами отображается отформатированная строка.
Для простоты я использовал в коде строковые литералы. В нормальном коде, эти строковые литералы лучше перенести в strings.xml, это позволяет в дальнейшем локализовать их.
Использование полученного view
Попробуем использовать наш view. В xml файл разметки добавим 5 custom View.
А в activity в методе onCreate() после метода setContentView() добавим следующие строки.
Соберем проект и загрузим его в эмулятор. Полученный результат можно видеть на изображении ниже.
Добавление xml атрибутов
Если бы мы могли задавать шаблон, то созданный нами компонент был бы более универсальным. По идее можно добавить метод, который позволит нам устанавливать шаблон, однако это не так просто. Если подумать, длительности нам нужно устанавливать динамически, а шаблон скорее всего будет все время одинаковым. Поэтому вместо метода, давайте добавим нашему custom view новые xml атрибуты.
В первую очередь нужно создать в папке values файл attrs.xml, где мы сможем определить атрибуты. Для этого view мы добавим один атрибут — template типа string. Выглядеть это будет так:
Мы записали тег declare-styleable и задали имя TemplateTextView. Имя может быть произвольным, но обычно задают имя, совпадающее с именем view. В данном случае я не задаю имя DurationTextView, потому что планирую использовать template атрибут еще в другом view.
Далее мы определили новый атрибут, установив ему имя template и формат string. Помимо string существует множество других форматов, например, Color, Integer, Boolean, Reference и так далее.
Использовать новый атрибут в layout xml можно так.
Обратите внимание, в корневом элементе мы добавляем такую строчку.
xmlns:app= «http://schemas.android.com/apk/res-auto»
Это позволяет нам использовать атрибуты, которые определены в файле attrs.xml, в нашем layout. В данном случае мы задали префикс «app», хотя он может быть произвольным.
Для того чтобы использовать новый атрибут, нужно получить его значение в нашем custom view. Сначала заменяем приватную статическую константу «TEMPLATE» переменной «template». Затем добавляем в наш конструктор следующий код.
В первой строке мы получаем набор атрибутов, который содержит все атрибуты, существующие в xml файле. Метод obtainStyledAttributes() делает две основные вещи. Во-первых, он фильтрует все атрибуты из attrs с помощью контекста, применяет стили и resolves reference to values. Во-вторых, он возвращает только те атрибуты, которые вы определили. Они задаются вторым аргументом, который представляет собой массив ссылок на требуемые атрибуты. В данном случае в R.styleable.TemplateTextView у нас только один атрибут R.attr.template, который мы создали в файле attrs.xml.
Далее мы используем массив attributes, чтобы получить значение атрибута. Если значение шаблона не было определено или не содержит «%s», мы устанавливаем template = «%s». И в заключении нужно не забыть освободить ресурсы с помощью функции recycle().
После некоторых изменений в layout, приложение будет выглядеть так:
Здесь я установил шаблон для первых трех custom view
Большая часть view в Android имеет методы, которые позволяют устанавливать значения XML атрибутов в коде. Возможно вам тоже понадобиться добавить какой-нибудь метод, это зависит от того, как вы будете использовать view.
В следующей части мы посмотрим как нарисовать свой собственное содержимое view компонента и сделаем view для отображения графика.
Исходники к статье можно скачать c GitHub.
По материалам сайта Jayway
Вольный перевод — Pavel Bobkov.
Источник
Custom View, скроллинг и жесты в Android на примере простого вьювера картинок
В статье описываются детали реализации простого вьювера картинок и показываются некоторые тонкости имплементации скроллинга и обработки жестов.
И так, начнем. Ми будем разрабатывать приложения для просмотра картинок. Готовое приложение выглядит так (хотя скриншоты, конечно, слабо передают функционал):
Установить приложение можно либо из Маркета, либо установив вручную отсюда. Исходный код доступен здесь.
Главным элементом нашего приложения является класс ImageViewer который мы и будем разрабатывать. Но нужно также отметить, что для выбора файла для просмотра я не стал изобретать велосипед и взял готовый «компонент» здесь.
Компонент представляет собой activity, который вызывается при старте из главного activity. После выбора файла, мы его загружаем и показываем на экране с помощью класса ImageViewer. Рассмотрим класс более подробно.
Класс является наследником класса View и переопределяет только один его метод onDraw. Также класс содержит конструктор и метод загрузки изображения:
Если мы загрузим картинку по размерам больше чем экран смартфона, то отобразится только часть ее и у нас не будет способа ее подвинуть или уменьшить.
Добавим теперь возможность скроллинга. Скроллинг по своей сути представляет собой жест, при котором пользователь дотрагивается пальцем к экрану, передвигает его не отрывая, и отпускает. Для того чтоб иметь возможность обрабатывать события связанные с тач-скрином, нужно переопределить метод onTouchEvent. Метод принимает один параметр типа MotionEvent и должен возвратить true в случае обработки события. Через этот метод можно реализовать поддержку любого жеста, включая скроллинг.
Для распознавания скроллинга нам нужно зафиксировать момент дотрагивания, перемещения и отпускания. К счастью нету необходимости делать это вручную так как в Android SDK есть класс делающий всю работу за нас. Таким образом для того чтоб распознать жест скроллинга, нужно добавить в наш класс поле типа GestureDetector которое инициализируется объектом реализующим интерфейс OnGestureListener (именно этот объект будет получать события скроллинга). Также нужно переопределить метод onTouchEvent в классе ImageViewer и передавать обработку событий из него в наш объект типа OnGestureListener. Измененный класс ImageViewer (без неизмененных методов) представлен ниже:
Как видно на самом деле ми наследуем MyGestureListener не от OnGestureListener, а от SimpleOnGestureListener. Последний класс просто реализует интерфейс OnGestureListener с помощью пустых методов. Этим мы избавляем себя от реализации всех методов, выбирая только те, что нужно.
Теперь если загрузить большую картинку, мы, по крайней мере, сможем скролить ее. Но: во первых мы можем проскроллить за рамки картинки, во вторых нету скроллбаров, которые бы подсказали нам где мы находимся и сколько еще осталось до краев.
Решим для начала вторую проблему. Поиск в Интернет приводит нас к тому, что нужно переопределить методы computeHorizontalScrollRange и computeVerticalScrollRange. Эти методы должны возвратить реальные размеры картинки (на самом деле есть еще методы которые имеют отношение к скроллбарам – это методы computeHorizontalScrollExtent, computeHorizontalScrollOffset и такая же пара для вертикального скроллбара. Если переопределить и их, то тогда возвращать можно более произвольные значения). Но этого оказывается недостаточно – скроллбары в первых нужно включить, во вторых проинициализировать. Включаются они методами setHorizontalScrollBarEnabled и setVerticalScrollBarEnabled, инициализируются методом initializeScrollbars. Но вот незадача – последний метод принимает немного непонятный параметр типа TypedArray. Этот параметр должен содержать в себе набор стандартных для View атрибутов. Список можно увидеть здесь в таблице «XML Attributes». Если бы мы создавали наш view из XML, Android runtime бы автоматически составил такой список. Но так как мы создаем класс программно, нужно также создать этот список программно. Для этого нужно создать файл attrs.xml в каталоге res\values с таким содержимым:
В файле просто перечислены все атрибуты, которые были указаны в таблице, упомянутой выше (кроме некоторых на которые указывает компилятор как на ошибку – видимо в документации список приведен самый последний). Измененный класс ImageViewer (кроме неизменных методов):
Не хотелось бы на этом останавливаться, поэтому давайте добавим поддержку жеста «бросок» (fling). Этот жест есть просто дополнение к жесту скроллинга, но учитывается скорость перемещения пальца в последние моменты (перед отпусканием), и если она не нулевая, скроллинг продолжается с постепенным затуханием. Поддержка этого жеста уже заложена в GestureDetector – поэтому нам нужно всего лишь переопределить метод onFling в классе MyGestureListener. Отловив это событие нам нужно еще некоторое время изменять положение скроллинга. Конечно, это можно сделать «вручную» с помощью таймеров или еще как, но опять же в Android SDK уже есть класс, реализующий нужный функционал. Поэтому нужно добавить в класс ImageViewer еще одно поле типа Scroller, которое и будет заниматься «остаточным» скроллингом – для старта скроллинга нужно вызвать его метод fling. Также нужно показать скроллбары (они ведь прячутся когда не нужны) вызовом метода awakenScrollBars. И последнее что нужно сделать – это переопределить метод computeScroll, который должен непосредственно делать скроллинг с помощью метода scrollTo (класс Scroller сам не занимается скроллингом – он просто работает с координатами). Код измененного класса ImageViewer представлен ниже:
В завершения разговора о жесте fling надо сделать одну мелочь – при прикосновении пальцем во время скроллинга от броска, нужно остановить скроллинг. На этот раз мы это сделаем «вручную» в методе onTouchEvent. Измененный метод представлен ниже:
Уже можно любоваться достаточно интересной физикой, но можно увидеть некоторые «глюки» при скроллинге за пределы картинки. Это происходит из-за того, что fling работает только в пределах картинки, а скроллинг без броска работает везде. Т.е. мы сможем выйти за рамки картинки только если очень плавно скролить (чтоб не срабатывал fling). Исправить этот «косяк» можно путем введения ограничение на обработку в метод onFling и обрабатывать бросок только если он не выходит за границы картинки. Измененный метод представлен ниже:
Теперь мы опять можем беспрепятственно скролить за рамки картинки. Кажется, эту проблему мы уже вспоминали… У нее есть элегантное решение, лежащее в том, что при отпускании пальца (при завершении скроллинга за рамками картинки) нужно картинку плавно вернуть в «положенное» место. И опять мы это сделаем «вручную» в методе onTouchEvent:
Вот теперь с уверенностью можно сказать что со скроллингом мы разобрались. Можем переходить к последнему жесту который хотелось бы реализовать – это жест pinch zoom.
Со стороны жест выглядит как растягивание или сжатие чего-то воображаемого на экране смартфона двумя пальцами. Пошагово жест происходит так: нажатие одним пальцем, нажатие вторым пальцем, изменение положения одного или двух пальцев не отпуская, отпускание второго пальца. Для определения величины масштабирования нужно вычислить соотношение между расстояниями между пальцами в момент начала жеста и в момент окончания жеста. Расстояние между пальцами находится по формуле sqrt(pow(x2 – x1, 2) + pow(y2 – y1, 2)). Также нужно отметить некоторое положение скроллинга которое нужно сохранять – ведь если жестом увеличить картинку, то положение скроллинга изменится (из-за измененного размера картинки). Это положение – а точнее точка, положение которой нужно сохранить, в терминологии Android SDK называется фокальной точкой, и находиться она посередине между двумя пальцами.
Реализовать жест как всегда можно самому, но и это к счастью уже реализовано в Android SDK (правда, только начиная с версии 2.2). Поможет в этом класс ScaleGestureDetector, инстанс которого добавим в наш класс. ScaleGestureDetector инициализируется обьектом, поддерживающим интерфейс OnScaleGestureListener, поэтому создадим также внутренний класс MyScaleGestureListener, который реализует методы onScaleBegin, onScale и onScaleEnd. Не забываем передать управление ScaleGestureDetector из метода onTouchEvent. Ну и самое главное – нужно как-то использовать данные масштабирования: их нужно учитывать во всех местах, где раньше фигурировали ширина и высота картинки (т.е. фактически нужно умножить эти параметры на коэффициент масштабирования). Финальный код класса ImageViewer можно посмотреть в исходниках.
На этом все. Надеюсь статься окажется полезной.
Источник