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