/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.text; import android.annotation.IntRange; import android.annotation.NonNull; import android.graphics.TemporaryBuffer; import android.graphics.text.GraphemeBreak; /** * Implementation of {@code SegmentFinder} using grapheme clusters as the text segment. Whitespace * characters are included as segments. * *

For example, the text "a pot" would be divided into five text segments: "a", " ", "p", "o", * "t". * * @see Unicode Text * Segmentation - Grapheme Cluster Boundaries */ public class GraphemeClusterSegmentFinder extends SegmentFinder { private static AutoGrowArray.FloatArray sTempAdvances = null; private final boolean[] mIsGraphemeBreak; /** * Constructs a GraphemeClusterSegmentFinder instance for the specified text which uses the * provided TextPaint to determine grapheme cluster boundaries. * * @param text text to be segmented * @param textPaint TextPaint used to draw the text */ public GraphemeClusterSegmentFinder( @NonNull CharSequence text, @NonNull TextPaint textPaint) { if (sTempAdvances == null) { sTempAdvances = new AutoGrowArray.FloatArray(text.length()); } else if (sTempAdvances.size() < text.length()) { sTempAdvances.resize(text.length()); } mIsGraphemeBreak = new boolean[text.length()]; float[] advances = sTempAdvances.getRawArray(); char[] chars = TemporaryBuffer.obtain(text.length()); TextUtils.getChars(text, 0, text.length(), chars, 0); textPaint.getTextWidths(chars, 0, text.length(), advances); GraphemeBreak.isGraphemeBreak(advances, chars, /* start= */ 0, /* end= */ text.length(), mIsGraphemeBreak); TemporaryBuffer.recycle(chars); } private int previousBoundary(@IntRange(from = 0) int offset) { if (offset <= 0) return DONE; do { --offset; } while (offset > 0 && !mIsGraphemeBreak[offset]); return offset; } private int nextBoundary(@IntRange(from = 0) int offset) { if (offset >= mIsGraphemeBreak.length) return DONE; do { ++offset; } while (offset < mIsGraphemeBreak.length && !mIsGraphemeBreak[offset]); return offset; } @Override public int previousStartBoundary(@IntRange(from = 0) int offset) { return previousBoundary(offset); } @Override public int previousEndBoundary(@IntRange(from = 0) int offset) { if (offset == 0) return DONE; int boundary = previousBoundary(offset); // Check that there is another cursor position before, otherwise this is not a valid // end boundary. if (boundary == DONE || previousBoundary(boundary) == DONE) { return DONE; } return boundary; } @Override public int nextStartBoundary(@IntRange(from = 0) int offset) { if (offset == mIsGraphemeBreak.length) return DONE; int boundary = nextBoundary(offset); // Check that there is another cursor position after, otherwise this is not a valid // start boundary. if (boundary == DONE || nextBoundary(boundary) == DONE) { return DONE; } return boundary; } @Override public int nextEndBoundary(@IntRange(from = 0) int offset) { return nextBoundary(offset); } }