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