Guest User

AutoFitText

a guest
Apr 23rd, 2013
160
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Java 13.78 KB | None | 0 0
  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>();
  208.  
  209.             for (String s : text.split(" ")) {
  210.                 Log.i("tag", "Word: " + s);
  211.                 words.add(s);
  212.             }            
  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. }
Advertisement
Add Comment
Please, Sign In to add comment