Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to fade out the end of the last line in a TextView?

How can we achieve the fade-out effect on the last line of a TextView, like in the "WHAT'S NEW" section in the Play Store app?

Screenshot of Play Store app

like image 885
Vijay Makwana Avatar asked Mar 15 '18 09:03

Vijay Makwana


1 Answers

That fade effect can be accomplished by subclassing a TextView class to intercept its draw, and doing something like what the View class does to fade out edges, but only in the last stretch of the final text line.

In this example, we create a unit horizontal linear gradient that goes from transparent to solid black. As we prepare to draw, this unit gradient is scaled to a length calculated as a simple fraction of the TextView's final line length, and then positioned accordingly.

An off-screen buffer is created, and we let the TextView draw its content to that. We then draw the fade gradient over it with a transfer mode of PorterDuff.Mode.DST_OUT, which essentially clears the underlying content to a degree relative to the gradient's opacity at a given point. Drawing that buffer back on-screen results in the desired fade, no matter what is in the background.

public class FadingTextView extends AppCompatTextView {

    private static final float FADE_LENGTH_FACTOR = .4f;

    private final RectF drawRect = new RectF();
    private final Rect realRect = new Rect();
    private final Path selection = new Path();
    private final Matrix matrix = new Matrix();
    private final Paint paint = new Paint();
    private final Shader shader =
            new LinearGradient(0f, 0f, 1f, 0f, 0x00000000, 0xFF000000, Shader.TileMode.CLAMP);

    public FadingTextView(Context context) {
        this(context, null);
    }

    public FadingTextView(Context context, AttributeSet attrs) {
        this(context, attrs, android.R.attr.textViewStyle);
    }

    public FadingTextView(Context context, AttributeSet attrs, int defStyleAttribute) {
        super(context, attrs, defStyleAttribute);

        paint.setShader(shader);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // Locals
        final RectF drawBounds = drawRect;
        final Rect realBounds = realRect;
        final Path selectionPath = selection;
        final Layout layout = getLayout();

        // Figure last line index, and text offsets there
        final int lastLineIndex = getLineCount() - 1;
        final int lastLineStart = layout.getLineStart(lastLineIndex);
        final int lastLineEnd = layout.getLineEnd(lastLineIndex);

        // Let the Layout figure a Path that'd cover the last line text
        layout.getSelectionPath(lastLineStart, lastLineEnd, selectionPath);
        // Convert that Path to a RectF, which we can more easily modify
        selectionPath.computeBounds(drawBounds, false);

        // Naive text direction determination; may need refinement
        boolean isRtl =
                layout.getParagraphDirection(lastLineIndex) == Layout.DIR_RIGHT_TO_LEFT;

        // Narrow the bounds to just the fade length
        if (isRtl) {
            drawBounds.right = drawBounds.left + drawBounds.width() * FADE_LENGTH_FACTOR;
        } else {
            drawBounds.left = drawBounds.right - drawBounds.width() * FADE_LENGTH_FACTOR;
        }
        // Adjust for drawables and paddings
        drawBounds.offset(getTotalPaddingLeft(), getTotalPaddingTop());

        // Convert drawing bounds to real bounds to determine
        // if we need to do the fade, or a regular draw
        drawBounds.round(realBounds);
        realBounds.offset(-getScrollX(), -getScrollY());
        boolean needToFade = realBounds.intersects(getTotalPaddingLeft(), getTotalPaddingTop(),
                getWidth() - getTotalPaddingRight(), getHeight() - getTotalPaddingBottom());

        if (needToFade) {
            // Adjust and set the Shader Matrix
            final Matrix shaderMatrix = matrix;
            shaderMatrix.reset();
            shaderMatrix.setScale(drawBounds.width(), 1f);
            if (isRtl) {
                shaderMatrix.postRotate(180f, drawBounds.width() / 2f, 0f);
            }
            shaderMatrix.postTranslate(drawBounds.left, drawBounds.top);
            shader.setLocalMatrix(shaderMatrix);

            // Save, and start drawing to an off-screen buffer
            final int saveCount;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                saveCount = canvas.saveLayer(null, null);
            } else {
                saveCount = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);
            }

            // Let TextView draw itself to the buffer
            super.onDraw(canvas);

            // Draw the fade to the buffer, over the TextView content
            canvas.drawRect(drawBounds, paint);

            // Restore, and draw the buffer back to the Canvas
            canvas.restoreToCount(saveCount);
        } else {
            // Regular draw
            super.onDraw(canvas);
        }
    }
}

This is a drop-in replacement for TextView, and you'd use it in your layout similarly.

<com.example.app.FadingTextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#e2f3eb"
    android:textColor="#0b8043"
    android:lineSpacingMultiplier="1.2"
    android:text="@string/umang" />

screenshot


Notes:

  • The fade length calculation is based on a constant fraction of the final line's text length, here determined by FADE_LENGTH_FACTOR. This seems to be the same basic methodology of the Play Store component, as the absolute length of the fade appears to vary with line length. The FADE_LENGTH_FACTOR value can be altered as desired.

  • FadingTextView currently extends AppCompatTextView, but it works perfectly well as a plain TextView, if you should need that instead. I would think that it will work as a MaterialTextView too, though I've not tested that thoroughly.

  • This example is geared mainly toward relatively plain use; i.e., as a simple wrapped, static label. Though I've attempted to account for and test every TextView setting I could think of that might affect this – e.g., compound drawables, paddings, selectable text, scrolling, text direction and alignment, etc. – I can't guarantee that I've thought of everything.

like image 164
Mike M. Avatar answered Oct 22 '22 11:10

Mike M.