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.graphics.Paint; 20 import android.util.Pair; 21 22 import androidx.annotation.IntRange; 23 import androidx.annotation.NonNull; 24 25 import java.util.ArrayList; 26 import java.util.Arrays; 27 import java.util.List; 28 import java.util.Objects; 29 30 /** 31 * A class that represents of the highlight of the text. 32 */ 33 @android.ravenwood.annotation.RavenwoodKeepWholeClass 34 public class Highlights { 35 private final List<Pair<Paint, int[]>> mHighlights; 36 Highlights(List<Pair<Paint, int[]>> highlights)37 private Highlights(List<Pair<Paint, int[]>> highlights) { 38 mHighlights = highlights; 39 } 40 41 /** 42 * Returns the number of highlight. 43 * 44 * @return the number of highlight. 45 * 46 * @see Builder#addRange(Paint, int, int) 47 * @see Builder#addRanges(Paint, int...) 48 */ getSize()49 public @IntRange(from = 0) int getSize() { 50 return mHighlights.size(); 51 } 52 53 /** 54 * Returns a paint used for the i-th highlight. 55 * 56 * @param index an index of the highlight. Must be between 0 and {@link #getSize()} 57 * @return the paint object 58 * 59 * @see Builder#addRange(Paint, int, int) 60 * @see Builder#addRanges(Paint, int...) 61 */ getPaint(@ntRangefrom = 0) int index)62 public @NonNull Paint getPaint(@IntRange(from = 0) int index) { 63 return mHighlights.get(index).first; 64 } 65 66 /** 67 * Returns ranges of the i-th highlight. 68 * 69 * Ranges are represented of flattened inclusive start and exclusive end integers array. The 70 * inclusive start offset of the {@code i}-th range is stored in {@code 2 * i}-th of the array. 71 * The exclusive end offset of the {@code i}-th range is stored in {@code 2* i + 1}-th of the 72 * array. For example, the two ranges: (1, 2) and (3, 4) are flattened into single int array 73 * [1, 2, 3, 4]. 74 * 75 * @param index an index of the highlight. Must be between 0 and {@link #getSize()} 76 * @return the flattened ranges. 77 * 78 * @see Builder#addRange(Paint, int, int) 79 * @see Builder#addRanges(Paint, int...) 80 */ getRanges(int index)81 public @NonNull int[] getRanges(int index) { 82 return mHighlights.get(index).second; 83 } 84 85 /** 86 * A builder for the Highlights. 87 */ 88 public static final class Builder { 89 private final List<Pair<Paint, int[]>> mHighlights = new ArrayList<>(); 90 91 /** 92 * Add single range highlight. 93 * 94 * The {@link android.widget.TextView} and underlying {@link Layout} draws highlight in the 95 * order of the {@link #addRange} calls. 96 * 97 * For example, the following code draws (1, 2) with red and (2, 5) with blue. 98 * <code> 99 * val redPaint = Paint().apply { color = Color.RED } 100 * val bluePaint = Paint().apply { color = Color.BLUE } 101 * val highlight = Highlights.Builder() 102 * .addRange(redPaint, 1, 4) 103 * .addRange(bluePaint, 2, 5) 104 * .build() 105 * </code> 106 * 107 * 108 * @param paint a paint object used for drawing highlight path. 109 * @param start an inclusive offset of the text. 110 * @param end an exclusive offset of the text. 111 * @return this builder instance. 112 */ addRange(@onNull Paint paint, @IntRange(from = 0) int start, @IntRange(from = 0) int end)113 public @NonNull Builder addRange(@NonNull Paint paint, @IntRange(from = 0) int start, 114 @IntRange(from = 0) int end) { 115 if (start > end) { 116 throw new IllegalArgumentException("start must not be larger than end: " 117 + start + ", " + end); 118 } 119 Objects.requireNonNull(paint); 120 121 int[] range = new int[] {start, end}; 122 mHighlights.add(new Pair<>(paint, range)); 123 return this; 124 } 125 126 /** 127 * Add multiple ranges highlight. 128 * 129 * For example, the following code draws (1, 2) with red and (2, 5) with blue. 130 * <code> 131 * val redPaint = Paint().apply { color = Color.RED } 132 * val bluePaint = Paint().apply { color = Color.BLUE } 133 * val highlight = Highlights.Builder() 134 * .addRange(redPaint, 1, 4) 135 * .addRange(bluePaint, 2, 5) 136 * .build() 137 * </code> 138 * 139 * @param paint a paint object used for drawing highlight path. 140 * @param ranges a flatten ranges. The {@code 2 * i}-th element is an inclusive start offset 141 * of the {@code i}-th character. The {@code 2 * i + 1}-th element is an 142 * exclusive end offset of the {@code i}-th character. 143 * @return this builder instance. 144 */ addRanges(@onNull Paint paint, @NonNull int... ranges)145 public @NonNull Builder addRanges(@NonNull Paint paint, @NonNull int... ranges) { 146 if (ranges.length % 2 == 1) { 147 throw new IllegalArgumentException( 148 "Flatten ranges must have even numbered elements"); 149 } 150 for (int j = 0; j < ranges.length / 2; ++j) { 151 int start = ranges[j * 2]; 152 int end = ranges[j * 2 + 1]; 153 if (start > end) { 154 throw new IllegalArgumentException( 155 "Reverse range found in the flatten range: " + Arrays.toString( 156 ranges)); 157 } 158 } 159 Objects.requireNonNull(paint); 160 mHighlights.add(new Pair<>(paint, ranges)); 161 return this; 162 } 163 164 /** 165 * Build a new Highlights instance. 166 * 167 * @return a new Highlights instance. 168 */ build()169 public @NonNull Highlights build() { 170 return new Highlights(mHighlights); 171 } 172 } 173 } 174