Главная » Разработка для Android » РИСОВАНИЕ ДВУХМЕРНОЙ И ТРЕХМЕРНОЙ ГРАФИКИ – программирование Android

0

 

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

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

СОЗДАНИЕ СОБСТВЕННЫХ ВИДЖЕТОВ

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

Можно достичь многого и не создавая новых виджетов. В этой книге мы уже сделали несколько приложений, состоящих только из готовых виджетов, а также простых подклассов данных виджетов. Код этих приложений просто выстраивает деревья видов, компонуя их в коде или загружая из макетов, хранящихся в XML-файлах ресурсов.

Непростое приложение Microjobs, которое мы рассмотрим, имеет вид, который содержит список названий, соответствующих пунктам на карте. По мере того как на карту добавляются новые пункты, новые виджеты с названиями пунктов динамически добавляются в список. Даже в этом динамически изменяемом макете используются только готовые виджеты; новые виджеты здесь не создаются. Технологии Microjobs, образно говоря, добавляют или удаляют рамки из такого дерева, как показано на рис. 7.3.

Мы покажем, как создать собственный виджет. Для этого придется заглянуть «под капот» класса View. TextView, Button и DatePicker – примеры виджетов, имеющихся в инструментарии для создания пользовательских интерфейсов Android. Вы можете реализовать собственный виджет в качестве подкласса от одного из перечисленных выше виджетов либо как прямой подкласс от View.

Более сложный виджет (такой, в который могут вкладываться новые виджеты) должен быть подклассом ViewGroup, который, в свою очередь, является подклассом View. Очень сложный виджет, используемый, например, как инструмент интерфейса и реализуемый в нескольких местах (и даже несколькими приложениями), может представлять собой целый пакет классов, лишь один из которых является потомком View.

Посвящена графике, а значит, компоненту View (Вид) из паттерна «Модель-вид-контроллер» (MVC). Виджеты также содержат код контроллера, и такое решение является хорошим, поскольку в таком случае весь код, важный для поведения и его представления на экране, располагается в одном месте.

Поэтому, сосредоточившись на графической информации, мы сможем разбить задачи, на две важные части: нахождение пространства на экране и рисование в данном пространстве. Первая задача связана с компоновкой, или построением макета (layout). «Листовой» виджет может сообщать о том, что ему требуется пространство, определяя метод onMeasure, который будет вызываться фреймворком пользовательского интерфейса Android в нужное время. Вторая задача – отображение виджета как таковое – обрабатывается методом onDraw, относящимся к виджету.

Макет

Большая часть сложной работы в механизме построения макетов во фреймворке Android реализуется при помощи контейнерных видов. Контейнерный вид, как понятно из названия, содержит другие виды. Такие виды являются внутренними узлами в дереве видов и в подклассах ViewGroup. Инструментарий фреймворка предоставляет сложные контейнерные виды, предлагающие мощные и адаптируемые стратегии размещения информации на экране. Например, часто применяются контейнерные виды LinearLayout и RelativeLayout. Пользоваться ими обоими относительно просто, а вот правильно реализовать самостоятельно – очень сложно. Поскольку удобные и мощные контейнерные виды и так существуют, вам, возможно, никогда не придется самостоятельно реализовывать такой вид или его алгоритм компоновки, о котором здесь пойдет речь. Но понимание того, как все это работает (как фреймворк пользовательского интерфейса Android управляет процессом компоновки), поможет вам писать правильные и надежные виджеты.

В примере 9.1 показан, пожалуй, простейший рабочий виджет, который может написать каждый. При добавлении такого виджета в дерево видов какой-нибудь активности (Acti vity) он будет заполнять выделенное ему пространство голубым цветом. Не очень интересно, но прежде, чем перейти к созданию чего-нибудь более сложного, внимательно рассмотрим, как в этом простом примере решаются две основные задачи компоновки и отрисовки. Начнем с процесса компоновки; рисование будет описано ниже, в подразделе «Рисование с применением Canvas (холста)».

Пример 9.1. Простейший виджет

Динамический макет требуется потому, что потребность виджета в свободном пространстве также изменяется динамически. Предположим, например, что виджет в программе с поддержкой GPS-навигации отображает название города, к которому вы в настоящий момент едете на машине. Когда вы направляетесь из Эли в Пост Миллз, виджет получает уведомление об изменении местоположения. Но когда программа собирается переписать название города, то обнаруживает, что на экране мало места и название нового города там не умещается. Программа должна послать экрану запрос на перерисовку, так, чтобы для названия города освободилось больше места, если это возможно.

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

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

