我们都知道 Android 中使用 Spannable 可以实现 TextView 富文本的显示,但是在自定义控件中如何使用 Spannable 绘制不同样式的文字呢?
例如这种效果,标题中的 分数字61
是粗体,分
是常规字体,并且相对于 61
更小些。
第一反应可能是使用 SpannableString.setSpan()
设置 RelativeSizeSpan
, 然后在 onDraw()
中进行绘制,事实是这样实现是没有效果的,因为 onDraw()
中只能获取到 SpannableString
中的内容,拿不到 Span
.
那如何在自定义View 中使用 Spannable 呢? 答案就是系统提供的 Layout
类,
/**
* A base class that manages text layout in visual elements on
* the screen.
* For text that will be edited, use a {@link DynamicLayout},
* which will be updated as the text changes.
* For text that will not change, use a {@link StaticLayout}.
*/
public abstract class Layout {
}
可以看到 Layout
是一个抽象类,有三个子类,可以实现一些自动换行的显示效果。
<resources>
<declare-styleable name="ArcProgressView">
<attr name="arcBackgroundColor" format="color" />
<attr name="arcProgressColor" format="color" />
<attr name="arcSubTitleColor" format="color" />
<attr name="arcStrokeWidth" format="dimension" />
<attr name="arcTitleTextSize" format="dimension" />
<attr name="arcSubTitleTextSize" format="dimension" />
<attr name="arcProgress" format="float" />
<attr name="arcTitleNumber" format="integer" />
declare-styleable>
resources>
onDraw()
中绘制public class ArcProgressView extends View {
private int arcBackgroundColor; // 圆弧背景颜色
private int arcProgressColor; // 圆弧进度颜色
private int arcSubTitleColor; // 副标题颜色
private float arcStrokeWidth; // 圆弧线的厚度
private float arcTitleTextSize; // 标题文字大小
private float arcSubTitleTextSize; // 副标题文字大小
private float arcProgress; // 进度
private int arcTitleNumber; // 值
private Paint paint;
private float centerX;
private float centerY;
private float radius; // 半径
private RectF rectF;
private int startAngle = 135;
private int sweepAngle = 270;
private String subTitle = "1月份";
private SpannableString spannableString;
private TextPaint textPaint;
private RelativeSizeSpan relativeSizeSpan;
private DynamicLayout dynamicLayout;
private String text = "11分";
private StyleSpan styleSpan;
private float curProgress; // 当前进度
private int curNumber;
public ArcProgressView(Context context) {
this(context, null);
}
public ArcProgressView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ArcProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
readAttrs(context, attrs);
init(context);
}
private void readAttrs(Context context, AttributeSet attributeSet) {
TypedArray typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.ArcProgressView);
arcBackgroundColor = typedArray.getColor(R.styleable.ArcProgressView_arcBackgroundColor, 0x1c979797);
arcProgressColor = typedArray.getColor(R.styleable.ArcProgressView_arcProgressColor, 0xff3372FF);
arcSubTitleColor = typedArray.getColor(R.styleable.ArcProgressView_arcSubTitleColor, 0x66000000);
arcStrokeWidth = typedArray.getDimensionPixelSize(R.styleable.ArcProgressView_arcStrokeWidth, dp2px(5));
arcTitleTextSize = typedArray.getDimensionPixelSize(R.styleable.ArcProgressView_arcTitleTextSize, dp2px(30));
arcSubTitleTextSize = typedArray.getDimensionPixelSize(R.styleable.ArcProgressView_arcSubTitleTextSize, dp2px(14));
arcProgress = typedArray.getFloat(R.styleable.ArcProgressView_arcProgress, 1.0f);
arcTitleNumber = typedArray.getInt(R.styleable.ArcProgressView_arcTitleNumber, 100);
typedArray.recycle();
}
private void init(Context context) {
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStrokeCap(Paint.Cap.ROUND);
relativeSizeSpan = new RelativeSizeSpan(0.6f);
styleSpan = new StyleSpan(android.graphics.Typeface.BOLD);
textPaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG);
textPaint.setColor(arcProgressColor);
// textPaint.setTextAlign(Paint.Align.CENTER); // 设置该属性导致文字间有间隔
textPaint.setTextSize(sp2px(22));
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
centerX = w / 2f;
centerY = h / 2f;
radius = (Math.min(w, h) - arcStrokeWidth) / 2f;
rectF = new RectF(-radius, -radius, radius, radius);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredSize(widthMeasureSpec, dp2px(100));
int height = getMeasuredSize(heightMeasureSpec, dp2px(100));
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制圆弧和进度
drawArc(canvas);
// 绘制文字 title
drawTitleText(canvas);
// 绘制文字副标题
drawSubTitle(canvas);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
startAnimation();
}
private void startAnimation() {
ValueAnimator progressAnimator = ValueAnimator.ofFloat(0f, arcProgress);
ValueAnimator numberAnimator = ValueAnimator.ofInt(0, arcTitleNumber);
progressAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
curProgress = (float) animation.getAnimatedValue();
invalidate();
}
});
numberAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
curNumber = (int) animation.getAnimatedValue();
text = curNumber + "分";
}
});
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(progressAnimator, numberAnimator);
animatorSet.setDuration(700);
animatorSet.setInterpolator(new LinearInterpolator());
animatorSet.start();
}
private void drawSubTitle(Canvas canvas) {
canvas.save();
canvas.translate(centerX, centerY);
paint.setTextSize(arcSubTitleTextSize);
paint.setTextAlign(Paint.Align.CENTER);
paint.setColor(arcSubTitleColor);
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(0);
canvas.drawText(subTitle, 0, 60, paint);
canvas.restore();
}
private void drawArc(Canvas canvas) {
canvas.save();
canvas.translate(centerX, centerY);
paint.setColor(arcBackgroundColor);
paint.setStrokeWidth(arcStrokeWidth);
paint.setStyle(Paint.Style.STROKE);
canvas.drawArc(rectF, startAngle, sweepAngle, false, paint);
paint.setColor(arcProgressColor);
canvas.drawArc(rectF, startAngle, sweepAngle * curProgress, false, paint);
canvas.restore();
}
private void drawTitleText(Canvas canvas) {
canvas.save();
textPaint.setTextSize(arcTitleTextSize);
float textWidth = textPaint.measureText(text); // 文字宽度
float textHeight = -textPaint.ascent() + textPaint.descent(); // 文字高度
// 由于 StaticLayout 绘制文字时,默认画在Canvas的(0,0)点位置,所以居中绘制居中位置,需要将画布 translate到中间位置。
canvas.translate(centerX - textWidth * 2 / 5f, centerY - textHeight * 2 / 3f);
spannableString = SpannableString.valueOf(text);
spannableString.setSpan(styleSpan, 0, text.length() - 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
spannableString.setSpan(relativeSizeSpan, text.length() - 1, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
dynamicLayout = new DynamicLayout(spannableString, textPaint, getWidth(), Layout.Alignment.ALIGN_NORMAL, 0, 0, false);
dynamicLayout.draw(canvas);
canvas.restore();
}
/**
* 对外提供方法,设置进度
*
* @param percent
*/
public void setArcProgress(float percent) {
this.curProgress = percent;
invalidate();
}
private int getMeasuredSize(int measureSpec, int defvalue) {
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
if (mode == MeasureSpec.EXACTLY) {
return size;
}
return Math.min(size, defvalue);
}
private int dp2px(int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
private int sp2px(int sp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
}
}
<com.xing.bottomsheetsample.ArcProgressView
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginTop="20dp"
app:arcProgress="0.6"
app:arcSubTitleTextSize="14sp"
app:arcTitleNumber="61"
app:arcTitleTextSize="28sp" />