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 public class GraphemeClusterSegmentFinder extends SegmentFinder { 35 private static AutoGrowArray.FloatArray sTempAdvances = null; 36 private final boolean[] mIsGraphemeBreak; 37 38 /** 39 * Constructs a GraphemeClusterSegmentFinder instance for the specified text which uses the 40 * provided TextPaint to determine grapheme cluster boundaries. 41 * 42 * @param text text to be segmented 43 * @param textPaint TextPaint used to draw the text 44 */ GraphemeClusterSegmentFinder( @onNull CharSequence text, @NonNull TextPaint textPaint)45 public GraphemeClusterSegmentFinder( 46 @NonNull CharSequence text, @NonNull TextPaint textPaint) { 47 48 if (sTempAdvances == null) { 49 sTempAdvances = new AutoGrowArray.FloatArray(text.length()); 50 } else if (sTempAdvances.size() < text.length()) { 51 sTempAdvances.resize(text.length()); 52 } 53 54 mIsGraphemeBreak = new boolean[text.length()]; 55 float[] advances = sTempAdvances.getRawArray(); 56 char[] chars = TemporaryBuffer.obtain(text.length()); 57 58 TextUtils.getChars(text, 0, text.length(), chars, 0); 59 60 textPaint.getTextWidths(chars, 0, text.length(), advances); 61 62 GraphemeBreak.isGraphemeBreak(advances, chars, /* start= */ 0, /* end= */ text.length(), 63 mIsGraphemeBreak); 64 TemporaryBuffer.recycle(chars); 65 } 66 previousBoundary(@ntRangefrom = 0) int offset)67 private int previousBoundary(@IntRange(from = 0) int offset) { 68 if (offset <= 0) return DONE; 69 do { 70 --offset; 71 } while (offset > 0 && !mIsGraphemeBreak[offset]); 72 return offset; 73 } 74 nextBoundary(@ntRangefrom = 0) int offset)75 private int nextBoundary(@IntRange(from = 0) int offset) { 76 if (offset >= mIsGraphemeBreak.length) return DONE; 77 do { 78 ++offset; 79 } while (offset < mIsGraphemeBreak.length && !mIsGraphemeBreak[offset]); 80 return offset; 81 } 82 83 @Override previousStartBoundary(@ntRangefrom = 0) int offset)84 public int previousStartBoundary(@IntRange(from = 0) int offset) { 85 return previousBoundary(offset); 86 } 87 88 @Override previousEndBoundary(@ntRangefrom = 0) int offset)89 public int previousEndBoundary(@IntRange(from = 0) int offset) { 90 if (offset == 0) return DONE; 91 int boundary = previousBoundary(offset); 92 // Check that there is another cursor position before, otherwise this is not a valid 93 // end boundary. 94 if (boundary == DONE || previousBoundary(boundary) == DONE) { 95 return DONE; 96 } 97 return boundary; 98 } 99 100 @Override nextStartBoundary(@ntRangefrom = 0) int offset)101 public int nextStartBoundary(@IntRange(from = 0) int offset) { 102 if (offset == mIsGraphemeBreak.length) return DONE; 103 int boundary = nextBoundary(offset); 104 // Check that there is another cursor position after, otherwise this is not a valid 105 // start boundary. 106 if (boundary == DONE || nextBoundary(boundary) == DONE) { 107 return DONE; 108 } 109 return boundary; 110 } 111 112 @Override nextEndBoundary(@ntRangefrom = 0) int offset)113 public int nextEndBoundary(@IntRange(from = 0) int offset) { 114 return nextBoundary(offset); 115 } 116 } 117