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
相关产品推荐
相关产品推荐

