📜 ⬆️ ⬇️

Themes and styles in Android apps


Every Android developer had to work with styles one way or another. Someone feels confident with them, someone has only superficial knowledge, which often does not allow them to solve the problem on their own.


In anticipation of the release of the dark theme, it was decided to refresh in memory all the information relating to themes and styles in Android applications.


What will be discussed:



Let's start with the basics


In their structure, themes and styles have a common structure:


<style name="MyStyleOrTheme"> <item name="key">value</item> </style> 

To create, use the style tag. Each style has a name and it stores the key-value parameters.


Everything is quite simple. But what is the difference between theme and style?


The only difference is how we use them.


Theme


A theme is a set of parameters that apply to the entire application, Activity, or View component. It contains the basic colors of the application, styles for rendering all components of the application and various settings.


Example topic:


 <style name="Theme.MyApp.Main" parent="Theme.MaterialComponents.Light.NoActionBar"> <!--Base colors--> <item name="colorPrimary">@color/color_primary</item> <item name="colorPrimaryVariant">@color/color_primary_variant</item> <item name="colorSecondary">@color/color_secondary</item> <item name="colorOnPrimary">@color/color_on_primary</item> <item name="colorOnError">@color/color_on_error</item> <!--Style attributes--> <item name="textAppearanceHeadline1">@style/TextAppearance.MyTheme.Headline1</item> <item name="bottomSheetDialogTheme">@style/ThemeOverlay.MyTheme.BottomSheetDialog</item> <item name="chipStyle">@style/Widget.MaterialComponents.Chip.Action</item> <item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.FilledBox</item> <!--Params--> <item name="android:windowTranslucentStatus">true</item> <!-- ... --> </style> 

The theme has redefined the main colors of the application ( colorPrimary , colorSecondary ), the style for the text ( textAppearanceHeadline1 ) and some standard application components, as well as the parameter for the transparent status bar.


In order for the style to become a real topic, it is necessary to inherit (we will talk about inheritance a bit later) from the default implementation of the topic.


Style


A style is a set of parameters for styling a single View component.


Sample style for TextInputLayout :


 <style name="Widget.MyApp.CustomTextInputLayout" parent="Widget.MaterialComponents.TextInputLayout.FilledBox"> <item name="boxBackgroundMode">outline</item> <item name="boxStrokeColor">@color/color_primary</item> <item name="shapeAppearanceOverlay">@style/MyShapeAppearanceOverlay</item> </style> 

Attribute


An attribute is a style or theme key. These are small bricks from which everything is built:


 colorPrimary colorSecondary colorOnError boxBackgroundMod boxStrokeColor shapeAppearanceOverlay ... 

All of these keys are standard attributes.


We can create our own attributes:


 <attr name="myFavoriteColor" format="color|reference" /> 

The myFavoriteColor attribute will point to a color or link to a color resource.


In the format, we can specify quite standard values:



By its nature, an attribute is an interface . It must be implemented in the topic:


 <style name="Theme.MyApp.Main" parent="Theme.MaterialComponents.Light.NoActionBar"> <!-- ... --> <item name="myFavoriteColor">@color/color_favorite</item> </style> 

Now we can refer to it. The general structure of the appeal looks like this:



 1 —   ,     ; 2 — namespace   (   Material Components Library); 3 —   ,     (); 4 —  . 

Well, finally, let's change, for example, the text color of the field:


 <androidx.appcompat.widget.AppCompatTextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="?attr/myFavoriteColor"/> 

Thanks to the attributes, we can add any kind of abstraction that will change inside the topic.


Inheriting themes and styles


As in OOP, we can adopt the functionality of an existing implementation. There are two ways to do this:



For explicit inheritance, we specify the parent using the parent keyword:


 <style name="SnackbarStyle" parent="Widget.MaterialComponents.Snackbar"> <!-- ... --> </style> 

For implicit inheritance, we use dot-notation to indicate the parent:


 <style name="SnackbarStyle.Green"> <!-- ... --> </style> 

There is no difference in the work of these approaches.


Very often we can meet similar styles:


 <style name="Widget.MyApp.Snackbar" parent="Widget.MaterialComponents.Snackbar"> <!-- ... --> </style> 

It may seem that the style is created by double inheritance. This is actually not the case. Multiple inheritance is prohibited. In this definition, explicit inheritance always wins .


That is, a style will be created with the name Widget.MyApp.Snackbar , which is the descendant of Widget.MaterialComponents.Snackbar .


Themeoverlay


ThemeOverlay - these are special "lightweight" themes that allow you to override the attributes of the main theme for the View-component.


We will not go far for an example, but take a case from our application. Designers decided that we need to create a standard login field, which will have a color different from the main style.


