前言 Android提供了TextView这个类作为Android开发当中展示文字的工作,最近笔者在做类似于一个展示类型的APP,发现TextView这个类真的有点力不从心,好多的功能都让笔者特别头疼,于是就有了今天这篇技术博客。
原生的APi当中提供了TextView这个控件供开发者使用,一般需求都不在话下,可以设置显示文本的字体大小,字体颜色以及字体的显示格式,如:密码格式、数字格式等等。但是在千变万化的开发当中,还是不能满足开发者的需求,比如控制下拉的长文本,以及富文本。我们先来看几种需求。
上面看到了几点需求,还有好多,笔者相信各位看客心中都懂,所以就不展示太多了,我们今天要做到就是通过几种方式来优化我们TextView,好吧,我们开始。
1、TextView实现长文本的分段展示。 长文本:这个没什么好解释的,就是比较长的文本。直接显示就OK,但是我们知道Android当中的屏幕尺寸是有限的,我们要在有限的屏幕内合理的显示很多的内容,当然这个是侧滑菜单栏出现的原因。我们要让TextView通过用户的交互来显示合理的内容,比如在用户并不对该文本关系的前提,显示重要的前几行就OK ,如果用户想看文本内容,用户可以通过点击当前的TextView进行显示其与的内容,根据这个简单的需求,我们来对TextView进行定制。
首先我们先计划一下我们怎么对当前的TextView进行定制呢!
1、我们继承一个现有的ViewGroup,当中含有一个Button、TextView。实际让Button去控制TextView的显示方式。
2、我们初始化的时候可以根据TextView的长度,来决定是否显示Button,因为我们知道TextView在我们有限的空间里面可以完全显示的时候,也就不需要下拉的功能。
3、通过TextView可显示的行数,完全显示的行数去测量TextView的高度。
4、通过Button的点击去切换可显示的行数、完全显示的行数
5、加入动画,笔者这里加入的属性动画
6、解决不友好的BUG,类似于ViewGroup改变,而当ViewGroup改变动画结束,TextView才完全显示,这里会贴图给看客展示。
7、添加回调定制完成,效果图展示
1、继承ViewGroup开始定制 1 2 3 4 5 6 7 8 public class PullDownTextView extends LinearLayout implements View .OnClickListener { private TextView mTextView ; private ImageButton mImageButton; }
我们这里选择的是LinearLayout,原因就是我们TextView和Button排列方式是线性布局。
2、初始化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class PullDownTextView extends LinearLayout implements View .OnClickListener { private boolean isPull ; private TextView mTextView ; private ImageButton mImageButton; private Drawable mPullDownDrawable ; private Drawable mUpDownDrawable ; private void initPullDownTextView () { mPullDownDrawable = getDrawable(R.drawable.ic_pull_small_light); mUpDownDrawable = getDrawable(R.drawable.ic_not_small_light); setOrientation(LinearLayout.VERTICAL); setVisibility(View.GONE); } @Override protected void onFinishInflate () { super .onFinishInflate(); initPullDownTextView(); mTextView = (TextView) this .getChildAt(0 ); mImageButton = (ImageButton) this .getChildAt(1 ); mImageButton.setOnClickListener(this ); mImageButton.setImageDrawable(isPull ? mUpDownDrawable : mPullDownDrawable); } @Override public void setOrientation (int orientation) { if (orientation == LinearLayout.HORIZONTAL){ throw new IllegalArgumentException("参数错误:当前控件,不支持水平" ); } super .setOrientation(orientation); } }
我们对当前的ViewGroup进行初始化设置。对Button的图片进行初始化,以及事件的初始化
3、测量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 public class PullDownTextView extends LinearLayout implements View .OnClickListener { private int mTextViewPullHeight ; private int mTextViewNotPullHeight ; private boolean isPull ; private boolean isReLayout ; private boolean isAnimator ; private boolean isMaxHeightMeasure; private boolean isMinHeightMeasure; private TextView mTextView ; private ImageButton mImageButton; private Drawable mPullDownDrawable ; private Drawable mUpDownDrawable ; private int mTextVisibilityCount = 3 ; private int mAnimatorDuration = 500 ; ... @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { if (!isReLayout || getVisibility() == View.GONE){ super .onMeasure(widthMeasureSpec, heightMeasureSpec); return ; } if (mTextView.getLineCount() <= mTextVisibilityCount){ mTextView.setVisibility(View.VISIBLE); super .onMeasure(widthMeasureSpec, heightMeasureSpec); return ; } mImageButton.setVisibility(View.VISIBLE); if (!isMaxHeightMeasure && mTextViewPullHeight == 0 ){ mTextView.setMaxLines(Integer.MAX_VALUE); super .onMeasure(widthMeasureSpec, heightMeasureSpec); mTextViewPullHeight = mTextView.getMeasuredHeight() ; isMaxHeightMeasure = true ; } if (!isMinHeightMeasure && mTextViewNotPullHeight == 0 ){ mTextView.setMaxLines(mTextVisibilityCount); super .onMeasure(widthMeasureSpec, heightMeasureSpec); mTextViewNotPullHeight = mTextView.getMeasuredHeight(); isMinHeightMeasure = true ; } super .onMeasure(widthMeasureSpec, heightMeasureSpec); } }
在这里测量为什么只测量一次呢,因为后续我们会通过属性动画去改变TextView的高度,而我们改变后,我们获取就会导致我们获取到的高度不是定值,而是改变后的值。测量这里我们主要分为三种情况,上述代码当中注释也说的很清楚
1、没有内容的时候,
2、有内容,但是内容比较短的时候,正常显示TextView,但是相应的隐藏ImageButton
3、有内容,并且显示的内容比较长的时候,这里我们显示TextView、ImageButton。
4、点击事件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override public void onClick (View v) { if (isAnimator){ return ; } if (isPull){ startAnimator(mTextView, mTextViewPullHeight, mTextViewNotPullHeight); } else { startAnimator(mTextView, mTextViewNotPullHeight, mTextViewPullHeight); } if (this .mOnTextViewPullListener != null ){ this .mOnTextViewPullListener.textViewPull(mTextView, isPull); } isPull = !isPull ; mImageButton.setImageDrawable(isPull ? mUpDownDrawable : mPullDownDrawable); }
根据用户点击状态去切换Button的图标,还有根据刚刚测量的高度进行开启动画
5、开启动画 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 private void startAnimator (final TextView view, int startHeight, int endHeight) { ValueAnimator valueAnimator = ValueAnimator.ofInt(startHeight , endHeight ).setDuration(mAnimatorDuration); valueAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart (Animator animation) {} @Override public void onAnimationEnd (Animator animation) { isAnimator = false ; } @Override public void onAnimationCancel (Animator animation) {} @Override public void onAnimationRepeat (Animator animation) {} }); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate (ValueAnimator animation) { int animatedValue = (int ) animation.getAnimatedValue(); ViewGroup.LayoutParams params = view.getLayoutParams(); params.height = animatedValue ; view.setMaxHeight(animatedValue); view.setLayoutParams(params); } }); isAnimator = true ; valueAnimator.start(); }
这个地方有一个坑,笔者也是想了很久,才弄明白的,说不太清楚,看下效果图吧。为了各位看客能很清楚的BUG,笔者在这里加入不同的背景。
这种效果就是在刚刚开始动画的时候,应该加入
view.setMaxHeight(animatedValue);
6、回调接口 1 2 3 4 5 6 7 8 public interface OnTextViewPullListener { void textViewPull (TextView textView, boolean isPull) ; } public void setOnTextViewPullListener (OnTextViewPullListener listener) { this .mOnTextViewPullListener = listener ; }
7、在MainActivity当中使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); PullDownTextView text1 = (PullDownTextView)findViewById(R.id.expand_text_view); text1.setText(getString(R.string.long_text1)); PullDownTextView text2 = (PullDownTextView)findViewById(R.id.expand1_text_view); text2.setText(getString(R.string.long_text1)); } }
这里没有什么好说的,相信各位看客都懂。这就是大体的流程,回调接口就是在onClick事件回调的,上述代码也有说明。到这里就差不多了,源码在文章末尾给出,看一下我们的效果吧
2、TextView显示富文本 我们在开始编码之前,我们先来了解一下什么是富文本, 富文本(Rich Text Format):这个有些开发者比较陌生,那么什么是富文本呢?其实就是一段带有自己的格式的文本。这么说有点抽象,我们来举个例子,其实就是我们常用的Word编辑器所写的文本,每一个字都是带有格式的。我们看下面的一个例子就理解了什么是富文本。
1 2 Hello! This is some bold text.
仔细观察,上述的一段文字是带有格式。这就是我们常见的富文本。现在我们看富文本的相应代码
1 2 3 4 {\rtf1\ansi Hello!\par This is some {\b bold} text.\par }
上述的富文本格式代码,貌似存在一定的规则可寻。什么规则呢,这里大体的描述一下,因为笔者这里语法也没有太多的深入,反斜线(\)标着这个RTE(富文本)控制的开始。(\par)表示开始新的一行,有点类似于HTML当中的标签了。(\b)将文字粗体显示。({})大括号定义了一个群组,上述例子中使用了一个群组来限制代码\b的作用范围。合法的RTF文档是一个以代码\rtf开始的群组。
了解了基本的什么是富文本之后,我们开始思考在本文开后的图1里面效果,如果让大家在Wold编辑器当中编写,会非常简单。当然Google也考虑到了这一种情况,所以我们不需要定制View就可以达到这种效果,Google为我们提供一个类用来封装我们带有格式的富文本,然后丢给TextView进行显示就OK,
1、Google提供富文本封装类Spannable Spannable是一个接口,有两个实现类分别是SpannableString 和SpannableStringBuilder ,我们知道我们以后要使用的话,肯定就是这里面的这两个类啦。那么这两个类有什么区别呢,其实和我们早前学过的String,和StringBuilder是一样的,一个为定长字符串,一个为可变字符串的区别,可以根据看客自己的需求去选择
Spannable里面定义了两个方法,和一个静态工厂,通过静态工厂拿到Spannable默认实现类是SpannableString。具体代码如下所示: Spannable.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static class Factory { private static Spannable.Factory sInstance = new Spannable.Factory(); public static Spannable.Factory getInstance () { return sInstance; } public Spannable newSpannable (CharSequence source) { return new SpannableString(source); } }
Spannable里面定义了两个方法,分别是:
1 2 > public void setSpan (Object what, int start, int end, int flags) ; > public void removeSpan (Object what) ;
这里有几个参数,需要说一下,
what : 样式
start : 该样式作用范围的起始位置
end : 该样式作用范围的结束位置
flags : 模式,
最后一个参数的模式,相对的有点抽象,其实看客可以理解成为枚举,也就是说模式是系统为我们定义好的,让我去选择使用就OK了。在系统当中由Spanned给出。
Spanned.SPAN_INCLUSIVE_INCLUSIVE 起始结束都包括
Spanned.SPAN_EXCLUSIVE_INCLUSIVE 起始不包括,结束包括
Spanned.SPAN_INCLUSIVE_EXCLUSIVE 起始包括,结束不包括
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE 起始结束都不包括
1.1、获取Spannable实例 我们刚刚看过源码,知道了Spannable内部又一个静态的工厂类,那我们就使用这个类来获取实例。
1 > Spannable spannable = Spannable.Factory.getInstance().newSpannable(string);
1.2、Google为我们提供的样式 其实这里的样式是特别的多的,Google主要按照分类,分成了两大类,分别是字体的样式,和段落的样式。笔者在这里找几个常用的到的进行说明,其他的请各位看客自行去了解CharacterStyle, ParagraphStyle的实现子类,好找出看客所需要的样式。
1.3、颜色相关 颜色相关主要分为一个字体的颜色(ForegroundColorSpan),一个背景的颜色(BackgroundColorSpan)。
在这里我专门给测试TextView加入了背景和字体,我们发现,在背景方面,Span只能作用于Text的绘制区域。在字体颜色方面Span是优于我们设置的字体颜色的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void colorSpan () { Spannable spannable = Spannable.Factory.getInstance().newSpannable(text); BackgroundColorSpan backgroundColorSpan = new BackgroundColorSpan(Color.parseColor("#FF0000" )); spannable.setSpan(backgroundColorSpan, 0 , 9 , Spanned.SPAN_INCLUSIVE_INCLUSIVE); mTextView.setText(spannable); Spannable spannable1 = Spannable.Factory.getInstance().newSpannable(text); ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(Color.parseColor("#FF0000" )); spannable1.setSpan(foregroundColorSpan, 0 , 9 , Spanned.SPAN_INCLUSIVE_INCLUSIVE); mTextView1.setText(spannable1); }
1.4、大小位置相关 大小方面主要是RelativeSizeSpan,构造时传入一个数值来说明比较当前字体大小的变化,大于0为变大,小于0为变小 位置方面主要是上移(SuperscriptSpan),下移(SubscriptSpan),移动完成以后大小是不会变化的,上移距离为当前文本高度的一半,下一距离也是当前文本的一半。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public void sizeSpan () { Spannable spannable = Spannable.Factory.getInstance().newSpannable(text); RelativeSizeSpan sizeSpanBig = new RelativeSizeSpan(1.4f ); RelativeSizeSpan sizeSpanSmall = new RelativeSizeSpan(0.6f ); spannable.setSpan(sizeSpanSmall, 0 , 9 , Spanned.SPAN_INCLUSIVE_INCLUSIVE); spannable.setSpan(sizeSpanBig, 11 , spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); mTextView.setText(spannable); Spannable SuperscriptSpanAble = Spannable.Factory.getInstance().newSpannable(text); SuperscriptSpan sizeSpan = new SuperscriptSpan(); SuperscriptSpanAble.setSpan(sizeSpan, 12 , spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); mTextView1.setText(SuperscriptSpanAble); Spannable SubscriptSpanAble = Spannable.Factory.getInstance().newSpannable(text); SubscriptSpan SubscriptSpan = new SubscriptSpan(); SubscriptSpanAble.setSpan(SubscriptSpan, 12 , spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); mTextView2.setText(SubscriptSpanAble); }
1.5、常见样式相关 常见样式有下划线,删除线,textStyle(粗体、斜体)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public void styleSpan () { Spannable spannable = Spannable.Factory.getInstance().newSpannable(text); StrikethroughSpan strikethroughSpan = new StrikethroughSpan(); spannable.setSpan(strikethroughSpan, 0 , 9 , Spanned.SPAN_INCLUSIVE_INCLUSIVE); mTextView.setText(spannable); Spannable underlineSpanAble = Spannable.Factory.getInstance().newSpannable(text); UnderlineSpan sizeSpan = new UnderlineSpan(); underlineSpanAble.setSpan(sizeSpan, 11 , spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); mTextView1.setText(underlineSpanAble); Spannable styleSpan = Spannable.Factory.getInstance().newSpannable(text); StyleSpan styleSpan_Bold = new StyleSpan(Typeface.BOLD); StyleSpan styleSpan_Italic = new StyleSpan(Typeface.ITALIC); styleSpan.setSpan(styleSpan_Bold, 0 , 9 , Spanned.SPAN_INCLUSIVE_INCLUSIVE); styleSpan.setSpan(styleSpan_Italic, 11 , spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); mTextView2.setText(styleSpan); }
1.6、跳转相关 常见的跳转相关有,点击事件,超链接。其实超链接的实现就是点击事件,只不过点击以后由当前手机的默认浏览器去打开。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public void clickSpan () { Spannable spannable = Spannable.Factory.getInstance().newSpannable(text); spannable.setSpan(new ClickableSpan() { @Override public void onClick (View widget) { Toast.makeText(RichTextActivity.this , "点击测试" , Toast.LENGTH_LONG).show(); } }, 9 , spannable.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); mTextView.setMovementMethod(LinkMovementMethod.getInstance()); mTextView.setText(spannable); Spannable spannableUrl = Spannable.Factory.getInstance().newSpannable(text); URLSpan urlSpan = new URLSpan("http://blog.csdn.net/lpc_java?viewmode=list" ); spannableUrl.setSpan(urlSpan, 9 , spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); mTextView1.setText(spannableUrl); mTextView1.setMovementMethod(LinkMovementMethod.getInstance()); mTextView2.setVisibility(View.GONE); }
注意:mTextView1.setMovementMethod(LinkMovementMethod.getInstance());必须设置TextView的MovementMethod才有点击效果
1.7、图片相关 在文字当中使用图片,其实这个我们可以联想一下社交软件当中的聊天表情。
在这里我们发现,是图片去替代了我们原有的文字,看客们在这里注意一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void imageSpan () { Spannable spannableImgae = Spannable.Factory.getInstance().newSpannable(text); Drawable image = this .getResources().getDrawable(R.mipmap.star); image.setBounds(0 ,0 ,60 ,60 ); ImageSpan imageSpan = new ImageSpan(image); spannableImgae.setSpan(imageSpan, spannableImgae.length()-2 , spannableImgae.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); mTextView.setText(spannableImgae); mTextView1.setVisibility(View.GONE); mTextView2.setVisibility(View.GONE); }
好了基本的几种在这里都已经说完
2 、其他方式实现富文本显示 HTML,也可以实现上述的功能,就是把含有HTML标签的语句直接在TextView当中进行显示,
1 2 3 4 5 6 7 8 9 10 public void html () { String html = "测 <br/> 试 <br/> 文 <br/> 字 <br/>" ; mTextView.setText(Html.fromHtml(html)); String html1 = "<h1>标题</h1>" ; mTextView1.setText(Html.fromHtml(html1)); String html2 = "<font color ='#FF0000'>测试文字</font>" ; mTextView2.setText(Html.fromHtml(html2)); }
但是TextView对HTML的支持不是很全,下面就把TextView对HTML的支持列举一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 a href="..."> 定义链接内容 b> 定义粗体文字 b 是blod的缩写 big> 定义大字体的文字 blockquote> 引用块标签 属性: Common -- 一般属性 cite -- 被引用内容的URI br> 定义换行 cite> 表示引用的URI dfn> 定义标签 dfn 是defining instance的缩写 div align="..."> em> 强调标签 em 是emphasis的缩写 font size="..." color="..." face="..."> h1> h2> h3> h4> h5> h6> i> 定义斜体文字 img src="..."> p> 段落标签,里面可以加入文字,列表,表格等 small> 定义小字体的文字 strike> 定义删除线样式的文字 不符合标准网页设计的理念,不赞成使用. strike是strikethrough的缩写 strong> 重点强调标签 sub> 下标标签 sub 是subscript的缩写 sup> 上标标签 sup 是superscript的缩写 tt> 定义monospaced字体的文字 不赞成使用. 此标签对中文没意义 tt是teletype or monospaced text style的意思 u> 定义带有下划线的文字 u是underlined text style的意思
笔者在这里把第一个 < 取消啦 因为格式会乱,相信各位看客也能理解
结束语 在此 算是结束啦 在这里附上源码链接,源码下载
参考文献https://developer.android.com/reference/android/text/Spannable.html https://developer.android.com/reference/android/text/style/CharacterStyle.html https://developer.android.com/reference/android/text/style/ParagraphStyle.html https://github.com/Manabu-GT/ExpandableTextView
http://www.jianshu.com/p/84067ad289d2 http://www.jianshu.com/p/aa53ee98d954 http://2960629.blog.51cto.com/2950629/751360 https://juejin.im/entry/5729d28f1ea49300606854c9