Search code examples
androidanimationtextviewmarquee

Android TextView: is there a way to force the marquee animation with short text?


I have a TextView with a some text inside and I want it to animate with the scrolling marquee animation. I saw this popular question about forcing the marquee animation, however the code in the answers only work if the text is long enough to go outside the bounds of the TextView (and thus the text is truncated), I was looking for a solution to permanently make the text have this marquee animation on regardless of the width of the text; is this possible?


Solution

  • Taking @JodiMiddleton's suggestion about padding the text I constructed a few helper methods to pad the text to a target width based on a TextPaint object (ensuring correct sizing from fonts etc when measuring):

    /**
     * Pad a target string of text with spaces on the right to fill a target
     * width
     * 
     * @param text The target text
     * @param paint The TextPaint used to measure the target text and
     *            whitespaces
     * @param width The target width to fill
     * @return the original text with extra padding to fill the width
     */
    public static CharSequence padText(CharSequence text, TextPaint paint, int width) {
    
        // First measure the width of the text itself
        Rect textbounds = new Rect();
        paint.getTextBounds(text.toString(), 0, text.length(), textbounds);
    
        /**
         * check to see if it does indeed need padding to reach the target width
         */
        if (textbounds.width() > width) {
            return text;
        }
    
        /*
         * Measure the text of the space character (there's a bug with the
         * 'getTextBounds() method of Paint that trims the white space, thus
         * making it impossible to measure the width of a space without
         * surrounding it in arbitrary characters)
         */
        String workaroundString = "a a";
        Rect spacebounds = new Rect();
        paint.getTextBounds(workaroundString, 0, workaroundString.length(), spacebounds);
    
        Rect abounds = new Rect();
        paint.getTextBounds(new char[] {
            'a'
        }, 0, 1, abounds);
    
        float spaceWidth = spacebounds.width() - (abounds.width() * 2);
    
        /*
         * measure the amount of spaces needed based on the target width to fill
         * (using Math.ceil to ensure the maximum whole number of spaces)
         */
        int amountOfSpacesNeeded = (int)Math.ceil((width - textbounds.width()) / spaceWidth);
    
        // pad with spaces til the width is less than the text width
        return amountOfSpacesNeeded > 0 ? padRight(text.toString(), text.toString().length()
                + amountOfSpacesNeeded) : text;
    }
    
    /**
     * Pads a string with white space on the right of the original string
     * 
     * @param s The target string
     * @param n The new target length of the string
     * @return The target string padded with whitespace on the right to its new
     *         length
     */
    public static String padRight(String s, int n) {
        return String.format("%1$-" + n + "s", s);
    }
    

    So when you use the methods based on a TextView you would call:

    textView.setText(padText(myTargetString, textView.getPaint(), textView.getWidth()));
    

    It's not elegant and I'm almost certain there's improvements that could be made (not to mention a better way of doing it) but nonetheless I'm using it in my code and it appears to be doing the trick :)

    Updated answer

    The measureText allows for a more accurate measurement (using the above I found the space character was slightly shorter than measured, enough to not marquee).

    Adjusting the answer above would condense down to:

    public static CharSequence padText(CharSequence text, TextPaint paint, int width) {
    
            float widthOfText = paint.measureText(text.toString());
    
            if (widthOfText > width) {
                return text;
            }
    
            // Calculate how many spaces are required
            float widthOfSpace = paint.measureText(" ");
    
            int amountOfSpacesNeeded = (int) Math.ceil((width - widthOfText) / widthOfSpace);
    
            return amountOfSpacesNeeded > 0 ? padRight(text.toString(), text.toString().length()
                    + amountOfSpacesNeeded) : text;
        }