1 /*
2  * Copyright (C) 2006 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.style;
18 
19 import android.annotation.ColorInt;
20 import android.annotation.IntRange;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.annotation.Px;
24 import android.compat.annotation.UnsupportedAppUsage;
25 import android.graphics.Canvas;
26 import android.graphics.Paint;
27 import android.os.Build;
28 import android.os.Parcel;
29 import android.text.Layout;
30 import android.text.ParcelableSpan;
31 import android.text.Spanned;
32 import android.text.TextUtils;
33 
34 /**
35  * A span which styles paragraphs as bullet points (respecting layout direction).
36  * <p>
37  * BulletSpans must be attached from the first character to the last character of a single
38  * paragraph, otherwise the bullet point will not be displayed but the first paragraph encountered
39  * will have a leading margin.
40  * <p>
41  * BulletSpans allow configuring the following elements:
42  * <ul>
43  * <li><b>gap width</b> - the distance, in pixels, between the bullet point and the paragraph.
44  * Default value is 2px.</li>
45  * <li><b>color</b> - the bullet point color. By default, the bullet point color is 0 - no color,
46  * so it uses the TextView's text color.</li>
47  * <li><b>bullet radius</b> - the radius, in pixels, of the bullet point. Default value is
48  * 4px.</li>
49  * </ul>
50  * For example, a BulletSpan using the default values can be constructed like this:
51  * <pre>{@code
52  *  SpannableString string = new SpannableString("Text with\nBullet point");
53  *string.setSpan(new BulletSpan(), 10, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
54  * <img src="{@docRoot}reference/android/images/text/style/defaultbulletspan.png" />
55  * <figcaption>BulletSpan constructed with default values.</figcaption>
56  * <p>
57  * <p>
58  * To construct a BulletSpan with a gap width of 40px, green bullet point and bullet radius of
59  * 20px:
60  * <pre>{@code
61  *  SpannableString string = new SpannableString("Text with\nBullet point");
62  *string.setSpan(new BulletSpan(40, color, 20), 10, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
63  * <img src="{@docRoot}reference/android/images/text/style/custombulletspan.png" />
64  * <figcaption>Customized BulletSpan.</figcaption>
65  */
66 public class BulletSpan implements LeadingMarginSpan, ParcelableSpan {
67     // Bullet is slightly bigger to avoid aliasing artifacts on mdpi devices.
68     private static final int STANDARD_BULLET_RADIUS = 4;
69     public static final int STANDARD_GAP_WIDTH = 2;
70     private static final int STANDARD_COLOR = 0;
71 
72     @Px
73     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
74     private final int mGapWidth;
75     @Px
76     private final int mBulletRadius;
77     @ColorInt
78     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
79     private final int mColor;
80     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
81     private final boolean mWantColor;
82 
83     /**
84      * Creates a {@link BulletSpan} with the default values.
85      */
BulletSpan()86     public BulletSpan() {
87         this(STANDARD_GAP_WIDTH, STANDARD_COLOR, false, STANDARD_BULLET_RADIUS);
88     }
89 
90     /**
91      * Creates a {@link BulletSpan} based on a gap width
92      *
93      * @param gapWidth the distance, in pixels, between the bullet point and the paragraph.
94      */
BulletSpan(int gapWidth)95     public BulletSpan(int gapWidth) {
96         this(gapWidth, STANDARD_COLOR, false, STANDARD_BULLET_RADIUS);
97     }
98 
99     /**
100      * Creates a {@link BulletSpan} based on a gap width and a color integer.
101      *
102      * @param gapWidth the distance, in pixels, between the bullet point and the paragraph.
103      * @param color    the bullet point color, as a color integer
104      * @see android.content.res.Resources#getColor(int, Resources.Theme)
105      */
BulletSpan(int gapWidth, @ColorInt int color)106     public BulletSpan(int gapWidth, @ColorInt int color) {
107         this(gapWidth, color, true, STANDARD_BULLET_RADIUS);
108     }
109 
110     /**
111      * Creates a {@link BulletSpan} based on a gap width and a color integer.
112      *
113      * @param gapWidth     the distance, in pixels, between the bullet point and the paragraph.
114      * @param color        the bullet point color, as a color integer.
115      * @param bulletRadius the radius of the bullet point, in pixels.
116      * @see android.content.res.Resources#getColor(int, Resources.Theme)
117      */
BulletSpan(int gapWidth, @ColorInt int color, @IntRange(from = 0) int bulletRadius)118     public BulletSpan(int gapWidth, @ColorInt int color, @IntRange(from = 0) int bulletRadius) {
119         this(gapWidth, color, true, bulletRadius);
120     }
121 
BulletSpan(int gapWidth, @ColorInt int color, boolean wantColor, @IntRange(from = 0) int bulletRadius)122     private BulletSpan(int gapWidth, @ColorInt int color, boolean wantColor,
123             @IntRange(from = 0) int bulletRadius) {
124         mGapWidth = gapWidth;
125         mBulletRadius = bulletRadius;
126         mColor = color;
127         mWantColor = wantColor;
128     }
129 
130     /**
131      * Creates a {@link BulletSpan} from a parcel.
132      */
BulletSpan(@onNull Parcel src)133     public BulletSpan(@NonNull Parcel src) {
134         mGapWidth = src.readInt();
135         mWantColor = src.readInt() != 0;
136         mColor = src.readInt();
137         mBulletRadius = src.readInt();
138     }
139 
140     @Override
getSpanTypeId()141     public int getSpanTypeId() {
142         return getSpanTypeIdInternal();
143     }
144 
145     /** @hide */
146     @Override
getSpanTypeIdInternal()147     public int getSpanTypeIdInternal() {
148         return TextUtils.BULLET_SPAN;
149     }
150 
151     @Override
describeContents()152     public int describeContents() {
153         return 0;
154     }
155 
156     @Override
writeToParcel(@onNull Parcel dest, int flags)157     public void writeToParcel(@NonNull Parcel dest, int flags) {
158         writeToParcelInternal(dest, flags);
159     }
160 
161     /** @hide */
162     @Override
writeToParcelInternal(@onNull Parcel dest, int flags)163     public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
164         dest.writeInt(mGapWidth);
165         dest.writeInt(mWantColor ? 1 : 0);
166         dest.writeInt(mColor);
167         dest.writeInt(mBulletRadius);
168     }
169 
170     @Override
getLeadingMargin(boolean first)171     public int getLeadingMargin(boolean first) {
172         return 2 * mBulletRadius + mGapWidth;
173     }
174 
175     /**
176      * Get the distance, in pixels, between the bullet point and the paragraph.
177      *
178      * @return the distance, in pixels, between the bullet point and the paragraph.
179      */
getGapWidth()180     public int getGapWidth() {
181         return mGapWidth;
182     }
183 
184     /**
185      * Get the radius, in pixels, of the bullet point.
186      *
187      * @return the radius, in pixels, of the bullet point.
188      */
getBulletRadius()189     public int getBulletRadius() {
190         return mBulletRadius;
191     }
192 
193     /**
194      * Get the bullet point color.
195      *
196      * @return the bullet point color
197      */
getColor()198     public int getColor() {
199         return mColor;
200     }
201 
202     @Override
drawLeadingMargin(@onNull Canvas canvas, @NonNull Paint paint, int x, int dir, int top, int baseline, int bottom, @NonNull CharSequence text, int start, int end, boolean first, @Nullable Layout layout)203     public void drawLeadingMargin(@NonNull Canvas canvas, @NonNull Paint paint, int x, int dir,
204             int top, int baseline, int bottom,
205             @NonNull CharSequence text, int start, int end,
206             boolean first, @Nullable Layout layout) {
207         if (((Spanned) text).getSpanStart(this) == start) {
208             Paint.Style style = paint.getStyle();
209             int oldcolor = 0;
210 
211             if (mWantColor) {
212                 oldcolor = paint.getColor();
213                 paint.setColor(mColor);
214             }
215 
216             paint.setStyle(Paint.Style.FILL);
217 
218             if (layout != null) {
219                 // "bottom" position might include extra space as a result of line spacing
220                 // configuration. Subtract extra space in order to show bullet in the vertical
221                 // center of characters.
222                 final int line = layout.getLineForOffset(start);
223                 bottom = bottom - layout.getLineExtra(line);
224             }
225 
226             final float yPosition = (top + bottom) / 2f;
227             final float xPosition = x + dir * mBulletRadius;
228 
229             canvas.drawCircle(xPosition, yPosition, mBulletRadius, paint);
230 
231             if (mWantColor) {
232                 paint.setColor(oldcolor);
233             }
234 
235             paint.setStyle(style);
236         }
237     }
238 
239     @Override
toString()240     public String toString() {
241         return "BulletSpan{"
242                 + "gapWidth=" + getGapWidth()
243                 + ", bulletRadius=" + getBulletRadius()
244                 + ", color=" + String.format("%08X", getColor())
245                 + '}';
246     }
247 }
248