TextView里画世界——ReplacementSpan实践
来源:互联网 发布:微信小视频特效软件 编辑:程序博客网 时间:2024/06/18 11:35
需求&效果
相信很多同学都多多少少接触过一些常用的Span,例如,用于设置TextView里某段文字字体大小的AbsoluteSizeSpan,可以改变背景颜色的BackgroundColorSpan,还有可以直接画出一个图片ImageSpan等等,常用的Span用法百度谷歌一下一大把,这里就不再赘述。今天,我想和大家分享稍微高♂级一点的内容:如何通过extends ReplacementSpan在TextView控件区域内画自己想画的东西。单是用文字说,你可能会感觉有点懵,下面我们先来看来自某个知名App里的ListView item的效果图:
上图里自带背景的“精”、“热”这两个小icon,就是我想要实现的最终效果。可能有人要说,这还不简单,我做一个水平LinearLayout,然后往里面放三个TextView也能实现,根本不需要用到Span。用常规的布局实现缺点太多了,容我一个个给你数:
1. 只用一个水平的LinearLayout实现不了,“阿狸”的“狸”字会出现在“热”icon的右下方,而不是在行首。
2. 这个是ListView的Item,如果icon显示的个数由服务端控制,动态addView会导致ListView滑动不够流畅,而提前在xml写好若干个View(如最大个数9个),会导致单个Item里View的个数增加,不够优雅,性能也不好。
3. 效果要求:垂直方向,icon的中轴线与正常文字的中轴线要对齐,一旦正常文字的字号改变,icon的位置也要手动调整。
4. ..
如果icon是单纯的图片,其实用文章开始提到的ImageSpan就可以搞定,但是如果ui组的同事比较懒,不愿意因为改变色值或内容而重新切图呢,比如我们公司的(逃- -!!,那背景和字都得我们自己画,开始动手吧!
ReplacementSpan实践
关于ReplacementSpan这个类,开发者文档和源码里几乎没明确说明这玩意儿到底是啥。不过通过名称,我们可以猜测,这个是可以用作替换功能的Span,也就是用这个Span替代文字。
ReplacementSpan只有两个抽象方法需要我们@Override:
/** * Returns the width of the span */public abstract int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm);/** * Draws the span into the canvas. */public abstract void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint);
第一个方法getSize(),返回值就是Span替换文字后所占的宽度
第二个方法draw(),在TextView绘制时被调用,与此同时,会把canvas,text,paint以及一堆坐标传给我们,我们覆盖这个方法,就可以在特定位置画一些我们想画的东西了。
下面我盗了一张FontMetrics图以助于你理解,其实TextView里文字所占的高度就是FontMetrics的descent到ascent的距离。PS:以baseline为参考线,descent为正,ascent为负
接下来show your code:
IconTextSpan.java
public class IconTextSpan extends ReplacementSpan { private Context mContext; private int mBgColorResId; //Icon背景颜色 private String mText; //Icon内文字 private float mBgHeight; //Icon背景高度 private float mBgWidth; //Icon背景宽度 private float mRadius; //Icon圆角半径 private float mRightMargin; //右边距 private float mTextSize; //文字大小 private int mTextColorResId; //文字颜色 private Paint mBgPaint; //icon背景画笔 private Paint mTextPaint; //icon文字画笔 public IconTextSpan(Context context, int bgColorResId, String text) { if (TextUtils.isEmpty(text)) { return; } //初始化默认数值 initDefaultValue(context, bgColorResId, text); //计算背景的宽度 this.mBgWidth = caculateBgWidth(text); //初始化画笔 initPaint(); } /** * 初始化画笔 */ private void initPaint() { //初始化背景画笔 mBgPaint = new Paint(); mBgPaint.setColor(mContext.getResources().getColor(mBgColorResId)); mBgPaint.setStyle(Paint.Style.FILL); mBgPaint.setAntiAlias(true); //初始化文字画笔 mTextPaint = new TextPaint(); mTextPaint.setColor(mContext.getResources().getColor(mTextColorResId)); mTextPaint.setTextSize(mTextSize); mTextPaint.setAntiAlias(true); mTextPaint.setTextAlign(Paint.Align.CENTER); } /** * 初始化默认数值 * * @param context */ private void initDefaultValue(Context context, int bgColorResId, String text) { this.mContext = context.getApplicationContext(); this.mBgColorResId = bgColorResId; this.mText = text; this.mBgHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 17f, mContext.getResources().getDisplayMetrics()); this.mRightMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, mContext.getResources().getDisplayMetrics()); this.mRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, mContext.getResources().getDisplayMetrics()); this.mTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 13, mContext.getResources().getDisplayMetrics()); this.mTextColorResId = R.color.white_a; } /** * 计算icon背景宽度 * * @param text icon内文字 */ private float caculateBgWidth(String text) { if (text.length() > 1) { //多字,宽度=文字宽度+padding Rect textRect = new Rect(); Paint paint = new Paint(); paint.setTextSize(mTextSize); paint.getTextBounds(text, 0, text.length(), textRect); float padding = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, mContext.getResources().getDisplayMetrics()); return textRect.width() + padding * 2; } else { //单字,宽高一致为正方形 return mBgHeight; } } /** * 设置右边距 * * @param rightMarginDpValue */ public void setRightMarginDpValue(int rightMarginDpValue) { this.mRightMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, rightMarginDpValue, mContext.getResources().getDisplayMetrics()); } /** * 设置宽度,宽度=背景宽度+右边距 */ @Override public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { return (int) (mBgWidth + mRightMargin); } @Override public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) { //获取当前TextView所用画笔的FontMetrics Paint.FontMetrics metrics = paint.getFontMetrics(); //计算画icon背景的起始y坐标,计算公式:TextView文字开始画的位置 + (文字高度 - icon背景高度) / 2; float bgStartY = metrics.ascent - metrics.top + ((metrics.descent - metrics.ascent) - mBgHeight) / 2; if (bgStartY < 0) { bgStartY = 0; } //画背景 RectF bgRect = new RectF(x, bgStartY, x + mBgWidth, bgStartY + mBgHeight); canvas.drawRoundRect(bgRect, mRadius, mRadius, mBgPaint); //计算画icon文字起始y坐标,计算公式:画icon背景的起始坐标 + (icon背景高度 - icon文字高度 / 2) - TextView paint的fontMetrics.top //注意:最后多减去一个fontMetrics.top,是因为drawText方法的指定的y是画文字baseline的y,fontMetrics里baseline和top之间的距离为-top Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics(); float iconTextHeight = fontMetrics.bottom - fontMetrics.top; float iconTextStartY = bgStartY + ((mBgHeight - iconTextHeight) / 2 - fontMetrics.top); //画文字 canvas.drawText(mText, x + mBgWidth / 2, iconTextStartY, mTextPaint); }}
MainActivity.java
public class MainActivity extends AppCompatActivity { private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView) findViewById(R.id.tv); textView.setTextSize(20); List<ReplacementSpan> spans = new ArrayList<>(); String content = "Android是一种基于Linux的自由及开放源代码的操作系统,主要使用于移动设备,如智能手机和平板电脑,由Google公司和开放手机联盟领导及开发。尚未有统一中文名称,中国大陆地区较多人使用“安卓”或“安致”。"; StringBuilder stringBuilder = new StringBuilder(); //第一个Span stringBuilder.append(" "); IconTextSpan topSpan = new IconTextSpan(getApplicationContext(), R.color.colorPrimary, "置顶"); spans.add(topSpan); //第二个Span stringBuilder.append(" "); IconTextSpan hotSpan = new IconTextSpan(getApplicationContext(), R.color.colorAccent, "热"); hotSpan.setRightMarginDpValue(5); spans.add(hotSpan); stringBuilder.append(content); SpannableString spannableString = new SpannableString(stringBuilder.toString()); //循环遍历设置Span for (int i = 0; i < spans.size(); i++) { spannableString.setSpan(spans.get(i), i, i + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } textView.setText(spannableString); }}
最终的显示效果是这样的,聪明的小伙伴们,你们看懂了么~
- TextView里画世界——ReplacementSpan实践
- 标题带"精""热"。ReplacementSpan实践
- 【UML】徜徉在UML的世界里—视频总结
- 第七周实践上机项目—— 星星的世界
- textview里放入图片
- 《世界因你不同》——大学里的转系
- 黑马程序员--【高清视频】黑马,在游戏世界里驰骋——程传鹏专访
- 阅读人生-《世界因你不同—李开复自传》里的话
- FishC笔记—27 讲 集合:在我的世界里,你就是唯一!
- 盒子里的世界
- 耳机里听世界
- 影子里的世界
- 高墙里的世界
- Android ReplacementSpan 文字对齐问题
- TextView——setCompoundDrawables
- Android——textView
- TextView里的setText方法
- android里TextView加下划线
- C语言嵌入式系统编程-----软件架构篇
- 系统学习Java和无基础自学python的一些感受
- R tutorial 09 - Advance Data.frame 进阶函数-数据
- signal函数、sigaction函数及信号集操作函数
- wpf 学习足迹
- TextView里画世界——ReplacementSpan实践
- 安装redis
- Web前端使用PS前如何进行初始化设置
- Unity初级——NGUI插件学习(1)
- 第九周项目2---对称矩阵压缩存储的实现与应用
- IIS下配置php+mysql的过程
- HDU2040亲和数
- android 切换到有ScrollView +Gridview 或者listview 布局的Activity时会上拉一点
- 刮刮蕾的简单设计