Метод requestLayout заставляет фреймворк пользовательского интерфейса Android поставить событие в очередь событий пользовательского интерфейса. Когда каждое из этих событий будет обработано в порядке очереди, фреймворк предоставит всем контейнерным видам возможность запросить у каждого из его дочерних виджетов, сколько места нужно этому виджету для отрисовки. Этот процесс делится на два этапа: измерение дочерних видов и последующее упорядочение их в новых позициях. Первую фазу должны реализовывать все виды, вторая же важна только для реализаций контейнерных видов, которым потребуется управлять компоновкой их дочерних видов.

Измерение

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

Например, самый верхний LinearLayout опрашивает каждый из вложенных виджетов LinearLayout о том, какие размеры ему следует иметь. Эти виджеты, в свою очередь, опрашивают содержащиеся в них кнопки (Button) или виджеты EditText относительно их размеров. Каждый дочерний элемент сообщает об оптимальном для себя размере родительскому элементу. Затем родительские элементы складывают данные, полученные от дочерних элементов, плюс все отступы, которые передает им родительский элемент, и сообщают сумму самому верхнему LinearLayout.

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

Аргументы метода onMeasure описывают пространство, которое готов выделить родительский элемент: указывают высоту и ширину. Эти величины выражаются в пикселах. Фреймворк предполагает, что ни один вид не может быть по размеру меньше 0 и больше 230 пикселов, поэтому использует старшие двоичные разряды переданного параметра іnt для кодирования режима спецификации измерений (measurement specification mode). Ситуация такова, как если бы onMeasure вызывался с четырьмя аргументами: режимом спецификации ширины, шириной, режимом спецификации высоты и высотой. Не пытайтесь сами выполнять смещение битов для разделения пар аргументов! Вместо этого следует использовать статические методы MeasureSpec. getMode и MeasureSpec. getSize.

Режимы спецификацци описывают, как контейнерный вид требует от дочернего вида интерпретировать размеры, переданные ему. Таких режимов три:

MeasureSpec. EXACTLY – контейнерный вид, выполняющий вызов, уже определил точный размер дочернего вида;

MeasureSpec.AT_MOST – контейнерный вид, выполняющий вызов, задал максимальное значение для этого параметра, но дочерний вид может запросить и меньше места;

MeasureSpec.UNSPECIFIED – контейнерный вид, выполняющий вызов, не задал дочернему виду никаких ограничений; дочерний вид может запросить любой требуемый размер.

Виджет всегда должен сам сообщить своему родительскому элементу в дереве видов, сколько ему (виджету) требуется места. Это делается путем вызова метода setMeasuredDi mensi ons, задающего значения высоты и ширины. Родительский элемент может позже получить данные об этих свойствах при помощи методов getMeasuredHei ght и getMeasuredWidth. Если ваша реализация переопределяет onMeasure, но не вызывает setMeasuredDimensions, то метод measure не завершится нормально, а выдаст исключение LegalStateException.

Стандартная реализация onMeasure, наследуемая от View, вызывает setMeasuredDimensіons с одним из двух значений для каждого направления. Если родительский узел задает режим MeasureSpec. UNSPECIFIED, то метод setMeasuredDimensions, относящийся к дочернему элементу, использует стандартный размер вида. В таком качестве применяется значение, получаемое или от getSuggestedMinimumWidth, или от getSuggestedMinimumHeight. Если родительский элемент использует один из двух других режимов спецификации, то стандартная реализация применяет размер, предложенный родительским элементом. Это очень целесообразная стратегия, и она позволяет типичной реализации виджета полностью прорабатывать весь этап измерений, просто задавая значения, возвращенные методами getSuggestedMinimumWidth и getSuggestedMinimumHeight. Именно такой минималистский прием мы использовали в примере 9.1.

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

Контейнерный вид полностью контролирует расположение своих дочерних элементов. В вышеописанных обстоятельствах он может «поступить справделиво» и выделить каждому дочернему элементу по 33 % доступной ширины. С тем же успехом он может отдать 50 пикселов самому левому виджету и по 50 – двум остальным. И наконец он может отвести все 100 пикселов единственному виджету, а двум остальным ничего не оставить. Но независимо от метода именно родительский элемент определяет размер и положение ограничивающей рамки для каждого дочернего элемента.

