- What’s your text’s appearance?
- Understanding how to declaratively style text on Android.
- Show some style
- TextAppearance
- Sensible defaults
- Theme
- Темы и стили в Android-приложениях
- О чем пойдет речь:
- Начнем с основ
- Стиль
- Атрибут
- Наследование тем и стилей
- ThemeOverlay
- Последовательность применения тем и стилей ко View-компоненту
- Да прибудет с нами Material Components
- Перейдем к практике
- Что там по темной теме?
- А если мы хотим все делать самостоятельно?
- 9. Тема, стиль или… ?
- 10. Использовать TextAppearance
- Заключение
What’s your text’s appearance?
Understanding how to declaratively style text on Android.
When styling text in Android apps, TextView offers multiple attributes and different ways to apply them. You can set attributes directly in your layout, you can apply a style to a view, or a theme to a layout or perhaps set a text appearance. But which should you use? What happens if you combine them?
This article outlines the different approaches to declaratively styling text (i.e. when you inflate an XML layout), looking at their scope and precedence and when you should use each technique.
You really should read the whole post but here’s a summary.
Be aware of the precedence order of different styling techniques — if you’re trying to style some text and not seeing the results you expect then your changes are likely being overridden by something higher up in this hierarchy:
I’d suggest this workflow for styling text:
- Set any app wide styling in a textViewStyle default style in your theme.
- Set up a (small) selection of TextAppearance s your app will use (or use/extend from MaterialComponent’s styles) and reference these directly from your views
- Create styles setting any attributes not supported by TextAppearance (which themselves specify one of your TextAppearances ).
- Perform any unique styling directly in your layout.
Show some style
While you can directly set TextView attributes in your layout, this approach can be tedious and error prone. Imagine trying to update the color of all text views in your app this way 🙀. As with all views, you can (and should!) instead use styles to promote consistency, reuse and enable easy updates. To this end, I’d recommend creating styles for text whenever you’re likely to want to apply the same styling to multiple views. This is extremely simple and largely handled by the Android view system.
What’s happening under the hood when you set a style on a view? If you’ve ever written a custom view, you’ve likely seen a call to context.obtainStyledAttributes(AttributeSet, int[], int, int) . This is how the Android view system delivers the attributes specified in your layout to the view. The AttributeSet parameter can essentially be thought of as a map of the XML parameters you specify in your layout. If this AttributeSet specifies a style then the style is read first, then the attributes specified directly on the view are applied on top of that. In this way we arrive at our first rule of precedence.
Attributes defined directly on a view always “‘win” and will override those specified in a style. Note that the combination of the style and view attributes are applied; defining an attribute on a view that also appears in the style doesn’t discard the entire style. It’s also interesting to note that there’s no real way in your views to determine where styling is coming from; it’s resolved by the view system for you in this single call. You can’t receive both and pick.
Whilst styles are extremely useful, they do have their limits. One of which is that you can only apply a single style to a view (unlike something like CSS on the web where you can apply multiple classes). TextView however, has a trick up its sleeve, it offers a TextAppearance attribute which functions similarly to a style . If you supply text styling via a TextAppearance that leaves the style attribute free for other styling which sounds useful. Let’s take a closer look at what TextAppearance is and how it works.
TextAppearance
There’s nothing magical about TextAppearance (like a secret mode to apply multiple styles that Android doesn’t want you to know about. 1!), TextView does some very helpful legwork for you. Let’s peek at some of TextView ’s constructor to see what’s going on.
So what is happening here? Essentially TextView first looks if you’ve supplied android:textAppearance , if so it loads that style and applies any properties it specifies. Later on, it then loads all attributes of the view (which remember, includes the style) and applies them. We therefore arrive at our second precedence rule:
Because the text appearance is checked first, any attributes defined either directly on a view or in a style will override the text appearance.
There’s one other caveat to be aware of with TextAppearance which is that is supports a subset of styling attributes that TextView offers. To understand this, let’s go back to this line:
We’ve looked at the 4 arg version of obtainStyledAttributes , this 2 arg version is slightly different. It instead looks at a given style (as identified by the first id parameter) and filters it to only the attributes in this style which appear in the second attrs array param. As such the styleable android.R.styleable.TextAppearance defines the scope of what TextAppearance understands. Looking at the definition of this we can see that TextAppearance supports many but not all attributes that TextView supports.
Styling attributes supported by TextAppearance s
Some common TextView attributes not included are lineHeight[Multiplier|Extra] , lines , breakStrategy & hyphenationFrequency . TextAppearance works at the character level, not paragraph so attributes affecting the whole layout are not supported.
So TextAppearance is very useful, it lets us define a style focused on text styling attributes and leaves a view’s style free for other uses. It does however have a limited scope and is at the bottom of the precedence chain, so be aware of its limitations.
Sensible defaults
When we took a look at how the Android view system resolved attributes ( context.obtainStyledAttributes ) we actually simplified things a little. This calls through to theme.obtainStyledAttributes (using the current Theme of the Context ). Checking the reference this shows the precedence order we looked at before and specifies 2 more places it looks to resolve attributes: the view’s default style and the theme.
We’ll come back to the theme but let’s take a look at default styles. What the heck is a default style? To answer this I think it’s illustrative to take a quick detour from TextView and look at the humble Button . When you drop a into your layout, it looks something like this.
Why? If you look at the source code to Button , it’s pretty sparse:
That’s it! The entire class (less comments). You can check here. I’ll wait. So where does the background, the capitalized text, the ripple etc all come from? You might have missed it, but it’s all in the 2 arg constructor; the one called when a layout is inflated from XML. It’s the last param which specifies a defaultStyleAttr of com.android.internal.R.attr.buttonStyle . This is the default style which is essentially a point of indirection allowing you to specify a, well, style to use by default. It doesn’t directly point to a style, but lets you point to one in your theme which it will check when resolving attributes. And that’s exactly what all themes you’d typically inherit from do to provide the default look and feel for common widgets. Looking at the Material theme for example, it defines @style/Widget.Material.Light.Button and it is this style which supplies all of the attributes, which theme.obtainStyledAttributes will deliver if you don’t specify anything else.
Going back to TextView , it also offers a default style: textViewStyle . This can be very handy if you want to apply some styling to each and every TextView in your app. Say you wanted to set a default line spacing multiplier of 1.2 throughout. You could do this through style s/ TextAppearance s and try to enforce this through code review (or perhaps even a fancy custom Lint rule) but you’d have to be vigilant and be sure to onboard new team members and be careful with refactors etc.
A better approach might be to specify your own default style for all TextView s in the app, encoding the desired behavior. You can do this by setting your own style for textViewStyle which extends from the platform or MaterialComponents/AppCompat default.
So factoring this in, our precedence rules become:
View > Style > Default Style > TextAppearance
As part of the view systems attribute resolution, this slots in after styles (so anything in a default style is trumped by an applied style or view attribute) but still will override a text appearance. Default styles can be pretty handy. If you’re ever writing your own custom view then they can be a powerful way to implement default behavior while allowing easy customization.
If you’re subclassing a widget and not specifying your own default style then be sure to use the parent classes default style in your constructors (don’t just pass 0). For example, if you’re extending AppCompatTextView and write your own 2 arg constructor, be sure to pass android.R.attr.textViewStyle as the defaultStyleAttr (like this) otherwise you’ll lose the parent’s behavior.
Theme
As mentioned before, there’s one (final, I promise) way to provide styling information. The other place theme.obtainStyledAttributes will look is directly in the theme itself. That is if you supply a style attribute like android:textColor in your theme, the view system will pick this up as a last resort. Generally it’s a Bad Idea™ to mix theme attributes and style attributes i.e. something you’d apply directly to a view should generally never be set on a theme (and vice versa) but there are a couple of rare exceptions.
One example might be if you’re trying to change the font throughout your app. You could use one of the techniques above but manually setting up styles/text appearances everywhere would be repetitive and error prone and default styles only work at the widget level; subclasses might override this behavior e.g. buttons define their own android:buttonStyle which wouldn’t pick up your custom android:textViewStyle . Instead you could specify the font in your theme:
Now any view which supports this attribute will pick this up unless overridden by something with higher precedence:
View > Style > Default Style > Theme > TextAppearance
Again, as this is part of the view styling system, it will override anything supplied in a text appearance, but be overridden by any more specific attributes.
Be mindful of this precedence. In our app-wide-font example you might expect Toolbar s to pick up this font as they contain a title which is a TextView . The Toolbar class itself however defines a default style containing a titleTextAppearance which itself specifies android:fontFamily , and directly sets this on the title TextView , overriding the theme level value. Theme level styling can be useful, but is easily overridden so be sure to check that it’s applied as expected.
Источник
Темы и стили в Android-приложениях
Каждому Android-разработчику так или иначе приходилось работать со стилями. Кто-то чувствует себя с ними уверенно, у кого-то есть только поверхностные знания, которые зачастую не позволяют самостоятельно решить поставленную задачу.
В преддверии выхода темной темы было решено освежить в памяти всю информацию, касающуюся тем и стилей в Android-приложениях.
О чем пойдет речь:
Начнем с основ
По своей структуре темы и стили имеют общее строение:
Для создания используется тег style . У каждого cтиля есть имя и он хранит в себе параметры key-value .
Все достаточно просто. Но в чем же разница между темой и стилем?
Единственное отличие заключается в том, как мы их используем.
Тема — это набор параметров, которые применяются ко всему приложению, Activity или View-компоненту. Она содержит базовые цвета приложения, стили для отрисовки всех компонентов приложения и различные настройки.
В теме переопределены основные цвета приложения ( colorPrimary , colorSecondary ), стиль для текста ( textAppearanceHeadline1 ) и некоторых стандартных компонентов приложения, а также параметр для прозрачного статус-бара.
Для того чтобы стиль стал настоящей темой, необходимо отнаследоваться (о наследовании мы поговорим чуть позже) от дефолтной реализации темы.
Стиль
Стиль — это набор параметров для стилизации одного View-компонента.
Атрибут
Атрибутом принято называть ключ стиля или темы. Это маленькие кирпичики из которых все строится:
Все эти ключи являются стандартными атрибутами.
Мы можем создавать свои атрибуты:
Атрибут myFavoriteColor будет указывать на цвет или ссылку на ресурс цвета.
В формате мы можем указать вполне стандартные значения:
По своей природе атрибут является интерфейсом. Его необходимо реализовать в теме:
Теперь мы можем на него ссылаться. Общая структура обращения выглядит так:
Ну и, наконец, давайте поменяем, например, цвет текста у поля:
Благодаря атрибутам мы можем добавлять какие-угодно абстракции, которые будут изменяться внутри темы.
Наследование тем и стилей
Как и в ООП, мы можем перенимать функционал существующей реализации. Сделать это можно двумя способами:
При явном наследовании мы указываем родителя с помощью ключевого слова parent :
При неявном наследовании мы используем dot-notation для указания родителя:
Никакой разницы в работе этих подходов нет.
Очень часто мы можем встретить подобные стили:
Может показаться, что стиль создан путем двойного наследования. На самом деле это не так. Множественное наследование запрещено. В таком определении явное наследование всегда выигрывает.
То есть будет создан стиль с именем Widget.MyApp.Snackbar , который является наследником Widget.MaterialComponents.Snackbar .
ThemeOverlay
ThemeOverlay — это специальные «легковесные» темы, которые позволяют переопределить атрибуты основной темы для View-компонента.
За примером далеко ходить не будем, а возьмем кейс из нашего приложения. Дизайнеры решили, что нам нужно сделать стандартное поле для ввода логина, которое будет иметь отличный от основного стиля цвет.
С основной темой поле ввода выглядит так:
Выглядит отлично, но дизайнеры настаивают на том, чтобы поле было в коричневом стиле.
Окей, как мы можем решить такую задачу?
Да, мы можем переопределить стиль и вручную поменять основные цвета вьюшки, но для этого нужно будет писать много кода, да и есть шанс, что мы про что-нибудь забудем.
Написать свою вьюшку по гайдлайнам и с кастомными параметрами?
Хороший вариант, так мы сможем удовлетворить любые хотелки дизайнеров и заодно прокачать скилл, но все это трудозатратно и может привести к нежелательным багам.
Переопределить основной цвет в теме?
Мы выяснили, что для нужного нам вида достаточно поменять colorPrimary в теме. Рабочий вариант, но так мы затронем внешний вид остальных компонентов, а нам это не нужно.
Правильное решение — это использовать ThemeOverlay.
Создаем ThemeOverlay и переопределяем основной цвет темы:
Далее указываем его с помощью специального тега android:theme в наш TextInputLayout :
Все работает так, как нам и нужно.
Конечно же возникает вопрос — как это работает под капотом?
Эту магию позволяет провернуть ContextThemeWrapper . При создании View в LayoutInflater будет создан контекст, где за основу будет взята текущая тема и в ней будут переопределены параметры, которые мы указали в нашей Overlay теме.
Аналогичным образом мы можем самостоятельно переопределить любой параметр темы в приложении.
Последовательность применения тем и стилей ко View-компоненту
Главный приоритет имеет файл разметки. Если в нем определен параметр, то далее все аналогичные параметры будут игнорироваться.
Следующий приоритет имеет стиль View:
Далее используются предопределенные стили для компонента:
Если параметры не были найдены, то используются атрибуты темы:
В общем-то это все, что нужно знать для того чтобы начать работу с темами. Теперь кратко посмотрим на обновленную дизайн-библиотеку Material Components.
Да прибудет с нами Material Components
Material Сomponents была представлена на Google I/O 2018 и является заменой Design Support Library.
Библиотека дает нам возможность использовать обновленные компоненты из Material Design 2.0. Кроме того, в ней появилось множество интересных настроек по кастомизации. Все это позволяет писать яркие и уникальные приложения.
Вот некоторые примеры приложений в новом стиле: Owl, Reply, Crane.
Перейдем к практике
Для создания темы нужно отнаследоваться от базовой темы:
Все они очень похожи на AppCompat темы, но имеют дополнительные атрибуты и настройки.
Подробнее с новыми атрибутами можно познакомиться на material.io.
Если по каким-то причинам вы сейчас не можете переключиться на новую тему, то вам подойдут Bridge темы. Они наследуются от AppCompat тем и имеют все новые атрибуты Material Components. Нужно всего лишь добавить постфикс Bridge и использовать все возможности без опасений:
А вот и наша тема:
Важно понимать, что когда вы переопределяете стиль в теме, он применится ко всем View этого типа в приложении (Activity).
Если же вы хотите применить стиль только к одной конкретной View, то нужно использовать тег style в файле с разметкой:
Одно из нововведений, которое меня действительно впечатлило — это ShapeAppearance. Оно позволяет изменять форму компонентов прямо в теме!
Каждый View-компонент относится к определенной группе:
shapeAppearanceSmallComponent
shapeAppearanceMediumComponent
shapeAppearanceLargeComponent
Как мы можем понять из названия, в группах вьюшки разных размеров.
Проверим на практике:
Мы создали Widget.MyApp.SmallShapeAppearance для «маленьких» компонентов. Закруглили верхний левый угол на 20dp и правый нижний угол срезали на 15dp .
Получили такой результат:
Выглядит интересно. Будет ли это работать в реальной жизни? Время покажет.
Как и для стилей, мы можем применить ShapeAppearance только для одного View-компонента.
Что там по темной теме?
Совсем скоро состоится релиз Android Q, а вместе с ним к нам придет и официальная темная тема.
Пожалуй, одна из самых интересных и эффектных возможностей новой версии Android — это автоматическое применение темной темы для всего приложения одной строчкой кода.
Звучит здорово, давайте пробовать. Предлагаю взять всеми любимый гитлаб клиент от terrakok.
Разрешаем перекрашивать приложение (по умолчанию запрещено):
Атрибут android:forceDarkAllowed доступен с API 29 (Android Q).
Запускаем, смотрим что получилось:
Согласитесь, что для одной строчки кода выглядит очень круто.
Конечно, есть проблемы — BottomNavigationBar сливается с фоном, лоадер остался белым, выделение кода страдает и, вроде бы, все, по крайне мере мне больше ничего серьезного в глаза не бросилось.
Уверен, что потратив не так много времени, можно решить основные проблемы. Например, отключив автоматический темный режим для отдельных вьюшек (да, так тоже можно — android:forceDarkAllowed доступен для View в файле-разметке).
Следует помнить, что данный режим доступен только для светлых тем, если вы используете темную, то принудительная темная тема работать не будет.
Рекомендации по работе можно почитать в документации и на material.io.
А если мы хотим все делать самостоятельно?
Как бы не было просто использовать принудительную темную тему, этот режим лишен гибкости. Фактически, все работает по заранее определенным правилам, которые могут не устраивать нас и, что более важно, заказчика. Думаю, что такое решение можно рассматривать как временное, до тех пор пока мы не сделаем свою реализацию темной темы.
В API 8 (Froyo) был добавлен квалификатор -night , который и по сей день используется для применения темной темы. Он позволяет автоматически применять нужную тему в зависимости от времени суток.
В темах DayNight уже используется такая реализация, нам достаточно отнаследоваться от них.
Давайте попробуем написать свою:
Нам теперь на каждую версию API делать тему со всеми параметрами? Нет, конечно! Мы сделаем базовую тему, где будут определены базовые атрибуты, доступные для всех версий API и отнаследуемся от нее в нужной версии API:
9. Тема, стиль или… ?
При созданий собственных тем и стилей будет здорово, если вы укажите префикс, говорящий о том что это за стиль и для чего он определен. Такое именование позволит очень просто структурировать и расширять стили.
10. Использовать TextAppearance
Хорошим тоном будет расширить основные стили для текста и везде их использовать.
Много полезной информации можно найти на сайте Material Design: Typography, Typography Theming.
Заключение
В заключение хочется сказать, что стилизация приложения — это обязанность не только разработчиков, но и дизайнеров. Только благодаря тесному взаимодействию мы сможем получить по-настоящему хороший и красивый продукт. Дизайнеры должны иметь представления о платформе и возможностях Material Components. Ведь именно на их плечи ложится ответственность по поддержке визуальной составляющей приложения. Дизайнерам доступен специальный плагин для Sketch — Material Theme Editor. В нем очень просто выбирать цвета для приложения и строить экраны на основе стандартных компонентов. Если ваши дизайнеры еще не знают о нем, то обязательно расскажите им.
Начать изучать Material Components можно с репозитория на GitHub — Modular and customizable Material Design UI components for Android. В нем собрано очень много информации по стандартным стилям и их возможностям. Кроме того, там есть приложение — sample, чтобы все сразу попробовать на практике.
Источник