With the main topic, the input field looks like this:



It looks great, but the designers insist that the field be in a brown style.


Okay, how can we solve such a problem?



The correct solution is to use ThemeOverlay .


Create ThemeOverlay and redefine the main color of the theme :


 <style name="ThemeOverlay.MyApp.Login" parent="ThemeOverlay.MaterialComponents.TextInputEditText"> <item name="colorPrimary">@color/colorBrown</item> </style> 

Next, we specify it using the special android:theme tag in our TextInputLayout :


 <com.google.android.material.textfield.TextInputLayout android:theme="@style/ThemeOverlay.MyApp.Login" android:hint="Login" ... > <com.google.android.material.textfield.TextInputEditText ... /> </com.google.android.material.textfield.TextInputLayout> 

Everything works as we need.



Of course, the question arises - how does it work under the hood?


This magic allows you to crank ContextThemeWrapper . When creating a View in LayoutInflater , a context will be created where the current theme will be taken as a basis and the parameters that we specified in our Overlay theme will be redefined in it.


 ../LayoutInflater.java final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME); final int themeResId = ta.getResourceId(0, 0); if (themeResId != 0) { context = new ContextThemeWrapper(context, themeResId); } ta.recycle(); 

Similarly, we can independently override any theme parameter in the application.


The sequence of applying themes and styles to the View component



  1. The main priority is the markup file. If a parameter is defined in it, then all similar parameters will be ignored.


     <Button android:textColor="@color/colorRed" ... /> 

  2. The next priority is the View style:


     <Button style=“@Widget.MyApp.ButtonStyle" ... /> 

  3. The following uses predefined styles for the component:


     <style name="Theme.MyApp.Main" parent="Theme..."> <item name=“materialButtonStyle”>@Widget.MyApp.ButtonStyle</item> <!-- ... --> </style> 

  4. If no parameters were found, then the theme attributes are used:


     <style name="Theme.MyApp.Main" parent="Theme..."> <item name=“colorPrimary”>@colorPrimary</item> <!-- ... --> </style> 


In general, this is all you need to know in order to start working with topics. Now let's take a quick look at the updated Material Components design library.


May Material Components Come With Us


Material Components was introduced at Google I / O 2018 and is a replacement for Design Support Library.


The library gives us the opportunity to use the updated components from Material Design 2.0. In addition, a lot of interesting customization options appeared in it. All this allows you to write bright and unique applications.




Here are some examples of applications in the new style: Owl , Reply , Crane .


Let's move on to practice


To create a topic, you need to inherit from the base topic:


 Theme.MaterialComponents Theme.MaterialComponents.NoActionBar Theme.MaterialComponents.Light Theme.MaterialComponents.Light.NoActionBar Theme.MaterialComponents.Light.DarkActionBar Theme.MaterialComponents.DayNight Theme.MaterialComponents.DayNight.NoActionBar Theme.MaterialComponents.DayNight.DarkActionBar 

All of them are very similar to AppCompat themes, but have additional attributes and settings.


You can learn more about the new attributes at material.io .


If for some reason you can’t switch to a new topic now, then Bridge themes will do. They inherit from AppCompat themes and have all new Material Components attributes. You just need to add the Bridge postfix and use all the features without fear:


 <!-- ... --> Theme.MaterialComponents.Light.Bridge <!-- ... --> 

And here is our topic:


 <style name="Theme.MyApp.Main" parent="Theme.MaterialComponents.Light.NoActionBar"> <item name="colorPrimary">@color/color_primary</item> <item name="colorPrimaryVariant">@color/color_primary_variant</item> <item name="colorSecondary">@color/color_secondary</item> <item name="colorSecondaryVariant">@color/color_secondary_variant</item> <style> 

The names of the primary colors (brand-colors) have changed:


 colorPrimary —       (   AppCompat); colorPrimaryVariant —    ( colorPrimaryDark  AppCompat); colorSecondary —        ( colorAccent  AppCompat); colorSecondaryVariant —   . 

Further information on colors can be found at material.io .


I already mentioned that the theme contains standard styles for each View component. For example, for Snackbar style will be called snackbarStyle , for checkbox will be called checkboxStyle and then everything will be similar. An example will put everything in its place:



Create your own style and apply it to the theme:


 <style name="Theme.MyApp.Main" parent="Theme.MaterialComponents.Light.NoActionBar"> <!-- ... --> <item name="snackbarStyle">@style/Widget.MyApp.SnackbarStyle</item> </style> <style name="Widget.MyApp.SnackbarStyle" parent="Widget.MaterialComponents.Snackbar"> <!-- ... --> </style> 