Другой вариант того, как контейнерный вид управляет пространством, отводимым каждому виджету, понятен из примера 9.1. Виджет, показанный в этом примере, всегда запрашивает столько пространства, сколько ему требуется, независимо от того, сколько места ему предлагается (в отличие от стандартной реализации). Такой стратегией удобно пользоваться при работе с виджетами, которые будут добавляться к контейнерам из стандартного инструментария, в частности к контейнеру LinearLayout. Такие контейнеры реализуют свойство притяжения (gravity). Гравитация – это свойство, используемое в некоторых видах для указания выравнивания своих подэлементов. Когда вы начнете работать с одним из этих контейнеров, вас, возможно, удивит одно обстоятельство. Оказывается, что по умолчанию отрисовывается только первый из созданных вами виджетов! Чтобы исправить это, можно либо изменить свойство притяжения на Gravity.FILL при помощи метода setGravity, либо заставить виджеты «настаивать» на получении именно такого количества пространства, которое им требуется.

Необходимо также отметить, что контейнерный вид может несколько раз вызывать метод measure дочернего элемента за один этап измерений. При реализации onMeasure правильно сделанный контейнерный вид, пытающийся построить горизонтальный ряд виджетов, может, например, вызвать метод measure каждого дочернего виджета в режиме MEASURE_SPEC. UNSPECIFIED и с шириной 0, чтобы определить, какой размер «предпочтет» тот или иной виджет. Собрав, таким образом, предпочтительные значения ширины для всех своих дочерних элементов, он может сравнить сумму этих значений с шириной, доступной в данной ситуации (эта ширина указывалась в вызове, посланном от родительского элемента к методу measure). Теперь родительский элемент снова может вызвать метод measure каждого дочернего виджета, но на этот раз – уже в режиме MeasureSpec. AT_MOST с указанием ширины, которая пропорционально лучше всего подходит каждому виджету с учетом имеющегося пространства. Поскольку measure можно вызывать несколько раз, реализация onMeasure должна быть идемпотентной и не менять состояния приложения.

Понятно, что реализация метода onMeasure с контейнерным видом обычно довольно сложна. ViewGroup, суперкласс всех контейнерных видов, не дает стандартной реализации этого метода. В каждом контейнерном виде фреймворка пользовательских интерфейсов Android такая реализация – собственная. Если вы подумываете реализовать контейнерный вид, можно выбрать один из таких методов в качестве стандартного и ориентироваться на него. Если же, напротив, вам придется выполнять измерения на ходу, то, скорее всего, вам нужно будет вызывать метод measure для каждого дочернего элемента, а также испробовать на практике вспомогательные методы ViewGroup: measureChild, measureChildren и measureChildWithMargins. При завершении этапа измерений контейнерный вид, как и любой другой виджет, должен сообщить о том, сколько места ему требуется. Это делается путем вызова setMeasuredDimensions.

Упорядочение

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

Поскольку относящийся к виду метод onMeasure можно вызывать несколько раз, фреймворк должен использовать иной метод, чтобы сигнализировать, что данный этап измерений завершен и что контейнерные виды должны окончательно зафиксировать положение своих дочерних элементов. Как и этап измерений, этап упорядочения реализуется при помощи двух методов. Фреймворк активирует финальный метод 1 ayout в верхней точке дерева видов. Метод 1 ayout обрабатывает все виды по общему принципу, а потом вызывает метод onLayout. Пользовательские виджеты переопределяют метод onLayout, чтобы реализовывать собственные поведения. Пользовательская реализация onLayout должна как минимум рассчитать размеры рабочего прямоугольника, который будет предоставляться для каждого дочернего элемента при отрисовке, и, в свою очередь, активировать метод layout для каждого дочернего элемента (ведь этот элемент сам по себе может быть родительским для других виджетов). Этот процесс может быть довольно сложен. Если вашему виджету требуется упорядочивать дочерние виды, попробуйте взять в качестве основы для него уже имеющийся контейнер, например LinearLayout или RelativeLayout.

Еще раз отметим, что виджет не обязательно получает столько пространства, сколько запрашивает. Он должен быть способен отрисоваться независимо от того, сколько именно пространства ему отводится. Если он пытается отрисовываться вне того пространства, которое ему выделил родительский элемент, то рисунок будет обрезаться прямоугольником отсечения (clip rectangle). Чтобы обеспечить филигранный контроль (то есть, например, чтобы виджет заполнял ровно столько пространства, сколько ему выделено), нужно либо реализовать onLayout и записать параметры отведенного пространства, либо следить за прямоугольником отсечения Canvas, аргументом onDraw.

Источник: Android. Программирование на Java для нового поколения мобильных устройств

По теме:

  • Комментарии