/* * 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(); } }