Skip to content

Instantly share code, notes, and snippets.

@mostafah
Last active November 10, 2017 09:32
Show Gist options
  • Save mostafah/2acbec5b323fd01105930ff69c8149fe to your computer and use it in GitHub Desktop.
Save mostafah/2acbec5b323fd01105930ff69c8149fe to your computer and use it in GitHub Desktop.
Applying different typefaces to different scripts of text in Android

This is a simple solution for applying two different typefaces to Arabic and non-Arabic parts of a text in Android. This enables having two font files for Arabic and Latin scripts and applying them to texts that might be a mix of them.

Quality disclaimer first: I had written this code a few years ago and have not worked on it since then. I am publishing it now because someone on Twitter asked for something like this. I have not even compiled this code recently, so it has never been tested on recent versions of Android. But it worked fined a few years ago and I hope it still does.

How to Use

The main class is DualStyler. It can get a string and apply the typefaces to its Arabic and non-Arabic parts and return the result.

Assuming that you have your font files in your assets/fonts directory and have loaded them in your code like this:

Typeface arabicTypeface = Typeface.createFromAsset(context.getAssets(), "fonts/Arabic.otf");
Typeface latinTypeface = Typeface.createFromAsset(context.getAssets(), "fonts/Latin.otf");

Then you create a new instance of DualStyler:

DualStyler dualStyler = new DualStyler(latinTypeface, arabicTypeface);

You can also use different text sizes. For example, if your Latin typeface looks bigger than your Arabic typeface you can create an instance of DualStyler that makes Latin text, say, 20% smaller:

DualStyler dualStyler = new DualStyler(latinTypeface, arabicTypeface, 0.8, 1f);

Then you can apply it to your string like this:

CharSequence styled = dualStyler.apply(string);

For convenience, there is an applyToTextView that can be used to style TextViews directly:

TextView tv = (TextView) findViewById(R.id.text_view);
dualStyler.applyToTextView(tv);

For user-editable text, there is StyledEditText that can be used instead of Android’s EditText like this in your layout files:

<com.example.StyledEditText android:id="@+id/edit_id"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:hint="@string/hint_string"/>

As far as I remember, there was no performance problem with StyledEditText and it supported all the standard edit actions, because it uses a StyleFilter.

Problem with Existing Spans

I never tested this, but I guess that existing spans (bold parts, links, …) in texts can be messed up in DualStyler. It’s not that hard to fix this too, but I didn’t need that when I wrote that code.

