Android 自定义 View 中使用 Spannable的实例详解

所属分类: 软件编程 / Android 阅读数: 104
收藏 0 赞 0 分享

我们都知道 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.
 * <p>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 是一个抽象类,有三个子类,可以实现一些自动换行的显示效果。

  • BoringLayout
  • DynamicLayout
  • StaticLayout

实现代码

1. 定义自定义属性

<?xml version="1.0" encoding="utf-8"?>
<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>

2. 继承 View, 在 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());
 }
}

3. 在布局中引用

<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" />
更多精彩内容其他人还在看

Android框架学习之Volley和Glide详解

这篇文章主要给大家介绍了关于Android框架学习之Volley和Glide的相关资料,文中通过示例代码介绍的非常详细,对各位Android开发者们具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
收藏 0 赞 0 分享

Android中Fragment的基本用法示例总结

Fragment是activity的界面中的一部分或一种行为,下面这篇文章主要给大家介绍了关于Android中Fragment的基本用法的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下
收藏 0 赞 0 分享

Android.mk引入第三方jar包和so库文件的方法

这篇文章主要介绍了Android.mk引入第三方jar包和so库文件的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
收藏 0 赞 0 分享

Android仿微信录制小视频

这篇文章主要为大家详细介绍了Android仿微信录制小视频,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
收藏 0 赞 0 分享

android实现一键锁屏和一键卸载的方法实例

这篇文章主要给大家介绍了关于android如何实现一键锁屏和一键卸载的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
收藏 0 赞 0 分享

Android手势密码--设置和校验功能的实现代码

这篇文章主要介绍了Android手势密码--设置和校验功能的实现代码,非常不错,具有一定的参考校验价值,需要的朋友可以参考下
收藏 0 赞 0 分享

Kotlin学习笔记之const val与val

这篇文章主要给大家介绍了关于Kotlin学习笔记之const val与val的相关资料,并给大家介绍了const val和val区别以及Kotlin中var和val的区别,需要的朋友可以参考借鉴,下面随着小编来一起学习学习吧
收藏 0 赞 0 分享

Android实现调用系统分享功能示例的总结

这篇文章主要介绍了通过Android调用系统分享文本信息、单张图片、多个文件和指定分享到微信、QQ,同时分享图片和文字的功能示例,小编觉得挺不错,一起跟随小编过来看看吧
收藏 0 赞 0 分享

Android自定义view实现输入控件

这篇文章主要为大家详细介绍了Android自定义view实现输入控件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
收藏 0 赞 0 分享

Android使用Intent.ACTION_SEND分享图片和文字内容的示例代码

这篇文章主要介绍了Android使用Intent.ACTION_SEND分享图片和文字内容的示例代码的实例代码,具有很好的参考价值,希望对大家有所帮助,一起跟随小编过来看看吧
收藏 0 赞 0 分享
查看更多