因为公司项目需要全文收起的功能,一共有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链接