Custom View: mastering onMeasure
Jan 23, 2017 · 4 min read
Custom views are not just about onDraw. onMeasure can be equally important and here’s how…
If you have ever built a custom view on Android before, you probably know that there i s often no need to override onMeasure, but it’s not a bad idea to do it anyway: the default implementation is not aware of how much space your view actually takes (since it takes into account only a combination of the layout params, usually specified in the XML, and the minWidth / minHeight of our view), hence you could end up with your view taking up way more space than it actually needs to.
Let’s get started
Since we’re going to handle the measurement of the view we don’t need the call to super.onMeasure : when we decided to override onMeasure it becomes our duty to call setMeasuredDimension(int width, int height) so we don’t need the default implementation of the View class. While implementing our onMeasure we should keep in mind:
- The padding
- The minimum width and minimum height of our view (use getSuggestedMinimumWidth() and getSuggestedMinimumHeight() to get those values)
- The widthMeasureSpec and heightMeasureSpec which are the requirements passed to us by the parent
Note: getSuggestedMinimumWidth() differs from getMinimumWidth() in that it also accounts for the minimum width of the background drawable of the view, if it’s not null, and it returns the maximum of the two
So, the parameters we receive in our onMeasure , widthMeasureSpec and heightMeasureSpec, are compound bit shifted integer variables composed by a mode and a size (the structure was made this way to reduce objects allocation). We can make them explicit using:
While the size is simply the number of pixel, the mode is a more abstract concept. There are three possible modes:
- MeasureSpec.EXACTLY means our view should be exactly the size specified. This could happen when we use a fixed size (like android:layout_width=»64dp» ) or even match_parent (though the behaviour is the layout responsibility)
- MeasureSpec.AT_MOST means that our view can be as big as it wants up to specified size. This could happen when we use wrap_content or also match_parent
- MeasureSpec.UNSPECIFIED means that this view can take as much space as it wants. Sometimes this is used when the parent is trying to determine how big every child wants to be before calling measure again.
ResolveSize
So, until now we looked at the constraints provided by the parent. But how big do you want your view to be? You should take your time and ask yourself what does the size depend on. If you’re drawing text for example you could measure it (if you want to know more about it read this piece by Chris Banes). Once you obtained a desired width and height (don’t forget to take into account the padding), use the resolveSize(int size, int measureSpec) method which reconciles your demand with the parent constraint.
What resolveSize does is calling resolveSizeAndState and then deletes the optional bit (which can be MEASURED_STATE_TOO_SMALL ) from the result.
How does resolveSize / resolveSizeAndState pick the best option for you? It will return the size you calculated if mode is MeasureSpec.UNSPECIFIED , the size contained in measureSpec if MeasureSpec.EXACTLY , or the minimum between the two if MeasureSpec.AT_MOST .
If you think the resolveSize default behaviour does not suit you, you can always change it: the example method below behaves as resolveSize but also logs if the final result is smaller than the desiredSize.
As you can see, if specMode is equal to EXACTLY then we just set the specSize as result, else we use our minWidth summed with the padding. That’s basically the UNSPECIFIED case, but if size is AT_MOST we keep the minimum between this value and the specSize.
Remember that the onMeasure contract requires you to call the setMeasuredDimension(int width, int height) once you know how big your view should be. If you don’t, an IllegalStateException will be thrown.
So your onMeasure would look like this:
Additional tip
While debugging your custom view you can put this code at the beginning of your onMeasure :
Which will print something like this:
So you will always be aware of the constraint passed in from the parent and you’ll be able to see if your view is behaving accordingly.
Источник
Как реализовать метод View.onMeasure()?
onMeasure() задает размеры view и является важной частью контракта между view и layout. onMeasure() вызывается после вызова метода measure(). Обычно это делает layout для всех дочерних view, чтобы определить их размеры.
В некоторых случаях при реализации кастомной view требуется переопределить этот метод.
Для правильной реализации метода onMeasure() нужно сделать следующее:
1. onMeasure() вызывается с аргументами widthMeasureSpec и heightMeasureSpec . Это целочисленные значения в которых закодирована информация о предпочтениях layout к размерам view. На этом шаге вам нужно декодировать measure spec и получить значение размера и режим, определяющий как этот размер применять.
В следующем посте разберем measure spec подробнее.
2. Вычислить ширину и высоту view. При вычислении размеров необходимо учитывать значения паддингов и measure spec. Если вычисленный размер превышает measure spec, то layout выбирает что делать. Layout может обрезать view, добавить скроллинг, бросить исключение или вызвать onMeasure() еще раз с новыми значениями measure specs.
3. После вычисления ширины и высоты необходимо вызвать метод setMeasuredDimension(width: Int, height: Int). Если не вызвать этот метод, то будет брошен IllegalStateException .
Для правильной реализации метода onMeasure() необходимо учитывать значения параметров widthMeasureSpec и heightMeasureSpec , с которыми вызван onMeasure() .
Эти параметры имеют тип int , и представляют собой целые числа, в которых с помощью битового сдвига закодировано два значения: размер в пикселях и режим (mode) применения этого размера. Для значений measure specs используется тип int , а не специальный класс, чтобы сэкономить память, используемую при отрисовки UI.
Для получения значений размера и режима используются статические методы MeasureSpec.getSize(measureSpec: Int) и MeasureSpec.getMode(measureSpec: Int) соответственно.
Режим может иметь одно из трех значений:
UNSPECIFIED – у родителя нет предпочтений к размеру view, размер может быть произвольным. Иногда это значение используется лэйаутом при первом проходе для определения желаемых размеров каждой из view. После чего measure() вызывается еще раз, но уже с другим режимом.
EXACTLY – родитель определил и передал точный размер view. View будет иметь этот размер независимо от того, какого размера view хочет быть.
AT_MOST – родитель определил и передал верхнюю границу размера view. View может быть любого размера в пределах этой границы.
На втором шаге, описанном в первом посте, нужно определить желаемые размеры view. Тут нет универсального решения. Размер зависит от целей view и от того, что нужно отобразить. При определении размера не забывайте учитывать заданные паддинги. Паддинги получают методами getPadding. () .
После определения желаемого размера нужно сопоставить его с требуемыми measure specs. Для этого удобно использовать статический метод resolveSize(size: Int, measureSpec: Int). resolveSize() принимает параметрами желаемый размер и measure spec и возвращает новое значение размера. Если значение measuare spec EXACTLY , то resolveSize() возвращает размер, переданный в measure spec. Если AT_MOST , то возвращается минимальное значение из желаемого размера и размера measure spec. Если UNSPECIFIED , то resolveSize() возвращает желаемый размер.
На скриншоте реализация onMeasure() с использованием resolveSize() . calculateHeight() и calculateWidth() – это ваши методы, которые считают желаемые высоту и ширину.
Статья с более подробной информацией и примерами реализации onMeasure() .
Источник
Реализация Custom View-компонента в Android
Каждый день мы пользуемся разными приложениями и несмотря на их различные цели, большинство из них очень похожи между собой с точки зрения дизайна. Исходя из этого, многие заказчики просят специфичные, индивидуальные макеты внешний вид, который не воплотило ещё ни одно приложение, чтобы сделать своё Android приложение уникальным и отличающимся от других.
Если какая-то специфичная особенность, из тех которые просит заказчик, требует особые функциональные возможности, которые невозможно сделать с помощью встроенных в Android View-компонентов, тогда нужно реализовывать собственный View-компонент (Custom View). Это не значит, что нужно всё взять и бросить, просто потребуется некоторое время на его реализацию, к тому же это довольно интересный и увлекательный процесс.
Я недавно попал в похожую ситуацию: мне нужно было создать индикатор страниц для Android ViewPager. В отличи от iOS, Android не предоставляет такой View-компонент, поэтому мне пришлось делать его самому.
Я потратил довольно много времени на его реализацию. К счастью, этот Custom View-компонент можно использовать и в других проектах, поэтому чтобы сэкономить личное время и время других разработчиков, я решил оформить всё это дело в виде библиотеки. Если вам нужен похожий функционал и не хватает времени на его реализацию собственными силами, можете взять его с этого GitHub репозитория.
Рисуем!
Так как в большинстве случаев разработка Custom View-комопонента занимает больше времени, чем работа с обычными View-компонентами, создавать их целесообразно только тогда, когда нет более простого способа реализовать специфичную особенность, или когда у вас есть ниже перечисленные проблемы, которые Custom View-компонент может решить:
- Производительность. Если у вас есть много View-компонентов в одном layout-файле и вы хотите оптимизировать это с помощью создания единственного Custom View-компонента;
- Большая иерархия View-компонентов, которая сложна в эксплуатации и поддержке;
- Полностью настраиваемый View-компонент, которому нужна ручная отрисовка;
Если вы ещё не пробовали разрабатывать Custom View, то эта статья — отличная возможность окунуться в эту тему. Здесь будет показана общая структура View-компонента, как реализовывать специфичные вещи, как избежать распространённые ошибки и даже как анимировать ваш View-компонент!
Первая вещь, которую нам нужно сделать, это погрузиться в жизненный цикл View. По какой-то причине Google не предоставляет официальную диаграмму жизненного цикла View-компонента, это довольно распространённое среди разработчиков непонимание, поэтому давайте рассмотрим её.
Constructor
Каждый View-компонент начинается с Constructor’а. И это даёт нам отличную возможность подготовить его, делая различные вычисления, устанавливая значения по умолчанию, ну или вообще всё что нам нужно.
Но для того чтобы сделать наш View-компонент простым в использовании и установке, существует полезный интерфейс AttributeSet. Его довольно просто реализовать и определённо стоит потратить на это время, потому что он поможет вам (и вашей команде) в настройке вашего View-компонента с помощью некоторых статических параметров на последующих экранах. Во-первых, создайте новый файл и назовите его «attrs.xml». Этот файл может содержать все атрибуты для различных Custom View-компонентов. Как вы можете видеть в этом примере есть View-компонент названный PageIndicatorView и один атрибут piv_count.
Во-вторых, в конструкторе вашего View-компонента, вам нужно получить атрибуты и использовать их как показано ниже.
- При создании кастомных атрибутов, добавьте простой префикс к их имени, чтобы избежать конфликтов имён с другими View-компонентами. Обычно добавляют аббревиатуру от названия View-компонента, поэтому у нас префикс «piv_»;
- Если вы используете Android Studio, то Lint будет советовать вам использовать метод recycle() до тех пор пока вы сделаете это с вашими атрибутами. Причина заключается в том, что вы можете избавиться от неэффективно связанных данных, которые не будут использоваться снова;
onAttachedToWindow
После того как родительский View-компонент вызовет метод addView(View), этот View-компонент будет прикреплён к окну. На этой стадии наш View-компонент будет знать о других View-компонентах, которые его окружают. Если ваш View-компонент работает с View-компонентами пользователя, расположенными в том же самом «layout.xml» файле, то это хорошее место найти их по идентификатору (который вы можете установить с помощью атрибутов) и сохранить их в качестве глобальной ссылки (если нужно).
onMeasure
Этот метод означает, что наш Custom View-компонент находится на стадии определения собственного размера. Это очень важный метод, так как в большинстве случаев вам нужно определить специфичный размер для вашего View-компонента, чтобы поместиться на вашем макете.
При переопределении этого метода, всё что вам нужно сделать, это установить setMeasuredDimension(int width, int height).
При настройке размера Custom View-компонента вы должны обработать случай, когда у View-компонента может быть определённый размер, который пользователь (прим. переводчика: программист работающий с вашим View-компонентом) будет устанавливать в файле layout.xml или программно. Для вычисления этого свойства, нужно проделать несколько шагов:
- Рассчитать размер необходимый для содержимого вашего View-компонента (ширину и высоту);
- Получить MeasureSpec вашего View-компонента (ширину и высоту) для размера и режима;
- Проверить MeasureSpec режим, который пользователь устанавливает и регулирует (для ширины и высоты);
Посмотрите на значения MeasureSpec:
- MeasureSpec.EXACTLY означает, что пользователь жёстко задал значения размера, независимо от размера вашего View-компонента, вы должны установить определённую ширину и высоту;
- MeasureSpec.AT_MOST используется для создания вашего View-компонента в соответствии с размером родителя, поэтому он может быть настолько большим, насколько это возможно;
- MeasureSpec.UNSPECIFIED — на самом деле размер обёртки View-компонента. Таким образом, с этим параметром вы можете использовать желаемый размер, который вы расчитали выше.
Перед установкой окончательных значений в setMeasuredDimension, на всякий случай проверьте эти значения на отрицательность Это позволит избежать любых проблем в предпросмотре макета.
onLayout
Этот метод позволяет присваивать размер и позицию дочерним View-компонентам. У нас нет дочерних View-компонентов, поэтому нет смысла переопределять этот метод.
onDraw
Вот здесь происходит магия. Два объекта, Canvas и Paint, позволяют вам нарисовать всё что вам нужно. Экземпляр объекта Canvas приходит в качестве параметра для метода onDraw, и по существу отвечает за рисование различных фигур, в то время как объект Paint отвечает за цвет этой фигуры. Простыми словами, Canvas отвечает за рисование объекта, а Paint за его стилизацию. И используется он в основном везде, где будет линия, круг или прямоугольник.
Создавая Custom View-компонент, всегда учитывайте, что вызов onDraw занимает довольно много времени. При каких-то изменениях, сроллинге, свайпе вы будете перерисовывать. Поэтому Andorid Studio рекомендует избегать выделение объекта во время выполнения onDraw, вместо этого создайте его один раз и используйте в дальнейшем.
- При отрисовке, имейте в виду переиспользование объектов вместо создания новых. Не полагайтесь на вашу IDE, которая должна подсветить потенциальную проблему, а сделайте это самостоятельно, потому что IDE может не увидеть этого, если вы создаёте объекты внутри методов вызываемых в onDraw;
- Во время отрисовки, не задавайте размер прямо в коде. Обрабатывайте случай, когда у других разработчиков может быть тот же самый View-компонент, но с другим размером, поэтому делайте ваш View-компонент зависимым от того размера, который ему присвоен;
View Update
Из диаграммы жизненного цикла View-компонента, вы можете заметить что существует два метода, которые заставляют View-компонент перерисовываться. Методы invalidate() и requestLayout() могут помочь вам сделать ваш Custom View-компонент интерактивным, что собственно поможет изменять его внешний вид во время выполнения. Но почему их два?
Метод invalidate() используется когда просто нужно перерисовать View-компонент. Например, когда ваш View-компонент обновляет свой текст, цвет или обрабатывает прикосновение. Это значит, что View-компонент будет вызывать только метод onDraw, чтобы обновить своё состояние.
Метод requestLayout(), как вы можете заметить будет производить обновление View-компонента через его жизненный цикл, только из метода onMeasure(). А это означает, что сразу после обновления View-компонента вам нужно его измерить, чтобы отрисовать его в соответствии с новыми размерами.
Animation
Анимации в Custom View-компонентах, это по кадровый процесс. Это означает, что если вы например захотите сделать анимированным процесс изменения радиуса круга от маленького к большому, то вам нужно увеличивать его последовательно и после каждого шага вызывать метод invalidate для отрисовки.
Ваш лучший друг в анимации Custom View-компонентов — это ValueAnimator. Этот класс будет помогать вам анимировать любые значения от начала до конца и даже обеспечит поддержку Interpolator (если нужно).
Не забывайте вызывать метод Invalidate каждый раз, когда изменяется значение анимации.
Надеюсь, что эта статья поможет вам сделать свой первый Custom View-компонент. Если вы захотите получить больше информации по этой теме, то может посмотреть хорошее видео.
Источник