Home | History | Annotate | Download | only in text
      1 /*
      2  * Copyright (C) 2006 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.text;
     18 
     19 import android.annotation.IntDef;
     20 import android.annotation.IntRange;
     21 import android.graphics.Canvas;
     22 import android.graphics.Paint;
     23 import android.graphics.Path;
     24 import android.graphics.Rect;
     25 import android.text.method.TextKeyListener;
     26 import android.text.style.AlignmentSpan;
     27 import android.text.style.LeadingMarginSpan;
     28 import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
     29 import android.text.style.LineBackgroundSpan;
     30 import android.text.style.ParagraphStyle;
     31 import android.text.style.ReplacementSpan;
     32 import android.text.style.TabStopSpan;
     33 
     34 import com.android.internal.annotations.VisibleForTesting;
     35 import com.android.internal.util.ArrayUtils;
     36 import com.android.internal.util.GrowingArrayUtils;
     37 
     38 import java.lang.annotation.Retention;
     39 import java.lang.annotation.RetentionPolicy;
     40 import java.util.Arrays;
     41 
     42 /**
     43  * A base class that manages text layout in visual elements on
     44  * the screen.
     45  * <p>For text that will be edited, use a {@link DynamicLayout},
     46  * which will be updated as the text changes.
     47  * For text that will not change, use a {@link StaticLayout}.
     48  */
     49 public abstract class Layout {
     50     /** @hide */
     51     @IntDef(prefix = { "BREAK_STRATEGY_" }, value = {
     52             BREAK_STRATEGY_SIMPLE,
     53             BREAK_STRATEGY_HIGH_QUALITY,
     54             BREAK_STRATEGY_BALANCED
     55     })
     56     @Retention(RetentionPolicy.SOURCE)
     57     public @interface BreakStrategy {}
     58 
     59     /**
     60      * Value for break strategy indicating simple line breaking. Automatic hyphens are not added
     61      * (though soft hyphens are respected), and modifying text generally doesn't affect the layout
     62      * before it (which yields a more consistent user experience when editing), but layout may not
     63      * be the highest quality.
     64      */
     65     public static final int BREAK_STRATEGY_SIMPLE = 0;
     66 
     67     /**
     68      * Value for break strategy indicating high quality line breaking, including automatic
     69      * hyphenation and doing whole-paragraph optimization of line breaks.
     70      */
     71     public static final int BREAK_STRATEGY_HIGH_QUALITY = 1;
     72 
     73     /**
     74      * Value for break strategy indicating balanced line breaking. The breaks are chosen to
     75      * make all lines as close to the same length as possible, including automatic hyphenation.
     76      */
     77     public static final int BREAK_STRATEGY_BALANCED = 2;
     78 
     79     /** @hide */
     80     @IntDef(prefix = { "HYPHENATION_FREQUENCY_" }, value = {
     81             HYPHENATION_FREQUENCY_NORMAL,
     82             HYPHENATION_FREQUENCY_FULL,
     83             HYPHENATION_FREQUENCY_NONE
     84     })
     85     @Retention(RetentionPolicy.SOURCE)
     86     public @interface HyphenationFrequency {}
     87 
     88     /**
     89      * Value for hyphenation frequency indicating no automatic hyphenation. Useful
     90      * for backward compatibility, and for cases where the automatic hyphenation algorithm results
     91      * in incorrect hyphenation. Mid-word breaks may still happen when a word is wider than the
     92      * layout and there is otherwise no valid break. Soft hyphens are ignored and will not be used
     93      * as suggestions for potential line breaks.
     94      */
     95     public static final int HYPHENATION_FREQUENCY_NONE = 0;
     96 
     97     /**
     98      * Value for hyphenation frequency indicating a light amount of automatic hyphenation, which
     99      * is a conservative default. Useful for informal cases, such as short sentences or chat
    100      * messages.
    101      */
    102     public static final int HYPHENATION_FREQUENCY_NORMAL = 1;
    103 
    104     /**
    105      * Value for hyphenation frequency indicating the full amount of automatic hyphenation, typical
    106      * in typography. Useful for running text and where it's important to put the maximum amount of
    107      * text in a screen with limited space.
    108      */
    109     public static final int HYPHENATION_FREQUENCY_FULL = 2;
    110 
    111     private static final ParagraphStyle[] NO_PARA_SPANS =
    112         ArrayUtils.emptyArray(ParagraphStyle.class);
    113 
    114     /** @hide */
    115     @IntDef(prefix = { "JUSTIFICATION_MODE_" }, value = {
    116             JUSTIFICATION_MODE_NONE,
    117             JUSTIFICATION_MODE_INTER_WORD
    118     })
    119     @Retention(RetentionPolicy.SOURCE)
    120     public @interface JustificationMode {}
    121 
    122     /**
    123      * Value for justification mode indicating no justification.
    124      */
    125     public static final int JUSTIFICATION_MODE_NONE = 0;
    126 
    127     /**
    128      * Value for justification mode indicating the text is justified by stretching word spacing.
    129      */
    130     public static final int JUSTIFICATION_MODE_INTER_WORD = 1;
    131 
    132     /*
    133      * Line spacing multiplier for default line spacing.
    134      */
    135     public static final float DEFAULT_LINESPACING_MULTIPLIER = 1.0f;
    136 
    137     /*
    138      * Line spacing addition for default line spacing.
    139      */
    140     public static final float DEFAULT_LINESPACING_ADDITION = 0.0f;
    141 
    142     /**
    143      * Return how wide a layout must be in order to display the specified text with one line per
    144      * paragraph.
    145      *
    146      * <p>As of O, Uses
    147      * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR} as the default text direction heuristics. In
    148      * the earlier versions uses {@link TextDirectionHeuristics#LTR} as the default.</p>
    149      */
    150     public static float getDesiredWidth(CharSequence source,
    151                                         TextPaint paint) {
    152         return getDesiredWidth(source, 0, source.length(), paint);
    153     }
    154 
    155     /**
    156      * Return how wide a layout must be in order to display the specified text slice with one
    157      * line per paragraph.
    158      *
    159      * <p>As of O, Uses
    160      * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR} as the default text direction heuristics. In
    161      * the earlier versions uses {@link TextDirectionHeuristics#LTR} as the default.</p>
    162      */
    163     public static float getDesiredWidth(CharSequence source, int start, int end, TextPaint paint) {
    164         return getDesiredWidth(source, start, end, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR);
    165     }
    166 
    167     /**
    168      * Return how wide a layout must be in order to display the
    169      * specified text slice with one line per paragraph.
    170      *
    171      * @hide
    172      */
    173     public static float getDesiredWidth(CharSequence source, int start, int end, TextPaint paint,
    174             TextDirectionHeuristic textDir) {
    175         return getDesiredWidthWithLimit(source, start, end, paint, textDir, Float.MAX_VALUE);
    176     }
    177     /**
    178      * Return how wide a layout must be in order to display the
    179      * specified text slice with one line per paragraph.
    180      *
    181      * If the measured width exceeds given limit, returns limit value instead.
    182      * @hide
    183      */
    184     public static float getDesiredWidthWithLimit(CharSequence source, int start, int end,
    185             TextPaint paint, TextDirectionHeuristic textDir, float upperLimit) {
    186         float need = 0;
    187 
    188         int next;
    189         for (int i = start; i <= end; i = next) {
    190             next = TextUtils.indexOf(source, '\n', i, end);
    191 
    192             if (next < 0)
    193                 next = end;
    194 
    195             // note, omits trailing paragraph char
    196             float w = measurePara(paint, source, i, next, textDir);
    197             if (w > upperLimit) {
    198                 return upperLimit;
    199             }
    200 
    201             if (w > need)
    202                 need = w;
    203 
    204             next++;
    205         }
    206 
    207         return need;
    208     }
    209 
    210     /**
    211      * Subclasses of Layout use this constructor to set the display text,
    212      * width, and other standard properties.
    213      * @param text the text to render
    214      * @param paint the default paint for the layout.  Styles can override
    215      * various attributes of the paint.
    216      * @param width the wrapping width for the text.
    217      * @param align whether to left, right, or center the text.  Styles can
    218      * override the alignment.
    219      * @param spacingMult factor by which to scale the font size to get the
    220      * default line spacing
    221      * @param spacingAdd amount to add to the default line spacing
    222      */
    223     protected Layout(CharSequence text, TextPaint paint,
    224                      int width, Alignment align,
    225                      float spacingMult, float spacingAdd) {
    226         this(text, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
    227                 spacingMult, spacingAdd);
    228     }
    229 
    230     /**
    231      * Subclasses of Layout use this constructor to set the display text,
    232      * width, and other standard properties.
    233      * @param text the text to render
    234      * @param paint the default paint for the layout.  Styles can override
    235      * various attributes of the paint.
    236      * @param width the wrapping width for the text.
    237      * @param align whether to left, right, or center the text.  Styles can
    238      * override the alignment.
    239      * @param spacingMult factor by which to scale the font size to get the
    240      * default line spacing
    241      * @param spacingAdd amount to add to the default line spacing
    242      *
    243      * @hide
    244      */
    245     protected Layout(CharSequence text, TextPaint paint,
    246                      int width, Alignment align, TextDirectionHeuristic textDir,
    247                      float spacingMult, float spacingAdd) {
    248 
    249         if (width < 0)
    250             throw new IllegalArgumentException("Layout: " + width + " < 0");
    251 
    252         // Ensure paint doesn't have baselineShift set.
    253         // While normally we don't modify the paint the user passed in,
    254         // we were already doing this in Styled.drawUniformRun with both
    255         // baselineShift and bgColor.  We probably should reevaluate bgColor.
    256         if (paint != null) {
    257             paint.bgColor = 0;
    258             paint.baselineShift = 0;
    259         }
    260 
    261         mText = text;
    262         mPaint = paint;
    263         mWidth = width;
    264         mAlignment = align;
    265         mSpacingMult = spacingMult;
    266         mSpacingAdd = spacingAdd;
    267         mSpannedText = text instanceof Spanned;
    268         mTextDir = textDir;
    269     }
    270 
    271     /** @hide */
    272     protected void setJustificationMode(@JustificationMode int justificationMode) {
    273         mJustificationMode = justificationMode;
    274     }
    275 
    276     /**
    277      * Replace constructor properties of this Layout with new ones.  Be careful.
    278      */
    279     /* package */ void replaceWith(CharSequence text, TextPaint paint,
    280                               int width, Alignment align,
    281                               float spacingmult, float spacingadd) {
    282         if (width < 0) {
    283             throw new IllegalArgumentException("Layout: " + width + " < 0");
    284         }
    285 
    286         mText = text;
    287         mPaint = paint;
    288         mWidth = width;
    289         mAlignment = align;
    290         mSpacingMult = spacingmult;
    291         mSpacingAdd = spacingadd;
    292         mSpannedText = text instanceof Spanned;
    293     }
    294 
    295     /**
    296      * Draw this Layout on the specified Canvas.
    297      */
    298     public void draw(Canvas c) {
    299         draw(c, null, null, 0);
    300     }
    301 
    302     /**
    303      * Draw this Layout on the specified canvas, with the highlight path drawn
    304      * between the background and the text.
    305      *
    306      * @param canvas the canvas
    307      * @param highlight the path of the highlight or cursor; can be null
    308      * @param highlightPaint the paint for the highlight
    309      * @param cursorOffsetVertical the amount to temporarily translate the
    310      *        canvas while rendering the highlight
    311      */
    312     public void draw(Canvas canvas, Path highlight, Paint highlightPaint,
    313             int cursorOffsetVertical) {
    314         final long lineRange = getLineRangeForDraw(canvas);
    315         int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
    316         int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
    317         if (lastLine < 0) return;
    318 
    319         drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
    320                 firstLine, lastLine);
    321         drawText(canvas, firstLine, lastLine);
    322     }
    323 
    324     private boolean isJustificationRequired(int lineNum) {
    325         if (mJustificationMode == JUSTIFICATION_MODE_NONE) return false;
    326         final int lineEnd = getLineEnd(lineNum);
    327         return lineEnd < mText.length() && mText.charAt(lineEnd - 1) != '\n';
    328     }
    329 
    330     private float getJustifyWidth(int lineNum) {
    331         Alignment paraAlign = mAlignment;
    332 
    333         int left = 0;
    334         int right = mWidth;
    335 
    336         final int dir = getParagraphDirection(lineNum);
    337 
    338         ParagraphStyle[] spans = NO_PARA_SPANS;
    339         if (mSpannedText) {
    340             Spanned sp = (Spanned) mText;
    341             final int start = getLineStart(lineNum);
    342 
    343             final boolean isFirstParaLine = (start == 0 || mText.charAt(start - 1) == '\n');
    344 
    345             if (isFirstParaLine) {
    346                 final int spanEnd = sp.nextSpanTransition(start, mText.length(),
    347                         ParagraphStyle.class);
    348                 spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class);
    349 
    350                 for (int n = spans.length - 1; n >= 0; n--) {
    351                     if (spans[n] instanceof AlignmentSpan) {
    352                         paraAlign = ((AlignmentSpan) spans[n]).getAlignment();
    353                         break;
    354                     }
    355                 }
    356             }
    357 
    358             final int length = spans.length;
    359             boolean useFirstLineMargin = isFirstParaLine;
    360             for (int n = 0; n < length; n++) {
    361                 if (spans[n] instanceof LeadingMarginSpan2) {
    362                     int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount();
    363                     int startLine = getLineForOffset(sp.getSpanStart(spans[n]));
    364                     if (lineNum < startLine + count) {
    365                         useFirstLineMargin = true;
    366                         break;
    367                     }
    368                 }
    369             }
    370             for (int n = 0; n < length; n++) {
    371                 if (spans[n] instanceof LeadingMarginSpan) {
    372                     LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
    373                     if (dir == DIR_RIGHT_TO_LEFT) {
    374                         right -= margin.getLeadingMargin(useFirstLineMargin);
    375                     } else {
    376                         left += margin.getLeadingMargin(useFirstLineMargin);
    377                     }
    378                 }
    379             }
    380         }
    381 
    382         final Alignment align;
    383         if (paraAlign == Alignment.ALIGN_LEFT) {
    384             align = (dir == DIR_LEFT_TO_RIGHT) ?  Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
    385         } else if (paraAlign == Alignment.ALIGN_RIGHT) {
    386             align = (dir == DIR_LEFT_TO_RIGHT) ?  Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
    387         } else {
    388             align = paraAlign;
    389         }
    390 
    391         final int indentWidth;
    392         if (align == Alignment.ALIGN_NORMAL) {
    393             if (dir == DIR_LEFT_TO_RIGHT) {
    394                 indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
    395             } else {
    396                 indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
    397             }
    398         } else if (align == Alignment.ALIGN_OPPOSITE) {
    399             if (dir == DIR_LEFT_TO_RIGHT) {
    400                 indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
    401             } else {
    402                 indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
    403             }
    404         } else { // Alignment.ALIGN_CENTER
    405             indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
    406         }
    407 
    408         return right - left - indentWidth;
    409     }
    410 
    411     /**
    412      * @hide
    413      */
    414     public void drawText(Canvas canvas, int firstLine, int lastLine) {
    415         int previousLineBottom = getLineTop(firstLine);
    416         int previousLineEnd = getLineStart(firstLine);
    417         ParagraphStyle[] spans = NO_PARA_SPANS;
    418         int spanEnd = 0;
    419         final TextPaint paint = mWorkPaint;
    420         paint.set(mPaint);
    421         CharSequence buf = mText;
    422 
    423         Alignment paraAlign = mAlignment;
    424         TabStops tabStops = null;
    425         boolean tabStopsIsInitialized = false;
    426 
    427         TextLine tl = TextLine.obtain();
    428 
    429         // Draw the lines, one at a time.
    430         // The baseline is the top of the following line minus the current line's descent.
    431         for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) {
    432             int start = previousLineEnd;
    433             previousLineEnd = getLineStart(lineNum + 1);
    434             final boolean justify = isJustificationRequired(lineNum);
    435             int end = getLineVisibleEnd(lineNum, start, previousLineEnd);
    436             paint.setHyphenEdit(getHyphen(lineNum));
    437 
    438             int ltop = previousLineBottom;
    439             int lbottom = getLineTop(lineNum + 1);
    440             previousLineBottom = lbottom;
    441             int lbaseline = lbottom - getLineDescent(lineNum);
    442 
    443             int dir = getParagraphDirection(lineNum);
    444             int left = 0;
    445             int right = mWidth;
    446 
    447             if (mSpannedText) {
    448                 Spanned sp = (Spanned) buf;
    449                 int textLength = buf.length();
    450                 boolean isFirstParaLine = (start == 0 || buf.charAt(start - 1) == '\n');
    451 
    452                 // New batch of paragraph styles, collect into spans array.
    453                 // Compute the alignment, last alignment style wins.
    454                 // Reset tabStops, we'll rebuild if we encounter a line with
    455                 // tabs.
    456                 // We expect paragraph spans to be relatively infrequent, use
    457                 // spanEnd so that we can check less frequently.  Since
    458                 // paragraph styles ought to apply to entire paragraphs, we can
    459                 // just collect the ones present at the start of the paragraph.
    460                 // If spanEnd is before the end of the paragraph, that's not
    461                 // our problem.
    462                 if (start >= spanEnd && (lineNum == firstLine || isFirstParaLine)) {
    463                     spanEnd = sp.nextSpanTransition(start, textLength,
    464                                                     ParagraphStyle.class);
    465                     spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class);
    466 
    467                     paraAlign = mAlignment;
    468                     for (int n = spans.length - 1; n >= 0; n--) {
    469                         if (spans[n] instanceof AlignmentSpan) {
    470                             paraAlign = ((AlignmentSpan) spans[n]).getAlignment();
    471                             break;
    472                         }
    473                     }
    474 
    475                     tabStopsIsInitialized = false;
    476                 }
    477 
    478                 // Draw all leading margin spans.  Adjust left or right according
    479                 // to the paragraph direction of the line.
    480                 final int length = spans.length;
    481                 boolean useFirstLineMargin = isFirstParaLine;
    482                 for (int n = 0; n < length; n++) {
    483                     if (spans[n] instanceof LeadingMarginSpan2) {
    484                         int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount();
    485                         int startLine = getLineForOffset(sp.getSpanStart(spans[n]));
    486                         // if there is more than one LeadingMarginSpan2, use
    487                         // the count that is greatest
    488                         if (lineNum < startLine + count) {
    489                             useFirstLineMargin = true;
    490                             break;
    491                         }
    492                     }
    493                 }
    494                 for (int n = 0; n < length; n++) {
    495                     if (spans[n] instanceof LeadingMarginSpan) {
    496                         LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
    497                         if (dir == DIR_RIGHT_TO_LEFT) {
    498                             margin.drawLeadingMargin(canvas, paint, right, dir, ltop,
    499                                                      lbaseline, lbottom, buf,
    500                                                      start, end, isFirstParaLine, this);
    501                             right -= margin.getLeadingMargin(useFirstLineMargin);
    502                         } else {
    503                             margin.drawLeadingMargin(canvas, paint, left, dir, ltop,
    504                                                      lbaseline, lbottom, buf,
    505                                                      start, end, isFirstParaLine, this);
    506                             left += margin.getLeadingMargin(useFirstLineMargin);
    507                         }
    508                     }
    509                 }
    510             }
    511 
    512             boolean hasTab = getLineContainsTab(lineNum);
    513             // Can't tell if we have tabs for sure, currently
    514             if (hasTab && !tabStopsIsInitialized) {
    515                 if (tabStops == null) {
    516                     tabStops = new TabStops(TAB_INCREMENT, spans);
    517                 } else {
    518                     tabStops.reset(TAB_INCREMENT, spans);
    519                 }
    520                 tabStopsIsInitialized = true;
    521             }
    522 
    523             // Determine whether the line aligns to normal, opposite, or center.
    524             Alignment align = paraAlign;
    525             if (align == Alignment.ALIGN_LEFT) {
    526                 align = (dir == DIR_LEFT_TO_RIGHT) ?
    527                     Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
    528             } else if (align == Alignment.ALIGN_RIGHT) {
    529                 align = (dir == DIR_LEFT_TO_RIGHT) ?
    530                     Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
    531             }
    532 
    533             int x;
    534             final int indentWidth;
    535             if (align == Alignment.ALIGN_NORMAL) {
    536                 if (dir == DIR_LEFT_TO_RIGHT) {
    537                     indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
    538                     x = left + indentWidth;
    539                 } else {
    540                     indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
    541                     x = right - indentWidth;
    542                 }
    543             } else {
    544                 int max = (int)getLineExtent(lineNum, tabStops, false);
    545                 if (align == Alignment.ALIGN_OPPOSITE) {
    546                     if (dir == DIR_LEFT_TO_RIGHT) {
    547                         indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
    548                         x = right - max - indentWidth;
    549                     } else {
    550                         indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
    551                         x = left - max + indentWidth;
    552                     }
    553                 } else { // Alignment.ALIGN_CENTER
    554                     indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
    555                     max = max & ~1;
    556                     x = ((right + left - max) >> 1) + indentWidth;
    557                 }
    558             }
    559 
    560             Directions directions = getLineDirections(lineNum);
    561             if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTab && !justify) {
    562                 // XXX: assumes there's nothing additional to be done
    563                 canvas.drawText(buf, start, end, x, lbaseline, paint);
    564             } else {
    565                 tl.set(paint, buf, start, end, dir, directions, hasTab, tabStops);
    566                 if (justify) {
    567                     tl.justify(right - left - indentWidth);
    568                 }
    569                 tl.draw(canvas, x, ltop, lbaseline, lbottom);
    570             }
    571         }
    572 
    573         TextLine.recycle(tl);
    574     }
    575 
    576     /**
    577      * @hide
    578      */
    579     public void drawBackground(Canvas canvas, Path highlight, Paint highlightPaint,
    580             int cursorOffsetVertical, int firstLine, int lastLine) {
    581         // First, draw LineBackgroundSpans.
    582         // LineBackgroundSpans know nothing about the alignment, margins, or
    583         // direction of the layout or line.  XXX: Should they?
    584         // They are evaluated at each line.
    585         if (mSpannedText) {
    586             if (mLineBackgroundSpans == null) {
    587                 mLineBackgroundSpans = new SpanSet<LineBackgroundSpan>(LineBackgroundSpan.class);
    588             }
    589 
    590             Spanned buffer = (Spanned) mText;
    591             int textLength = buffer.length();
    592             mLineBackgroundSpans.init(buffer, 0, textLength);
    593 
    594             if (mLineBackgroundSpans.numberOfSpans > 0) {
    595                 int previousLineBottom = getLineTop(firstLine);
    596                 int previousLineEnd = getLineStart(firstLine);
    597                 ParagraphStyle[] spans = NO_PARA_SPANS;
    598                 int spansLength = 0;
    599                 TextPaint paint = mPaint;
    600                 int spanEnd = 0;
    601                 final int width = mWidth;
    602                 for (int i = firstLine; i <= lastLine; i++) {
    603                     int start = previousLineEnd;
    604                     int end = getLineStart(i + 1);
    605                     previousLineEnd = end;
    606 
    607                     int ltop = previousLineBottom;
    608                     int lbottom = getLineTop(i + 1);
    609                     previousLineBottom = lbottom;
    610                     int lbaseline = lbottom - getLineDescent(i);
    611 
    612                     if (start >= spanEnd) {
    613                         // These should be infrequent, so we'll use this so that
    614                         // we don't have to check as often.
    615                         spanEnd = mLineBackgroundSpans.getNextTransition(start, textLength);
    616                         // All LineBackgroundSpans on a line contribute to its background.
    617                         spansLength = 0;
    618                         // Duplication of the logic of getParagraphSpans
    619                         if (start != end || start == 0) {
    620                             // Equivalent to a getSpans(start, end), but filling the 'spans' local
    621                             // array instead to reduce memory allocation
    622                             for (int j = 0; j < mLineBackgroundSpans.numberOfSpans; j++) {
    623                                 // equal test is valid since both intervals are not empty by
    624                                 // construction
    625                                 if (mLineBackgroundSpans.spanStarts[j] >= end ||
    626                                         mLineBackgroundSpans.spanEnds[j] <= start) continue;
    627                                 spans = GrowingArrayUtils.append(
    628                                         spans, spansLength, mLineBackgroundSpans.spans[j]);
    629                                 spansLength++;
    630                             }
    631                         }
    632                     }
    633 
    634                     for (int n = 0; n < spansLength; n++) {
    635                         LineBackgroundSpan lineBackgroundSpan = (LineBackgroundSpan) spans[n];
    636                         lineBackgroundSpan.drawBackground(canvas, paint, 0, width,
    637                                 ltop, lbaseline, lbottom,
    638                                 buffer, start, end, i);
    639                     }
    640                 }
    641             }
    642             mLineBackgroundSpans.recycle();
    643         }
    644 
    645         // There can be a highlight even without spans if we are drawing
    646         // a non-spanned transformation of a spanned editing buffer.
    647         if (highlight != null) {
    648             if (cursorOffsetVertical != 0) canvas.translate(0, cursorOffsetVertical);
    649             canvas.drawPath(highlight, highlightPaint);
    650             if (cursorOffsetVertical != 0) canvas.translate(0, -cursorOffsetVertical);
    651         }
    652     }
    653 
    654     /**
    655      * @param canvas
    656      * @return The range of lines that need to be drawn, possibly empty.
    657      * @hide
    658      */
    659     public long getLineRangeForDraw(Canvas canvas) {
    660         int dtop, dbottom;
    661 
    662         synchronized (sTempRect) {
    663             if (!canvas.getClipBounds(sTempRect)) {
    664                 // Negative range end used as a special flag
    665                 return TextUtils.packRangeInLong(0, -1);
    666             }
    667 
    668             dtop = sTempRect.top;
    669             dbottom = sTempRect.bottom;
    670         }
    671 
    672         final int top = Math.max(dtop, 0);
    673         final int bottom = Math.min(getLineTop(getLineCount()), dbottom);
    674 
    675         if (top >= bottom) return TextUtils.packRangeInLong(0, -1);
    676         return TextUtils.packRangeInLong(getLineForVertical(top), getLineForVertical(bottom));
    677     }
    678 
    679     /**
    680      * Return the start position of the line, given the left and right bounds
    681      * of the margins.
    682      *
    683      * @param line the line index
    684      * @param left the left bounds (0, or leading margin if ltr para)
    685      * @param right the right bounds (width, minus leading margin if rtl para)
    686      * @return the start position of the line (to right of line if rtl para)
    687      */
    688     private int getLineStartPos(int line, int left, int right) {
    689         // Adjust the point at which to start rendering depending on the
    690         // alignment of the paragraph.
    691         Alignment align = getParagraphAlignment(line);
    692         int dir = getParagraphDirection(line);
    693 
    694         if (align == Alignment.ALIGN_LEFT) {
    695             align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
    696         } else if (align == Alignment.ALIGN_RIGHT) {
    697             align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
    698         }
    699 
    700         int x;
    701         if (align == Alignment.ALIGN_NORMAL) {
    702             if (dir == DIR_LEFT_TO_RIGHT) {
    703                 x = left + getIndentAdjust(line, Alignment.ALIGN_LEFT);
    704             } else {
    705                 x = right + getIndentAdjust(line, Alignment.ALIGN_RIGHT);
    706             }
    707         } else {
    708             TabStops tabStops = null;
    709             if (mSpannedText && getLineContainsTab(line)) {
    710                 Spanned spanned = (Spanned) mText;
    711                 int start = getLineStart(line);
    712                 int spanEnd = spanned.nextSpanTransition(start, spanned.length(),
    713                         TabStopSpan.class);
    714                 TabStopSpan[] tabSpans = getParagraphSpans(spanned, start, spanEnd,
    715                         TabStopSpan.class);
    716                 if (tabSpans.length > 0) {
    717                     tabStops = new TabStops(TAB_INCREMENT, tabSpans);
    718                 }
    719             }
    720             int max = (int)getLineExtent(line, tabStops, false);
    721             if (align == Alignment.ALIGN_OPPOSITE) {
    722                 if (dir == DIR_LEFT_TO_RIGHT) {
    723                     x = right - max + getIndentAdjust(line, Alignment.ALIGN_RIGHT);
    724                 } else {
    725                     // max is negative here
    726                     x = left - max + getIndentAdjust(line, Alignment.ALIGN_LEFT);
    727                 }
    728             } else { // Alignment.ALIGN_CENTER
    729                 max = max & ~1;
    730                 x = (left + right - max) >> 1 + getIndentAdjust(line, Alignment.ALIGN_CENTER);
    731             }
    732         }
    733         return x;
    734     }
    735 
    736     /**
    737      * Return the text that is displayed by this Layout.
    738      */
    739     public final CharSequence getText() {
    740         return mText;
    741     }
    742 
    743     /**
    744      * Return the base Paint properties for this layout.
    745      * Do NOT change the paint, which may result in funny
    746      * drawing for this layout.
    747      */
    748     public final TextPaint getPaint() {
    749         return mPaint;
    750     }
    751 
    752     /**
    753      * Return the width of this layout.
    754      */
    755     public final int getWidth() {
    756         return mWidth;
    757     }
    758 
    759     /**
    760      * Return the width to which this Layout is ellipsizing, or
    761      * {@link #getWidth} if it is not doing anything special.
    762      */
    763     public int getEllipsizedWidth() {
    764         return mWidth;
    765     }
    766 
    767     /**
    768      * Increase the width of this layout to the specified width.
    769      * Be careful to use this only when you know it is appropriate&mdash;
    770      * it does not cause the text to reflow to use the full new width.
    771      */
    772     public final void increaseWidthTo(int wid) {
    773         if (wid < mWidth) {
    774             throw new RuntimeException("attempted to reduce Layout width");
    775         }
    776 
    777         mWidth = wid;
    778     }
    779 
    780     /**
    781      * Return the total height of this layout.
    782      */
    783     public int getHeight() {
    784         return getLineTop(getLineCount());
    785     }
    786 
    787     /**
    788      * Return the total height of this layout.
    789      *
    790      * @param cap if true and max lines is set, returns the height of the layout at the max lines.
    791      *
    792      * @hide
    793      */
    794     public int getHeight(boolean cap) {
    795         return getHeight();
    796     }
    797 
    798     /**
    799      * Return the base alignment of this layout.
    800      */
    801     public final Alignment getAlignment() {
    802         return mAlignment;
    803     }
    804 
    805     /**
    806      * Return what the text height is multiplied by to get the line height.
    807      */
    808     public final float getSpacingMultiplier() {
    809         return mSpacingMult;
    810     }
    811 
    812     /**
    813      * Return the number of units of leading that are added to each line.
    814      */
    815     public final float getSpacingAdd() {
    816         return mSpacingAdd;
    817     }
    818 
    819     /**
    820      * Return the heuristic used to determine paragraph text direction.
    821      * @hide
    822      */
    823     public final TextDirectionHeuristic getTextDirectionHeuristic() {
    824         return mTextDir;
    825     }
    826 
    827     /**
    828      * Return the number of lines of text in this layout.
    829      */
    830     public abstract int getLineCount();
    831 
    832     /**
    833      * Return the baseline for the specified line (0&hellip;getLineCount() - 1)
    834      * If bounds is not null, return the top, left, right, bottom extents
    835      * of the specified line in it.
    836      * @param line which line to examine (0..getLineCount() - 1)
    837      * @param bounds Optional. If not null, it returns the extent of the line
    838      * @return the Y-coordinate of the baseline
    839      */
    840     public int getLineBounds(int line, Rect bounds) {
    841         if (bounds != null) {
    842             bounds.left = 0;     // ???
    843             bounds.top = getLineTop(line);
    844             bounds.right = mWidth;   // ???
    845             bounds.bottom = getLineTop(line + 1);
    846         }
    847         return getLineBaseline(line);
    848     }
    849 
    850     /**
    851      * Return the vertical position of the top of the specified line
    852      * (0&hellip;getLineCount()).
    853      * If the specified line is equal to the line count, returns the
    854      * bottom of the last line.
    855      */
    856     public abstract int getLineTop(int line);
    857 
    858     /**
    859      * Return the descent of the specified line(0&hellip;getLineCount() - 1).
    860      */
    861     public abstract int getLineDescent(int line);
    862 
    863     /**
    864      * Return the text offset of the beginning of the specified line (
    865      * 0&hellip;getLineCount()). If the specified line is equal to the line
    866      * count, returns the length of the text.
    867      */
    868     public abstract int getLineStart(int line);
    869 
    870     /**
    871      * Returns the primary directionality of the paragraph containing the
    872      * specified line, either 1 for left-to-right lines, or -1 for right-to-left
    873      * lines (see {@link #DIR_LEFT_TO_RIGHT}, {@link #DIR_RIGHT_TO_LEFT}).
    874      */
    875     public abstract int getParagraphDirection(int line);
    876 
    877     /**
    878      * Returns whether the specified line contains one or more
    879      * characters that need to be handled specially, like tabs.
    880      */
    881     public abstract boolean getLineContainsTab(int line);
    882 
    883     /**
    884      * Returns the directional run information for the specified line.
    885      * The array alternates counts of characters in left-to-right
    886      * and right-to-left segments of the line.
    887      *
    888      * <p>NOTE: this is inadequate to support bidirectional text, and will change.
    889      */
    890     public abstract Directions getLineDirections(int line);
    891 
    892     /**
    893      * Returns the (negative) number of extra pixels of ascent padding in the
    894      * top line of the Layout.
    895      */
    896     public abstract int getTopPadding();
    897 
    898     /**
    899      * Returns the number of extra pixels of descent padding in the
    900      * bottom line of the Layout.
    901      */
    902     public abstract int getBottomPadding();
    903 
    904     /**
    905      * Returns the hyphen edit for a line.
    906      *
    907      * @hide
    908      */
    909     public int getHyphen(int line) {
    910         return 0;
    911     }
    912 
    913     /**
    914      * Returns the left indent for a line.
    915      *
    916      * @hide
    917      */
    918     public int getIndentAdjust(int line, Alignment alignment) {
    919         return 0;
    920     }
    921 
    922     /**
    923      * Returns true if the character at offset and the preceding character
    924      * are at different run levels (and thus there's a split caret).
    925      * @param offset the offset
    926      * @return true if at a level boundary
    927      * @hide
    928      */
    929     public boolean isLevelBoundary(int offset) {
    930         int line = getLineForOffset(offset);
    931         Directions dirs = getLineDirections(line);
    932         if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) {
    933             return false;
    934         }
    935 
    936         int[] runs = dirs.mDirections;
    937         int lineStart = getLineStart(line);
    938         int lineEnd = getLineEnd(line);
    939         if (offset == lineStart || offset == lineEnd) {
    940             int paraLevel = getParagraphDirection(line) == 1 ? 0 : 1;
    941             int runIndex = offset == lineStart ? 0 : runs.length - 2;
    942             return ((runs[runIndex + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK) != paraLevel;
    943         }
    944 
    945         offset -= lineStart;
    946         for (int i = 0; i < runs.length; i += 2) {
    947             if (offset == runs[i]) {
    948                 return true;
    949             }
    950         }
    951         return false;
    952     }
    953 
    954     /**
    955      * Returns true if the character at offset is right to left (RTL).
    956      * @param offset the offset
    957      * @return true if the character is RTL, false if it is LTR
    958      */
    959     public boolean isRtlCharAt(int offset) {
    960         int line = getLineForOffset(offset);
    961         Directions dirs = getLineDirections(line);
    962         if (dirs == DIRS_ALL_LEFT_TO_RIGHT) {
    963             return false;
    964         }
    965         if (dirs == DIRS_ALL_RIGHT_TO_LEFT) {
    966             return  true;
    967         }
    968         int[] runs = dirs.mDirections;
    969         int lineStart = getLineStart(line);
    970         for (int i = 0; i < runs.length; i += 2) {
    971             int start = lineStart + runs[i];
    972             int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
    973             if (offset >= start && offset < limit) {
    974                 int level = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
    975                 return ((level & 1) != 0);
    976             }
    977         }
    978         // Should happen only if the offset is "out of bounds"
    979         return false;
    980     }
    981 
    982     /**
    983      * Returns the range of the run that the character at offset belongs to.
    984      * @param offset the offset
    985      * @return The range of the run
    986      * @hide
    987      */
    988     public long getRunRange(int offset) {
    989         int line = getLineForOffset(offset);
    990         Directions dirs = getLineDirections(line);
    991         if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) {
    992             return TextUtils.packRangeInLong(0, getLineEnd(line));
    993         }
    994         int[] runs = dirs.mDirections;
    995         int lineStart = getLineStart(line);
    996         for (int i = 0; i < runs.length; i += 2) {
    997             int start = lineStart + runs[i];
    998             int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
    999             if (offset >= start && offset < limit) {
   1000                 return TextUtils.packRangeInLong(start, limit);
   1001             }
   1002         }
   1003         // Should happen only if the offset is "out of bounds"
   1004         return TextUtils.packRangeInLong(0, getLineEnd(line));
   1005     }
   1006 
   1007     /**
   1008      * Checks if the trailing BiDi level should be used for an offset
   1009      *
   1010      * This method is useful when the offset is at the BiDi level transition point and determine
   1011      * which run need to be used. For example, let's think about following input: (L* denotes
   1012      * Left-to-Right characters, R* denotes Right-to-Left characters.)
   1013      * Input (Logical Order): L1 L2 L3 R1 R2 R3 L4 L5 L6
   1014      * Input (Display Order): L1 L2 L3 R3 R2 R1 L4 L5 L6
   1015      *
   1016      * Then, think about selecting the range (3, 6). The offset=3 and offset=6 are ambiguous here
   1017      * since they are at the BiDi transition point.  In Android, the offset is considered to be
   1018      * associated with the trailing run if the BiDi level of the trailing run is higher than of the
   1019      * previous run.  In this case, the BiDi level of the input text is as follows:
   1020      *
   1021      * Input (Logical Order): L1 L2 L3 R1 R2 R3 L4 L5 L6
   1022      *              BiDi Run: [ Run 0 ][ Run 1 ][ Run 2 ]
   1023      *            BiDi Level:  0  0  0  1  1  1  0  0  0
   1024      *
   1025      * Thus, offset = 3 is part of Run 1 and this method returns true for offset = 3, since the BiDi
   1026      * level of Run 1 is higher than the level of Run 0.  Similarly, the offset = 6 is a part of Run
   1027      * 1 and this method returns false for the offset = 6 since the BiDi level of Run 1 is higher
   1028      * than the level of Run 2.
   1029      *
   1030      * @returns true if offset is at the BiDi level transition point and trailing BiDi level is
   1031      *          higher than previous BiDi level. See above for the detail.
   1032      */
   1033     private boolean primaryIsTrailingPrevious(int offset) {
   1034         int line = getLineForOffset(offset);
   1035         int lineStart = getLineStart(line);
   1036         int lineEnd = getLineEnd(line);
   1037         int[] runs = getLineDirections(line).mDirections;
   1038 
   1039         int levelAt = -1;
   1040         for (int i = 0; i < runs.length; i += 2) {
   1041             int start = lineStart + runs[i];
   1042             int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
   1043             if (limit > lineEnd) {
   1044                 limit = lineEnd;
   1045             }
   1046             if (offset >= start && offset < limit) {
   1047                 if (offset > start) {
   1048                     // Previous character is at same level, so don't use trailing.
   1049                     return false;
   1050                 }
   1051                 levelAt = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
   1052                 break;
   1053             }
   1054         }
   1055         if (levelAt == -1) {
   1056             // Offset was limit of line.
   1057             levelAt = getParagraphDirection(line) == 1 ? 0 : 1;
   1058         }
   1059 
   1060         // At level boundary, check previous level.
   1061         int levelBefore = -1;
   1062         if (offset == lineStart) {
   1063             levelBefore = getParagraphDirection(line) == 1 ? 0 : 1;
   1064         } else {
   1065             offset -= 1;
   1066             for (int i = 0; i < runs.length; i += 2) {
   1067                 int start = lineStart + runs[i];
   1068                 int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
   1069                 if (limit > lineEnd) {
   1070                     limit = lineEnd;
   1071                 }
   1072                 if (offset >= start && offset < limit) {
   1073                     levelBefore = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
   1074                     break;
   1075                 }
   1076             }
   1077         }
   1078 
   1079         return levelBefore < levelAt;
   1080     }
   1081 
   1082     /**
   1083      * Computes in linear time the results of calling
   1084      * #primaryIsTrailingPrevious for all offsets on a line.
   1085      * @param line The line giving the offsets we compute the information for
   1086      * @return The array of results, indexed from 0, where 0 corresponds to the line start offset
   1087      */
   1088     private boolean[] primaryIsTrailingPreviousAllLineOffsets(int line) {
   1089         int lineStart = getLineStart(line);
   1090         int lineEnd = getLineEnd(line);
   1091         int[] runs = getLineDirections(line).mDirections;
   1092 
   1093         boolean[] trailing = new boolean[lineEnd - lineStart + 1];
   1094 
   1095         byte[] level = new byte[lineEnd - lineStart + 1];
   1096         for (int i = 0; i < runs.length; i += 2) {
   1097             int start = lineStart + runs[i];
   1098             int limit = start + (runs[i + 1] & RUN_LENGTH_MASK);
   1099             if (limit > lineEnd) {
   1100                 limit = lineEnd;
   1101             }
   1102             level[limit - lineStart - 1] =
   1103                     (byte) ((runs[i + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK);
   1104         }
   1105 
   1106         for (int i = 0; i < runs.length; i += 2) {
   1107             int start = lineStart + runs[i];
   1108             byte currentLevel = (byte) ((runs[i + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK);
   1109             trailing[start - lineStart] = currentLevel > (start == lineStart
   1110                     ? (getParagraphDirection(line) == 1 ? 0 : 1)
   1111                     : level[start - lineStart - 1]);
   1112         }
   1113 
   1114         return trailing;
   1115     }
   1116 
   1117     /**
   1118      * Get the primary horizontal position for the specified text offset.
   1119      * This is the location where a new character would be inserted in
   1120      * the paragraph's primary direction.
   1121      */
   1122     public float getPrimaryHorizontal(int offset) {
   1123         return getPrimaryHorizontal(offset, false /* not clamped */);
   1124     }
   1125 
   1126     /**
   1127      * Get the primary horizontal position for the specified text offset, but
   1128      * optionally clamp it so that it doesn't exceed the width of the layout.
   1129      * @hide
   1130      */
   1131     public float getPrimaryHorizontal(int offset, boolean clamped) {
   1132         boolean trailing = primaryIsTrailingPrevious(offset);
   1133         return getHorizontal(offset, trailing, clamped);
   1134     }
   1135 
   1136     /**
   1137      * Get the secondary horizontal position for the specified text offset.
   1138      * This is the location where a new character would be inserted in
   1139      * the direction other than the paragraph's primary direction.
   1140      */
   1141     public float getSecondaryHorizontal(int offset) {
   1142         return getSecondaryHorizontal(offset, false /* not clamped */);
   1143     }
   1144 
   1145     /**
   1146      * Get the secondary horizontal position for the specified text offset, but
   1147      * optionally clamp it so that it doesn't exceed the width of the layout.
   1148      * @hide
   1149      */
   1150     public float getSecondaryHorizontal(int offset, boolean clamped) {
   1151         boolean trailing = primaryIsTrailingPrevious(offset);
   1152         return getHorizontal(offset, !trailing, clamped);
   1153     }
   1154 
   1155     private float getHorizontal(int offset, boolean primary) {
   1156         return primary ? getPrimaryHorizontal(offset) : getSecondaryHorizontal(offset);
   1157     }
   1158 
   1159     private float getHorizontal(int offset, boolean trailing, boolean clamped) {
   1160         int line = getLineForOffset(offset);
   1161 
   1162         return getHorizontal(offset, trailing, line, clamped);
   1163     }
   1164 
   1165     private float getHorizontal(int offset, boolean trailing, int line, boolean clamped) {
   1166         int start = getLineStart(line);
   1167         int end = getLineEnd(line);
   1168         int dir = getParagraphDirection(line);
   1169         boolean hasTab = getLineContainsTab(line);
   1170         Directions directions = getLineDirections(line);
   1171 
   1172         TabStops tabStops = null;
   1173         if (hasTab && mText instanceof Spanned) {
   1174             // Just checking this line should be good enough, tabs should be
   1175             // consistent across all lines in a paragraph.
   1176             TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
   1177             if (tabs.length > 0) {
   1178                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
   1179             }
   1180         }
   1181 
   1182         TextLine tl = TextLine.obtain();
   1183         tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops);
   1184         float wid = tl.measure(offset - start, trailing, null);
   1185         TextLine.recycle(tl);
   1186 
   1187         if (clamped && wid > mWidth) {
   1188             wid = mWidth;
   1189         }
   1190         int left = getParagraphLeft(line);
   1191         int right = getParagraphRight(line);
   1192 
   1193         return getLineStartPos(line, left, right) + wid;
   1194     }
   1195 
   1196     /**
   1197      * Computes in linear time the results of calling
   1198      * #getHorizontal for all offsets on a line.
   1199      * @param line The line giving the offsets we compute information for
   1200      * @param clamped Whether to clamp the results to the width of the layout
   1201      * @param primary Whether the results should be the primary or the secondary horizontal
   1202      * @return The array of results, indexed from 0, where 0 corresponds to the line start offset
   1203      */
   1204     private float[] getLineHorizontals(int line, boolean clamped, boolean primary) {
   1205         int start = getLineStart(line);
   1206         int end = getLineEnd(line);
   1207         int dir = getParagraphDirection(line);
   1208         boolean hasTab = getLineContainsTab(line);
   1209         Directions directions = getLineDirections(line);
   1210 
   1211         TabStops tabStops = null;
   1212         if (hasTab && mText instanceof Spanned) {
   1213             // Just checking this line should be good enough, tabs should be
   1214             // consistent across all lines in a paragraph.
   1215             TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
   1216             if (tabs.length > 0) {
   1217                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
   1218             }
   1219         }
   1220 
   1221         TextLine tl = TextLine.obtain();
   1222         tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops);
   1223         boolean[] trailings = primaryIsTrailingPreviousAllLineOffsets(line);
   1224         if (!primary) {
   1225             for (int offset = 0; offset < trailings.length; ++offset) {
   1226                 trailings[offset] = !trailings[offset];
   1227             }
   1228         }
   1229         float[] wid = tl.measureAllOffsets(trailings, null);
   1230         TextLine.recycle(tl);
   1231 
   1232         if (clamped) {
   1233             for (int offset = 0; offset <= wid.length; ++offset) {
   1234                 if (wid[offset] > mWidth) {
   1235                     wid[offset] = mWidth;
   1236                 }
   1237             }
   1238         }
   1239         int left = getParagraphLeft(line);
   1240         int right = getParagraphRight(line);
   1241 
   1242         int lineStartPos = getLineStartPos(line, left, right);
   1243         float[] horizontal = new float[end - start + 1];
   1244         for (int offset = 0; offset < horizontal.length; ++offset) {
   1245             horizontal[offset] = lineStartPos + wid[offset];
   1246         }
   1247         return horizontal;
   1248     }
   1249 
   1250     /**
   1251      * Get the leftmost position that should be exposed for horizontal
   1252      * scrolling on the specified line.
   1253      */
   1254     public float getLineLeft(int line) {
   1255         int dir = getParagraphDirection(line);
   1256         Alignment align = getParagraphAlignment(line);
   1257 
   1258         if (align == Alignment.ALIGN_LEFT) {
   1259             return 0;
   1260         } else if (align == Alignment.ALIGN_NORMAL) {
   1261             if (dir == DIR_RIGHT_TO_LEFT)
   1262                 return getParagraphRight(line) - getLineMax(line);
   1263             else
   1264                 return 0;
   1265         } else if (align == Alignment.ALIGN_RIGHT) {
   1266             return mWidth - getLineMax(line);
   1267         } else if (align == Alignment.ALIGN_OPPOSITE) {
   1268             if (dir == DIR_RIGHT_TO_LEFT)
   1269                 return 0;
   1270             else
   1271                 return mWidth - getLineMax(line);
   1272         } else { /* align == Alignment.ALIGN_CENTER */
   1273             int left = getParagraphLeft(line);
   1274             int right = getParagraphRight(line);
   1275             int max = ((int) getLineMax(line)) & ~1;
   1276 
   1277             return left + ((right - left) - max) / 2;
   1278         }
   1279     }
   1280 
   1281     /**
   1282      * Get the rightmost position that should be exposed for horizontal
   1283      * scrolling on the specified line.
   1284      */
   1285     public float getLineRight(int line) {
   1286         int dir = getParagraphDirection(line);
   1287         Alignment align = getParagraphAlignment(line);
   1288 
   1289         if (align == Alignment.ALIGN_LEFT) {
   1290             return getParagraphLeft(line) + getLineMax(line);
   1291         } else if (align == Alignment.ALIGN_NORMAL) {
   1292             if (dir == DIR_RIGHT_TO_LEFT)
   1293                 return mWidth;
   1294             else
   1295                 return getParagraphLeft(line) + getLineMax(line);
   1296         } else if (align == Alignment.ALIGN_RIGHT) {
   1297             return mWidth;
   1298         } else if (align == Alignment.ALIGN_OPPOSITE) {
   1299             if (dir == DIR_RIGHT_TO_LEFT)
   1300                 return getLineMax(line);
   1301             else
   1302                 return mWidth;
   1303         } else { /* align == Alignment.ALIGN_CENTER */
   1304             int left = getParagraphLeft(line);
   1305             int right = getParagraphRight(line);
   1306             int max = ((int) getLineMax(line)) & ~1;
   1307 
   1308             return right - ((right - left) - max) / 2;
   1309         }
   1310     }
   1311 
   1312     /**
   1313      * Gets the unsigned horizontal extent of the specified line, including
   1314      * leading margin indent, but excluding trailing whitespace.
   1315      */
   1316     public float getLineMax(int line) {
   1317         float margin = getParagraphLeadingMargin(line);
   1318         float signedExtent = getLineExtent(line, false);
   1319         return margin + (signedExtent >= 0 ? signedExtent : -signedExtent);
   1320     }
   1321 
   1322     /**
   1323      * Gets the unsigned horizontal extent of the specified line, including
   1324      * leading margin indent and trailing whitespace.
   1325      */
   1326     public float getLineWidth(int line) {
   1327         float margin = getParagraphLeadingMargin(line);
   1328         float signedExtent = getLineExtent(line, true);
   1329         return margin + (signedExtent >= 0 ? signedExtent : -signedExtent);
   1330     }
   1331 
   1332     /**
   1333      * Like {@link #getLineExtent(int,TabStops,boolean)} but determines the
   1334      * tab stops instead of using the ones passed in.
   1335      * @param line the index of the line
   1336      * @param full whether to include trailing whitespace
   1337      * @return the extent of the line
   1338      */
   1339     private float getLineExtent(int line, boolean full) {
   1340         final int start = getLineStart(line);
   1341         final int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
   1342 
   1343         final boolean hasTabs = getLineContainsTab(line);
   1344         TabStops tabStops = null;
   1345         if (hasTabs && mText instanceof Spanned) {
   1346             // Just checking this line should be good enough, tabs should be
   1347             // consistent across all lines in a paragraph.
   1348             TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
   1349             if (tabs.length > 0) {
   1350                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
   1351             }
   1352         }
   1353         final Directions directions = getLineDirections(line);
   1354         // Returned directions can actually be null
   1355         if (directions == null) {
   1356             return 0f;
   1357         }
   1358         final int dir = getParagraphDirection(line);
   1359 
   1360         final TextLine tl = TextLine.obtain();
   1361         final TextPaint paint = mWorkPaint;
   1362         paint.set(mPaint);
   1363         paint.setHyphenEdit(getHyphen(line));
   1364         tl.set(paint, mText, start, end, dir, directions, hasTabs, tabStops);
   1365         if (isJustificationRequired(line)) {
   1366             tl.justify(getJustifyWidth(line));
   1367         }
   1368         final float width = tl.metrics(null);
   1369         TextLine.recycle(tl);
   1370         return width;
   1371     }
   1372 
   1373     /**
   1374      * Returns the signed horizontal extent of the specified line, excluding
   1375      * leading margin.  If full is false, excludes trailing whitespace.
   1376      * @param line the index of the line
   1377      * @param tabStops the tab stops, can be null if we know they're not used.
   1378      * @param full whether to include trailing whitespace
   1379      * @return the extent of the text on this line
   1380      */
   1381     private float getLineExtent(int line, TabStops tabStops, boolean full) {
   1382         final int start = getLineStart(line);
   1383         final int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
   1384         final boolean hasTabs = getLineContainsTab(line);
   1385         final Directions directions = getLineDirections(line);
   1386         final int dir = getParagraphDirection(line);
   1387 
   1388         final TextLine tl = TextLine.obtain();
   1389         final TextPaint paint = mWorkPaint;
   1390         paint.set(mPaint);
   1391         paint.setHyphenEdit(getHyphen(line));
   1392         tl.set(paint, mText, start, end, dir, directions, hasTabs, tabStops);
   1393         if (isJustificationRequired(line)) {
   1394             tl.justify(getJustifyWidth(line));
   1395         }
   1396         final float width = tl.metrics(null);
   1397         TextLine.recycle(tl);
   1398         return width;
   1399     }
   1400 
   1401     /**
   1402      * Get the line number corresponding to the specified vertical position.
   1403      * If you ask for a position above 0, you get 0; if you ask for a position
   1404      * below the bottom of the text, you get the last line.
   1405      */
   1406     // FIXME: It may be faster to do a linear search for layouts without many lines.
   1407     public int getLineForVertical(int vertical) {
   1408         int high = getLineCount(), low = -1, guess;
   1409 
   1410         while (high - low > 1) {
   1411             guess = (high + low) / 2;
   1412 
   1413             if (getLineTop(guess) > vertical)
   1414                 high = guess;
   1415             else
   1416                 low = guess;
   1417         }
   1418 
   1419         if (low < 0)
   1420             return 0;
   1421         else
   1422             return low;
   1423     }
   1424 
   1425     /**
   1426      * Get the line number on which the specified text offset appears.
   1427      * If you ask for a position before 0, you get 0; if you ask for a position
   1428      * beyond the end of the text, you get the last line.
   1429      */
   1430     public int getLineForOffset(int offset) {
   1431         int high = getLineCount(), low = -1, guess;
   1432 
   1433         while (high - low > 1) {
   1434             guess = (high + low) / 2;
   1435 
   1436             if (getLineStart(guess) > offset)
   1437                 high = guess;
   1438             else
   1439                 low = guess;
   1440         }
   1441 
   1442         if (low < 0) {
   1443             return 0;
   1444         } else {
   1445             return low;
   1446         }
   1447     }
   1448 
   1449     /**
   1450      * Get the character offset on the specified line whose position is
   1451      * closest to the specified horizontal position.
   1452      */
   1453     public int getOffsetForHorizontal(int line, float horiz) {
   1454         return getOffsetForHorizontal(line, horiz, true);
   1455     }
   1456 
   1457     /**
   1458      * Get the character offset on the specified line whose position is
   1459      * closest to the specified horizontal position.
   1460      *
   1461      * @param line the line used to find the closest offset
   1462      * @param horiz the horizontal position used to find the closest offset
   1463      * @param primary whether to use the primary position or secondary position to find the offset
   1464      *
   1465      * @hide
   1466      */
   1467     public int getOffsetForHorizontal(int line, float horiz, boolean primary) {
   1468         // TODO: use Paint.getOffsetForAdvance to avoid binary search
   1469         final int lineEndOffset = getLineEnd(line);
   1470         final int lineStartOffset = getLineStart(line);
   1471 
   1472         Directions dirs = getLineDirections(line);
   1473 
   1474         TextLine tl = TextLine.obtain();
   1475         // XXX: we don't care about tabs as we just use TextLine#getOffsetToLeftRightOf here.
   1476         tl.set(mPaint, mText, lineStartOffset, lineEndOffset, getParagraphDirection(line), dirs,
   1477                 false, null);
   1478         final HorizontalMeasurementProvider horizontal =
   1479                 new HorizontalMeasurementProvider(line, primary);
   1480 
   1481         final int max;
   1482         if (line == getLineCount() - 1) {
   1483             max = lineEndOffset;
   1484         } else {
   1485             max = tl.getOffsetToLeftRightOf(lineEndOffset - lineStartOffset,
   1486                     !isRtlCharAt(lineEndOffset - 1)) + lineStartOffset;
   1487         }
   1488         int best = lineStartOffset;
   1489         float bestdist = Math.abs(horizontal.get(lineStartOffset) - horiz);
   1490 
   1491         for (int i = 0; i < dirs.mDirections.length; i += 2) {
   1492             int here = lineStartOffset + dirs.mDirections[i];
   1493             int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK);
   1494             boolean isRtl = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0;
   1495             int swap = isRtl ? -1 : 1;
   1496 
   1497             if (there > max)
   1498                 there = max;
   1499             int high = there - 1 + 1, low = here + 1 - 1, guess;
   1500 
   1501             while (high - low > 1) {
   1502                 guess = (high + low) / 2;
   1503                 int adguess = getOffsetAtStartOf(guess);
   1504 
   1505                 if (horizontal.get(adguess) * swap >= horiz * swap) {
   1506                     high = guess;
   1507                 } else {
   1508                     low = guess;
   1509                 }
   1510             }
   1511 
   1512             if (low < here + 1)
   1513                 low = here + 1;
   1514 
   1515             if (low < there) {
   1516                 int aft = tl.getOffsetToLeftRightOf(low - lineStartOffset, isRtl) + lineStartOffset;
   1517                 low = tl.getOffsetToLeftRightOf(aft - lineStartOffset, !isRtl) + lineStartOffset;
   1518                 if (low >= here && low < there) {
   1519                     float dist = Math.abs(horizontal.get(low) - horiz);
   1520                     if (aft < there) {
   1521                         float other = Math.abs(horizontal.get(aft) - horiz);
   1522 
   1523                         if (other < dist) {
   1524                             dist = other;
   1525                             low = aft;
   1526                         }
   1527                     }
   1528 
   1529                     if (dist < bestdist) {
   1530                         bestdist = dist;
   1531                         best = low;
   1532                     }
   1533                 }
   1534             }
   1535 
   1536             float dist = Math.abs(horizontal.get(here) - horiz);
   1537 
   1538             if (dist < bestdist) {
   1539                 bestdist = dist;
   1540                 best = here;
   1541             }
   1542         }
   1543 
   1544         float dist = Math.abs(horizontal.get(max) - horiz);
   1545 
   1546         if (dist <= bestdist) {
   1547             best = max;
   1548         }
   1549 
   1550         TextLine.recycle(tl);
   1551         return best;
   1552     }
   1553 
   1554     /**
   1555      * Responds to #getHorizontal queries, by selecting the better strategy between:
   1556      * - calling #getHorizontal explicitly for each query
   1557      * - precomputing all #getHorizontal measurements, and responding to any query in constant time
   1558      * The first strategy is used for LTR-only text, while the second is used for all other cases.
   1559      * The class is currently only used in #getOffsetForHorizontal, so reuse with care in other
   1560      * contexts.
   1561      */
   1562     private class HorizontalMeasurementProvider {
   1563         private final int mLine;
   1564         private final boolean mPrimary;
   1565 
   1566         private float[] mHorizontals;
   1567         private int mLineStartOffset;
   1568 
   1569         HorizontalMeasurementProvider(final int line, final boolean primary) {
   1570             mLine = line;
   1571             mPrimary = primary;
   1572             init();
   1573         }
   1574 
   1575         private void init() {
   1576             final Directions dirs = getLineDirections(mLine);
   1577             if (dirs == DIRS_ALL_LEFT_TO_RIGHT) {
   1578                 return;
   1579             }
   1580 
   1581             mHorizontals = getLineHorizontals(mLine, false, mPrimary);
   1582             mLineStartOffset = getLineStart(mLine);
   1583         }
   1584 
   1585         float get(final int offset) {
   1586             if (mHorizontals == null) {
   1587                 return getHorizontal(offset, mPrimary);
   1588             } else {
   1589                 return mHorizontals[offset - mLineStartOffset];
   1590             }
   1591         }
   1592     }
   1593 
   1594     /**
   1595      * Return the text offset after the last character on the specified line.
   1596      */
   1597     public final int getLineEnd(int line) {
   1598         return getLineStart(line + 1);
   1599     }
   1600 
   1601     /**
   1602      * Return the text offset after the last visible character (so whitespace
   1603      * is not counted) on the specified line.
   1604      */
   1605     public int getLineVisibleEnd(int line) {
   1606         return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1));
   1607     }
   1608 
   1609     private int getLineVisibleEnd(int line, int start, int end) {
   1610         CharSequence text = mText;
   1611         char ch;
   1612         if (line == getLineCount() - 1) {
   1613             return end;
   1614         }
   1615 
   1616         for (; end > start; end--) {
   1617             ch = text.charAt(end - 1);
   1618 
   1619             if (ch == '\n') {
   1620                 return end - 1;
   1621             }
   1622 
   1623             if (!TextLine.isLineEndSpace(ch)) {
   1624                 break;
   1625             }
   1626 
   1627         }
   1628 
   1629         return end;
   1630     }
   1631 
   1632     /**
   1633      * Return the vertical position of the bottom of the specified line.
   1634      */
   1635     public final int getLineBottom(int line) {
   1636         return getLineTop(line + 1);
   1637     }
   1638 
   1639     /**
   1640      * Return the vertical position of the bottom of the specified line without the line spacing
   1641      * added.
   1642      *
   1643      * @hide
   1644      */
   1645     public final int getLineBottomWithoutSpacing(int line) {
   1646         return getLineTop(line + 1) - getLineExtra(line);
   1647     }
   1648 
   1649     /**
   1650      * Return the vertical position of the baseline of the specified line.
   1651      */
   1652     public final int getLineBaseline(int line) {
   1653         // getLineTop(line+1) == getLineTop(line)
   1654         return getLineTop(line+1) - getLineDescent(line);
   1655     }
   1656 
   1657     /**
   1658      * Get the ascent of the text on the specified line.
   1659      * The return value is negative to match the Paint.ascent() convention.
   1660      */
   1661     public final int getLineAscent(int line) {
   1662         // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line)
   1663         return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line));
   1664     }
   1665 
   1666     /**
   1667      * Return the extra space added as a result of line spacing attributes
   1668      * {@link #getSpacingAdd()} and {@link #getSpacingMultiplier()}. Default value is {@code zero}.
   1669      *
   1670      * @param line the index of the line, the value should be equal or greater than {@code zero}
   1671      * @hide
   1672      */
   1673     public int getLineExtra(@IntRange(from = 0) int line) {
   1674         return 0;
   1675     }
   1676 
   1677     public int getOffsetToLeftOf(int offset) {
   1678         return getOffsetToLeftRightOf(offset, true);
   1679     }
   1680 
   1681     public int getOffsetToRightOf(int offset) {
   1682         return getOffsetToLeftRightOf(offset, false);
   1683     }
   1684 
   1685     private int getOffsetToLeftRightOf(int caret, boolean toLeft) {
   1686         int line = getLineForOffset(caret);
   1687         int lineStart = getLineStart(line);
   1688         int lineEnd = getLineEnd(line);
   1689         int lineDir = getParagraphDirection(line);
   1690 
   1691         boolean lineChanged = false;
   1692         boolean advance = toLeft == (lineDir == DIR_RIGHT_TO_LEFT);
   1693         // if walking off line, look at the line we're headed to
   1694         if (advance) {
   1695             if (caret == lineEnd) {
   1696                 if (line < getLineCount() - 1) {
   1697                     lineChanged = true;
   1698                     ++line;
   1699                 } else {
   1700                     return caret; // at very end, don't move
   1701                 }
   1702             }
   1703         } else {
   1704             if (caret == lineStart) {
   1705                 if (line > 0) {
   1706                     lineChanged = true;
   1707                     --line;
   1708                 } else {
   1709                     return caret; // at very start, don't move
   1710                 }
   1711             }
   1712         }
   1713 
   1714         if (lineChanged) {
   1715             lineStart = getLineStart(line);
   1716             lineEnd = getLineEnd(line);
   1717             int newDir = getParagraphDirection(line);
   1718             if (newDir != lineDir) {
   1719                 // unusual case.  we want to walk onto the line, but it runs
   1720                 // in a different direction than this one, so we fake movement
   1721                 // in the opposite direction.
   1722                 toLeft = !toLeft;
   1723                 lineDir = newDir;
   1724             }
   1725         }
   1726 
   1727         Directions directions = getLineDirections(line);
   1728 
   1729         TextLine tl = TextLine.obtain();
   1730         // XXX: we don't care about tabs
   1731         tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null);
   1732         caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft);
   1733         TextLine.recycle(tl);
   1734         return caret;
   1735     }
   1736 
   1737     private int getOffsetAtStartOf(int offset) {
   1738         // XXX this probably should skip local reorderings and
   1739         // zero-width characters, look at callers
   1740         if (offset == 0)
   1741             return 0;
   1742 
   1743         CharSequence text = mText;
   1744         char c = text.charAt(offset);
   1745 
   1746         if (c >= '\uDC00' && c <= '\uDFFF') {
   1747             char c1 = text.charAt(offset - 1);
   1748 
   1749             if (c1 >= '\uD800' && c1 <= '\uDBFF')
   1750                 offset -= 1;
   1751         }
   1752 
   1753         if (mSpannedText) {
   1754             ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
   1755                                                        ReplacementSpan.class);
   1756 
   1757             for (int i = 0; i < spans.length; i++) {
   1758                 int start = ((Spanned) text).getSpanStart(spans[i]);
   1759                 int end = ((Spanned) text).getSpanEnd(spans[i]);
   1760 
   1761                 if (start < offset && end > offset)
   1762                     offset = start;
   1763             }
   1764         }
   1765 
   1766         return offset;
   1767     }
   1768 
   1769     /**
   1770      * Determine whether we should clamp cursor position. Currently it's
   1771      * only robust for left-aligned displays.
   1772      * @hide
   1773      */
   1774     public boolean shouldClampCursor(int line) {
   1775         // Only clamp cursor position in left-aligned displays.
   1776         switch (getParagraphAlignment(line)) {
   1777             case ALIGN_LEFT:
   1778                 return true;
   1779             case ALIGN_NORMAL:
   1780                 return getParagraphDirection(line) > 0;
   1781             default:
   1782                 return false;
   1783         }
   1784 
   1785     }
   1786     /**
   1787      * Fills in the specified Path with a representation of a cursor
   1788      * at the specified offset.  This will often be a vertical line
   1789      * but can be multiple discontinuous lines in text with multiple
   1790      * directionalities.
   1791      */
   1792     public void getCursorPath(final int point, final Path dest, final CharSequence editingBuffer) {
   1793         dest.reset();
   1794 
   1795         int line = getLineForOffset(point);
   1796         int top = getLineTop(line);
   1797         int bottom = getLineBottomWithoutSpacing(line);
   1798 
   1799         boolean clamped = shouldClampCursor(line);
   1800         float h1 = getPrimaryHorizontal(point, clamped) - 0.5f;
   1801         float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point, clamped) - 0.5f : h1;
   1802 
   1803         int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) |
   1804                    TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING);
   1805         int fn = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_ALT_ON);
   1806         int dist = 0;
   1807 
   1808         if (caps != 0 || fn != 0) {
   1809             dist = (bottom - top) >> 2;
   1810 
   1811             if (fn != 0)
   1812                 top += dist;
   1813             if (caps != 0)
   1814                 bottom -= dist;
   1815         }
   1816 
   1817         if (h1 < 0.5f)
   1818             h1 = 0.5f;
   1819         if (h2 < 0.5f)
   1820             h2 = 0.5f;
   1821 
   1822         if (Float.compare(h1, h2) == 0) {
   1823             dest.moveTo(h1, top);
   1824             dest.lineTo(h1, bottom);
   1825         } else {
   1826             dest.moveTo(h1, top);
   1827             dest.lineTo(h1, (top + bottom) >> 1);
   1828 
   1829             dest.moveTo(h2, (top + bottom) >> 1);
   1830             dest.lineTo(h2, bottom);
   1831         }
   1832 
   1833         if (caps == 2) {
   1834             dest.moveTo(h2, bottom);
   1835             dest.lineTo(h2 - dist, bottom + dist);
   1836             dest.lineTo(h2, bottom);
   1837             dest.lineTo(h2 + dist, bottom + dist);
   1838         } else if (caps == 1) {
   1839             dest.moveTo(h2, bottom);
   1840             dest.lineTo(h2 - dist, bottom + dist);
   1841 
   1842             dest.moveTo(h2 - dist, bottom + dist - 0.5f);
   1843             dest.lineTo(h2 + dist, bottom + dist - 0.5f);
   1844 
   1845             dest.moveTo(h2 + dist, bottom + dist);
   1846             dest.lineTo(h2, bottom);
   1847         }
   1848 
   1849         if (fn == 2) {
   1850             dest.moveTo(h1, top);
   1851             dest.lineTo(h1 - dist, top - dist);
   1852             dest.lineTo(h1, top);
   1853             dest.lineTo(h1 + dist, top - dist);
   1854         } else if (fn == 1) {
   1855             dest.moveTo(h1, top);
   1856             dest.lineTo(h1 - dist, top - dist);
   1857 
   1858             dest.moveTo(h1 - dist, top - dist + 0.5f);
   1859             dest.lineTo(h1 + dist, top - dist + 0.5f);
   1860 
   1861             dest.moveTo(h1 + dist, top - dist);
   1862             dest.lineTo(h1, top);
   1863         }
   1864     }
   1865 
   1866     private void addSelection(int line, int start, int end,
   1867             int top, int bottom, SelectionRectangleConsumer consumer) {
   1868         int linestart = getLineStart(line);
   1869         int lineend = getLineEnd(line);
   1870         Directions dirs = getLineDirections(line);
   1871 
   1872         if (lineend > linestart && mText.charAt(lineend - 1) == '\n') {
   1873             lineend--;
   1874         }
   1875 
   1876         for (int i = 0; i < dirs.mDirections.length; i += 2) {
   1877             int here = linestart + dirs.mDirections[i];
   1878             int there = here + (dirs.mDirections[i + 1] & RUN_LENGTH_MASK);
   1879 
   1880             if (there > lineend) {
   1881                 there = lineend;
   1882             }
   1883 
   1884             if (start <= there && end >= here) {
   1885                 int st = Math.max(start, here);
   1886                 int en = Math.min(end, there);
   1887 
   1888                 if (st != en) {
   1889                     float h1 = getHorizontal(st, false, line, false /* not clamped */);
   1890                     float h2 = getHorizontal(en, true, line, false /* not clamped */);
   1891 
   1892                     float left = Math.min(h1, h2);
   1893                     float right = Math.max(h1, h2);
   1894 
   1895                     final @TextSelectionLayout int layout =
   1896                             ((dirs.mDirections[i + 1] & RUN_RTL_FLAG) != 0)
   1897                                     ? TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT
   1898                                     : TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT;
   1899 
   1900                     consumer.accept(left, top, right, bottom, layout);
   1901                 }
   1902             }
   1903         }
   1904     }
   1905 
   1906     /**
   1907      * Fills in the specified Path with a representation of a highlight
   1908      * between the specified offsets.  This will often be a rectangle
   1909      * or a potentially discontinuous set of rectangles.  If the start
   1910      * and end are the same, the returned path is empty.
   1911      */
   1912     public void getSelectionPath(int start, int end, Path dest) {
   1913         dest.reset();
   1914         getSelection(start, end, (left, top, right, bottom, textSelectionLayout) ->
   1915                 dest.addRect(left, top, right, bottom, Path.Direction.CW));
   1916     }
   1917 
   1918     /**
   1919      * Calculates the rectangles which should be highlighted to indicate a selection between start
   1920      * and end and feeds them into the given {@link SelectionRectangleConsumer}.
   1921      *
   1922      * @param start    the starting index of the selection
   1923      * @param end      the ending index of the selection
   1924      * @param consumer the {@link SelectionRectangleConsumer} which will receive the generated
   1925      *                 rectangles. It will be called every time a rectangle is generated.
   1926      * @hide
   1927      * @see #getSelectionPath(int, int, Path)
   1928      */
   1929     public final void getSelection(int start, int end, final SelectionRectangleConsumer consumer) {
   1930         if (start == end) {
   1931             return;
   1932         }
   1933 
   1934         if (end < start) {
   1935             int temp = end;
   1936             end = start;
   1937             start = temp;
   1938         }
   1939 
   1940         final int startline = getLineForOffset(start);
   1941         final int endline = getLineForOffset(end);
   1942 
   1943         int top = getLineTop(startline);
   1944         int bottom = getLineBottomWithoutSpacing(endline);
   1945 
   1946         if (startline == endline) {
   1947             addSelection(startline, start, end, top, bottom, consumer);
   1948         } else {
   1949             final float width = mWidth;
   1950 
   1951             addSelection(startline, start, getLineEnd(startline),
   1952                     top, getLineBottom(startline), consumer);
   1953 
   1954             if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT) {
   1955                 consumer.accept(getLineLeft(startline), top, 0, getLineBottom(startline),
   1956                         TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT);
   1957             } else {
   1958                 consumer.accept(getLineRight(startline), top, width, getLineBottom(startline),
   1959                         TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT);
   1960             }
   1961 
   1962             for (int i = startline + 1; i < endline; i++) {
   1963                 top = getLineTop(i);
   1964                 bottom = getLineBottom(i);
   1965                 if (getParagraphDirection(i) == DIR_RIGHT_TO_LEFT) {
   1966                     consumer.accept(0, top, width, bottom, TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT);
   1967                 } else {
   1968                     consumer.accept(0, top, width, bottom, TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT);
   1969                 }
   1970             }
   1971 
   1972             top = getLineTop(endline);
   1973             bottom = getLineBottomWithoutSpacing(endline);
   1974 
   1975             addSelection(endline, getLineStart(endline), end, top, bottom, consumer);
   1976 
   1977             if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT) {
   1978                 consumer.accept(width, top, getLineRight(endline), bottom,
   1979                         TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT);
   1980             } else {
   1981                 consumer.accept(0, top, getLineLeft(endline), bottom,
   1982                         TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT);
   1983             }
   1984         }
   1985     }
   1986 
   1987     /**
   1988      * Get the alignment of the specified paragraph, taking into account
   1989      * markup attached to it.
   1990      */
   1991     public final Alignment getParagraphAlignment(int line) {
   1992         Alignment align = mAlignment;
   1993 
   1994         if (mSpannedText) {
   1995             Spanned sp = (Spanned) mText;
   1996             AlignmentSpan[] spans = getParagraphSpans(sp, getLineStart(line),
   1997                                                 getLineEnd(line),
   1998                                                 AlignmentSpan.class);
   1999 
   2000             int spanLength = spans.length;
   2001             if (spanLength > 0) {
   2002                 align = spans[spanLength-1].getAlignment();
   2003             }
   2004         }
   2005 
   2006         return align;
   2007     }
   2008 
   2009     /**
   2010      * Get the left edge of the specified paragraph, inset by left margins.
   2011      */
   2012     public final int getParagraphLeft(int line) {
   2013         int left = 0;
   2014         int dir = getParagraphDirection(line);
   2015         if (dir == DIR_RIGHT_TO_LEFT || !mSpannedText) {
   2016             return left; // leading margin has no impact, or no styles
   2017         }
   2018         return getParagraphLeadingMargin(line);
   2019     }
   2020 
   2021     /**
   2022      * Get the right edge of the specified paragraph, inset by right margins.
   2023      */
   2024     public final int getParagraphRight(int line) {
   2025         int right = mWidth;
   2026         int dir = getParagraphDirection(line);
   2027         if (dir == DIR_LEFT_TO_RIGHT || !mSpannedText) {
   2028             return right; // leading margin has no impact, or no styles
   2029         }
   2030         return right - getParagraphLeadingMargin(line);
   2031     }
   2032 
   2033     /**
   2034      * Returns the effective leading margin (unsigned) for this line,
   2035      * taking into account LeadingMarginSpan and LeadingMarginSpan2.
   2036      * @param line the line index
   2037      * @return the leading margin of this line
   2038      */
   2039     private int getParagraphLeadingMargin(int line) {
   2040         if (!mSpannedText) {
   2041             return 0;
   2042         }
   2043         Spanned spanned = (Spanned) mText;
   2044 
   2045         int lineStart = getLineStart(line);
   2046         int lineEnd = getLineEnd(line);
   2047         int spanEnd = spanned.nextSpanTransition(lineStart, lineEnd,
   2048                 LeadingMarginSpan.class);
   2049         LeadingMarginSpan[] spans = getParagraphSpans(spanned, lineStart, spanEnd,
   2050                                                 LeadingMarginSpan.class);
   2051         if (spans.length == 0) {
   2052             return 0; // no leading margin span;
   2053         }
   2054 
   2055         int margin = 0;
   2056 
   2057         boolean useFirstLineMargin = lineStart == 0 || spanned.charAt(lineStart - 1) == '\n';
   2058         for (int i = 0; i < spans.length; i++) {
   2059             if (spans[i] instanceof LeadingMarginSpan2) {
   2060                 int spStart = spanned.getSpanStart(spans[i]);
   2061                 int spanLine = getLineForOffset(spStart);
   2062                 int count = ((LeadingMarginSpan2) spans[i]).getLeadingMarginLineCount();
   2063                 // if there is more than one LeadingMarginSpan2, use the count that is greatest
   2064                 useFirstLineMargin |= line < spanLine + count;
   2065             }
   2066         }
   2067         for (int i = 0; i < spans.length; i++) {
   2068             LeadingMarginSpan span = spans[i];
   2069             margin += span.getLeadingMargin(useFirstLineMargin);
   2070         }
   2071 
   2072         return margin;
   2073     }
   2074 
   2075     private static float measurePara(TextPaint paint, CharSequence text, int start, int end,
   2076             TextDirectionHeuristic textDir) {
   2077         MeasuredParagraph mt = null;
   2078         TextLine tl = TextLine.obtain();
   2079         try {
   2080             mt = MeasuredParagraph.buildForBidi(text, start, end, textDir, mt);
   2081             final char[] chars = mt.getChars();
   2082             final int len = chars.length;
   2083             final Directions directions = mt.getDirections(0, len);
   2084             final int dir = mt.getParagraphDir();
   2085             boolean hasTabs = false;
   2086             TabStops tabStops = null;
   2087             // leading margins should be taken into account when measuring a paragraph
   2088             int margin = 0;
   2089             if (text instanceof Spanned) {
   2090                 Spanned spanned = (Spanned) text;
   2091                 LeadingMarginSpan[] spans = getParagraphSpans(spanned, start, end,
   2092                         LeadingMarginSpan.class);
   2093                 for (LeadingMarginSpan lms : spans) {
   2094                     margin += lms.getLeadingMargin(true);
   2095                 }
   2096             }
   2097             for (int i = 0; i < len; ++i) {
   2098                 if (chars[i] == '\t') {
   2099                     hasTabs = true;
   2100                     if (text instanceof Spanned) {
   2101                         Spanned spanned = (Spanned) text;
   2102                         int spanEnd = spanned.nextSpanTransition(start, end,
   2103                                 TabStopSpan.class);
   2104                         TabStopSpan[] spans = getParagraphSpans(spanned, start, spanEnd,
   2105                                 TabStopSpan.class);
   2106                         if (spans.length > 0) {
   2107                             tabStops = new TabStops(TAB_INCREMENT, spans);
   2108                         }
   2109                     }
   2110                     break;
   2111                 }
   2112             }
   2113             tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops);
   2114             return margin + Math.abs(tl.metrics(null));
   2115         } finally {
   2116             TextLine.recycle(tl);
   2117             if (mt != null) {
   2118                 mt.recycle();
   2119             }
   2120         }
   2121     }
   2122 
   2123     /**
   2124      * @hide
   2125      */
   2126     /* package */ static class TabStops {
   2127         private int[] mStops;
   2128         private int mNumStops;
   2129         private int mIncrement;
   2130 
   2131         TabStops(int increment, Object[] spans) {
   2132             reset(increment, spans);
   2133         }
   2134 
   2135         void reset(int increment, Object[] spans) {
   2136             this.mIncrement = increment;
   2137 
   2138             int ns = 0;
   2139             if (spans != null) {
   2140                 int[] stops = this.mStops;
   2141                 for (Object o : spans) {
   2142                     if (o instanceof TabStopSpan) {
   2143                         if (stops == null) {
   2144                             stops = new int[10];
   2145                         } else if (ns == stops.length) {
   2146                             int[] nstops = new int[ns * 2];
   2147                             for (int i = 0; i < ns; ++i) {
   2148                                 nstops[i] = stops[i];
   2149                             }
   2150                             stops = nstops;
   2151                         }
   2152                         stops[ns++] = ((TabStopSpan) o).getTabStop();
   2153                     }
   2154                 }
   2155                 if (ns > 1) {
   2156                     Arrays.sort(stops, 0, ns);
   2157                 }
   2158                 if (stops != this.mStops) {
   2159                     this.mStops = stops;
   2160                 }
   2161             }
   2162             this.mNumStops = ns;
   2163         }
   2164 
   2165         float nextTab(float h) {
   2166             int ns = this.mNumStops;
   2167             if (ns > 0) {
   2168                 int[] stops = this.mStops;
   2169                 for (int i = 0; i < ns; ++i) {
   2170                     int stop = stops[i];
   2171                     if (stop > h) {
   2172                         return stop;
   2173                     }
   2174                 }
   2175             }
   2176             return nextDefaultStop(h, mIncrement);
   2177         }
   2178 
   2179         public static float nextDefaultStop(float h, int inc) {
   2180             return ((int) ((h + inc) / inc)) * inc;
   2181         }
   2182     }
   2183 
   2184     /**
   2185      * Returns the position of the next tab stop after h on the line.
   2186      *
   2187      * @param text the text
   2188      * @param start start of the line
   2189      * @param end limit of the line
   2190      * @param h the current horizontal offset
   2191      * @param tabs the tabs, can be null.  If it is null, any tabs in effect
   2192      * on the line will be used.  If there are no tabs, a default offset
   2193      * will be used to compute the tab stop.
   2194      * @return the offset of the next tab stop.
   2195      */
   2196     /* package */ static float nextTab(CharSequence text, int start, int end,
   2197                                        float h, Object[] tabs) {
   2198         float nh = Float.MAX_VALUE;
   2199         boolean alltabs = false;
   2200 
   2201         if (text instanceof Spanned) {
   2202             if (tabs == null) {
   2203                 tabs = getParagraphSpans((Spanned) text, start, end, TabStopSpan.class);
   2204                 alltabs = true;
   2205             }
   2206 
   2207             for (int i = 0; i < tabs.length; i++) {
   2208                 if (!alltabs) {
   2209                     if (!(tabs[i] instanceof TabStopSpan))
   2210                         continue;
   2211                 }
   2212 
   2213                 int where = ((TabStopSpan) tabs[i]).getTabStop();
   2214 
   2215                 if (where < nh && where > h)
   2216                     nh = where;
   2217             }
   2218 
   2219             if (nh != Float.MAX_VALUE)
   2220                 return nh;
   2221         }
   2222 
   2223         return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT;
   2224     }
   2225 
   2226     protected final boolean isSpanned() {
   2227         return mSpannedText;
   2228     }
   2229 
   2230     /**
   2231      * Returns the same as <code>text.getSpans()</code>, except where
   2232      * <code>start</code> and <code>end</code> are the same and are not
   2233      * at the very beginning of the text, in which case an empty array
   2234      * is returned instead.
   2235      * <p>
   2236      * This is needed because of the special case that <code>getSpans()</code>
   2237      * on an empty range returns the spans adjacent to that range, which is
   2238      * primarily for the sake of <code>TextWatchers</code> so they will get
   2239      * notifications when text goes from empty to non-empty.  But it also
   2240      * has the unfortunate side effect that if the text ends with an empty
   2241      * paragraph, that paragraph accidentally picks up the styles of the
   2242      * preceding paragraph (even though those styles will not be picked up
   2243      * by new text that is inserted into the empty paragraph).
   2244      * <p>
   2245      * The reason it just checks whether <code>start</code> and <code>end</code>
   2246      * is the same is that the only time a line can contain 0 characters
   2247      * is if it is the final paragraph of the Layout; otherwise any line will
   2248      * contain at least one printing or newline character.  The reason for the
   2249      * additional check if <code>start</code> is greater than 0 is that
   2250      * if the empty paragraph is the entire content of the buffer, paragraph
   2251      * styles that are already applied to the buffer will apply to text that
   2252      * is inserted into it.
   2253      */
   2254     /* package */static <T> T[] getParagraphSpans(Spanned text, int start, int end, Class<T> type) {
   2255         if (start == end && start > 0) {
   2256             return ArrayUtils.emptyArray(type);
   2257         }
   2258 
   2259         if(text instanceof SpannableStringBuilder) {
   2260             return ((SpannableStringBuilder) text).getSpans(start, end, type, false);
   2261         } else {
   2262             return text.getSpans(start, end, type);
   2263         }
   2264     }
   2265 
   2266     private void ellipsize(int start, int end, int line,
   2267                            char[] dest, int destoff, TextUtils.TruncateAt method) {
   2268         final int ellipsisCount = getEllipsisCount(line);
   2269         if (ellipsisCount == 0) {
   2270             return;
   2271         }
   2272         final int ellipsisStart = getEllipsisStart(line);
   2273         final int lineStart = getLineStart(line);
   2274 
   2275         final String ellipsisString = TextUtils.getEllipsisString(method);
   2276         final int ellipsisStringLen = ellipsisString.length();
   2277         // Use the ellipsis string only if there are that at least as many characters to replace.
   2278         final boolean useEllipsisString = ellipsisCount >= ellipsisStringLen;
   2279         for (int i = 0; i < ellipsisCount; i++) {
   2280             final char c;
   2281             if (useEllipsisString && i < ellipsisStringLen) {
   2282                 c = ellipsisString.charAt(i);
   2283             } else {
   2284                 c = TextUtils.ELLIPSIS_FILLER;
   2285             }
   2286 
   2287             final int a = i + ellipsisStart + lineStart;
   2288             if (start <= a && a < end) {
   2289                 dest[destoff + a - start] = c;
   2290             }
   2291         }
   2292     }
   2293 
   2294     /**
   2295      * Stores information about bidirectional (left-to-right or right-to-left)
   2296      * text within the layout of a line.
   2297      */
   2298     public static class Directions {
   2299         /**
   2300          * Directions represents directional runs within a line of text. Runs are pairs of ints
   2301          * listed in visual order, starting from the leading margin.  The first int of each pair is
   2302          * the offset from the first character of the line to the start of the run.  The second int
   2303          * represents both the length and level of the run. The length is in the lower bits,
   2304          * accessed by masking with RUN_LENGTH_MASK.  The level is in the higher bits, accessed by
   2305          * shifting by RUN_LEVEL_SHIFT and masking by RUN_LEVEL_MASK. To simply test for an RTL
   2306          * direction, test the bit using RUN_RTL_FLAG, if set then the direction is rtl.
   2307          * @hide
   2308          */
   2309         @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
   2310         public int[] mDirections;
   2311 
   2312         /**
   2313          * @hide
   2314          */
   2315         @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
   2316         public Directions(int[] dirs) {
   2317             mDirections = dirs;
   2318         }
   2319     }
   2320 
   2321     /**
   2322      * Return the offset of the first character to be ellipsized away,
   2323      * relative to the start of the line.  (So 0 if the beginning of the
   2324      * line is ellipsized, not getLineStart().)
   2325      */
   2326     public abstract int getEllipsisStart(int line);
   2327 
   2328     /**
   2329      * Returns the number of characters to be ellipsized away, or 0 if
   2330      * no ellipsis is to take place.
   2331      */
   2332     public abstract int getEllipsisCount(int line);
   2333 
   2334     /* package */ static class Ellipsizer implements CharSequence, GetChars {
   2335         /* package */ CharSequence mText;
   2336         /* package */ Layout mLayout;
   2337         /* package */ int mWidth;
   2338         /* package */ TextUtils.TruncateAt mMethod;
   2339 
   2340         public Ellipsizer(CharSequence s) {
   2341             mText = s;
   2342         }
   2343 
   2344         public char charAt(int off) {
   2345             char[] buf = TextUtils.obtain(1);
   2346             getChars(off, off + 1, buf, 0);
   2347             char ret = buf[0];
   2348 
   2349             TextUtils.recycle(buf);
   2350             return ret;
   2351         }
   2352 
   2353         public void getChars(int start, int end, char[] dest, int destoff) {
   2354             int line1 = mLayout.getLineForOffset(start);
   2355             int line2 = mLayout.getLineForOffset(end);
   2356 
   2357             TextUtils.getChars(mText, start, end, dest, destoff);
   2358 
   2359             for (int i = line1; i <= line2; i++) {
   2360                 mLayout.ellipsize(start, end, i, dest, destoff, mMethod);
   2361             }
   2362         }
   2363 
   2364         public int length() {
   2365             return mText.length();
   2366         }
   2367 
   2368         public CharSequence subSequence(int start, int end) {
   2369             char[] s = new char[end - start];
   2370             getChars(start, end, s, 0);
   2371             return new String(s);
   2372         }
   2373 
   2374         @Override
   2375         public String toString() {
   2376             char[] s = new char[length()];
   2377             getChars(0, length(), s, 0);
   2378             return new String(s);
   2379         }
   2380 
   2381     }
   2382 
   2383     /* package */ static class SpannedEllipsizer extends Ellipsizer implements Spanned {
   2384         private Spanned mSpanned;
   2385 
   2386         public SpannedEllipsizer(CharSequence display) {
   2387             super(display);
   2388             mSpanned = (Spanned) display;
   2389         }
   2390 
   2391         public <T> T[] getSpans(int start, int end, Class<T> type) {
   2392             return mSpanned.getSpans(start, end, type);
   2393         }
   2394 
   2395         public int getSpanStart(Object tag) {
   2396             return mSpanned.getSpanStart(tag);
   2397         }
   2398 
   2399         public int getSpanEnd(Object tag) {
   2400             return mSpanned.getSpanEnd(tag);
   2401         }
   2402 
   2403         public int getSpanFlags(Object tag) {
   2404             return mSpanned.getSpanFlags(tag);
   2405         }
   2406 
   2407         @SuppressWarnings("rawtypes")
   2408         public int nextSpanTransition(int start, int limit, Class type) {
   2409             return mSpanned.nextSpanTransition(start, limit, type);
   2410         }
   2411 
   2412         @Override
   2413         public CharSequence subSequence(int start, int end) {
   2414             char[] s = new char[end - start];
   2415             getChars(start, end, s, 0);
   2416 
   2417             SpannableString ss = new SpannableString(new String(s));
   2418             TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0);
   2419             return ss;
   2420         }
   2421     }
   2422 
   2423     private CharSequence mText;
   2424     private TextPaint mPaint;
   2425     private TextPaint mWorkPaint = new TextPaint();
   2426     private int mWidth;
   2427     private Alignment mAlignment = Alignment.ALIGN_NORMAL;
   2428     private float mSpacingMult;
   2429     private float mSpacingAdd;
   2430     private static final Rect sTempRect = new Rect();
   2431     private boolean mSpannedText;
   2432     private TextDirectionHeuristic mTextDir;
   2433     private SpanSet<LineBackgroundSpan> mLineBackgroundSpans;
   2434     private int mJustificationMode;
   2435 
   2436     /** @hide */
   2437     @IntDef(prefix = { "DIR_" }, value = {
   2438             DIR_LEFT_TO_RIGHT,
   2439             DIR_RIGHT_TO_LEFT
   2440     })
   2441     @Retention(RetentionPolicy.SOURCE)
   2442     public @interface Direction {}
   2443 
   2444     public static final int DIR_LEFT_TO_RIGHT = 1;
   2445     public static final int DIR_RIGHT_TO_LEFT = -1;
   2446 
   2447     /* package */ static final int DIR_REQUEST_LTR = 1;
   2448     /* package */ static final int DIR_REQUEST_RTL = -1;
   2449     /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2;
   2450     /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2;
   2451 
   2452     /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff;
   2453     /* package */ static final int RUN_LEVEL_SHIFT = 26;
   2454     /* package */ static final int RUN_LEVEL_MASK = 0x3f;
   2455     /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT;
   2456 
   2457     public enum Alignment {
   2458         ALIGN_NORMAL,
   2459         ALIGN_OPPOSITE,
   2460         ALIGN_CENTER,
   2461         /** @hide */
   2462         ALIGN_LEFT,
   2463         /** @hide */
   2464         ALIGN_RIGHT,
   2465     }
   2466 
   2467     private static final int TAB_INCREMENT = 20;
   2468 
   2469     /** @hide */
   2470     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
   2471     public static final Directions DIRS_ALL_LEFT_TO_RIGHT =
   2472         new Directions(new int[] { 0, RUN_LENGTH_MASK });
   2473 
   2474     /** @hide */
   2475     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
   2476     public static final Directions DIRS_ALL_RIGHT_TO_LEFT =
   2477         new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG });
   2478 
   2479     /** @hide */
   2480     @Retention(RetentionPolicy.SOURCE)
   2481     @IntDef(prefix = { "TEXT_SELECTION_LAYOUT_" }, value = {
   2482             TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT,
   2483             TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT
   2484     })
   2485     public @interface TextSelectionLayout {}
   2486 
   2487     /** @hide */
   2488     public static final int TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT = 0;
   2489     /** @hide */
   2490     public static final int TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT = 1;
   2491 
   2492     /** @hide */
   2493     @FunctionalInterface
   2494     public interface SelectionRectangleConsumer {
   2495         /**
   2496          * Performs this operation on the given rectangle.
   2497          *
   2498          * @param left   the left edge of the rectangle
   2499          * @param top    the top edge of the rectangle
   2500          * @param right  the right edge of the rectangle
   2501          * @param bottom the bottom edge of the rectangle
   2502          * @param textSelectionLayout the layout (RTL or LTR) of the text covered by this
   2503          *                            selection rectangle
   2504          */
   2505         void accept(float left, float top, float right, float bottom,
   2506                 @TextSelectionLayout int textSelectionLayout);
   2507     }
   2508 
   2509 }
   2510