package com.example;
import java.lang.Character;
import java.lang.Character.UnicodeBlock;
import java.util.ArrayList;
import java.util.List;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
import android.widget.TextView;
/** This class can detect Arabic and non-Arabic parts of a text and apply
different styles to them. This allows programs to use different typefaces
for Arabic and non-Arabic texts at the same time. */
public class DualStyler {
private Typeface mLatinTypeface;
private Typeface mArabicTypeface;
private float mLatinScale;
private float mArabicScale;
/** Creates an instance of DualStyler with the given typefaces for Latin and
Arabic. */
public DualStyler(Typeface latin, Typeface arabic) {
this(latin, arabic, 1, 1);
}
/** Creates an instance of DualStyler with the given typefaces and text
size scales for Latin and Arabic. */
public DualStyler(Typeface latin, Typeface arabic, float latinScale,
float arabicScale) {
mLatinTypeface = latin;
mArabicTypeface = arabic;
mLatinScale = latinScale;
mArabicScale = arabicScale;
}
/** Set scales for each text style. The text size for each style will by
multiplied by these scale numbers. Default scale is 1. */
public void setScales(float latin, float arabic) {
mLatinScale = latin;
mArabicScale = arabic;
}
/** This applies different styles to Arabic and non-Arabic parts of any
CharSequence by splitting the CharSequence into Spans. The returned
CharSequence has styled Spans for different parts of it. */
public CharSequence apply(CharSequence text) {
// don't crash on null input
if (text == null) {
return text;
}
// detect language runs
List<Run> runs = detectRuns(text);
// convert text to spannable
SpannableString spannable = new SpannableString(text);
// loop over each run and set its typeface
for (Run r : runs) {
CustomStylerSpan span;
if (r.getKind() == Kind.ARABIC) {
span = new CustomStylerSpan(mArabicTypeface, mArabicScale);
} else {
span = new CustomStylerSpan(mLatinTypeface, mLatinScale);
}
spannable.setSpan(span, r.getStart(), r.getEnd(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
return spannable;
}
/** This uses "apply" to change styles directly on a TextView. */
public void applyToTextView(TextView tv) {
tv.setText(this.apply(tv.getText()));
}
// Finds runs of the text that are either completely Arabic or Latin. This
// gets a CharSequence that may include Arabic and Latin characters, and
// returns a list of runs that show where do un-interrupted blocks of Arabic
// or Latin text start or end. Neutral characters, like spaces and most
// punctiations are ignored in detections.
private List<Run> detectRuns(CharSequence text) {
List<Run> runs = new ArrayList<Run>();
// start by assuming nothing
Kind prev = Kind.NEUTRAL;
int start = 0;
for (int i = 0; i < text.length(); i++) {
Kind kind = kind(text.charAt(i));
// ignore neutral characters, or characters similar to previous ones
if ((kind == Kind.NEUTRAL) || (kind == prev)) {
continue;
}
// languages has changed, but it is the first non-neutral character,
// so previous characters should be assumed to be of the same kind
// and continue until a real language change happens
if (prev == Kind.NEUTRAL) {
prev = kind;
continue;
}
// language changes; save the current run and prepare for the next
runs.add(new Run(start, i, prev));
start = i;
prev = kind;
}
// finish off the last run
runs.add(new Run(start, text.length(), prev));
return runs;
}
// This holds a run of text that holds either only Arabic or only Latin
// characters. Of course, the neutral characters can be included too. Any
// string can be split into a number of runs that each one holds only one
// kind of characters and hence can be styled appropriately.
//
// A Run instance doesn't hold characters. It just refers to indexes in a
// string.
private class Run {
private int mStart;
private int mEnd;
private Kind mKind;
private Run(int start, int end, Kind kind) {
mStart = start;
mEnd = end;
mKind = kind;
}
private void setStart(int start) {
mStart = start;
}
private int getStart() {
return mStart;
}
private void setEnd(int end) {
mEnd = end;
}
private int getEnd() {
return mEnd;
}
private void setKind(Kind kind) {
mKind = kind;
}
private Kind getKind() {
return mKind;
}
}
// A character is either Arabic, non-Arabic, or netural. Neutral is used for
// spaces, most punctuations, and other characters that can belong to both
// Arabic and Latin text. This type defines these three states.
private enum Kind { NEUTRAL, ARABIC, LATIN };
// Finds whether a character is Arabic, Latin, or neutral.
private Kind kind(char ch) {
// it's Arabic if it's in Unicode's four Arabic blocks
UnicodeBlock ub = UnicodeBlock.of(ch);
if ((ub == UnicodeBlock.ARABIC)
|| (ub == UnicodeBlock.ARABIC_PRESENTATION_FORMS_A)
|| (ub == UnicodeBlock.ARABIC_PRESENTATION_FORMS_B)
|| (ub == UnicodeBlock.ARABIC_SUPPLEMENT)) {
return Kind.ARABIC;
}
// list of neutral characters
final String punctuation = " .:!…-‐‑‒–—―|/\\*(){}[]«»+−×÷=≠≈<>≤≥∞§¶•\u200b\u200c\u200d\u200f";
if (punctuation.indexOf(ch) != -1) {
return Kind.NEUTRAL;
}
// it's not Arabic and not neutral; it's Latin then
return Kind.LATIN;
}
// This class implements a span that can apply a custom typeface to text.
private class CustomStylerSpan extends MetricAffectingSpan {
private final Typeface mTypeface;
private float mScale = 1;
// Creates a new instance with the given typeface and text size
// multiplier for texts that get this span.
public CustomStylerSpan(final Typeface typeface, final float scale) {
mTypeface = typeface;
mScale = scale;
}
@Override
public void updateDrawState(final TextPaint drawState) {
apply(drawState);
}
@Override
public void updateMeasureState(final TextPaint paint) {
apply(paint);
}
// This gives the typeface to the paint used for drawing the text.
private void apply(final Paint paint) {
paint.setTextSize(paint.getTextSize()*mScale);
paint.setTypeface(mTypeface);
}
}
}
package com.example;
import java.util.Arrays;
import android.content.Context;
import android.graphics.Typeface;
import android.text.InputFilter;
import android.text.Spanned;
import android.util.AttributeSet;
import android.widget.EditText;
import com.example.DualStyler;
/** This class extends EditText and adds custom typeface style to its hint and
* main text. Since the text is editable, its style should be updated with
* every edit.
*/
public class StyledEditText extends EditText {
// An instance of DualStyler.
private DualStyler mDualStyler;
public StyledEditText(Context context) {
super(context);
init(context);
}
public StyledEditText(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public StyledEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
// Initiate the EditText.
private void init(Context context) {
// prepare the DualStyler instance
Typeface latinTypeface = Typeface.createFromAsset(context.getAssets(), "fonts/LatinTypeface.otf");
Typeface arabicTypeface = Typeface.createFromAsset(context.getAssets(), "fonts/ArabicTypeface.ttf");
mDualStyler = new DualStyler(latinTypeface, arabicTypeface, 1f, 1f);
// add the InputFilter that applies custom style to the EditText
InputFilter[] filters = getFilters();
filters = Arrays.copyOf(filters, filters.length + 1);
filters[filters.length - 1] = new StyleFilter();
setFilters(filters);
// apply custom typeface to the hint text
setHint(mDualStyler.apply(getHint()));
}
// This InputFilter applies DualStyler to its text, used for keeping text in
// the EditText in style.
private class StyleFilter implements InputFilter {
@Override
public CharSequence filter(CharSequence source, int start, int end,
Spanned dest, int dstart, int dend) {
return mDualStyler.apply(source);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment