You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android带双裁剪TextView的ProgressBar实现问题求助

解决带裁剪TextView的ProgressBar动画闪烁与显示异常问题

首先咱们先拆解下你当前方案出问题的核心原因:

  • 动画帧不同步:你在动画的每一帧都调用leftFrontText.post(),这个Runnable会被放到UI消息队列,要等到下一次刷新才执行,导致进度条宽度更新和TextView padding更新不在同一帧,视觉上就会出现闪烁。
  • 延迟任务积压:动画过程中多次post的任务可能会积压,动画结束后这些延迟的任务还在执行,错误覆盖了最终的padding值,导致TextView变成全白。

一、修复现有方案的问题

1. 提前缓存TextView宽度,避免重复post

我们可以在View布局完成后一次性获取TextView宽度,不需要在动画每一帧都异步获取。用ViewTreeObserver.OnGlobalLayoutListener就能准确拿到布局完成后的宽度:

TextView leftFrontText;
private int leftTextWidth; // 缓存TextView宽度

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    View view = View.inflate(this, R.layout.test_page_layout, containerLayout);
    View progressBar = view.findViewById(R.id.progress_bar);
    leftFrontText = view.findViewById(R.id.left_front_textview);

    // 等待布局完成后获取宽度,再启动动画
    leftFrontText.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            leftFrontText.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            leftTextWidth = leftFrontText.getWidth();
            startProgressAnimation(progressBar, 59);
        }
    });
}

private void startProgressAnimation(View progressBar, int progress) {
    ValueAnimator va = ValueAnimator.ofFloat(0f, progress);
    va.setDuration(1000);
    va.addUpdateListener(animation -> {
        float value = (float) animation.getAnimatedValue();
        setViewWidth(progressBar, (int) value);
        // 直接用缓存的宽度计算,无需post
        setLeftFrontTextViewClip((int) value);
    });
    va.addListener(new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {}
        @Override
        public void onAnimationEnd(Animator animation) {
            setLeftFrontTextViewClip(progress);
        }
        @Override
        public void onAnimationCancel(Animator animation) {}
        @Override
        public void onAnimationRepeat(Animator animation) {}
    });
    va.start();
}

2. 替换padding为ClipBounds,更可靠的裁剪逻辑

原方案用负padding的逻辑是错误的,负padding不会裁剪内容,反而会扩大TextView的可绘制区域。改用setClipBounds直接限制TextView的显示区域,效果更稳定:

private void setLeftFrontTextViewClip(int progressBarDp) {
    int marginStart = ThemeUtil.dpToPx(this, 12);
    int progressbarDistance = ThemeUtil.dpToPx(this, progressBarDp);
    if (leftTextWidth > 0) {
        // 计算TextView能显示的宽度:进度条宽度 - 左侧margin
        int visibleWidth = progressbarDistance - marginStart;
        visibleWidth = Math.max(0, visibleWidth);
        // 设置裁剪区域,只显示左侧visibleWidth的部分
        Rect clipRect = new Rect(0, 0, visibleWidth, leftFrontText.getHeight());
        leftFrontText.setClipBounds(clipRect);
    }
}

二、更优实现:自定义ProgressTextBar

如果想要更稳定、可复用的效果,建议封装成自定义View,把所有逻辑整合在一起,避免布局层级同步问题:

自定义View核心代码

public class ProgressTextBar extends View {
    private Paint mBgTextPaint;
    private Paint mProgressTextPaint;
    private Paint mBarPaint;
    private String mText = "Entertainment";
    private String mRightText = "$0 / $0";
    private float mProgress = 0f; // 0-100
    private int mPaddingStart = dpToPx(12);
    private int mPaddingEnd = dpToPx(12);
    private Rect mTextBounds = new Rect();

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

    public ProgressTextBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        initAttrs(attrs);
        initPaints();
    }

    private void initAttrs(AttributeSet attrs) {
        TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.ProgressTextBar);
        mText = ta.getString(R.styleable.ProgressTextBar_ptb_text);
        mRightText = ta.getString(R.styleable.ProgressTextBar_ptb_rightText);
        mProgress = ta.getFloat(R.styleable.ProgressTextBar_ptb_progress, 0f);
        int bgTextColor = ta.getColor(R.styleable.ProgressTextBar_ptb_bgTextColor, getResources().getColor(R.color.grey1));
        int progressTextColor = ta.getColor(R.styleable.ProgressTextBar_ptb_progressTextColor, Color.WHITE);
        int barColor = ta.getColor(R.styleable.ProgressTextBar_ptb_barColor, getResources().getColor(R.color.blue));
        ta.recycle();

        mBgTextPaint.setColor(bgTextColor);
        mProgressTextPaint.setColor(progressTextColor);
        mBarPaint.setColor(barColor);
    }

    private void initPaints() {
        mBgTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBgTextPaint.setTextSize(getResources().getDimension(R.dimen.main_text_size));

        mProgressTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mProgressTextPaint.setTextSize(getResources().getDimension(R.dimen.main_text_size));

        mBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int barWidth = (int) (mProgress / 100f * getWidth());

        // 绘制进度条背景
        canvas.drawRoundRect(0, 0, getWidth(), getHeight(), dpToPx(14), dpToPx(14), new Paint(){{
            setColor(getResources().getColor(R.color.bg_grey));
        }});
        // 绘制进度条
        canvas.drawRoundRect(0, 0, barWidth, getHeight(), dpToPx(14), dpToPx(14), mBarPaint);

        // 绘制左侧背景文本
        mBgTextPaint.getTextBounds(mText, 0, mText.length(), mTextBounds);
        float textY = getHeight() / 2f + mTextBounds.height() / 2f;
        canvas.drawText(mText, mPaddingStart, textY, mBgTextPaint);

        // 绘制左侧进度文本(裁剪显示)
        canvas.save();
        canvas.clipRect(mPaddingStart, 0, barWidth, getHeight());
        canvas.drawText(mText, mPaddingStart, textY, mProgressTextPaint);
        canvas.restore();

        // 绘制右侧金额文本
        float rightTextX = getWidth() - mPaddingEnd - mBgTextPaint.measureText(mRightText);
        canvas.drawText(mRightText, rightTextX, textY, barWidth >= getWidth() - mPaddingEnd ? mBgTextPaint : mProgressTextPaint);
    }

    public void setProgress(float progress) {
        mProgress = progress;
        invalidate();
    }

    private int dpToPx(int dp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
    }
}

布局中使用

<com.yourpackage.ProgressTextBar
    android:id="@+id/progress_bar"
    android:layout_width="match_parent"
    android:layout_height="28dp"
    app:ptb_text="Entertainment"
    app:ptb_rightText="$0 / $0"
    android:layout_marginStart="16dp"
    android:layout_marginEnd="16dp"/>

启动动画

ProgressTextBar progressBar = findViewById(R.id.progress_bar);
ValueAnimator va = ValueAnimator.ofFloat(0f, 59f);
va.setDuration(1000);
va.addUpdateListener(animation -> progressBar.setProgress((float) animation.getAnimatedValue()));
va.start();

这种方案的优势:

  • 所有绘制逻辑在同一View内,和动画帧完全同步,不会出现闪烁
  • 代码更简洁,可复用性强,容易扩展圆角、颜色、文本等属性
  • 避免了布局层级冲突和异步任务积压的问题

内容的提问来源于stack exchange,提问作者Heaven

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 07:18:17