如何淡出 TextView 中最后一行的结尾?

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

我们如何才能在 TextView 的最后一行实现淡出效果,就像在 Play 商店应用的 "WHAT'S NEW" 部分中一样?

淡入淡出效果可以通过子classing a TextView class拦截它的绘制,然后做类似View [=56=的事情来实现] 确实会淡出边缘,但仅限于最终文本行的最后一段。

在这个例子中,我们创建了一个从透明到纯黑色的单位水平线性渐变。当我们准备绘制时,这个单位梯度被缩放到一个长度,计算为 TextView 最终线长度的一个简单分数,然后相应地定位。

创建了一个 off-screen 缓冲区,我们让 TextView 将其内容绘制到该缓冲区。然后,我们使用 PorterDuff.Mode.DST_OUT 的传输模式在其上绘制淡入淡出渐变,这实质上将底层内容清除到相对于给定点处渐变不透明度的一定程度。将缓冲区拉回 on-screen 会导致所需的淡入淡出,无论背景是什么。

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);
        }
    }
}

这是 TextView 的 drop-in 替代品,您可以类似地在布局中使用它。

<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" />


备注:

  • 淡入淡出长度的计算基于最后一行文本长度的常数分数,此处由 FADE_LENGTH_FACTOR 确定。这似乎与 Play Store 组件的基本方法相同,因为淡入淡出的绝对长度似乎随行长度而变化。 FADE_LENGTH_FACTOR 值可以根据需要更改。

  • FadingTextView 目前扩展了 AppCompatTextView,但如果您需要的话,它可以作为普通的 TextView 使用。我认为它也可以用作 MaterialTextView,尽管我还没有彻底测试过。

  • 本例主要针对比较普通的使用;即,作为一个简单的包装静态标签。虽然我已经尝试考虑并测试我能想到的每个 TextView 设置可能会影响到这一点——例如,复合可绘制对象、填充、可选文本、滚动、文本方向和对齐方式等——我不能保证我已经想到了一切。