1 /* 2 * Copyright (C) 2022 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.IntRange; 20 import android.annotation.NonNull; 21 import android.graphics.TemporaryBuffer; 22 import android.graphics.text.GraphemeBreak; 23 24 /** 25 * Implementation of {@code SegmentFinder} using grapheme clusters as the text segment. Whitespace 26 * characters are included as segments. 27 * 28 * <p>For example, the text "a pot" would be divided into five text segments: "a", " ", "p", "o", 29 * "t". 30 * 31 * @see <a href="https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries">Unicode Text 32 * Segmentation - Grapheme Cluster Boundaries</a> 33 */ 34 @android.ravenwood.annotation.RavenwoodKeepWholeClass 35 public class GraphemeClusterSegmentFinder extends SegmentFinder { 36 private static AutoGrowArray.FloatArray sTempAdvances = null; 37 private final boolean[] mIsGraphemeBreak; 38 39 /** 40 * Constructs a GraphemeClusterSegmentFinder instance for the specified text which uses the 41 * provided TextPaint to determine grapheme cluster boundaries. 42 * 43 * @param text text to be segmented 44 * @param textPaint TextPaint used to draw the text 45 */ GraphemeClusterSegmentFinder( @onNull CharSequence text, @NonNull TextPaint textPaint)46 public GraphemeClusterSegmentFinder( 47 @NonNull CharSequence text, @NonNull TextPaint textPaint) { 48 49 if (sTempAdvances == null) { 50 sTempAdvances = new AutoGrowArray.FloatArray(text.length()); 51 } else if (sTempAdvances.size() < text.length()) { 52 sTempAdvances.resize(text.length()); 53 } 54 55 mIsGraphemeBreak = new boolean[text.length()]; 56 float[] advances = sTempAdvances.getRawArray(); 57 char[] chars = TemporaryBuffer.obtain(text.length()); 58 59 TextUtils.getChars(text, 0, text.length(), chars, 0); 60 61 textPaint.getTextWidths(chars, 0, text.length(), advances); 62 63 GraphemeBreak.isGraphemeBreak(advances, chars, /* start= */ 0, /* end= */ text.length(), 64 mIsGraphemeBreak); 65 TemporaryBuffer.recycle(chars); 66 } 67 previousBoundary(@ntRangefrom = 0) int offset)68 private int previousBoundary(@IntRange(from = 0) int offset) { 69 if (offset <= 0) return DONE; 70 do { 71 --offset; 72 } while (offset > 0 && !mIsGraphemeBreak[offset]); 73 return offset; 74 } 75 nextBoundary(@ntRangefrom = 0) int offset)76 private int nextBoundary(@IntRange(from = 0) int offset) { 77 if (offset >= mIsGraphemeBreak.length) return DONE; 78 do { 79 ++offset; 80 } while (offset < mIsGraphemeBreak.length && !mIsGraphemeBreak[offset]); 81 return offset; 82 } 83 84 @Override previousStartBoundary(@ntRangefrom = 0) int offset)85 public int previousStartBoundary(@IntRange(from = 0) int offset) { 86 return previousBoundary(offset); 87 } 88 89 @Override previousEndBoundary(@ntRangefrom = 0) int offset)90 public int previousEndBoundary(@IntRange(from = 0) int offset) { 91 if (offset == 0) return DONE; 92 int boundary = previousBoundary(offset); 93 // Check that there is another cursor position before, otherwise this is not a valid 94 // end boundary. 95 if (boundary == DONE || previousBoundary(boundary) == DONE) { 96 return DONE; 97 } 98 return boundary; 99 } 100 101 @Override nextStartBoundary(@ntRangefrom = 0) int offset)102 public int nextStartBoundary(@IntRange(from = 0) int offset) { 103 if (offset == mIsGraphemeBreak.length) return DONE; 104 int boundary = nextBoundary(offset); 105 // Check that there is another cursor position after, otherwise this is not a valid 106 // start boundary. 107 if (boundary == DONE || nextBoundary(boundary) == DONE) { 108 return DONE; 109 } 110 return boundary; 111 } 112 113 @Override nextEndBoundary(@ntRangefrom = 0) int offset)114 public int nextEndBoundary(@IntRange(from = 0) int offset) { 115 return nextBoundary(offset); 116 } 117 } 118