SHOW:
|
|
- or go back to the newest paste.
1 | import java.util.ArrayList; | |
2 | import java.util.List; | |
3 | ||
4 | import android.content.Context; | |
5 | import android.graphics.Paint; | |
6 | import android.util.AttributeSet; | |
7 | import android.util.Log; | |
8 | import android.util.TypedValue; | |
9 | import android.view.ViewGroup.LayoutParams; | |
10 | - | import android.widget.TextView; |
10 | + | |
11 | import android.widget.TextView; | |
12 | ||
13 | /** | |
14 | * This class builds a new android Widget named AutoFitText which can be used instead of a TextView | |
15 | * to have the text font size in it automatically fit to match the screen width. Credits go largely | |
16 | * to Dunni, gjpc, gregm and speedplane from Stackoverflow, method has been (style-) optimized and | |
17 | * rewritten to match android coding standards and our MBC. This version upgrades the original | |
18 | * "AutoFitTextView" to now also be adaptable to height and to accept the different TextView types | |
19 | * (Button, TextClock etc.) | |
20 | * | |
21 | * @author pheuschk | |
22 | * @createDate: 18.04.2013 | |
23 | */ | |
24 | @SuppressWarnings("unused") | |
25 | public class AutoFitText extends TextView { | |
26 | ||
27 | /** Global min and max for text size. Remember: values are in pixels! */ | |
28 | private final int MIN_TEXT_SIZE = 10; | |
29 | private final int MAX_TEXT_SIZE = 400; | |
30 | ||
31 | /** Flag for singleLine */ | |
32 | private boolean mSingleLine = false; | |
33 | ||
34 | /** | |
35 | * A dummy {@link TextView} to test the text size without actually showing anything to the user | |
36 | */ | |
37 | private TextView mTestView; | |
38 | ||
39 | /** | |
40 | * A dummy {@link Paint} to test the text size without actually showing anything to the user | |
41 | */ | |
42 | private Paint mTestPaint; | |
43 | ||
44 | /** | |
45 | * Scaling factor for fonts. It's a method of calculating independently (!) from the actual | |
46 | * density of the screen that is used so users have the same experience on different devices. We | |
47 | * will use DisplayMetrics in the Constructor to get the value of the factor and then calculate | |
48 | * SP from pixel values | |
49 | */ | |
50 | private final float mScaledDensityFactor; | |
51 | ||
52 | /** | |
53 | * Defines how close we want to be to the factual size of the Text-field. Lower values mean | |
54 | * higher precision but also exponentially higher computing cost (more loop runs) | |
55 | */ | |
56 | private final float mThreshold = 0.5f; | |
57 | ||
58 | /** | |
59 | * Constructor for call without attributes --> invoke constructor with AttributeSet null | |
60 | * | |
61 | * @param context | |
62 | */ | |
63 | public AutoFitText(Context context) { | |
64 | this(context, null); | |
65 | } | |
66 | ||
67 | public AutoFitText(Context context, AttributeSet attrs) { | |
68 | super(context, attrs); | |
69 | ||
70 | mScaledDensityFactor = context.getResources().getDisplayMetrics().scaledDensity; | |
71 | mTestView = new TextView(context); | |
72 | ||
73 | mTestPaint = new Paint(); | |
74 | mTestPaint.set(this.getPaint()); | |
75 | ||
76 | this.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { | |
77 | ||
78 | @Override | |
79 | public void onGlobalLayout() { | |
80 | // make an initial call to onSizeChanged to make sure that refitText is triggered | |
81 | onSizeChanged(AutoFitText.this.getWidth(), AutoFitText.this.getHeight(), 0, 0); | |
82 | // Remove the LayoutListener immediately so we don't run into an infinite loop | |
83 | AutoFitText.this.getViewTreeObserver().removeOnGlobalLayoutListener(this); | |
84 | } | |
85 | }); | |
86 | } | |
87 | ||
88 | /** | |
89 | * Main method of this widget. Resizes the font so the specified text fits in the text box | |
90 | * assuming the text box has the specified width. This is done via a dummy text view that is | |
91 | * refit until it matches the real target width and height up to a certain threshold factor | |
92 | * | |
93 | * @param targetFieldWidth | |
94 | * The width that the TextView currently has and wants filled | |
95 | * @param targetFieldHeight | |
96 | * The width that the TextView currently has and wants filled | |
97 | */ | |
98 | private void refitText(String text, int targetFieldWidth, int targetFieldHeight) { | |
99 | ||
100 | // Variables need to be visible outside the loops for later use. Remember size is in pixels | |
101 | float lowerTextSize = MIN_TEXT_SIZE; | |
102 | float upperTextSize = MAX_TEXT_SIZE; | |
103 | ||
104 | // Force the text to wrap. In principle this is not necessary since the dummy TextView | |
105 | // already does this for us but in rare cases adding this line can prevent flickering | |
106 | this.setMaxWidth(targetFieldWidth); | |
107 | ||
108 | // Padding should not be an issue since we never define it programmatically in this app | |
109 | // but just to to be sure we cut it off here | |
110 | targetFieldWidth = targetFieldWidth - this.getPaddingLeft() - this.getPaddingRight(); | |
111 | targetFieldHeight = targetFieldHeight - this.getPaddingTop() - this.getPaddingBottom(); | |
112 | ||
113 | // Initialize the dummy with some params (that are largely ignored anyway, but this is | |
114 | // mandatory to not get a NullPointerException) | |
115 | mTestView.setLayoutParams(new LayoutParams(targetFieldWidth, targetFieldHeight)); | |
116 | ||
117 | // maxWidth is crucial! Otherwise the text would never line wrap but blow up the width | |
118 | mTestView.setMaxWidth(targetFieldWidth); | |
119 | ||
120 | if (mSingleLine) { | |
121 | // the user requested a single line. This is very easy to do since we primarily need to | |
122 | // respect the width, don't have to break, don't have to measure... | |
123 | ||
124 | /*************************** Converging algorithm 1 ***********************************/ | |
125 | for (float testSize; (upperTextSize - lowerTextSize) > mThreshold;) { | |
126 | ||
127 | // Go to the mean value... | |
128 | testSize = (upperTextSize + lowerTextSize) / 2; | |
129 | ||
130 | mTestView.setTextSize(TypedValue.COMPLEX_UNIT_SP, testSize / mScaledDensityFactor); | |
131 | mTestView.setText(text); | |
132 | mTestView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); | |
133 | ||
134 | if (mTestView.getMeasuredWidth() >= targetFieldWidth) { | |
135 | upperTextSize = testSize; // Font is too big, decrease upperSize | |
136 | } | |
137 | else { | |
138 | lowerTextSize = testSize; // Font is too small, increase lowerSize | |
139 | } | |
140 | } | |
141 | /**************************************************************************************/ | |
142 | ||
143 | // In rare cases with very little letters and width > height we have vertical overlap! | |
144 | mTestView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); | |
145 | ||
146 | if (mTestView.getMeasuredHeight() > targetFieldHeight) { | |
147 | upperTextSize = lowerTextSize; | |
148 | lowerTextSize = MIN_TEXT_SIZE; | |
149 | ||
150 | /*************************** Converging algorithm 1.5 *****************************/ | |
151 | for (float testSize; (upperTextSize - lowerTextSize) > mThreshold;) { | |
152 | ||
153 | // Go to the mean value... | |
154 | testSize = (upperTextSize + lowerTextSize) / 2; | |
155 | ||
156 | mTestView.setTextSize(TypedValue.COMPLEX_UNIT_SP, testSize | |
157 | / mScaledDensityFactor); | |
158 | mTestView.setText(text); | |
159 | mTestView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); | |
160 | ||
161 | if (mTestView.getMeasuredHeight() >= targetFieldHeight) { | |
162 | upperTextSize = testSize; // Font is too big, decrease upperSize | |
163 | } | |
164 | else { | |
165 | lowerTextSize = testSize; // Font is too small, increase lowerSize | |
166 | } | |
167 | } | |
168 | /**********************************************************************************/ | |
169 | } | |
170 | } | |
171 | else { | |
172 | ||
173 | /*********************** Converging algorithm 2 ***************************************/ | |
174 | // Upper and lower size converge over time. As soon as they're close enough the loop | |
175 | // stops | |
176 | // TODO probe the algorithm for cost (ATM possibly O(n^2)) and optimize if possible | |
177 | for (float testSize; (upperTextSize - lowerTextSize) > mThreshold;) { | |
178 | ||
179 | // Go to the mean value... | |
180 | testSize = (upperTextSize + lowerTextSize) / 2; | |
181 | ||
182 | // ... inflate the dummy TextView by setting a scaled textSize and the text... | |
183 | mTestView.setTextSize(TypedValue.COMPLEX_UNIT_SP, testSize / mScaledDensityFactor); | |
184 | mTestView.setText(text); | |
185 | ||
186 | // ... call measure to find the current values that the text WANTS to occupy | |
187 | mTestView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); | |
188 | int tempHeight = mTestView.getMeasuredHeight(); | |
189 | // int tempWidth = mTestView.getMeasuredWidth(); | |
190 | ||
191 | // LOG.debug("Measured: " + tempWidth + "x" + tempHeight); | |
192 | // LOG.debug("TextSize: " + testSize / mScaledDensityFactor); | |
193 | ||
194 | // ... decide whether those values are appropriate. | |
195 | if (tempHeight >= targetFieldHeight) { | |
196 | upperTextSize = testSize; // Font is too big, decrease upperSize | |
197 | } | |
198 | else { | |
199 | lowerTextSize = testSize; // Font is too small, increase lowerSize | |
200 | } | |
201 | } | |
202 | /**************************************************************************************/ | |
203 | ||
204 | // It is possible that a single word is wider than the box. The Android system would | |
205 | // wrap this for us. But if you want to decide fo yourself where exactly to break or to | |
206 | // add a hyphen or something than you're going to want to implement something like this: | |
207 | mTestPaint.setTextSize(lowerTextSize); | |
208 | List<String> words = new ArrayList<String>(); | |
209 | ||
210 | for (String s : text.split(" ")) { | |
211 | Log.i("tag", "Word: " + s); | |
212 | words.add(s); | |
213 | } | |
214 | for (String word : words) { | |
215 | if (mTestPaint.measureText(word) >= targetFieldWidth) { | |
216 | List<String> pieces = new ArrayList<String>(); | |
217 | // pieces = breakWord(word, mTestPaint.measureText(word), targetFieldWidth); | |
218 | ||
219 | // Add code to handle the pieces here... | |
220 | } | |
221 | } | |
222 | } | |
223 | ||
224 | /** | |
225 | * We are now at most the value of threshold away from the actual size. To rather undershoot | |
226 | * than overshoot use the lower value. To match different screens convert to SP first. See | |
227 | * {@link http://developer.android.com/guide/topics/resources/more-resources.html#Dimension} | |
228 | * for more details | |
229 | */ | |
230 | this.setTextSize(TypedValue.COMPLEX_UNIT_SP, lowerTextSize / mScaledDensityFactor); | |
231 | return; | |
232 | } | |
233 | ||
234 | /** | |
235 | * This method receives a call upon a change in text content of the TextView. Unfortunately it | |
236 | * is also called - among others - upon text size change which means that we MUST NEVER CALL | |
237 | * {@link #refitText(String)} from this method! Doing so would result in an endless loop that | |
238 | * would ultimately result in a stack overflow and termination of the application | |
239 | * | |
240 | * So for the time being this method does absolutely nothing. If you want to notify the view of | |
241 | * a changed text call {@link #setText(CharSequence)} | |
242 | */ | |
243 | @Override | |
244 | protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { | |
245 | // Super implementation is also intentionally empty so for now we do absolutely nothing here | |
246 | super.onTextChanged(text, start, lengthBefore, lengthAfter); | |
247 | } | |
248 | ||
249 | @Override | |
250 | protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { | |
251 | if (width != oldWidth && height != oldHeight) { | |
252 | refitText(this.getText().toString(), width, height); | |
253 | } | |
254 | } | |
255 | ||
256 | /** | |
257 | * This method is guaranteed to be called by {@link TextView#setText(CharSequence)} immediately. | |
258 | * Therefore we can safely add our modifications here and then have the parent class resume its | |
259 | * work. So if text has changed you should always call {@link TextView#setText(CharSequence)} or | |
260 | * {@link TextView#setText(CharSequence, BufferType)} if you know whether the {@link BufferType} | |
261 | * is normal, editable or spannable. Note: the method will default to {@link BufferType#NORMAL} | |
262 | * if you don't pass an argument. | |
263 | */ | |
264 | @Override | |
265 | public void setText(CharSequence text, BufferType type) { | |
266 | ||
267 | int targetFieldWidth = this.getWidth(); | |
268 | int targetFieldHeight = this.getHeight(); | |
269 | ||
270 | if (targetFieldWidth <= 0 || targetFieldHeight <= 0 || text.equals("")) { | |
271 | // Log.v("tag", "Some values are empty, AutoFitText was not able to construct properly"); | |
272 | } | |
273 | else { | |
274 | refitText(text.toString(), targetFieldWidth, targetFieldHeight); | |
275 | } | |
276 | super.setText(text, type); | |
277 | } | |
278 | ||
279 | /** | |
280 | * TODO add sensibility for {@link #setMaxLines(int)} invocations | |
281 | */ | |
282 | @Override | |
283 | public void setMaxLines(int maxLines) { | |
284 | // TODO Implement support for this. This could be relatively easy. The idea would probably | |
285 | // be to manipulate the targetHeight in the refitText-method and then have the algorithm do | |
286 | // its job business as usual. Nonetheless, remember the height will have to be lowered | |
287 | // dynamically as the font size shrinks so it won't be a walk in the park still | |
288 | if (maxLines == 1) { | |
289 | this.setSingleLine(true); | |
290 | } | |
291 | else { | |
292 | throw new UnsupportedOperationException( | |
293 | "MaxLines != 1 are not implemented in AutoFitText yet, use TextView instead"); | |
294 | } | |
295 | } | |
296 | ||
297 | @Override | |
298 | public void setSingleLine(boolean singleLine) { | |
299 | // save the requested value in an instance variable to be able to decide later | |
300 | mSingleLine = singleLine; | |
301 | super.setSingleLine(singleLine); | |
302 | } | |
303 | } |