It is important to understand that when you redefine a style in a theme, it will apply to all View of this type in the application (Activity).


If you want to apply the style to only one specific View, then you need to use the style tag in the markup file:


  <com.google.android.material.button.MaterialButton style="@style/Widget.MyApp.SnackbarStyle" ... /> 

One of the innovations that really impressed me was ShapeAppearance . It allows you to change the shape of components right in the subject!


Each View component belongs to a certain group:



As we can understand from the name, in groups of views of different sizes.



Check in practice:


 <style name="Theme.MyApp.Main" parent="Theme.MaterialComponents.Light.NoActionBar"> <!-- ... --> <item name="shapeAppearanceSmallComponent">@style/Widget.MyApp.SmallShapeAppearance</item> </style> <style name="Widget.MyApp.SmallShapeAppearance" parent=“ShapeAppearance.MaterialComponents.SmallComponent”> <item name="cornerFamilyTopLeft">rounded</item> <item name="cornerFamilyBottomRight">cut</item> <item name="cornerSizeTopLeft">20dp</item> <item name="cornerSizeBottomRight">15dp</item> <!--<item name="cornerFamily">cut</item>--> <!--<item name="cornerSize">8dp</item>--> </style> 

We created Widget.MyApp.SmallShapeAppearance for the "small" components. We rounded the upper left corner by 20dp and the lower right corner cut off by 15dp .


Got this result:



It looks interesting. Will it work in real life? Time will tell.


As with styles, we can apply ShapeAppearance only one View component.


What is there on a dark topic?


Soon Android Q release will take place, and with it the official dark theme will come to us.


Perhaps one of the most interesting and spectacular features of the new version of Android is the automatic application of a dark theme for the entire application with one line of code.


Sounds great, let's try. I suggest taking a favorite gitlab client from terrakok .


Allow repainting the application (disabled by default):


 <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <!-- ... --> <item name="android:forceDarkAllowed">true</item> </style> 

The android:forceDarkAllowed is available with API 29 (Android Q).


We start, look what happened:



Agree that for one line of code it looks very cool.


Of course, there are problems - BottomNavigationBar merges with the background, the loader remains white, the selection of code suffers and, it seems, everything, at least nothing serious has struck me.


I’m sure that spending not so much time can solve the main problems. For example, turning off the automatic dark mode for individual views (yes, it is also possible - android:forceDarkAllowed is available for View in a markup file).


It should be remembered that this mode is available only for light themes, if you use a dark one, then a forced dark theme will not work.


Work recommendations can be found in the documentation and on material.io .


And if we want to do everything on our own?


No matter how easy it is to use a forced dark theme, this mode is devoid of flexibility. In fact, everything works according to predefined rules that may not suit us and, more importantly, the customer. I think that such a decision can be considered temporary, until we make our implementation of a dark topic.


In API 8 (Froyo), the -night qualifier was added, which to this day is used to apply a dark theme. It allows you to automatically apply the desired theme depending on the time of day.


DayNight themes already use such an implementation, it’s enough for us to inherit from them.


Let's try to write our own:


 ../values/themes.xml <style name="Theme.DayNight.Base" parent="Theme.MaterialComponents.Light"/> <style name="Theme.MyApp.Main" parent="Theme.DayNight.Base> <!-- ... --> </style> ../values-night/themes.xml <style name="Theme.DayNight.Base" parent="Theme.MaterialComponents"/> 

In the usual resource for the theme ( values/themes.xml ) we inherit from the light theme, in the "night" ( values-night/themes.xml ) we inherit from the dark theme.


That's all. We got a library implementation of a dark theme. Now we should support resources for two topics.


To switch between themes while the application is running, you can use AppCompatDelegate.setDefaultNightMode , which takes the following parameters:



What should we consider when working with themes and styles?


As I noted, Google has officially begun to force a dark topic. I am sure that many customers began to receive questions - “Can we add a dark theme?”. It’s good if you do everything right from the very beginning and it’s easy for you to change the light colors to dark, while receiving a completely repainted application.


Unfortunately, this is not always the case. There are old applications that require significant effort in order to make the necessary changes.


Let's try to formulate recommendations for working with styles together:


1. The color picker


I think that every developer is faced with a situation when some strange color appears in the new layout that is not yet defined in the application palette. What to do in this case?


The correct answer is to talk to the designer and try to work out a color palette. Now there are many programs (Zeplin, Sketch, etc.) that allow you to render primary colors and then reuse them.


The sooner you do this, the less headaches you will have in the future.


2. Spell colors by their names


In each application, there is a color that has many brightness options. You can start inventing names for them:


 <color name="green_tiny">...</color> <color name="green_light">...</color> <color name="green_dark">...</color> 

