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 @android.ravenwood.annotation.RavenwoodKeepWholeClass 67 public class BulletSpan implements LeadingMarginSpan, ParcelableSpan { 68 // Bullet is slightly bigger to avoid aliasing artifacts on mdpi devices. 69 private static final int STANDARD_BULLET_RADIUS = 4; 70 public static final int STANDARD_GAP_WIDTH = 2; 71 private static final int STANDARD_COLOR = 0; 72 73 @Px 74 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 75 private final int mGapWidth; 76 @Px 77 private final int mBulletRadius; 78 @ColorInt 79 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 80 private final int mColor; 81 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 82 private final boolean mWantColor; 83 84 /** 85 * Creates a {@link BulletSpan} with the default values. 86 */ BulletSpan()87 public BulletSpan() { 88 this(STANDARD_GAP_WIDTH, STANDARD_COLOR, false, STANDARD_BULLET_RADIUS); 89 } 90 91 /** 92 * Creates a {@link BulletSpan} based on a gap width 93 * 94 * @param gapWidth the distance, in pixels, between the bullet point and the paragraph. 95 */ BulletSpan(int gapWidth)96 public BulletSpan(int gapWidth) { 97 this(gapWidth, STANDARD_COLOR, false, STANDARD_BULLET_RADIUS); 98 } 99 100 /** 101 * Creates a {@link BulletSpan} based on a gap width and a color integer. 102 * 103 * @param gapWidth the distance, in pixels, between the bullet point and the paragraph. 104 * @param color the bullet point color, as a color integer 105 * @see android.content.res.Resources#getColor(int, Resources.Theme) 106 */ BulletSpan(int gapWidth, @ColorInt int color)107 public BulletSpan(int gapWidth, @ColorInt int color) { 108 this(gapWidth, color, true, STANDARD_BULLET_RADIUS); 109 } 110 111 /** 112 * Creates a {@link BulletSpan} based on a gap width and a color integer. 113 * 114 * @param gapWidth the distance, in pixels, between the bullet point and the paragraph. 115 * @param color the bullet point color, as a color integer. 116 * @param bulletRadius the radius of the bullet point, in pixels. 117 * @see android.content.res.Resources#getColor(int, Resources.Theme) 118 */ BulletSpan(int gapWidth, @ColorInt int color, @IntRange(from = 0) int bulletRadius)119 public BulletSpan(int gapWidth, @ColorInt int color, @IntRange(from = 0) int bulletRadius) { 120 this(gapWidth, color, true, bulletRadius); 121 } 122 123 /** 124 * @hide 125 */ BulletSpan(int gapWidth, @ColorInt int color, boolean wantColor, @IntRange(from = 0) int bulletRadius)126 public BulletSpan(int gapWidth, @ColorInt int color, boolean wantColor, 127 @IntRange(from = 0) int bulletRadius) { 128 mGapWidth = gapWidth; 129 mBulletRadius = bulletRadius; 130 mColor = color; 131 mWantColor = wantColor; 132 } 133 134 /** 135 * Creates a {@link BulletSpan} from a parcel. 136 */ BulletSpan(@onNull Parcel src)137 public BulletSpan(@NonNull Parcel src) { 138 mGapWidth = src.readInt(); 139 mWantColor = src.readInt() != 0; 140 mColor = src.readInt(); 141 mBulletRadius = src.readInt(); 142 } 143 144 @Override getSpanTypeId()145 public int getSpanTypeId() { 146 return getSpanTypeIdInternal(); 147 } 148 149 /** @hide */ 150 @Override getSpanTypeIdInternal()151 public int getSpanTypeIdInternal() { 152 return TextUtils.BULLET_SPAN; 153 } 154 155 @Override describeContents()156 public int describeContents() { 157 return 0; 158 } 159 160 @Override writeToParcel(@onNull Parcel dest, int flags)161 public void writeToParcel(@NonNull Parcel dest, int flags) { 162 writeToParcelInternal(dest, flags); 163 } 164 165 /** @hide */ 166 @Override writeToParcelInternal(@onNull Parcel dest, int flags)167 public void writeToParcelInternal(@NonNull Parcel dest, int flags) { 168 dest.writeInt(mGapWidth); 169 dest.writeInt(mWantColor ? 1 : 0); 170 dest.writeInt(mColor); 171 dest.writeInt(mBulletRadius); 172 } 173 174 @Override getLeadingMargin(boolean first)175 public int getLeadingMargin(boolean first) { 176 return 2 * mBulletRadius + mGapWidth; 177 } 178 179 /** 180 * Get the distance, in pixels, between the bullet point and the paragraph. 181 * 182 * @return the distance, in pixels, between the bullet point and the paragraph. 183 */ getGapWidth()184 public int getGapWidth() { 185 return mGapWidth; 186 } 187 188 /** 189 * Get the radius, in pixels, of the bullet point. 190 * 191 * @return the radius, in pixels, of the bullet point. 192 */ getBulletRadius()193 public int getBulletRadius() { 194 return mBulletRadius; 195 } 196 197 /** 198 * Get the bullet point color. 199 * 200 * @return the bullet point color 201 */ getColor()202 public int getColor() { 203 return mColor; 204 } 205 206 /** 207 * @return true if the bullet should apply the color. 208 * @hide 209 */ getWantColor()210 public boolean getWantColor() { 211 return mWantColor; 212 } 213 214 @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)215 public void drawLeadingMargin(@NonNull Canvas canvas, @NonNull Paint paint, int x, int dir, 216 int top, int baseline, int bottom, 217 @NonNull CharSequence text, int start, int end, 218 boolean first, @Nullable Layout layout) { 219 if (((Spanned) text).getSpanStart(this) == start) { 220 Paint.Style style = paint.getStyle(); 221 int oldcolor = 0; 222 223 if (mWantColor) { 224 oldcolor = paint.getColor(); 225 paint.setColor(mColor); 226 } 227 228 paint.setStyle(Paint.Style.FILL); 229 230 if (layout != null) { 231 // "bottom" position might include extra space as a result of line spacing 232 // configuration. Subtract extra space in order to show bullet in the vertical 233 // center of characters. 234 final int line = layout.getLineForOffset(start); 235 bottom = bottom - layout.getLineExtra(line); 236 } 237 238 final float yPosition = (top + bottom) / 2f; 239 final float xPosition = x + dir * mBulletRadius; 240 241 canvas.drawCircle(xPosition, yPosition, mBulletRadius, paint); 242 243 if (mWantColor) { 244 paint.setColor(oldcolor); 245 } 246 247 paint.setStyle(style); 248 } 249 } 250 251 @Override toString()252 public String toString() { 253 return "BulletSpan{" 254 + "gapWidth=" + getGapWidth() 255 + ", bulletRadius=" + getBulletRadius() 256 + ", color=" + String.format("%08X", getColor()) 257 + '}'; 258 } 259 } 260