Advertisement
Guest User

AutoFitText

a guest
Apr 23rd, 2013
2,660
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  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.view.ViewTreeObserver.OnGlobalLayoutListener;
  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. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement