http://www.tuicool.com/articles/NNVZzuU
http://www.tuicool.com/articles/NNVZzuU
http://www.tuicool.com/articles/NNVZzuU
http://www.tuicool.com/articles/NNVZzuU
Android自定义控件之全文收起TextView(控件嵌套法)
时间 2016-06-29 23:04:20 Raye Blog
原文
http://www.raye.wang/2016/06/29/androidzi-ding-yi-kong-jian-zhi-quan-wen-shou-qi-textview-kong-jian-qian-tao-fa/
主题 TextView
前言
因为公司项目需要全文收起的功能,一共有2种UI,所以需要写2个全文收起的控件,之前也是用过一个全文收起的TextView控件,但是因为设计原因,在ListView刷新的时候会闪烁,我估计原因是因为控件本身的设计是需要先让TextView绘制完成,然后获取TextView一共有多少行,再判断是否需要全文收起按钮,如果需要,则吧TextView压缩回最大行数,添加全文按钮,这样就会造成ListView的Item先高后低,所以会发生闪烁,后面我也在网上找了几个,发现和之前的设计都差不多,虽然肯定是有解决了这个问题的控件,但是还是决定自己写了,毕竟找到控件后还需要测试,而现在的项目时间不充分啊(另外欢迎指教如何快速的找到自己需要的控件,有时候在Github上面搜索,都不知道具体该用什么关键字),而且自己写,也是一种锻炼。这里讲述的是布局式的实现,还有一个就直接继承TextView来实现那个会在下一篇文章讲述。
效果图
实现原理
其实很多全文收起的实现原理应该都差不多,首先外部是一个布局,里面放一个显示正文的TextView控件,设置文本后,判断正文TextView的控件到底有多少行,如果达到了全文收起的行数,则将TextView的高度修改为指定的行数高度,把状态设置为收起状态,并在布局中添加全文收起按钮,点击全文时,则把高度还原为控件本身的高度,把状态位置为全文状态,点击收起时,则把控件高度设置为指定行数的高度,状态设置为收起状态。
代码
package wang.raye.library.widge;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Build;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import android.widget.LinearLayout;
import android.widget.TextView;
import wang.raye.library.R;
/**
* 有全文和收起的TextView
* Created by Raye on 2016/6/24.
*/
public class MoreTextView extends LinearLayout {
/** TextView的实际高度*/
private int textViewHeight;
/** 默认全文的Text*/
private static final String EXPANDEDTEXT = "全文";
/** 默认收起的text*/
private static final String COLLAPSEDTEXT = "收起";
/** 全文的text*/
private String expandedText ;
/** 收起的text*/
private String collapsedText ;
/** 字体大小*/
private int textSize;
/** 字体颜色*/
private int textColor;
/** 超过多少行出现全文、收起按钮*/
private int trimLines;
/** 显示文本的TextView */
private TextView showTextView;
/** 全文和收起的TextView*/
private TextView collapseTextView;
/** 是否是收起状态,默认收起*/
private boolean collapsed = true;
public MoreTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context,attrs);
}
public MoreTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context,attrs);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public MoreTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initView(context,attrs);
}
private void initView(Context context,AttributeSet attrs){
showTextView = new TextView(context);
setOrientation(VERTICAL);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MoreTextView);
textColor = typedArray.getColor(R.styleable.MoreTextView_textColor, Color.GRAY);
textSize = typedArray.getDimensionPixelSize(R.styleable.MoreTextView_textSize,14);
expandedText = typedArray.getString(R.styleable.MoreTextView_expandedText);
if(TextUtils.isEmpty(expandedText)){
expandedText = EXPANDEDTEXT;
}
collapsedText = typedArray.getString(R.styleable.MoreTextView_collapsedText);
if(TextUtils.isEmpty(collapsedText)){
collapsedText = COLLAPSEDTEXT;
}
trimLines = typedArray.getInt(R.styleable.MoreTextView_trimLines,0);
typedArray.recycle();
showTextView.setTextSize(textSize);
showTextView.setTextColor(textColor);
addView(showTextView);
}
public void setText(CharSequence text){
globalLayout();
showTextView.setText(text);
}
/**
* 获取控件实际高度,并设置最大行数
*/
private void globalLayout() {
showTextView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
textViewHeight = showTextView.getHeight();
ViewTreeObserver obs = showTextView.getViewTreeObserver();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
obs.removeOnGlobalLayoutListener(this);
} else {
obs.removeGlobalOnLayoutListener(this);
}
TextPaint tp = showTextView.getPaint();
int allWidth = (int) tp.measureText(showTextView.getText().toString());
//计算总行数
int allLine = allWidth / showTextView.getWidth();
if(allWidth % showTextView.getWidth() == 0){
textViewHeight = showTextView.getLineHeight() * allLine;
}else{
allLine ++;
textViewHeight = showTextView.getLineHeight() * allLine;
}
if(trimLines > 0 && trimLines < allLine){
//需要全文和收起
if(collapsed) {
showTextView.setHeight(showTextView.getLineHeight() * trimLines);
}
if(collapseTextView == null) {
//全文和收起的textView
collapseTextView = new TextView(getContext());
collapseTextView.setTextSize(textSize);
collapseTextView.setTextColor(Color.BLUE);
collapseTextView.setText(expandedText);
LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT, Gravity.RIGHT | Gravity.BOTTOM);
collapseTextView.setLayoutParams(lp);
collapseTextView.setOnClickListener(collapseListener);
addView(collapseTextView);
}
}
}
});
}
private OnClickListener collapseListener = new OnClickListener() {
@Override
public void onClick(final View v) {
v.setEnabled(false);
final int startValue = showTextView.getHeight();
final int deltaValue ;
if(collapsed){
//是放大
deltaValue = textViewHeight - startValue;
}else{
deltaValue = showTextView.getLineHeight() * trimLines - startValue;
}
Animation animation = new Animation() {
protected void applyTransformation(float interpolatedTime, Transformation t) {
showTextView.setHeight((int) (startValue + deltaValue * interpolatedTime));
}
};
animation.setDuration(500);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
v.setEnabled(true);
collapsed = !collapsed;
collapseTextView.setText(collapsed?expandedText:collapsedText);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
showTextView.startAnimation(animation);
}
};
}
具体分析
初始化控件
private void initView(Context context,AttributeSet attrs){
showTextView = new TextView(context);
setOrientation(VERTICAL);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MoreTextView);
textColor = typedArray.getColor(R.styleable.MoreTextView_textColor, Color.GRAY);
textSize = typedArray.getDimensionPixelSize(R.styleable.MoreTextView_textSize,14);
expandedText = typedArray.getString(R.styleable.MoreTextView_expandedText);
if(TextUtils.isEmpty(expandedText)){
expandedText = EXPANDEDTEXT;
}
collapsedText = typedArray.getString(R.styleable.MoreTextView_collapsedText);
if(TextUtils.isEmpty(collapsedText)){
collapsedText = COLLAPSEDTEXT;
}
trimLines = typedArray.getInt(R.styleable.MoreTextView_trimLines,0);
typedArray.recycle();
showTextView.setTextSize(textSize);
showTextView.setTextColor(textColor);
addView(showTextView);
}
这里主要是获取自定义参数的属性,并且在布局中添加一个显示正文的TextView控件,以及设置控件相关属性
核心代码
/**
* 获取控件实际高度,并设置最大行数
*/
private void globalLayout() {
showTextView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
textViewHeight = showTextView.getHeight();
ViewTreeObserver obs = showTextView.getViewTreeObserver();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
obs.removeOnGlobalLayoutListener(this);
} else {
obs.removeGlobalOnLayoutListener(this);
}
TextPaint tp = showTextView.getPaint();
int allWidth = (int) tp.measureText(showTextView.getText().toString());
//计算总行数
int allLine = allWidth / showTextView.getWidth();
if(allWidth % showTextView.getWidth() == 0){
textViewHeight = showTextView.getLineHeight() * allLine;
}else{
allLine ++;
textViewHeight = showTextView.getLineHeight() * allLine;
}
if(trimLines > 0 && trimLines < allLine){
//需要全文和收起
if(collapsed) {
showTextView.setHeight(showTextView.getLineHeight() * trimLines);
}
if(collapseTextView == null) {
//全文和收起的textView
collapseTextView = new TextView(getContext());
collapseTextView.setTextSize(textSize);
collapseTextView.setTextColor(Color.BLUE);
collapseTextView.setText(expandedText);
LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT, Gravity.RIGHT | Gravity.BOTTOM);
collapseTextView.setLayoutParams(lp);
collapseTextView.setOnClickListener(collapseListener);
addView(collapseTextView);
}
}
}
});
}
这里主要是在GlobalLayoutListener监听中,获取控件的实际高度,因为第一次GlobalLayoutListener会在onDraw方法前面调用,所以不会造成闪烁,另外由于我之前试过获取一行中有多少个字符,发现TextView只有完全绘制成功后,获取到的每行字符才是正确的,所以我担心没有完成绘制完成后获取的行数也有误差,所以通过TextPaint来计算出文本总宽度,然后根据TextView宽度来计算出行数,最后判断总行数是否达到了需要收起的行数,如果达到了收起的行数,则设置textView的高度为行高*指定行数,因为没有padding等属性,所以不需要考虑,同时判断全文收起的按钮是否为空,为空就初始化控件,并添加到布局
点击事件
private OnClickListener collapseListener = new OnClickListener() {
@Override
public void onClick(final View v) {
v.setEnabled(false);
final int startValue = showTextView.getHeight();
final int deltaValue ;
if(collapsed){
//是放大
deltaValue = textViewHeight - startValue;
}else{
deltaValue = showTextView.getLineHeight() * trimLines - startValue;
}
Animation animation = new Animation() {
protected void applyTransformation(float interpolatedTime, Transformation t) {
showTextView.setHeight((int) (startValue + deltaValue * interpolatedTime));
}
};
animation.setDuration(500);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
v.setEnabled(true);
collapsed = !collapsed;
collapseTextView.setText(collapsed?expandedText:collapsedText);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
showTextView.startAnimation(animation);
}
};
这里是全文收起按钮的点击事件,获取控件目前的高度,同时判断目前的状态,根据状态判断是收起还是展开,获取应该添加的高度(收起的,高度是负数),同时设置动画,并启动动画, 动画过程中设置正文的高度。这样一个全文收起的TextView就实现了。
结语
当然这个控件是非常简陋的,而且还有一两个bug,大家可以猜一下到底是啥问题。另外,我想知道就是到底TextView绘制的时候能不能获取到正确的行数,以及为啥获取每行字数的时候会有误差,希望知道的解答一下,当然我自己也会查询资料了解,同时附上本控件源码和 demo github链接