|
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); |
|
} |
|
} |
|
} |