click tracking

Advanced Android TextView

by Chiu-Ki Chan

@chiuki
+ChiuKiChan

http://bit.ly/advtext

Lint error

This tag and its children can be replaced by one <TextView/> and a compound drawable

A LinearLayout which contains an ImageView and a TextView can be more efficiently handled as a compound drawable

Compound Drawable


<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="@string/animation"
  android:drawableLeft=
    "@drawable/rotating_loading"
  android:drawableRight=
    "@drawable/animated_wifi"
  android:drawableBottom=
    "@drawable/animated_clock"/>
              
Compound Drawable

AnimatedRotateDrawable


<!-- res/drawable/rotating_loading.xml -->
<animated-rotate
  android:pivotX="50%"
  android:pivotY="50%"
  android:drawable="@drawable/ic_loading"
  android:duration="500" />
            

AnimatedRotateDrawable implements Animatable

AnimationDrawable


<!-- res/drawable/animated_wifi.xml -->
<animation-list>
  <item android:drawable="@drawable/ic_wifi_0"
      android:duration="250" />
  <item android:drawable="@drawable/ic_wifi_1"
      android:duration="250" />
  <item android:drawable="@drawable/ic_wifi_2"
      android:duration="250" />
  <item android:drawable="@drawable/ic_wifi_3"
      android:duration="250" />
</animation-list>
            

AnimationDrawable implements Animatable

AnimatedVectorDrawable


<!-- res/drawable/animated_clock.xml -->
<animated-vector android:drawable="@drawable/clock">
  <target android:name="hours"
    android:animation="@anim/hours_rotation" />
  <target android:name="minutes"
    android:animation="@anim/minutes_rotation" />
</animated-vector>
            

AnimatedVectorDrawable implements Animatable

Introduced in Lollipop

Animatable


private void startAnimation(
    TextView textView) {
  Drawable[] drawables
      = textView.getCompoundDrawables();
  for (Drawable drawable : drawables) {
    if (drawable != null &&
        drawable instanceof Animatable) {
      ((Animatable) drawable).start();
    }
  }
}
              
Animated Compound Drawable

More XML fun

Shadow


<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:padding="12dp"
  android:text="@string/shadow"
  android:textSize="80sp"
  android:textStyle="bold"
  android:shadowColor="#7000"
  android:shadowDx="12"
  android:shadowDy="12"
  android:shadowRadius="8"/>
              
  • shadowColor, shadowDx, shadowDy, shadowRadius
  • px, not dp
  • Add padding to accommodate shadow
Shadow

Shadow, abused


<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:padding="12dp"
  android:text="@string/blocky"
  android:textColor="@color/purple"
  android:textSize="80sp"
  android:textStyle="bold"
  android:shadowColor="@color/green"
  android:shadowDx="4"
  android:shadowDy="-4"
  android:shadowRadius="1"/>
              
Compound Drawable

Shadow, abused


<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:padding="12dp"
  android:text="@string/glow"
  android:textSize="80sp"
  android:textStyle="bold"
  android:textColor="@android:color/white"
  android:background="@android:color/black"
  android:shadowColor="@color/yellow"
  android:shadowDx="0"
  android:shadowDy="0"
  android:shadowRadius="24"/>
                      
Compound Drawable

Fancy looks

Custom font


Typeface typeface
    = Typeface.createFromAsset(
        getAssets(), "Ruthie.ttf");
textView.setTypeface(typeface);
              
Custom font

Gradient


Shader shader = new LinearGradient(
    0, 0, 0, textView.getTextSize(),
    Color.RED, Color.BLUE,
    Shader.TileMode.CLAMP);
textView.getPaint().setShader(shader);
              
Text gradient

Pattern


Bitmap bitmap
    = BitmapFactory.decodeResource(
    getResources(),
    R.drawable.cheetah_tile);
Shader shader = new BitmapShader(
    bitmap,
    Shader.TileMode.REPEAT,
    Shader.TileMode.REPEAT);
textView.getPaint().setShader(shader);
              
Patterned Text

Multiple styles

HTML


<h1>Hello World</h1>
Here is an
<img src="octopus"><i>octopus</i>.<br>
And here is a
<a href="http://d.android.com">
link</a>.
              

<string name="from_html_text">
<![CDATA[
<h1>Hello World</h1>
Here is an
<img src="octopus"><i>octopus</i>.<br>
And here is a
<a href="http://d.android.com">
link</a>.
]]>
</string>
              
From HTML

setMovementMethod


String html = getString(R.string.from_html_text);
textView.setMovementMethod(
    LinkMovementMethod.getInstance());
textView.setText(Html.fromHtml(
    html, new ResourceImageGetter(this), null));
              

ResourceImageGetter


private static class ResourceImageGetter
    implements Html.ImageGetter {
  // Constructor takes a Context
  public Drawable getDrawable(String source) {
    int path = context.getResources().getIdentifier(
        source, "drawable", context.getPackageName());
    Drawable drawable = ContextCompat.getDrawable(context, path);
    drawable.setBounds(0, 0,
       drawable.getIntrinsicWidth(),
       drawable.getIntrinsicHeight());
    return drawable;
  }
}
              

Clickable link

From HTML

Supported tags

  • Defined by platform
  • Can be extended with custom TagHandler

Span

One <u>two</u> three

"One two three" + Underline from position 4 to 6

spannableString.setSpan(new UnderlineSpan(), 4, 6, flags);

CharacterStyle

  • Character-level text formatting
  • e.g. ForegroundColorSpan, BackgroundColorSpan, UnderlineSpan

ParagraphStyle

  • Paragraph-level text formatting
  • e.g. BulletSpan, IconMarginSpan, QuoteSpan

Spans, a Powerful Concept

by Flavien Laurent

http://flavienlaurent.com/blog/2014/01/31/spans/

Stacked fractions

Fraction equation

Custom Tag

TextPaint#setFontFeatureSettings

@TargetApi(LOLLIPOP)


<string name="fraction_text">
<![CDATA[
1<afrc>1/2</afrc> + <afrc>11/16</afrc>
= 2<afrc>3/16</afrc>
]]>
</string>
              
Fraction equation

Custom Tag Handler


Typeface typeface
    = Typeface.createFromAsset(
      getAssets(), "Nutso2.otf");
textView.setTypeface(typeface);
              

String html = getString(
  R.string.fraction_text);
textView.setText(Html.fromHtml(
    html, null,
    new FractionTagHandler()));
              
Fraction equation

Custom Tag Handler


private static class FractionTagHandler implements Html.TagHandler {
  public void handleTag(boolean opening,
      String tag, Editable output, XMLReader xmlReader) {
  if (!"afrc".equalsIgnoreCase(tag)) return;
  int len = output.length();
  if (opening) {
    output.setSpan(new FractionSpan(), len, len,
        Spannable.SPAN_MARK_MARK);
  } else {
    Object obj = getLast(output, FractionSpan.class);
    int where = output.getSpanStart(obj);
    output.removeSpan(obj);
    if (where != len) {
      output.setSpan(new FractionSpan(), where, len, 0);
    }
  }
}
              

getLast


private Object getLast(Editable text, Class kind) {
  Object[] objs = text.getSpans(0, text.length(), kind);
  if (objs.length == 0) return null;
  for (int i = objs.length - 1; i >= 0; --i) {
    if(text.getSpanFlags(objs[i]) == Spannable.SPAN_MARK_MARK) {
      return objs[i];
    }
  }
  return null;
}
              

FractionSpan


private static class FractionSpan extends MetricAffectingSpan {
  public void updateMeasureState(TextPaint textPaint) {
    textPaint.setFontFeatureSettings("afrc");
  }
  public void updateDrawState(TextPaint textPaint) {
    textPaint.setFontFeatureSettings("afrc");
  }
}
              

Arbitrary fractions

Fractions

Styled string


static SpannableString formatString(
    Context context,
    int textId, int styleId) {
  String text = context.getString(textId);
  SpannableString spannableString
    = new SpannableString(text);
  spannableString.setSpan(
      new TextAppearanceSpan(
        context, styleId),
      0, text.length(), 0);
  return spannableString;
}
                
Styled string

Styled string


SpannableStringBuilder builder
    = new SpannableStringBuilder()
  .append(formatString(
      this, R.string.big_red,
      R.style.BigRedTextAppearance))
  .append("\n")
  .append(formatString(
      this, R.string.medium_green,
      R.style.MediumGreenTextAppearance))
  .append("\n")
  .append(formatString(
      this, R.string.small_blue,
      R.style.SmallBlueTextAppearance));
                
Styled string

Styled string


textView.setText(
  builder.subSequence(
    0, builder.length()));
                

<style name="BigRedTextAppearance"
    parent="@android:style/TextAppearance">
  <item name="android:textSize">
    56sp</item>
  <item name="android:textColor">
    #c00</item>
</style>
                
Styled string

AlignmentSpan


public void click(View button) {
  String text
      = editText.getText().toString();
  Layout.Alignment align =
    button.getId() ==
        R.id.add_to_right_button ?
      Layout.Alignment.ALIGN_OPPOSITE :
      Layout.Alignment.ALIGN_NORMAL;
  appendText(text, align);
  editText.setText(null);
}
                
AlignmentSpan

AlignmentSpan


private void appendText(
    CharSequence text,
    Layout.Alignment align) {
  AlignmentSpan span
    = new AlignmentSpan.Standard(align);
  SpannableString spannableString
    = new SpannableString(text);
  spannableString.setSpan(
    span, 0, text.length(), 0);
  if (textView.length() > 0) {
    textView.append("\n\n");
  }
  textView.append(spannableString);
}
              
AlignmentSpan

Rainbow span


String text 
  = textView.getText().toString();
SpannableString spannableString 
  = new SpannableString(text);
int start 
  = text.toLowerCase().indexOf(substring);
int end 
  = start + substring.length();
spannableString.setSpan(
  new RainbowSpan(this), start, end, 0);
              
Rainbow span

Rainbow span


private static class RainbowSpan
    extends CharacterStyle
    implements UpdateAppearance {
  private final int[] colors;
  public RainbowSpan(Context context) {
    colors = context.getResources()
      .getIntArray(
        R.array.rainbow);
  }
              
Rainbow span

Rainbow span


public void updateDrawState(
    TextPaint paint) {
  paint.setStyle(Paint.Style.FILL);
  Shader shader = new LinearGradient(
      0, 0, 0,
      paint.getTextSize() * colors.length, 
      colors, null,
      Shader.TileMode.MIRROR);
  Matrix matrix = new Matrix();
  matrix.setRotate(90);
  shader.setLocalMatrix(matrix);
  paint.setShader(shader);
}
              
Rainbow span

Animated rainbow span


ObjectAnimator objectAnimator 
  = ObjectAnimator.ofFloat(
    span, 
    ANIMATED_COLOR_SPAN_FLOAT_PROPERTY, 
    0, 100);
objectAnimator.setEvaluator(
  new FloatEvaluator());
objectAnimator.addUpdateListener(
    new ValueAnimator.AnimatorUpdateListener() {
  public void onAnimationUpdate(
      ValueAnimator animation) {
    textView.setText(spannableString);
  }
});
              

objectAnimator.setInterpolator(
  new LinearInterpolator());
objectAnimator.setDuration(
  DateUtils.MINUTE_IN_MILLIS * 3);
objectAnimator.setRepeatCount(
  ValueAnimator.INFINITE);
objectAnimator.start();
              
Animated rainbow span

Animated rainbow span


private static final 
  Property<AnimatedColorSpan, Float>
    ANIMATED_COLOR_SPAN_FLOAT_PROPERTY
    = new Property<AnimatedColorSpan, Float>(
        Float.class, 
        "ANIMATED_COLOR_SPAN_FLOAT_PROPERTY") {
  public void set(
      AnimatedColorSpan span, Float value) {
    span.setTranslateXPercentage(value);
  }
  public Float get(AnimatedColorSpan span) {
    return span.getTranslateXPercentage();
  }
};
              
Animated rainbow span

Animated rainbow span


public void setTranslateXPercentage(
    float value) {
  translateXPercentage = value;
}
public float getTranslateXPercentage() {
  return translateXPercentage;
}
              
Animated rainbow span

Animated rainbow span


public void updateDrawState(
    TextPaint paint) {
  paint.setStyle(Paint.Style.FILL);
  float width 
    = paint.getTextSize() * colors.length;
  if (this.shader == null) {
    this.shader = new LinearGradient(
        0, 0, 0, width, colors, null, 
        Shader.TileMode.MIRROR);
  }
  this.matrix.reset();
  this.matrix.setRotate(90);
  this.matrix.postTranslate(
    width * translateXPercentage, 0);
  this.shader.setLocalMatrix(this.matrix);
  paint.setShader(this.shader);
}
              
Animated rainbow span

ClickableSpan

ClickableSpan

ClickableSpan


String text = textView.getText().toString();

String goToSettings = getString(R.string.go_to_settings);
int start = text.indexOf(goToSettings);
int end = start + goToSettings.length();

SpannableString spannableString = new SpannableString(text);
spannableString.setSpan(new GoToSettingsSpan(), start, end, 0);
textView.setText(spannableString);

textView.setMovementMethod(new LinkMovementMethod());
          

ClickableSpan


private static class GoToSettingsSpan extends ClickableSpan {
  public void onClick(View view) {
    view.getContext().startActivity(
      new Intent(android.provider.Settings.ACTION_SETTINGS));
  }
}
          

ClickableSpan


<TextView
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:text="@string/clickable_span_text"
  android:textColorLink="@color/go_to_settings"
  android:textColorHighlight="@color/light_green"/>
          

ClickableSpan

ClickableSpan

Lined paper

Lined paper

Lined paper


public class LinedEditText 
    extends EditText {
  private void init() {
    this.paint = new Paint();
    paint.setStyle(Paint.Style.STROKE);
    paint.setColor(
      getResources().getColor(
        R.color.paper_line));
    paint.setStrokeWidth(
        getLineHeight() / 10);
    paint.setStrokeCap(Paint.Cap.ROUND);
  }
}
              
Lined paper

Lined paper


protected void onDraw(Canvas canvas) {
  float startX = getPaddingLeft();
  float stopX 
    = getWidth() - getPaddingRight();
  float offsetY = getPaddingTop()
     - getPaint().getFontMetrics().top
     + paint.getStrokeWidth() / 2;
  for (int i = 0; i < getLineCount(); ++i) {
    float y = offsetY + getLineHeight() * i;
    canvas.drawLine(
      startX, y, stopX, y, paint);
  }

  super.onDraw(canvas);
}
            
fontMetrics.top
The maximum distance above the baseline for the tallest glyph in the font at a given text size.
Lined paper

Emoji

Emoji before rendering Emoji after rendering

Unicode with system font

Emoji before rendering Emoji after rendering

Unicode with custom font


String text 
  = textView.getText().toString();
SpannableString spannableString 
  = new SpannableString(text);
IconFontSpan iconFontSpan
  = new IconFontSpan(
    textView.getContext());
Pattern pattern 
  = Pattern.compile("\u26F7");  // skier
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
  spannableString.setSpan(iconFontSpan,
    matcher.start(), matcher.end(), 0);
}
                
Emoji

Unicode with custom font


private static class IconFontSpan
    extends MetricAffectingSpan {
  private static Typeface typeface = null;
  public IconFontSpan(Context context) {
    if (typeface == null) {
      typeface = Typeface.createFromAsset(
        context.getAssets(), "icomoon.ttf");
      }
    }
  public void updateMeasureState(
      TextPaint textPaint) {
    textPaint.setTypeface(typeface);
  }
  public void updateDrawState(
      TextPaint textPaint) {
    textPaint.setTypeface(typeface);
  }
}
                
Emoji

ImageSpan from resources


Pattern pattern = Pattern.compile(":octopus:");
Matcher matcher = pattern.matcher(text);
Bitmap octopus = null;
int size = (int) (-textView.getPaint().ascent());
while (matcher.find()) {
  if (octopus == null) {
    Bitmap bitmap = BitmapFactory.decodeResource(
        getResources(), R.drawable.octopus);
    octopus = Bitmap.createScaledBitmap(
        bitmap, size, size, true);
    bitmap.recycle();
  }
  ImageSpan span = new ImageSpan(
      this, octopus, ImageSpan.ALIGN_BASELINE);
  spannableString.setSpan(
      span, matcher.start(), matcher.end(), 0);
}
                
Font metrics
Emoji

Dynamic ImageSpan


// :speed_50: :speed_110:
Pattern pattern 
  = Pattern.compile(":speed_(\\d\\d\\d?):");
Pattern matcher = pattern.matcher(text);
while (matcher.find()) {
  SpeedSignDrawable drawable 
    = new SpeedSignDrawable(
      textView, matcher.group(1));
  ImageSpan span = new ImageSpan(
      drawable, ImageSpan.ALIGN_BASELINE);
  spannableString.setSpan(
      span, matcher.start(), matcher.end(), 0);
}
                
Emoji

SpeedSignDrawable


private static class SpeedSignDrawable 
    extends Drawable {
  public SpeedSignDrawable(
      TextView textView, String number) {
    this.ascent 
      = textView.getPaint().ascent();
    this.descent 
      = textView.getPaint().descent();
    this.textSize = textView.getTextSize();
    this.strokeWidth 
      = textView.getPaint().getStrokeWidth();
    this.number = number;
    int size = (int) -ascent;
    this.setBounds(0, 0, size, size);
  }
                
Emoji

SpeedSignDrawable


public void draw(Canvas canvas) {
  drawYellowCircle(canvas);
  drawRedRing(canvas);
  drawBlackNumber(canvas);
}

private void drawYellowCircle(Canvas canvas) {
  int size = (int) -ascent;
  Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
  paint.setStyle(Paint.Style.FILL);
  paint.setColor(Color.YELLOW);
  canvas.drawCircle(
    size/2, size/2, size/2, paint);
}
Emoji

SpeedSignDrawable


private void drawRedRing(Canvas canvas) {
  Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
  paint.setStyle(Paint.Style.STROKE);
  paint.setColor(Color.RED);
  float ringWidth = size / 10;
  paint.setStrokeWidth(ringWidth);
  canvas.drawCircle(
    size/2, size/2, size/2 - ringWidth/2, paint);
}
              
Emoji

SpeedSignDrawable


private void drawBlackNumber(Canvas canvas) {
  float ratio = 0.4f;
  Paint paint = new Paint(
    Paint.ANTI_ALIAS_FLAG);
  paint.setStyle(Paint.Style.FILL);
  paint.setColor(Color.BLACK);
  paint.setTextAlign(Paint.Align.CENTER);
  paint.setTextSize(textSize * ratio);
  paint.setStrokeWidth(strokeWidth);
  paint.setTypeface(
    Typeface.defaultFromStyle(Typeface.BOLD));
  int x = size / 2;
  int y = (int) (size/2 
    - ((descent + ascent)/2) * ratio);
  canvas.drawText(number, x, y, paint);
}
              
Emoji

Summary

  • Animated CompoundDrawable
  • Text shadow
  • Custom font
  • Gradient text
  • Patterned text
  • HTML.fromHtml()
  • setFontFeatureSettings()
  • Styled string
  • AlignmentSpan
  • ClickableSpan
  • Rainbow span, animated
  • Lined paper
  • Emoji

Beautiful Typography on

Beautiful Typography on Android https://www.youtube.com/watch?v=7GFRpP9a-eQ

Thank you!