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