Agree, it does not look very. The question immediately arises - which color is lighter than tiny or light ? And if we have a dozen options?


It’s best to stick with the Google concept and add the appropriate brightness to the color names (Google calls this color option - colorVariant ):


 <color name="material_green_300">...</color> <color name="material_green_700">...</color> <color name="material_green_900">...</color> 

With this approach, we can have any number of brightness options of one color and we don’t have to come up with specific names, which is really difficult.


3. To ignore a specific color if it changes in different topics


Since we are writing an application in which there will be at least two topics, we cannot afford to refer to a specific color if it is implemented differently in the themes.


Let's look at an example:



We see that in a light theme, for example, the toolbar is colored in purple, and in the dark it is dark gray. How would we implement this behavior using only the capabilities of themes?


Everything is quite simple - we will create an attribute and implement it in light and dark themes with the appropriate color, as described previously.


Google recommends associating attribute names with usage semantics.


4. Do not be afraid to create resource files


When a lot of different styles, themes and attributes are typed in the styles.xml file, it becomes difficult to maintain.


It’s best to group everything into separate files:


 themes.xml — Theme & ThemeOverlay styles.xml — Widget styles type.xml — TextAppearance, text size etc shape.xml — ShapeAppearance motion.xml — Animations styles system_ui.xml — Booleans, colors for UI control //may be other files 

Such a simple rule will avoid God-files and, therefore, it will be easier to maintain styles.


5. Reuse to the maximum


What will we do if we want to redefine an attribute that is available only from a specific version of the API?


We can create two separate topics:


 ../values/themes.xml <style name="Theme.MyApp.Main" parent=”Theme.MaterialComponents.NoActionBar”> <!--Many definition--> </style> ../values-v27/themes.xml <style name="Theme.MyApp.Main" parent=”Theme.MaterialComponents.NoActionBar”> <!--Many definition--> <name="android:windowLightNavigationBar">...</item> </style> 

Do we now have a theme with all the parameters for each version of the API? Of course not! We will make a basic topic where the basic attributes that are available for all versions of the API will be determined and inherited from it in the desired version of the API:


 ../values/themes.xml <style name="Theme.MyApp.Base" parent=”Theme.MaterialComponents.DayNight.NoActionBar”> <!--Many definition--> </style> <style name="Theme.MyApp.Main" parent=”Theme.MyApp.Base”/> ../values-v27/themes.xml <style name="Theme.MyApp.Base.V27" parent="Theme.MyApp.Base"> <name="android:windowLightNavigationBar">...</item> </style> <style name="Theme.MyApp.Main" parent=”Theme.MyApp.Base.V27”/> 

By this principle, all topics in the standard library are built.


6. Use vector resources and tint


I think it’s not worth saying why vector resources are good. Everyone already knows (just in case, a link to the documentation ). Well, tinting will help us color them in theme colors.


You can see what tinting is and how to work with it in this example .


7.? Android: attr / ... vs? Attr / ...


When accessing resources, we have the opportunity to use both system attributes and attributes from the Material Components library. It is important to understand that some attributes exist only with a specific version of the API. As we all know well, accessing a non-existent resource leads to a crash (lint, of course, will tell us if something is wrong, but you should not always rely on it)


 android:background="?android:attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless" 

In the first case, we access the system resource, as indicated by android . In the second case, to an attribute from the library where backward compatibility is implemented.


It is better to always use the second option.


8. Always specify parent for style


There can be parameters in the parent style, without which the component will be incorrectly rendered, so you should always specify the parent.


 <style name="Widget.MyApp.LoginInputLayout" parent="Widget.MaterialComponents.TextInputLayout.FilledBox"> <item name="errorTextColor">@color/colorError</item> </style> 

9. Theme, style or ...?


When creating your own themes and styles, it will be great if you specify a prefix that says what kind of style it is and what it is defined for. Such naming will make it very easy to structure and extend styles.


 <style name="Theme.MyApp.Main" parent=”...”/> <style name="Widget.MyApp.LoginInputLayout" parent="..."/> <style name="Widget.MyApp.LoginInputLayout.Brown"/> <style name="ThemeOverlay.MyApp.Login" parent=”...”/> 

10. Use TextAppearance


It will be a good tone to expand the basic styles for the text and use them everywhere.


A lot of useful information can be found on Material Design: Typography , Typography Theming .


Conclusion


In conclusion, I want to say that styling an application is the responsibility of not only developers but also designers. - . Material Components. . Sketch — Material Theme Editor . . , .


Material Components GitHub — Modular and customizable Material Design UI components for Android . . , — sample, .


Useful links:



')

Source: https://habr.com/ru/post/461201/


All Articles