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 static java.lang.annotation.RetentionPolicy.SOURCE;
20 
21 import android.annotation.IntDef;
22 import android.annotation.IntRange;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.compat.annotation.UnsupportedAppUsage;
26 import android.graphics.Canvas;
27 import android.graphics.Paint;
28 import android.graphics.Rect;
29 import android.graphics.drawable.Drawable;
30 
31 import java.lang.annotation.Retention;
32 import java.lang.ref.WeakReference;
33 
34 /**
35  * Span that replaces the text it's attached to with a {@link Drawable} that can be aligned with
36  * the bottom or with the baseline of the surrounding text.
37  * <p>
38  * For an implementation that constructs the drawable from various sources (<code>Bitmap</code>,
39  * <code>Drawable</code>, resource id or <code>Uri</code>) use {@link ImageSpan}.
40  * <p>
41  * A simple implementation of <code>DynamicDrawableSpan</code> that uses drawables from resources
42  * looks like this:
43  * <pre>
44  * class MyDynamicDrawableSpan extends DynamicDrawableSpan {
45  *
46  * private final Context mContext;
47  * private final int mResourceId;
48  *
49  * public MyDynamicDrawableSpan(Context context, @DrawableRes int resourceId) {
50  *     mContext = context;
51  *     mResourceId = resourceId;
52  * }
53  *
54  * {@literal @}Override
55  * public Drawable getDrawable() {
56  *      Drawable drawable = mContext.getDrawable(mResourceId);
57  *      drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
58  *      return drawable;
59  * }
60  * }</pre>
61  * The class can be used like this:
62  * <pre>
63  * SpannableString string = new SpannableString("Text with a drawable span");
64  * string.setSpan(new MyDynamicDrawableSpan(context, R.mipmap.ic_launcher), 12, 20, Spanned
65  * .SPAN_EXCLUSIVE_EXCLUSIVE);</pre>
66  * <img src="{@docRoot}reference/android/images/text/style/dynamicdrawablespan.png" />
67  * <figcaption>Replacing text with a drawable.</figcaption>
68  */
69 public abstract class DynamicDrawableSpan extends ReplacementSpan {
70 
71     /**
72      * A constant indicating that the bottom of this span should be aligned
73      * with the bottom of the surrounding text, i.e., at the same level as the
74      * lowest descender in the text.
75      */
76     public static final int ALIGN_BOTTOM = 0;
77 
78     /**
79      * A constant indicating that the bottom of this span should be aligned
80      * with the baseline of the surrounding text.
81      */
82     public static final int ALIGN_BASELINE = 1;
83 
84     /**
85      * A constant indicating that this span should be vertically centered between
86      * the top and the lowest descender.
87      */
88     public static final int ALIGN_CENTER = 2;
89 
90     /**
91      * Defines acceptable alignment types.
92      * @hide
93      */
94     @Retention(SOURCE)
95     @IntDef(prefix = { "ALIGN_" }, value = {
96             ALIGN_BOTTOM,
97             ALIGN_BASELINE,
98             ALIGN_CENTER
99     })
100     public @interface AlignmentType {}
101 
102     protected final int mVerticalAlignment;
103 
104     @UnsupportedAppUsage
105     private WeakReference<Drawable> mDrawableRef;
106 
107     /**
108      * Creates a {@link DynamicDrawableSpan}. The default vertical alignment is
109      * {@link #ALIGN_BOTTOM}
110      */
DynamicDrawableSpan()111     public DynamicDrawableSpan() {
112         mVerticalAlignment = ALIGN_BOTTOM;
113     }
114 
115     /**
116      * Creates a {@link DynamicDrawableSpan} based on a vertical alignment.\
117      *
118      * @param verticalAlignment one of {@link #ALIGN_BOTTOM}, {@link #ALIGN_BASELINE} or
119      *                          {@link #ALIGN_CENTER}
120      */
DynamicDrawableSpan(@lignmentType int verticalAlignment)121     protected DynamicDrawableSpan(@AlignmentType int verticalAlignment) {
122         mVerticalAlignment = verticalAlignment;
123     }
124 
125     /**
126      * Returns the vertical alignment of this span, one of {@link #ALIGN_BOTTOM},
127      * {@link #ALIGN_BASELINE} or {@link #ALIGN_CENTER}.
128      */
getVerticalAlignment()129     public @AlignmentType int getVerticalAlignment() {
130         return mVerticalAlignment;
131     }
132 
133     /**
134      * Your subclass must implement this method to provide the bitmap
135      * to be drawn.  The dimensions of the bitmap must be the same
136      * from each call to the next.
137      */
getDrawable()138     public abstract Drawable getDrawable();
139 
140     @Override
getSize(@onNull Paint paint, CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @Nullable Paint.FontMetricsInt fm)141     public int getSize(@NonNull Paint paint, CharSequence text,
142             @IntRange(from = 0) int start, @IntRange(from = 0) int end,
143             @Nullable Paint.FontMetricsInt fm) {
144         Drawable d = getCachedDrawable();
145         Rect rect = d.getBounds();
146 
147         if (fm != null) {
148             fm.ascent = -rect.bottom;
149             fm.descent = 0;
150 
151             fm.top = fm.ascent;
152             fm.bottom = 0;
153         }
154 
155         return rect.right;
156     }
157 
158     @Override
draw(@onNull Canvas canvas, CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, float x, int top, int y, int bottom, @NonNull Paint paint)159     public void draw(@NonNull Canvas canvas, CharSequence text,
160             @IntRange(from = 0) int start, @IntRange(from = 0) int end, float x,
161             int top, int y, int bottom, @NonNull Paint paint) {
162         Drawable b = getCachedDrawable();
163         canvas.save();
164 
165         int transY = bottom - b.getBounds().bottom;
166         if (mVerticalAlignment == ALIGN_BASELINE) {
167             transY -= paint.getFontMetricsInt().descent;
168         } else if (mVerticalAlignment == ALIGN_CENTER) {
169             transY = (bottom - top) / 2 - b.getBounds().height() / 2;
170         }
171 
172         canvas.translate(x, transY);
173         b.draw(canvas);
174         canvas.restore();
175     }
176 
getCachedDrawable()177     private Drawable getCachedDrawable() {
178         WeakReference<Drawable> wr = mDrawableRef;
179         Drawable d = null;
180 
181         if (wr != null) {
182             d = wr.get();
183         }
184 
185         if (d == null) {
186             d = getDrawable();
187             mDrawableRef = new WeakReference<Drawable>(d);
188         }
189 
190         return d;
191     }
192 }
193 
194