Custom view android kotlin пример

Exploring Kotlin initialization with Android custom views

A closer look at the relationship between Kotlin and programmatic/dynamic view inflation

Today, we explore the question “Where does initialization occur around the lifecycle of a view?” This is topic my teammates hotly debated over for months, so I’ve decided to see for myself. This blurb covers two ways of inflating a custom view for comparison: via layout resource and programmatically.

For this exploration, we look at a small compass application. For convenience, you can download the code on Github to follow along/run the app on your own device.

This app uses View Binding and a custom view to spin the needle based on where the user’s device is facing. The two different cases exist on different branches from the main branch.

When are Kotlin constructors and init blocks called?

Like Java, Kotlin can declare multiple constructors, but makes a differentiation between primary and secondary constructors. Both are denoted with the keyword constructor .

In most cases, a Kotlin class will only use a primary constructor. If there are no visibility modifiers or annotations, then a class can omit the constructor keyword. A Kotlin class without a primary constructor exists will generate one that does nothing.

A primary constructor may also include an initializer block denoted with the keyword init . The initializer block executes the logic as soon as the class is initialized as part of the primary constructor.

Secondary constructors are mostly used for Java interoperability. For the case of CompassView below, no primary constructor is declared, but multiple secondary constructors are:

These secondary constructors will delegate to the proper constructor in the super class, or find one that does. But in what order, specifically, would a secondary constructor execute in relation to the primary constructor?

From Kotlin official documentation:

Delegation to the primary constructor happens as the first statement of a secondary constructor, so the code in all initializer blocks and property initializers is executed before the secondary constructor body.

In CompassView , the initialization block executes before the secondary constructors do. But where does Kotlin class initialization fall in relation to the View lifecycle, exactly? We answer this by examining how a view is inflated.

What happens when a view is created?

The answer depends on how a view is added in a tree. On the screen, all views in Android exists in a single tree. You can add views to that tree two ways:

  • Programmatically: adding a View to that tree.
  • XML: specifying another tree to via an Android layout file.

In our compass application, we inflate our custom CompassView on to the MainActivity as the only custom view component. This article demonstrates the differences between inflating a custom view both via XML ( CompassView(context, attrs) ) and code( CompassView(context) ).

Case 1: Custom Kotlin View inflated via XML

When you put a view element into an XML file, Android will use a LayoutInflater to parse and map the corresponding objects in the XML to the inflated Views. LayoutInflater retrieves the resource by opening up ResourceManager and examining all the possible layouts it could match within its current configuration. Android will then parse back the appropriate binary XML resource.

For CompassView(context, attrs) the second argument will utilize the properties necessary to tell the view hierarchy how to size and place the element in the tree. This post does not focus on these phases (Measure/Layout/Draw) of the View lifecycle, but there is a totally awesome talk from Drawn out: How Android renders (Google I/O ’18) that dives deep down into the mechanisms for whomever is curious.

Читайте также:  Какие смартфоны будут без андроида

This process of inflation is then repeated for its children and its children’s children, recursively until all views have been inflated. When inflation is complete, LayoutInflater sends a callback onFinishInflate() from the children views back up to the root to indicate the view is ready to interact with.

Now that we’ve described the process a bit more, let’s examine the code needed to instantiate our custom CompassView with XML:

In MainActivity , the CompassView via the view binding reference from the XML. When initializing the custom view this way, the default secondary constructor is executed:

The Kotlin initialization block is called first, then the secondary constructor CompassView(context, attrs) . Remember, there is no primary constructor for extending a View class (unless you create one yourself), so it makes sense that init is executed first before the secondary constructor does. Because calling CompassView(context, attrs) uses LayoutInflater , the onFinishInflate() callback is made when all views have finished inflating.

Case 2: Custom Kotlin View inflated programmatically

For the most part, initializing views via XML is the preferred way of creating view elements in Android since it becomes part of the tree view hierarchy. There are advantages to this: using XML is much more friendly for Android memory since it can compress easily, and helps to take off runtime workload a programmatic draw might have to do.

Suppose you have a case where you cannot include the creation of a custom view in the XML, but rather, you must initialize the view only at runtime.

This is what the code might look like:

As you can see, there’s a significant work load setting up the UI programmatically just to make the CompassView element fit in with ConstraintLayout. This particular case would be incredibly impractical in real life, but there is a special reason we talk about initializing CompassView(this) . To demonstrate, we’ll run this code:

Like the previous case, the Kotlin initialization block executes first, then the secondary constructor CompassView(context) . However, you might have noticed that no onFinishInflate() callback is ever made. This is because CompassView(context) is not instantiated by XML, meaning that no LayoutInflater is put into play. There’s no children to wait on for recursive instantiation, and so there’s no onFinishInflate() for callback.

If you needed to add shared logic for different constructor calls that doesn’t depend on this system call, this might pose a problem for creating more manual view bindings in older code and other UI-related actions.

The Recommendation

You can annotate a custom view with the @JvmOverloads annotation, which tells the Kotlin compiler to generate overloaded methods. Every overload will substitute with default values for any parameters omitted in the generated methods, which means that the code below is equivalent to the CompassView class constructors written in the beginning of this article:

Instead of depending on the system to call onFinishInflate , you can use Kotlin init < >for shared logic (even with views dynamically inflated in the code). Running the inflate method in the initialization block will guarantee inflation occurs no matter how you choose to initialize your custom view.

I hope you enjoyed this blurb: in Android, looking at certain concepts through the lens of Kotlin can shed new perspective to seemingly basic questions. You can find the recommended version of the code on the main project, and additional resources below.

Источник

Реализация Custom View-компонента в Android

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

Читайте также:  Android maps data storage

Если какая-то специфичная особенность, из тех которые просит заказчик, требует особые функциональные возможности, которые невозможно сделать с помощью встроенных в 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-компонента, чтобы поместиться на вашем макете.

Читайте также:  Revo uninstaller mobile pro для android

При переопределении этого метода, всё что вам нужно сделать, это установить setMeasuredDimension(int width, int height).

При настройке размера Custom View-компонента вы должны обработать случай, когда у View-компонента может быть определённый размер, который пользователь (прим. переводчика: программист работающий с вашим View-компонентом) будет устанавливать в файле layout.xml или программно. Для вычисления этого свойства, нужно проделать несколько шагов:

  • Рассчитать размер необходимый для содержимого вашего View-компонента (ширину и высоту);
  • Получить MeasureSpec вашего View-компонента (ширину и высоту) для размера и режима;
  • Проверить MeasureSpec режим, который пользователь устанавливает и регулирует (для ширины и высоты);

Посмотрите на значения MeasureSpec:

  1. MeasureSpec.EXACTLY означает, что пользователь жёстко задал значения размера, независимо от размера вашего View-компонента, вы должны установить определённую ширину и высоту;
  2. MeasureSpec.AT_MOST используется для создания вашего View-компонента в соответствии с размером родителя, поэтому он может быть настолько большим, насколько это возможно;
  3. 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-компонент. Если вы захотите получить больше информации по этой теме, то может посмотреть хорошее видео.

Источник

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