What does your text look like?

Original author: Nick Butcher
  • Transfer
Friends, great all friday. We want to share with you a translation of an article prepared especially for students of the course “Android-developer. Advanced Course . Have a good reading.



How to declaratively style text on Android.


The Virginia Poltrack illustration

TextView in Android apps provides several attributes for styling text and various ways to apply them. These attributes can be set directly in the layout, apply a style to the view or theme to the layout, or, if you want, set textAppearance. But what of this should be used? And what happens if you combine them?


What and when to use?

This article describes various approaches to declarative stylization of text (that is, when you define styles in an XML file), discusses the scope and priority of each method.

tl; dr;


You should read the entire post, but here is a summary.

Remember the priority order of various styling methods - if you are trying to stylize some text and do not see the expected results, then most likely your changes are overridden by something with a higher priority in this hierarchy:


Hierarchy of text styling methods

I would suggest the following order actions for styling:

  1. Set any application style to textViewStyleas the default style for your theme.
  2. Install the (small) set TextAppearancethat your application will use (or use / inherit from MaterialComponent styles ), and reference them directly from your view
  3. Create styleby setting attributes that are not supported TextAppearance(which themselves will determine one of yours TextAppearance).
  4. Perform any unique styling directly in the layout.

Show some style


You can directly set attributes TextViewin a layout, but this approach can be more tedious and unsafe. Imagine that in this way you are trying to update the color of all TextViews in the application. As with all other views, you can (and should!) Use styles instead to ensure consistency, reuse, and ease of updating. For this purpose, I recommend creating styles for text whenever you probably want to apply the same style to multiple views. This is extremely simple and largely supported by the Android view system.

What happens under the hood when you style the view? If you ever wrote your custom view, you probably saw a call to context.obtainStyledAttributes (AttributeSet, int [], int, int). Thus, the view-system in Android passes to the view the attributes specified in the layout. The parameter AttributeSet, in fact, can be considered as a map of the XML parameters that you specify in your layout. If the AttributeSet sets the style, the style is read first , and then the attributes specified directly in the view are applied to it. Thus, we come to the first rule of priority.

View → Style The

attributes defined directly in the view always “prevail” and override the attributes defined in the style. Note that a combination of style and view attributes is applied ; defining an attribute in view, which is also specified in the style, does not cancelwhole style. It should also be noted that in your view there is no real way to determine where the stylization comes from; This is decided by the view system for you once in a similar call. You cannot get both options and choose.

Although styles are extremely useful, they have their limitations. One of them is that you can only apply one style to the view (as opposed to something like CSS, where you can apply multiple classes). We TextView, however, there is a trick, it provides an attribute TextAppearance, which works similarly style. If you style text through TextAppearance, then leave the attribute stylefree for other styles, which looks practical. Let's take a closer look at what it is TextAppearanceand how it works.

Textappearance


There TextAppearanceis nothing magical (for example, a secret mode for applying several styles that you should not know about !!!!), TextViewsaving you some unnecessary work. Let's look at the constructor TextViewto understand what is happening.

TypedArray a = theme.obtainStyledAttributes(attrs, com.android.internal.R.styleable.TextViewAppearance, defStyleAttr, defStyleRes);
TypedArray appearance = null;
int ap = a.getResourceId(com.android.internal.R.styleable.TextViewAppearance_textAppearance, -1);
a.recycle();
if (ap != -1) {
 appearance = theme.obtainStyledAttributes(ap, com.android.internal.R.styleable.TextAppearance);
}
if (appearance != null) {
 readTextAppearance(context, appearance, attributes, false);
 appearance.recycle();
}
// a little later
a = theme.obtainStyledAttributes(attrs, com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);
readTextAppearance(context, a, attributes, true);

So what is going on here? Essentially, it TextViewfirst looks to see if you indicated android:textAppearance, if so, it loads that style and applies all the properties that are listed there. Later, he loads all the attributes from the view (which he remembers, including the style) and applies them. So we come to the second priority rule:

View → Style → TextAppearance

Since the text appearance is checked first, any attributes defined either directly in the view or in style will override it.

C TextAppearanceshould be aware of another caveat: it supports a subset of the style attributes that it offers TextView. To better understand what I mean, let's go back to this line:

obtainStyledAttributes(ap, android.R.styleable.TextAppearance);

We reviewed the versionreceiveStyledAttributeswith 4 arguments, this 2-argument version is slightly different. She looks at the given style (as defined by the first parameter id) and filters it only according to the attributes in the style that appear in the second parameter, the array attrs. Thus, styleable android.R.styleable.TextAppearancedefines the scope TextAppearance. Looking at this definition, we see that it TextAppearancesupports many, but not all, attributes that it supportsTextView .


Styling attributes supportedTextAppearance

Here are some attributesTextViewthat are not included inTextAppearance:lineHeight[Multiplier|Extra],lines,breakStrategyandhyphenationFrequency. TextAppearanceworks at the level of characters, not paragraphs, so attributes that affect the entire layout are not supported.

Therefore,TextAppearanceit is very useful, it allows us to define a style oriented to the text stylization attributes, and leaves itstylefree in view for other purposes. However, it has a limited scope and is at the bottom of the priority chain, so don't forget about the limitations.

Reasonable defaults


