Как перерисовать view android

Введение

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

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

Если мы реализуем кнопки и установку новых данных, а потом запустим приложение, то мы не увидим никаких изменений в графике. Метод setChartData() устанавливает новые данные, но view не отображает их. Почему? Потому что мы забыли сообщить view компоненту о том, что это надо сделать. Перерисовка view выполняется с помощью метода invalidate(). Если мы добавим в метод setChartData() вызов метода invalidate() — это решит проблему, график будет обновляться при смене данных. Несмотря на полученный результат, можно сделать view компонент еще лучше, добавив анимацию, возникающую при смене данных.

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

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

Dynamics класс

Для решения этой задачи, мы будем использовать класс, который я назвал Dynamics. Объект Dynamics — это, по сути точка, которая имеет позицию и скорость. У нее также есть конечная позиция, куда она в итоге должна придти, и метод update(), который обновляет позицию и скорость . В сокращенном виде класс выглядит так.

Наибольший интерес здесь представляет метод update(). В первой строке метода выполняется вычисление временного интервала, прошедшего с момента последнего обновления. Чтобы избежать возможных сбоев в анимации, максимально возможное время ограничено 50 мс.

В следующей строке выполняется обновление скорости движения точки, величина которой зависит от удаления до конечной позиции и коэффициента упругости (springiness). Далее обновленное значение скорости умножается на коэффициент затухания, который принимает значения от 0 до 1. Если не использовать коэффициент затухания, точка будет продолжать двигаться всегда. Можно воспринимать эту часть как амортизатор, который гасит колебания в подвеске.

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

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

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

Метод возвращает true, если точка находится в конечной позиции и если скорость равна нулю. Поскольку проверять равенство двух float значений не очень хорошая идея, мы проверяем насколько эти значения близки к друг другу. В данной случае TOLERANCE равна 0,01 и для нас такой точности достаточно.

Использование dynamics

Чтобы использовать класc dynamics, нужно обновить view компонент, который мы создавали в предыдущей части. Для этого нужно поменять тип datapoints массива в тех методах, где он используется. Это несложно. Например, разница между старым и новым методами drawLineChart() будет выглядеть так.

В последнем случае для получения новой точки мы используем функцию getPosition().

Остальные изменения лучше посмотреть в коде проекта.

Запуск анимации

Теперь нам нужно решить, как обновлять позиции точек и перерисовывать экран. Я буду использовать для этого объект типа Runnable. Runnable – это интерфейс с методом run(). Любой объект, реализующий этот интерфейс, может быть запущен в отдельном потоке с помощью класса Thread. В данном случае, мы будет запускать задачу в потоке пользовательского интерфейса (UI-thread), поскольку только из него можно обновлять view компоненты.

Читайте также:  Zombie catchers моды андроид

Код будет выглядеть так:

Я создал объект типа Runnable и реализовал метод run(), в теле которого выполняется обновление позиций точек. Если хотя бы одна из точек продолжает движение, метод run() будет перезапускаться с небольшой задержкой. Перезапуск выполняется с помощью метода postDelayed(..). В конце метода run() вызывается метод invalidate(), который сообщает системе о необходимости перерисовки view компонента.

Что случится, если следующее обновление позиций точек выполнится до того как произойдет предыдущая перерисовка экрана? Теоретически это возможно, поскольку мы не можем контролировать этот процесс. По сути, ничего плохого не произойдет, потому что runnable объект «заворачивается» в сообщение и добавляется к очереди объекта Looper, обрабатываемого в UI потоке. То же самое происходит и при вызове метода ivalidate(). Looper обрабатывает сообщения в порядке поступления, поэтому перерисовка компонента будет выполняться корректно.

Комбинация dynamiсs и runnable объектов — удобный способ реализации анимации, который легко настраивать и легко расширять. Я регулярно использую этот паттерн с View и ViewGroup объектами. Обычно я реализую базовых функционал view компонента, а после того как все отлажу, добавляю анимацию, используя описанный выше подход.

Для запуска анимации я изменил метод setChartData(). Давайте посмотрим на него.

Метод обрабатывает две ситуации. Если у нас не было данных или новые данные отличаются по числу точек, мы создаем новый массив объектов типа Dynamics и инициализируем их. Текущую и конечную позиции точки мы устанавливаем равной Y значению, а скорость — 0. После инициализации всех точек, view компонент перерисовывается с помощью метода invalidate(). Анимация в этом случае не выполняется, поскольку изменения количества точек требует более сложного алгоритма.

С другой стороны, если у нас уже есть набор данных, нам нужно только поменять конечные позиции точек и запустить анимацию. Поскольку анимация уже может быть запущенной, мы сначала удаляем объект animator из очереди сообщений с помощью метода removeCallbacks(). Я бы рекомендовал вам всегда делать это перед запуском новой анимации. Ну а далее мы вызываем метод post(), передав ему Runnable объект, в данном случае это наш animator. Метод invalidate() здесь вызывать не нужно, потому что текущие позиции точек не изменились.

Сглаживание

Последний штрих. Вам не кажется, что график выглядит немного грубовато? Давайте поменяем методику создания объекта Path. Если использовать вместо метода lineTo() метод cubicTo(), можно нарисовать график кривыми Безье, правда это требует вычисления двух дополнительных контрольных точек.

Хороший способ получить эти точки — расположить первую контрольную точку на прямой проходящей через начальную точку i и имеющую наклон как прямая между точками i-1 и i+1. Вторая контрольная точку должна быть на прямой, проходящей через точку i+1 и имеющая такой же наклон как прямая между точками i и i+2. Добавив таким образом контрольных точек, мы отобразим наш график в виде непрерывной кривой.

На рисунке ниже можно видеть конечный результат.

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

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

Если вы запустите данный пример на эмуляторе, то увидите, что здесь такой проблемы нет. Изменение конечной позиции анимации не меняет текущую позицию или скорость напрямую. Анимация будет менять направление со временем, плавно двигаясь к новой конечной позиции.

Читайте также:  Учим алфавит для андроид

Тестовый проект для этой статьи можно скачать с GitHub.
По материалам сайта jayway.
Вольный перевод — Pavel Bobkov

Источник

Рисование собственных представлений (View) в Android

Получите полный контроль над представлением и оптимизируйте его производительность

В преддверии старта курса «Android Developer. Professional» приглашаем всех желающих принять участие в открытом вебинаре на тему «Пишем gradle plugin».

А пока делимся переводом полезного материала.

Введение

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

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

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

Зачем создавать собственные представления?

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

Производительность: в вашем макете много представлений и вы хотите оптимизировать их, нарисовав одно, более легкое собственное представление.

Имеется сложная иерархия представлений, которую трудно использовать и поддерживать.

Необходимо создать специализированное представление, требующее рисования вручную.

Общий подход

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

Создать класс, расширяющий базовый класс или подкласс представления.

Реализовать конструкторы, использующие атрибуты из XML-файла.

Переопределить некоторые методы родительского класса (onDraw(), onMeasure() и т. д.) в соответствии с нашими требованиями.

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

Пример

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

Шаг 1. Создадим класс с именем CircularTextView .

Шаг 2. Расширим класс виджета TextView. Здесь под TextView в IDE выдается ошибка, в которой сообщается, что у этого типа есть конструктор и он должен быть инициализирован.

Шаг 3. Добавим конструкторы в класс.

Это можно сделать двумя способами.

Первый способ добавления конструкторов в класс показан ниже.

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

Часто нас сбивает с толку то, что у представления есть несколько разных типов конструкторов.

View(Context context)

Простой конструктор для динамического создания представления из программного кода. Здесь параметр context — это контекст, в котором работает представление и через который можно получить доступ к текущей теме, ресурсам и т. д.

View(Context context, @Nullable AttributeSet attrs)

