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