View difference between Paste ID: e6WyrwSN and KibS3Tt0
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
}