Конструктор, который вызывается при формировании представления из XML-файла. Он вызывается, когда представление создается из XML-файла, содержащего атрибуты представления. В этом варианте конструктора используется стиль по умолчанию (0), поэтому применяются только те значения атрибутов, которые есть в теме контекста и заданном наборе AttributeSet .

Шаг 4. Самый важный шаг в отрисовке собственного представления — это переопределение метода onDraw() и реализация необходимой логики отрисовки внутри этого метода.

Метод OnDraw (canvas: Canvas?) имеет параметр Canvas (холст), с помощью которого компонент представления может отрисовывать себя. Для рисования на холсте необходимо создать объект Paint.

Как правило, процесс рисования определяется двумя аспектами:

что рисовать (определяется объектом Canvas);

как рисовать (определяется объектом Paint).

Например, Canvas предоставляет метод для рисования линии, а Paint предоставляет методы для определения цвета этой линии. В нашем случае объект Canvas в классе CircularTextView предоставляет метод для рисования окружности, а объект Paint заполняет ее цветом. Проще говоря, Canvas определяет, какие фигуры можно нарисовать на экране, а Paint определяет свойства нарисованных фигур — цвет, стиль, шрифт и т. д.

Читайте также:  Pixel dungeon для android

Давайте займемся кодом. Мы создаем объект Paint и присваиваем ему некоторые свойства, а затем рисуем фигуру на холсте (объект Canvas), используя наш объект Paint. Метод onDraw() будет выглядеть так:

IDE показывает предупреждение о том, что следует избегать выделения объектов во время операций отрисовки или операций с макетом. Это предупреждение возникает потому, что метод onDraw() много раз вызывается при отрисовке представления, в котором каждый раз создаются ненужные объекты. Поэтому, чтобы избежать ненужного создания объектов, мы вынесем соответствующую часть кода за пределы метода onDraw() , как показано ниже.

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

Шаг 5. Мы закончили с рисованием. Теперь давайте внесем этот класс представления в XML.

Добавьте этот XML-макет в вашу активность (Activity) и запустите приложение. Вот что будет на экране.

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

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

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

Неплохо, правда? Теперь давайте добавим контур к кружку. Контур будет задаваться двумя входными параметрами: шириной линии контура и ее цветом. Чтобы задать цвет линии контура, нам нужно создать объект Paint точно так же, как мы это делали для кружка. Чтобы задать ширину линии контура, мы создадим переменную, установим для нее нужное значение и используем его в методе onDraw() . Полный код будет выглядеть так:

Теперь в активности можно динамически настраивать эти атрибуты нужным образом.

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

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

Для начала создадим файл с именем attrs.xml в папке values. Этот файл будет содержать все атрибуты для различных представлений, которые мы создаем сами. В приведенном ниже примере у нашего представления под названием CircularTextView имеется атрибут ct_circle_fill_color , который принимает значение цвета. Аналогичным образом мы можем добавить и другие атрибуты.

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

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

В моем случае результат был таким:

Примечание. При рисовании не задавайте жестко размер вашего представления, так как им могут воспользоваться другие разработчики с применением других размеров. Рисуйте представление в соответствии с его текущим размером.

Обновление представления

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

invalidate()

invalidate() — это метод, который инициирует принудительную перерисовку определенного представления. Проще говоря, метод invalidate() следует вызывать в случае, когда требуется изменение внешнего вида представления.

requestLayout()

Если в какой-то момент происходит изменение состояния представления, то метод requestLayout() сообщает системе представлений, что необходимо сделать перерасчет фаз «измерение» (Measure) и «макет» (Layout) для данного представления (измерение → макет → рисование). Проще говоря, метод requestLayout() следует вызывать в случае, когда требуется изменение границ представления.

Теперь, я надеюсь, вы знаете в общих чертах, как создавать собственные представления. Чтобы они демонстрировали отличную производительность, необходимо освоить все описанные здесь методы.

Источник

Оцените статью