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 | } |