When we looked at how the Android view system resolves attributes ( context.obtainStyledAttributes), we actually simplified it a bit. It calls theme.obtainStyledAttributes (using the current Theme Context'a). When checking the link , the priority order that we examined earlier is shown, and 2 more places are indicated that he is looking for to resolve attributes: the default style for view and the theme.

When determining the final value of a particular attribute, four input parameters come into play:

  1. Any attribute values ​​in this AttributeSet.
  2. The style resource specified in the AttributeSet (named "style").
  3. The default style specified by defStyleAttr and defstyleres
  4. Basic values ​​in this thread.

The order of styling priorities from Theme documentation

We will return to the topics, but let's take a look at the default styles first. What is this default style? To answer this question, I think it would be useful to make a small exit from the topic TextViewand look at a simple one Button. When you paste <Button>into your layout, it looks something like this.


Standard Button

Why? If you look at the source code Button, you will see that it is rather meager:

public class Button extends TextView {
 public Button(Context context) {
   this(context, null);
 }
 public Button(Context context, AttributeSet attrs) {
   this(context, attrs, com.android.internal.R.attr.buttonStyle);
 }
 public Button(Context context, AttributeSet attrs, int defStyleAttr) {
   this(context, attrs, defStyleAttr, 0);
 }
 public Button(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
   super(context, attrs, defStyleAttr, defStyleRes);
 }
 @Override public CharSequence getAccessibilityClassName() {
   return Button.class.getName();
 }
 @Override public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
   if (getPointerIcon() == null && isClickable() && isEnabled()) {
     return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
   }
   return super.onResolvePointerIcon(event, pointerIndex);
 }
}

It's all! Here is the whole class (no comment). You can check it yourself here . I'll wait. So where does the background, capital letters, ripple, etc. come from? You may have missed, but it will all be defined in the constructor with 2 arguments; one that is called when layout is taken from XML. This is the last parameter that determines defaultStyleAttrincom.android.internal.R.attr.buttonStyle. This is the default style, which is essentially an indirect reference point, allowing you to specify the default style. It does not point directly to the style, but allows you to point to one of those specified in your topic that it will check when resolving attributes. And that’s exactly what all the themes you usually inherit from do to provide the look and feel of standard widgets. For example, if you look at the topic Material, it defines @style/Widget.Material.Light.Button, and it is this style that provides all the attributes that it will pass theme.obtainStyledAttributesif you did not specify anything else.

Back to TextView, it also offers a default style:textViewStyle. This can be very convenient if you want to apply some styles to every TextView in your application. Suppose you want to set the default line spacing to 1.2. You can do this with help style/TextAppearanceand try to apply it during the code review (or maybe even with the help of an elegant custom rule in Lint), but you need to be vigilant and be sure that you will recruit new team members, be careful with refactoring, etc.

The best approach might be to specify your own default style for everyone TextViewin the application that sets the desired behavior. You can do this by setting your own style for textViewStyle, which is inherited from the platform or from MaterialComponents/AppCompatthe default.


With this in mind, our priority rule takes the form:

View → Style → Default Style → TextAppearance

As part of the resolution of the view system’s attributes, this slot is filled after the styles (so that everything in the default style is canceled by the applied style or view attributes), but still will override text appearance. Default styles can be very convenient. If you ever decide to write your own custom view, they can be a powerful way to implement default behavior, making it easy to customize.

If you inherit the widget and do not specify your own default style, then be sure to use the default parent class style in the constructors (do not pass just 0). For example, if you inherit fromAppCompatTextViewand write your own constructor with 2 arguments, be sure to pass android.R.attr.textViewStyle как defaultStyleAttr( as here ), otherwise you will lose the behavior of the parent class.

Topics


As mentioned earlier, there is another (last, I promise) way of providing styling information. Another place theme.obtainStyledAttributeswill look right into the topic itself. That is, if you add a style attribute to your theme, for example android:textColor, the view system will select it as a last resort. As a rule, it is a bad idea to mix theme attributes and style attributes, that is, what you apply directly to the view, as a rule, should never be set for the theme (and vice versa), but there are a couple of rare exceptions.

One example would be when you try to change the font throughout the application. You can use one of the methods described above, but manually adjusting the styles / appearance of the text everywhere will be monotonous and unsafe, and the default styles will work only at the widget level; subclasses can override this behavior, for example buttons define their own android:buttonStyle, which yours won't pick up android:textViewStyle. Instead, you can specify the font in your theme:


Now any view that supports this attribute will pick it up if it is not overridden by something with a higher priority:

View → Style → Default Style → Theme → TextAppearance

Again, since this is part of the view styling system, it will override everything , which is provided in text form, but will be overridden by any more specific attributes.

Remember this priority. In our example with a font for the entire application, you can expect Toolbarthis font to pick up, as it contains the title, which is TextView. The Toolbar class itself, however, defines a default style, containing titleTextAppearanceone that defines android:fontFamily, and sets it directly in the headerTextViewby overriding the theme level value. Topic-level styles can be useful, but easy to override, so make sure they are applied properly.

Bonus: Unresolved Issues


This entire article was devoted to the declarative formatting of text at the view level, that is, how to style everything TextViewduring filling. Any style applied after filling (for example textView.setTextColor(…)) will override declarative styling. TextViewalso supports smaller styles through Span. I will not go into this topic, as it is described in detail in articles by Florina Muntenescu .

Spantastic text styling with Spans
Underspanding spans

I will mention this to complete the picture, to keep in mind that program styling and spans will be at the top of the order of priority:

Span → Setters → View → Style → Default Style → Theme → TextAppearance

Choose your style


Although there are several ways to style text, understanding the differences between methods and their limitations helps you find the right tool for a specific task or understand why one method takes precedence over another.

Have a nice text styling!

We invite everyone to a free webinar in the framework of which we will get acquainted with the DI framework Dagger 2: we will learn how Dagger2 generates code, we will deal with JSR 330 annotations and Dagger2 constructs, we will learn how to use Dagger2 in a multi-module application and consider the Dagger Android Injector.

Also popular now: