这个自定义TextView可以实现内部文字的折叠和扩展显示,效果如下:
有兴趣的同学可以自行点击下载源码,里面的注释已经写的很完善了。这里我将我的代码整理分享出来:
首先需要定义ExpandableTextView的属性资源文件
attrs.xml
:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TextViewExpandable">
<attr name="tvea_expandLines" format="integer"/>
<attr name="tvea_shrinkBitmap" format="reference" />
<attr name="tvea_expandBitmap" format="reference" />
<attr name="tvea_textStateColor" format="color" />
<attr name="tvea_textContentColor" format="color" />
<attr name="tvea_textContentSize" format="dimension" />
<attr name="tvea_textShrink" format="string" />
<attr name="tvea_textExpand" format="string" />
</declare-styleable>
</resources>
各属性的意义通过名称大家应该也都能理解出来,论学好英语的重要性啊......扯远了,接下来需要给ExpandableTextView指定布局文件layout_textview_expand_animation.xml
代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/ll_text_expand_animation_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tv_expand_text_view_animation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/color_gray_light_content_text"
android:textSize="@dimen/sp_txt_size_content"
tools:text="@string/tips" />
<RelativeLayout
android:id="@+id/rl_expand_text_view_animation_toggle_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
<ImageView
android:id="@+id/iv_expand_text_view_animation_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
/>
<TextView
android:id="@+id/tv_expand_text_view_animation_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toLeftOf="@+id/iv_expand_text_view_animation_toggle"
android:textColor="@color/colorPrimary"
android:textSize="@dimen/sp_txt_size_content"
tools:text="全部" />
<View
android:layout_width="match_parent"
android:layout_height="0.1dp"
android:layout_centerVertical="true"
android:layout_marginLeft="10dp"
android:layout_toLeftOf="@id/tv_expand_text_view_animation_hint"
android:background="@color/color_divider_line_gray" />
</RelativeLayout>
</LinearLayout>
好接下来就是我们的主题了,ExpandableTextView类的代码编写了。代码如下,我已经将需要注释的地方都进行了注释。
public class TextViewExpandable extends LinearLayout implements View.OnClickListener{
/**
* TextView
*/
private TextView textView;
/**
* 收起/全部TextView
* <br>shrink/expand TextView
*/
private TextView tvState;
/**
* 点击进行折叠/展开的图片
* <br>shrink/expand icon
*/
private ImageView ivExpandOrShrink;
/**
* 底部是否折叠/收起的父类布局
* <br>shrink/expand layout parent
*/
private RelativeLayout rlToggleLayout;
/**
* 提示折叠的图片资源
* <br>shrink drawable
*/
private Drawable drawableShrink;
/**
* 提示显示全部的图片资源
* <br>expand drawable
*/
private Drawable drawableExpand;
/**
* 全部/收起文本的字体颜色
* <br>color of shrink/expand text
*/
private int textViewStateColor;
/**
* 展开提示文本
* <br>expand text
*/
private String textExpand;
/**
* 收缩提示文本
* <br>shrink text
*/
private String textShrink;
/**
* 是否折叠显示的标示
* <br>flag of shrink/expand
*/
private boolean isShrink = false;
/**
* 是否需要折叠的标示
* <br>flag of expand needed
*/
private boolean isExpandNeeded = false;
/**
* 是否初始化TextView
* <br>flag of TextView Initialization
*/
private boolean isInitTextView = true;
/**
* 折叠显示的行数
* <br>number of lines to expand
*/
private int expandLines;
/**
* 文本的行数
* <br>Original number of lines
*/
private int textLines;
/**
* 显示的文本
* <br>content text
*/
private CharSequence textContent;
/**
* 显示的文本颜色
* <br>content color
*/
private int textContentColor;
/**
* 显示的文本字体大小
* <br>content text size
*/
private float textContentSize;
/**
* 动画线程
* <br>thread
*/
private Thread thread;
/**
* 动画过度间隔
* <br>animation interval
*/
private int sleepTime = 22;
/**
* handler信号
* <br>handler signal
*/
private final int WHAT = 2;
/**
* 动画结束信号
* <br>animation end signal of handler
*/
private final int WHAT_ANIMATION_END = 3;
/**
* 动画结束,只是改变图标,并不隐藏
* <br>animation end and expand only,but not disappear
*/
private final int WHAT_EXPAND_ONLY = 4;
public TextViewExpandable(Context context) {
this(context, null);
}
public TextViewExpandable(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TextViewExpandable(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initValue(context, attrs);
initView(context);
initClick();
}
private void initValue(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TextViewExpandable);
expandLines = typedArray.getInteger(
R.styleable.TextViewExpandable_tvea_expandLines, 6);
drawableShrink = typedArray.getDrawable(
R.styleable.TextViewExpandable_tvea_shrinkBitmap);
drawableExpand = typedArray
.getDrawable(R.styleable.TextViewExpandable_tvea_expandBitmap);
// 设置右下角显示状态的文字颜色
textViewStateColor = typedArray.getColor(
R.styleable.TextViewExpandable_tvea_textStateColor, ContextCompat.getColor(context, R.color.colorPrimary));
textShrink = typedArray.getString(R.styleable.TextViewExpandable_tvea_textShrink);
textExpand = typedArray.getString(R.styleable.TextViewExpandable_tvea_textExpand);
// 设置默认值
if (drawableShrink == null) {
// 支持包的获取Drawable资源的方法
drawableShrink = ContextCompat.getDrawable(context, R.drawable.icon_green_arrow_up);
}
if (drawableExpand == null) {
drawableExpand = ContextCompat.getDrawable(context, R.drawable.icon_green_arrow_down);
}
if (TextUtils.isEmpty(textShrink)) {
textShrink = context.getString(R.string.shrink);
}
if (TextUtils.isEmpty(textExpand)) {
textExpand = context.getString(R.string.expand);
}
textContentColor = typedArray.getColor(
R.styleable.TextViewExpandable_tvea_textContentColor, ContextCompat.getColor(context, R.color.color_gray_light_content_text));
textContentSize = typedArray.getDimension(R.styleable.TextViewExpandable_tvea_textContentSize, 14);
typedArray.recycle();
}
private void initView(Context context) {
// 得到系统的布局解析器
LayoutInflater inflate = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View layout = inflate.inflate(R.layout.layout_textview_expand_animation, this);
rlToggleLayout = (RelativeLayout) layout.findViewById(R.id.rl_expand_text_view_animation_toggle_layout);
textView = (TextView) layout.findViewById(R.id.tv_expand_text_view_animation);
textView.setTextColor(textContentColor);
textView.getPaint().setTextSize(textContentSize);
ivExpandOrShrink = (ImageView) layout.findViewById(R.id.iv_expand_text_view_animation_toggle);
tvState = (TextView) layout.findViewById(R.id.tv_expand_text_view_animation_hint);
tvState.setTextColor(textViewStateColor);
}
/**
* 设置显示文本的TextView和显示底部标志的Layout设置监听
*/
private void initClick() {
textView.setOnClickListener(this);
rlToggleLayout.setOnClickListener(this);
}
/**
*
* @param view
*/
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.tv_expand_text_view_animation:
clickImageToggle();
break;
case R.id.rl_expand_text_view_animation_toggle_layout:
clickImageToggle();
break;
}
}
private void clickImageToggle() {
if (isShrink) {// 如果是折叠状态进行非折叠处理
doAnimation(expandLines, textLines, WHAT_EXPAND_ONLY);
} else {
doAnimation(textLines, expandLines, WHAT_EXPAND_ONLY);
}
isShrink = !isShrink;
}
/**
* 对外设置文本的方法
* @param charSequence:是String的父接口,就是字符序列
*/
public void setText(CharSequence charSequence) {
textContent = charSequence;
// 设置显示的TextView显示文本
textView.setText(charSequence);
// A view tree observer is used to register listeners that can be notified of global changes in the view tree.
// 这是一个注册监听视图树的观察者(observer),在视图树种全局事件改变时得到通知。
// 这里指的全局 事件包括而且不局限在以下几个:整个视图树的布局变化,开始绘制视图,触摸模式改变等等。
ViewTreeObserver viewTreeObserver = textView.getViewTreeObserver();
// Register a callback to be invoked when the view tree is about to be drawn.
viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {// 返回true代表继续当前绘制,false代表取消
// 判断该View是否应该初始化
if (!isInitTextView) {
return true;
}
// 未初始化进行初始化
isInitTextView = false;
// 得到当前文本的总行数
textLines = textView.getLineCount();
// 设置是否需要显示扩展(总行数与当前显示的行数作比较)
isExpandNeeded = textLines > expandLines;
if (isExpandNeeded) {
// 是否启用折叠标志
isShrink = true;
// 调用动画
doAnimation(textLines, expandLines, WHAT_ANIMATION_END);
} else {
isShrink = false;
doNotExpand();
}
return true;
}
});
}
/**
* 处理消息
*/
//Indicates that Lint should ignore the specified warnings for the annotated element.
@SuppressLint("HandlerLeak")
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case WHAT:
// 不断修改当前TextView的最大行数
textView.setMaxLines(msg.arg1);
textView.invalidate();
break;
case WHAT_EXPAND_ONLY:
changeExpandState(msg.arg1);
break;
case WHAT_ANIMATION_END:
setExpandState(msg.arg1);
break;
}
}
};
/**
* @param startLines 开始动画的起点行数 <br> start index of animation
* @param endLines 结束动画的终点行数 <br> end index of animation
* @param what 动画结束后的handler信号标示 <br> signal of animation end
*/
private void doAnimation(final int startLines, final int endLines, final int what) {
new Thread(new Runnable() {
@Override
public void run() {
// 如果起始行大于终止行往上折叠到终止行
if (startLines > endLines) {
int count = startLines;
while (count-- > endLines) {
Message msg = handler.obtainMessage(WHAT, count, 0);
// 休眠一定时刻
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
handler.sendMessage(msg);
}
} else if (startLines < endLines) {
// 如果起始行小于终止行向下扩展到终止行
int count = startLines;
while (count++ < endLines) {
Message msg = handler.obtainMessage(WHAT, count, 0);
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
handler.sendMessage(msg);
}
}
// 动画结束后发送结束的信号
// animation end,send signal
Message msg = handler.obtainMessage(what, endLines, 0);
handler.sendMessage(msg);
}
}).start();
}
/**
* 负责改变折叠或展开状态的方法
* @param endLines
*/
private void changeExpandState(int endLines) {
rlToggleLayout.setVisibility(VISIBLE);
if (endLines > expandLines) {// 显示展开的状态
ivExpandOrShrink.setBackground(drawableShrink);
tvState.setText(textShrink);
} else {// 显示折叠的状态
ivExpandOrShrink.setBackground(drawableExpand);
tvState.setText(textExpand);
}
}
/**
* 设置折叠或展开的状态方法
* @param endLines
*/
private void setExpandState(int endLines) {
if (endLines < textLines) {// 小于总行数,设置为可以折叠或扩展状态
isShrink = true;
rlToggleLayout.setVisibility(VISIBLE);
ivExpandOrShrink.setBackground(drawableExpand);
textView.setOnClickListener(this);
tvState.setText(textExpand);
} else {// 设置为不显示折叠状态
Log.e("xns", "not show shrink");
isShrink = false;
rlToggleLayout.setVisibility(GONE);
ivExpandOrShrink.setBackground(drawableShrink);
textView.setOnClickListener(null);
tvState.setText(textShrink);
}
}
/**
* 无需折叠
* do not expand
*/
private void doNotExpand() {
textView.setMaxLines(expandLines);
rlToggleLayout.setVisibility(GONE);
textView.setOnClickListener(null);
}
public Drawable getDrawableShrink() {
return drawableShrink;
}
public void setDrawableShrink(Drawable drawableShrink) {
this.drawableShrink = drawableShrink;
}
public Drawable getDrawableExpand() {
return drawableExpand;
}
public void setDrawableExpand(Drawable drawableExpand) {
this.drawableExpand = drawableExpand;
}
public int getExpandLines() {
return expandLines;
}
public void setExpandLines(int newExpandLines) {
int startLines = isShrink ? expandLines : textLines;
int endLines = newExpandLines > textLines ? newExpandLines : textLines;
doAnimation(startLines, endLines, WHAT_ANIMATION_END);
expandLines = newExpandLines;
}
/**
* 取得显示的文本内容
* get content text
*
* @return content text
*/
public CharSequence getTextContent() {
return textContent;
}
public int getSleepTime() {
return sleepTime;
}
public void setSleepTime(int sleepTime) {
this.sleepTime = sleepTime;
}
}
- 整体设计的精髓即是通过handle接受Message后,不断的修改内部TextView的最大行数,然后调用该TextView的初始化方法,来完成动画的显示。
- 这里还涉及到一个新的知识点即ViewTreeObserver类,简单来说它是一个监听ViewTree的观察者,在视图树中全局事件改变时得到通知。全局事件包括但不局限于如:整个视图树的布局变化,开始绘制视图,触摸模式改变等。
ViewTreeObserver是不能被应用程序实例化的,因为它是由视图提供的,通过view.getViewTreeObserver()获取。
网上有很多关于这个类的介绍,这里我推荐一篇博文,你可以大致了解这个类的一些基本知识。
其余都是一些逻辑上的代码,通过注释我相信大家都可以读懂它的实现方法。朝阳在前方,同志们继续前进吧。