OSDN Git Service

[automerger] Optimise the hit test algorithm am: 71ecf5bd5c am: 42eaa8f932 am: a72cb4...
[android-x86/frameworks-base.git] / core / java / android / text / Layout.java
index 22ad634..c94e489 100644 (file)
@@ -17,7 +17,6 @@
 package android.text;
 
 import android.annotation.IntDef;
-import android.emoji.EmojiFactory;
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.graphics.Path;
@@ -103,19 +102,6 @@ public abstract class Layout {
     private static final ParagraphStyle[] NO_PARA_SPANS =
         ArrayUtils.emptyArray(ParagraphStyle.class);
 
-    /* package */ static final EmojiFactory EMOJI_FACTORY = EmojiFactory.newAvailableInstance();
-    /* package */ static final int MIN_EMOJI, MAX_EMOJI;
-
-    static {
-        if (EMOJI_FACTORY != null) {
-            MIN_EMOJI = EMOJI_FACTORY.getMinimumAndroidPua();
-            MAX_EMOJI = EMOJI_FACTORY.getMaximumAndroidPua();
-        } else {
-            MIN_EMOJI = -1;
-            MAX_EMOJI = -1;
-        }
-    }
-
     /**
      * Return how wide a layout must be in order to display the
      * specified text with one line per paragraph.
@@ -360,9 +346,9 @@ public abstract class Layout {
                 }
             }
 
-            boolean hasTabOrEmoji = getLineContainsTab(lineNum);
+            boolean hasTab = getLineContainsTab(lineNum);
             // Can't tell if we have tabs for sure, currently
-            if (hasTabOrEmoji && !tabStopsIsInitialized) {
+            if (hasTab && !tabStopsIsInitialized) {
                 if (tabStops == null) {
                     tabStops = new TabStops(TAB_INCREMENT, spans);
                 } else {
@@ -405,11 +391,11 @@ public abstract class Layout {
 
             paint.setHyphenEdit(getHyphen(lineNum));
             Directions directions = getLineDirections(lineNum);
-            if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTabOrEmoji) {
+            if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTab) {
                 // XXX: assumes there's nothing additional to be done
                 canvas.drawText(buf, start, end, x, lbaseline, paint);
             } else {
-                tl.set(paint, buf, start, end, dir, directions, hasTabOrEmoji, tabStops);
+                tl.set(paint, buf, start, end, dir, directions, hasTab, tabStops);
                 tl.draw(canvas, x, ltop, lbaseline, lbottom);
             }
             paint.setHyphenEdit(0);
@@ -710,8 +696,7 @@ public abstract class Layout {
 
     /**
      * Returns whether the specified line contains one or more
-     * characters that need to be handled specially, like tabs
-     * or emoji.
+     * characters that need to be handled specially, like tabs.
      */
     public abstract boolean getLineContainsTab(int line);
 
@@ -814,6 +799,57 @@ public abstract class Layout {
         return false;
     }
 
+    /**
+     * Returns the range of the run that the character at offset belongs to.
+     * @param offset the offset
+     * @return The range of the run
+     * @hide
+     */
+    public long getRunRange(int offset) {
+        int line = getLineForOffset(offset);
+        Directions dirs = getLineDirections(line);
+        if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) {
+            return TextUtils.packRangeInLong(0, getLineEnd(line));
+        }
+        int[] runs = dirs.mDirections;
+        int lineStart = getLineStart(line);
+        for (int i = 0; i < runs.length; i += 2) {
+            int start = lineStart + runs[i];
+            int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
+            if (offset >= start && offset < limit) {
+                return TextUtils.packRangeInLong(start, limit);
+            }
+        }
+        // Should happen only if the offset is "out of bounds"
+        return TextUtils.packRangeInLong(0, getLineEnd(line));
+    }
+
+    /**
+     * Checks if the trailing BiDi level should be used for an offset
+     *
+     * This method is useful when the offset is at the BiDi level transition point and determine
+     * which run need to be used. For example, let's think about following input: (L* denotes
+     * Left-to-Right characters, R* denotes Right-to-Left characters.)
+     * Input (Logical Order): L1 L2 L3 R1 R2 R3 L4 L5 L6
+     * Input (Display Order): L1 L2 L3 R3 R2 R1 L4 L5 L6
+     *
+     * Then, think about selecting the range (3, 6). The offset=3 and offset=6 are ambiguous here
+     * since they are at the BiDi transition point.  In Android, the offset is considered to be
+     * associated with the trailing run if the BiDi level of the trailing run is higher than of the
+     * previous run.  In this case, the BiDi level of the input text is as follows:
+     *
+     * Input (Logical Order): L1 L2 L3 R1 R2 R3 L4 L5 L6
+     *              BiDi Run: [ Run 0 ][ Run 1 ][ Run 2 ]
+     *            BiDi Level:  0  0  0  1  1  1  0  0  0
+     *
+     * Thus, offset = 3 is part of Run 1 and this method returns true for offset = 3, since the BiDi
+     * level of Run 1 is higher than the level of Run 0.  Similarly, the offset = 6 is a part of Run
+     * 1 and this method returns false for the offset = 6 since the BiDi level of Run 1 is higher
+     * than the level of Run 2.
+     *
+     * @returns true if offset is at the BiDi level transition point and trailing BiDi level is
+     *          higher than previous BiDi level. See above for the detail.
+     */
     private boolean primaryIsTrailingPrevious(int offset) {
         int line = getLineForOffset(offset);
         int lineStart = getLineStart(line);
@@ -864,6 +900,41 @@ public abstract class Layout {
     }
 
     /**
+     * Computes in linear time the results of calling
+     * #primaryIsTrailingPrevious for all offsets on a line.
+     * @param line The line giving the offsets we compute the information for
+     * @return The array of results, indexed from 0, where 0 corresponds to the line start offset
+     */
+    private boolean[] primaryIsTrailingPreviousAllLineOffsets(int line) {
+        int lineStart = getLineStart(line);
+        int lineEnd = getLineEnd(line);
+        int[] runs = getLineDirections(line).mDirections;
+
+        boolean[] trailing = new boolean[lineEnd - lineStart + 1];
+
+        byte[] level = new byte[lineEnd - lineStart + 1];
+        for (int i = 0; i < runs.length; i += 2) {
+            int start = lineStart + runs[i];
+            int limit = start + (runs[i + 1] & RUN_LENGTH_MASK);
+            if (limit > lineEnd) {
+                limit = lineEnd;
+            }
+            level[limit - lineStart - 1] =
+                    (byte) ((runs[i + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK);
+        }
+
+        for (int i = 0; i < runs.length; i += 2) {
+            int start = lineStart + runs[i];
+            byte currentLevel = (byte) ((runs[i + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK);
+            trailing[start - lineStart] = currentLevel > (start == lineStart
+                    ? (getParagraphDirection(line) == 1 ? 0 : 1)
+                    : level[start - lineStart - 1]);
+        }
+
+        return trailing;
+    }
+
+    /**
      * Get the primary horizontal position for the specified text offset.
      * This is the location where a new character would be inserted in
      * the paragraph's primary direction.
@@ -901,6 +972,10 @@ public abstract class Layout {
         return getHorizontal(offset, !trailing, clamped);
     }
 
+    private float getHorizontal(int offset, boolean primary) {
+        return primary ? getPrimaryHorizontal(offset) : getSecondaryHorizontal(offset);
+    }
+
     private float getHorizontal(int offset, boolean trailing, boolean clamped) {
         int line = getLineForOffset(offset);
 
@@ -911,11 +986,11 @@ public abstract class Layout {
         int start = getLineStart(line);
         int end = getLineEnd(line);
         int dir = getParagraphDirection(line);
-        boolean hasTabOrEmoji = getLineContainsTab(line);
+        boolean hasTab = getLineContainsTab(line);
         Directions directions = getLineDirections(line);
 
         TabStops tabStops = null;
-        if (hasTabOrEmoji && mText instanceof Spanned) {
+        if (hasTab && mText instanceof Spanned) {
             // Just checking this line should be good enough, tabs should be
             // consistent across all lines in a paragraph.
             TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
@@ -925,7 +1000,7 @@ public abstract class Layout {
         }
 
         TextLine tl = TextLine.obtain();
-        tl.set(mPaint, mText, start, end, dir, directions, hasTabOrEmoji, tabStops);
+        tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops);
         float wid = tl.measure(offset - start, trailing, null);
         TextLine.recycle(tl);
 
@@ -939,6 +1014,60 @@ public abstract class Layout {
     }
 
     /**
+     * Computes in linear time the results of calling
+     * #getHorizontal for all offsets on a line.
+     * @param line The line giving the offsets we compute information for
+     * @param clamped Whether to clamp the results to the width of the layout
+     * @param primary Whether the results should be the primary or the secondary horizontal
+     * @return The array of results, indexed from 0, where 0 corresponds to the line start offset
+     */
+    private float[] getLineHorizontals(int line, boolean clamped, boolean primary) {
+        int start = getLineStart(line);
+        int end = getLineEnd(line);
+        int dir = getParagraphDirection(line);
+        boolean hasTab = getLineContainsTab(line);
+        Directions directions = getLineDirections(line);
+
+        TabStops tabStops = null;
+        if (hasTab && mText instanceof Spanned) {
+            // Just checking this line should be good enough, tabs should be
+            // consistent across all lines in a paragraph.
+            TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
+            if (tabs.length > 0) {
+                tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
+            }
+        }
+
+        TextLine tl = TextLine.obtain();
+        tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops);
+        boolean[] trailings = primaryIsTrailingPreviousAllLineOffsets(line);
+        if (!primary) {
+            for (int offset = 0; offset < trailings.length; ++offset) {
+                trailings[offset] = !trailings[offset];
+            }
+        }
+        float[] wid = tl.measureAllOffsets(trailings, null);
+        TextLine.recycle(tl);
+
+        if (clamped) {
+            for (int offset = 0; offset <= wid.length; ++offset) {
+                if (wid[offset] > mWidth) {
+                    wid[offset] = mWidth;
+                }
+            }
+        }
+        int left = getParagraphLeft(line);
+        int right = getParagraphRight(line);
+
+        int lineStartPos = getLineStartPos(line, left, right);
+        float[] horizontal = new float[end - start + 1];
+        for (int offset = 0; offset < horizontal.length; ++offset) {
+            horizontal[offset] = lineStartPos + wid[offset];
+        }
+        return horizontal;
+    }
+
+    /**
      * Get the leftmost position that should be exposed for horizontal
      * scrolling on the specified line.
      */
@@ -1031,9 +1160,9 @@ public abstract class Layout {
         int start = getLineStart(line);
         int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
 
-        boolean hasTabsOrEmoji = getLineContainsTab(line);
+        boolean hasTabs = getLineContainsTab(line);
         TabStops tabStops = null;
-        if (hasTabsOrEmoji && mText instanceof Spanned) {
+        if (hasTabs && mText instanceof Spanned) {
             // Just checking this line should be good enough, tabs should be
             // consistent across all lines in a paragraph.
             TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
@@ -1049,7 +1178,7 @@ public abstract class Layout {
         int dir = getParagraphDirection(line);
 
         TextLine tl = TextLine.obtain();
-        tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops);
+        tl.set(mPaint, mText, start, end, dir, directions, hasTabs, tabStops);
         float width = tl.metrics(null);
         TextLine.recycle(tl);
         return width;
@@ -1066,12 +1195,12 @@ public abstract class Layout {
     private float getLineExtent(int line, TabStops tabStops, boolean full) {
         int start = getLineStart(line);
         int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
-        boolean hasTabsOrEmoji = getLineContainsTab(line);
+        boolean hasTabs = getLineContainsTab(line);
         Directions directions = getLineDirections(line);
         int dir = getParagraphDirection(line);
 
         TextLine tl = TextLine.obtain();
-        tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops);
+        tl.set(mPaint, mText, start, end, dir, directions, hasTabs, tabStops);
         float width = tl.metrics(null);
         TextLine.recycle(tl);
         return width;
@@ -1129,6 +1258,20 @@ public abstract class Layout {
      * closest to the specified horizontal position.
      */
     public int getOffsetForHorizontal(int line, float horiz) {
+        return getOffsetForHorizontal(line, horiz, true);
+    }
+
+    /**
+     * Get the character offset on the specified line whose position is
+     * closest to the specified horizontal position.
+     *
+     * @param line the line used to find the closest offset
+     * @param horiz the horizontal position used to find the closest offset
+     * @param primary whether to use the primary position or secondary position to find the offset
+     *
+     * @hide
+     */
+    public int getOffsetForHorizontal(int line, float horiz, boolean primary) {
         // TODO: use Paint.getOffsetForAdvance to avoid binary search
         final int lineEndOffset = getLineEnd(line);
         final int lineStartOffset = getLineStart(line);
@@ -1139,6 +1282,8 @@ public abstract class Layout {
         // XXX: we don't care about tabs as we just use TextLine#getOffsetToLeftRightOf here.
         tl.set(mPaint, mText, lineStartOffset, lineEndOffset, getParagraphDirection(line), dirs,
                 false, null);
+        final HorizontalMeasurementProvider horizontal =
+                new HorizontalMeasurementProvider(line, primary);
 
         final int max;
         if (line == getLineCount() - 1) {
@@ -1148,7 +1293,7 @@ public abstract class Layout {
                     !isRtlCharAt(lineEndOffset - 1)) + lineStartOffset;
         }
         int best = lineStartOffset;
-        float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz);
+        float bestdist = Math.abs(horizontal.get(lineStartOffset) - horiz);
 
         for (int i = 0; i < dirs.mDirections.length; i += 2) {
             int here = lineStartOffset + dirs.mDirections[i];
@@ -1164,7 +1309,7 @@ public abstract class Layout {
                 guess = (high + low) / 2;
                 int adguess = getOffsetAtStartOf(guess);
 
-                if (getPrimaryHorizontal(adguess) * swap >= horiz * swap)
+                if (horizontal.get(adguess) * swap >= horiz * swap)
                     high = guess;
                 else
                     low = guess;
@@ -1177,9 +1322,9 @@ public abstract class Layout {
                 int aft = tl.getOffsetToLeftRightOf(low - lineStartOffset, isRtl) + lineStartOffset;
                 low = tl.getOffsetToLeftRightOf(aft - lineStartOffset, !isRtl) + lineStartOffset;
                 if (low >= here && low < there) {
-                    float dist = Math.abs(getPrimaryHorizontal(low) - horiz);
+                    float dist = Math.abs(horizontal.get(low) - horiz);
                     if (aft < there) {
-                        float other = Math.abs(getPrimaryHorizontal(aft) - horiz);
+                        float other = Math.abs(horizontal.get(aft) - horiz);
 
                         if (other < dist) {
                             dist = other;
@@ -1194,7 +1339,7 @@ public abstract class Layout {
                 }
             }
 
-            float dist = Math.abs(getPrimaryHorizontal(here) - horiz);
+            float dist = Math.abs(horizontal.get(here) - horiz);
 
             if (dist < bestdist) {
                 bestdist = dist;
@@ -1202,7 +1347,7 @@ public abstract class Layout {
             }
         }
 
-        float dist = Math.abs(getPrimaryHorizontal(max) - horiz);
+        float dist = Math.abs(horizontal.get(max) - horiz);
 
         if (dist <= bestdist) {
             bestdist = dist;
@@ -1214,6 +1359,46 @@ public abstract class Layout {
     }
 
     /**
+     * Responds to #getHorizontal queries, by selecting the better strategy between:
+     * - calling #getHorizontal explicitly for each query
+     * - precomputing all #getHorizontal measurements, and responding to any query in constant time
+     * The first strategy is used for LTR-only text, while the second is used for all other cases.
+     * The class is currently only used in #getOffsetForHorizontal, so reuse with care in other
+     * contexts.
+     */
+    private class HorizontalMeasurementProvider {
+        private final int mLine;
+        private final boolean mPrimary;
+
+        private float[] mHorizontals;
+        private int mLineStartOffset;
+
+        HorizontalMeasurementProvider(final int line, final boolean primary) {
+            mLine = line;
+            mPrimary = primary;
+            init();
+        }
+
+        private void init() {
+            final Directions dirs = getLineDirections(mLine);
+            if (dirs == DIRS_ALL_LEFT_TO_RIGHT) {
+                return;
+            }
+
+            mHorizontals = getLineHorizontals(mLine, false, mPrimary);
+            mLineStartOffset = getLineStart(mLine);
+        }
+
+        float get(final int offset) {
+            if (mHorizontals == null) {
+                return getHorizontal(offset, mPrimary);
+            } else {
+                return mHorizontals[offset - mLineStartOffset];
+            }
+        }
+    }
+
+    /**
      * Return the text offset after the last character on the specified line.
      */
     public final int getLineEnd(int line) {
@@ -1841,7 +2026,11 @@ public abstract class Layout {
             return ArrayUtils.emptyArray(type);
         }
 
-        return text.getSpans(start, end, type);
+        if(text instanceof SpannableStringBuilder) {
+            return ((SpannableStringBuilder) text).getSpans(start, end, type, false);
+        } else {
+            return text.getSpans(start, end, type);
+        }
     }
 
     private char getEllipsisChar(TextUtils.TruncateAt method) {