热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Android,TextView优雅显示长文本、富文本

Android,TextView优雅显示长文本、富文本Android提供了TextView这个类作为Android开发当中展示文字的工作,最近笔者在做类似于一个展示类型的APP,发现TextV

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 ; //textView显示全部也就是下拉状态的高度
private int mTextViewNotPullHeight ;

private boolean isPull ; //是否处于展开状态。默认为隐藏
private boolean isReLayout ; //当前布局是否重新绘制。
private boolean isAnimator ; //是否处于动画当中
private boolean isMaxHeightMeasure; //是否进行TextView最大行数的测量
private boolean isMinHeightMeasure; //是否进行TexView可见行数的测量

private TextView mTextView ;
private ImageButton mImageButton;

/** ImageButton切换的两种图片 */
private Drawable mPullDownDrawable ;
private Drawable mUpDownDrawable ;

private int mTextVisibilityCount = 3; //隐藏时 TextView可以显示的最大的行数
private int mAnimatorDuration = 500 ;

```
/** 测量方发,测量自己的宽高,测量孩子的宽高 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//没有内容的时候,
if(!isReLayout || getVisibility() == View.GONE){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return ;
}

//有内容,但是内容比较短的时候,正常显示TextView,但是相应的隐藏ImageButton
if(mTextView.getLineCount() <= mTextVisibilityCount){
mTextView.setVisibility(View.VISIBLE);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return ;
}

//有内容,并且显示的内容比较长的时候,这里我们显示TextView、ImageButton。
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 ;
//这句,让TextView文本的高度随TextView高度进行变化
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是一个接口,有两个实现类分别是SpannableStringSpannableStringBuilder,我们知道我们以后要使用的话,肯定就是这里面的这两个类啦。那么这两个类有什么区别呢,其实和我们早前学过的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


推荐阅读
author-avatar
mobiledu2502853397
这个家伙很懒,什么也没留下!
Tags | 热门标签
RankList | 热门文章
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有