/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.text;
import android.view.View;
import java.nio.CharBuffer;
/**
* Some objects that implement {@link TextDirectionHeuristic}. Use these with
* the {@link BidiFormatter#unicodeWrap unicodeWrap()} methods in {@link BidiFormatter}.
* Also notice that these direction heuristics correspond to the same types of constants
* provided in the {@link android.view.View} class for {@link android.view.View#setTextDirection
* setTextDirection()}, such as {@link android.view.View#TEXT_DIRECTION_RTL}.
* <p>To support versions lower than {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
* you can use the support library's {@link android.support.v4.text.TextDirectionHeuristicsCompat}
* class.
*
*/
public class TextDirectionHeuristics {
/**
* Always decides that the direction is left to right.
*/
public static final TextDirectionHeuristic LTR =
new TextDirectionHeuristicInternal(null /* no algorithm */, false);
/**
* Always decides that the direction is right to left.
*/
public static final TextDirectionHeuristic RTL =
new TextDirectionHeuristicInternal(null /* no algorithm */, true);
/**
* Determines the direction based on the first strong directional character, including bidi
* format chars, falling back to left to right if it finds none. This is the default behavior
* of the Unicode Bidirectional Algorithm.
*/
public static final TextDirectionHeuristic FIRSTSTRONG_LTR =
new TextDirectionHeuristicInternal(FirstStrong.INSTANCE, false);
/**
* Determines the direction based on the first strong directional character, including bidi
* format chars, falling back to right to left if it finds none. This is similar to the default
* behavior of the Unicode Bidirectional Algorithm, just with different fallback behavior.
*/
public static final TextDirectionHeuristic FIRSTSTRONG_RTL =
new TextDirectionHeuristicInternal(FirstStrong.INSTANCE, true);
/**
* If the text contains any strong right to left non-format character, determines that the
* direction is right to left, falling back to left to right if it finds none.
*/
public static final TextDirectionHeuristic ANYRTL_LTR =
new TextDirectionHeuristicInternal(AnyStrong.INSTANCE_RTL, false);
/**
* Force the paragraph direction to the Locale direction. Falls back to left to right.
*/
public static final TextDirectionHeuristic LOCALE = TextDirectionHeuristicLocale.INSTANCE;
/**
* State constants for taking care about true / false / unknown
*/
private static final int STATE_TRUE = 0;
private static final int STATE_FALSE = 1;
private static final int STATE_UNKNOWN = 2;
/* Returns STATE_TRUE for strong RTL characters, STATE_FALSE for strong LTR characters, and
* STATE_UNKNOWN for everything else.
*/
private static int isRtlCodePoint(int codePoint) {
switch (Character.getDirectionality(codePoint)) {
case Character.DIRECTIONALITY_LEFT_TO_RIGHT:
return STATE_FALSE;
case Character.DIRECTIONALITY_RIGHT_TO_LEFT:
case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC:
return STATE_TRUE;
case Character.DIRECTIONALITY_UNDEFINED:
// Unassigned characters still have bidi direction, defined at:
// http://www.unicode.org/Public/UCD/latest/ucd/extracted/DerivedBidiClass.txt
if ((0x0590 <= codePoint && codePoint <= 0x08FF) ||
(0xFB1D <= codePoint && codePoint <= 0xFDCF) ||
(0xFDF0 <= codePoint && codePoint <= 0xFDFF) ||
(0xFE70 <= codePoint && codePoint <= 0xFEFF) ||
(0x10800 <= codePoint && codePoint <= 0x10FFF) ||
(0x1E800 <= codePoint && codePoint <= 0x1EFFF)) {
// Unassigned RTL character
return STATE_TRUE;
} else if (
// Potentially-unassigned Default_Ignorable. Ranges are from unassigned
// characters that have Unicode property Other_Default_Ignorable_Code_Point
// plus some enlargening to cover bidi isolates and simplify checks.
(0x2065 <= codePoint && codePoint <= 0x2069) ||
(0xFFF0 <= codePoint && codePoint <= 0xFFF8) ||
(0xE0000 <= codePoint && codePoint <= 0xE0FFF) ||
// Non-character
(0xFDD0 <= codePoint && codePoint <= 0xFDEF) ||
((codePoint & 0xFFFE) == 0xFFFE) ||
// Currency symbol
(0x20A0 <= codePoint && codePoint <= 0x20CF) ||
// Unpaired surrogate
(0xD800 <= codePoint && codePoint <= 0xDFFF)) {
return STATE_UNKNOWN;
} else {
// Unassigned LTR character
return STATE_FALSE;
}
default:
return STATE_UNKNOWN;
}
}
/**
* Computes the text direction based on an algorithm. Subclasses implement
* {@link #defaultIsRtl} to handle cases where the algorithm cannot determine the
* direction from the text alone.
*/
private static abstract class TextDirectionHeuristicImpl implements TextDirectionHeuristic {
private final TextDirectionAlgorithm mAlgorithm;
public TextDirectionHeuristicImpl(TextDirectionAlgorithm algorithm) {
mAlgorithm = algorithm;
}
/**
* Return true if the default text direction is rtl.
*/
abstract protected boolean defaultIsRtl();
@Override
public boolean isRtl(char[] array, int start, int count) {
return isRtl(CharBuffer.wrap(array), start, count);
}
@Override
public boolean isRtl(CharSequence cs, int start, int count) {
if (cs == null || start < 0 || count < 0 || cs.length() - count < start) {
throw new IllegalArgumentException();
}
if (mAlgorithm == null) {
return defaultIsRtl();
}
return doCheck(cs, start, count);
}
private boolean doCheck(CharSequence cs, int start, int count) {
switch(mAlgorithm.checkRtl(cs, start, count)) {
case STATE_TRUE:
return true;
case STATE_FALSE:
return false;
default:
return defaultIsRtl();
}
}
}
private static class TextDirectionHeuristicInternal extends TextDirectionHeuristicImpl {
private final boolean mDefaultIsRtl;
private TextDirectionHeuristicInternal(TextDirectionAlgorithm algorithm,
boolean defaultIsRtl) {
super(algorithm);
mDefaultIsRtl = defaultIsRtl;
}
@Override
protected boolean defaultIsRtl() {
return mDefaultIsRtl;
}
}
/**
* Interface for an algorithm to guess the direction of a paragraph of text.
*/
private static interface TextDirectionAlgorithm {
/**
* Returns whether the range of text is RTL according to the algorithm.
*/
int checkRtl(CharSequence cs, int start, int count);
}
/**
* Algorithm that uses the first strong directional character to determine the paragraph
* direction. This is the standard Unicode Bidirectional Algorithm (steps P2 and P3), with the
* exception that if no strong character is found, UNKNOWN is returned.
*/
private static class FirstStrong implements TextDirectionAlgorithm {
@Override
public int checkRtl(CharSequence cs, int start, int count) {
int result = STATE_UNKNOWN;
int openIsolateCount = 0;
for (int cp, i = start, end = start + count;
i < end && result == STATE_UNKNOWN;
i += Character.charCount(cp)) {
cp = Character.codePointAt(cs, i);
if (0x2066 <= cp && cp <= 0x2068) { // Opening isolates
openIsolateCount += 1;
} else if (cp == 0x2069) { // POP DIRECTIONAL ISOLATE (PDI)
if (openIsolateCount > 0) openIsolateCount -= 1;
} else if (openIsolateCount == 0) {
// Only consider the characters outside isolate pairs
result = isRtlCodePoint(cp);
}
}
return result;
}
private FirstStrong() {
}
public static final FirstStrong INSTANCE = new FirstStrong();
}
/**
* Algorithm that uses the presence of any strong directional character of the type indicated
* in the constructor parameter to determine the direction of text.
*
* Characters inside isolate pairs are skipped.
*/
private static class AnyStrong implements TextDirectionAlgorithm {
private final boolean mLookForRtl;
@Override
public int checkRtl(CharSequence cs, int start, int count) {
boolean haveUnlookedFor = false;
int openIsolateCount = 0;
for (int cp, i = start, end = start + count; i < end; i += Character.charCount(cp)) {
cp = Character.codePointAt(cs, i);
if (0x2066 <= cp && cp <= 0x2068) { // Opening isolates
openIsolateCount += 1;
} else if (cp == 0x2069) { // POP DIRECTIONAL ISOLATE (PDI)
if (openIsolateCount > 0) openIsolateCount -= 1;
} else if (openIsolateCount == 0) {
// Only consider the characters outside isolate pairs
switch (isRtlCodePoint(cp)) {
case STATE_TRUE:
if (mLookForRtl) {
return STATE_TRUE;
}
haveUnlookedFor = true;
break;
case STATE_FALSE:
if (!mLookForRtl) {
return STATE_FALSE;
}
haveUnlookedFor = true;
break;
default:
break;
}
}
}
if (haveUnlookedFor) {
return mLookForRtl ? STATE_FALSE : STATE_TRUE;
}
return STATE_UNKNOWN;
}
private AnyStrong(boolean lookForRtl) {
this.mLookForRtl = lookForRtl;
}
public static final AnyStrong INSTANCE_RTL = new AnyStrong(true);
public static final AnyStrong INSTANCE_LTR = new AnyStrong(false);
}
/**
* Algorithm that uses the Locale direction to force the direction of a paragraph.
*/
private static class TextDirectionHeuristicLocale extends TextDirectionHeuristicImpl {
public TextDirectionHeuristicLocale() {
super(null);
}
@Override
protected boolean defaultIsRtl() {
final int dir = TextUtils.getLayoutDirectionFromLocale(java.util.Locale.getDefault());
return (dir == View.LAYOUT_DIRECTION_RTL);
}
public static final TextDirectionHeuristicLocale INSTANCE =
new TextDirectionHeuristicLocale();
}
}