Android,TextView优雅显示长文本、富文本
Android提供了TextView这个类作为Android开发当中展示文字的工作,最近笔者在做类似于一个展示类型的APP,发现TextView这个类真的有点力不从心,好多的功能都让笔者特别头疼,于是就有了今天这篇技术博客。
原生的APi当中提供了TextView这个控件供开发者使用,一般需求都不在话下,可以设置显示文本的字体大小,字体颜色以及字体的显示格式,如:密码格式、数字格式等等。但是在千变万化的开发当中,还是不能满足开发者的需求,比如控制下拉的长文本,以及富文本。我们先来看几种需求。
图1
图2
上面看到了几点需求,还有好多,笔者相信各位看客心中都懂,所以就不展示太多了,我们今天要做到就是通过几种方式来优化我们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开始定制
/**
* 用于显示长文本,可以展开的TextView
* Created by suansuan on 2017/9/18.
*/
public class PullDownTextView extends LinearLayout implements View.OnClickListener{
private TextView mTextView ;
private ImageButton mImageButton;
}
我们这里选择的是LinearLayout,原因就是我们TextView和Button排列方式是线性布局。
2、初始化
public class PullDownTextView extends LinearLayout implements View.OnClickListener{
private boolean isPull ;
private TextView mTextView ;
private ImageButton mImageButton;
/** ImageButton切换的两种图片 */
private Drawable mPullDownDrawable ;
private Drawable mUpDownDrawable ;
/** 初始化PullDownTextView */
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);
}
/** 当加载完XML布局时回调 */
@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、测量
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;
/** ImageButton切换的两种图片 */
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、点击事件
@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、开启动画
/**
* 开始动画
*/
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、回调接口
/** TextView展开回调 */
public interface OnTextViewPullListener{
void textViewPull(TextView textView, boolean isPull) ;
}
public void setOnTextViewPullListener(OnTextViewPullListener listener){
this.mOnTextViewPullListener= listener ;
}
7、在MainActivity当中使用
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编辑器所写的文本,每一个字都是带有格式的。我们看下面的一个例子就理解了什么是富文本。
Hello!
This is some bold text.
仔细观察,上述的一段文字是带有格式。这就是我们常见的富文本。现在我们看富文本的相应代码
{\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
public static class Factory {
private static Spannable.Factory sInstance = new Spannable.Factory();
/**
* Returns the standard Spannable Factory.
*/
public static Spannable.Factory getInstance() {
return sInstance;
}
/**
* Returns a new SpannableString from the specified CharSequence.
* You can override this to provide a different kind of Spannable.
*/
public Spannable newSpannable(CharSequence source) {
return new SpannableString(source);
}
}
Spannable里面定义了两个方法,分别是:
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内部又一个静态的工厂类,那我们就使用这个类来获取实例。
Spannable spannable = Spannable.Factory.getInstance().newSpannable(string);
1.2、Google为我们提供的样式
其实这里的样式是特别的多的,Google主要按照分类,分成了两大类,分别是字体的样式,和段落的样式。笔者在这里找几个常用的到的进行说明,其他的请各位看客自行去了解CharacterStyle, ParagraphStyle的实现子类,好找出看客所需要的样式。
1.3、颜色相关
颜色相关主要分为一个字体的颜色(ForegroundColorSpan),一个背景的颜色(BackgroundColorSpan)。
在这里我专门给测试TextView加入了背景和字体,我们发现,在背景方面,Span只能作用于Text的绘制区域。在字体颜色方面Span是优于我们设置的字体颜色的。
/***
* 颜色相关
* BackgroundColorSpan : 背景颜色样式
* ForegroundColorSpan : 字体颜色
*/
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),移动完成以后大小是不会变化的,上移距离为当前文本高度的一半,下一距离也是当前文本的一半。
/**
* 大小相关
* RelativeSizeSpan :显示大小
*
* 位置相关
* SuperscriptSpan :上移
* SubscriptSpan : 下移
*/
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(粗体、斜体)。
/***
* 样式相关:
* StrikethroughSpan : 删除线
* UnderlineSpan : 下划线
*
* StyleSpan : 一般样式
*/
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、跳转相关
常见的跳转相关有,点击事件,超链接。其实超链接的实现就是点击事件,只不过点击以后由当前手机的默认浏览器去打开。
/**
* ClickableSpan : 可点击的文字
*
*/
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、图片相关
在文字当中使用图片,其实这个我们可以联想一下社交软件当中的聊天表情。
在这里我们发现,是图片去替代了我们原有的文字,看客们在这里注意一下。
/**
* 图片相关
* ImageSpan
*/
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当中进行显示,
public void html(){
String html = "测
试
文
字
" ;
mTextView.setText(Html.fromHtml(html));
String html1 = "标题
" ;
mTextView1.setText(Html.fromHtml(html1));
String html2 = "测试文字" ;
mTextView2.setText(Html.fromHtml(html2));
}
但是TextView对HTML的支持不是很全,下面就把TextView对HTML的支持列举一下
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