前言
因为公司项目需要全文收起的功能,一共有2种UI,所以需要写2个全文收起的控件,之前也是用过一个全文收起的TextView控件,但是因为设计原因,在ListView刷新的时候会闪烁,我估计原因是因为控件本身的设计是需要先让TextView绘制完成,然后获取TextView一共有多少行,再判断是否需要全文收起按钮,如果需要,则吧TextView压缩回最大行数,添加全文按钮,这样就会造成ListView的Item先高后低,所以会发生闪烁,后面我也在网上找了几个,发现和之前的设计都差不多,虽然肯定是有解决了这个问题的控件,但是还是决定自己写了,毕竟找到控件后还需要测试,而现在的项目时间不充分啊(另外欢迎指教如何快速的找到自己需要的控件,有时候在Github上面搜索,都不知道具体该用什么关键字),而且自己写,也是一种锻炼。这里讲述的是布局式的实现,还有一个就直接继承TextView来实现那个会在下一篇文章讲述。Android自定义控件之全文收起TextView(继承TextView法)
效果图
实现原理
其实很多全文收起的实现原理应该都差不多,首先外部是一个布局,里面放一个显示正文的TextView控件,设置文本后,判断正文TextView的控件到底有多少行,如果达到了全文收起的行数,则将TextView的高度修改为指定的行数高度,把状态设置为收起状态,并在布局中添加全文收起按钮,点击全文时,则把高度还原为控件本身的高度,把状态位置为全文状态,点击收起时,则把控件高度设置为指定行数的高度,状态设置为收起状态。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
| 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.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;
public class MoreTextView extends LinearLayout {
private int textViewHeight;
private static final String EXPANDEDTEXT = "全文";
private static final String COLLAPSEDTEXT = "收起";
private String expandedText;
private String collapsedText;
private int textSize;
private int textColor;
private int trimLines;
private TextView showTextView;
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() { ViewTreeObserver obs = showTextView.getViewTreeObserver(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { obs.removeOnGlobalLayoutListener(this); } else { obs.removeGlobalOnLayoutListener(this); }
int allLine = showTextView.getLineCount();
textViewHeight = showTextView.getLineHeight() * allLine;
if (trimLines > 0 && trimLines < allLine) { if (collapsed) { showTextView.setHeight(showTextView.getLineHeight() * trimLines); }
if (collapseTextView == null) { 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); } }; }
|
这里主要是获取自定义参数的属性,并且在布局中添加一个显示正文的TextView控件,以及设置控件相关属性
核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
private void globalLayout() { showTextView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { ViewTreeObserver obs = showTextView.getViewTreeObserver(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { obs.removeOnGlobalLayoutListener(this); } else { obs.removeGlobalOnLayoutListener(this); }
int allLine = showTextView.getLineCount();
textViewHeight = showTextView.getLineHeight() * allLine;
if (trimLines > 0 && trimLines < allLine) { if (collapsed) { showTextView.setHeight(showTextView.getLineHeight() * trimLines); }
if (collapseTextView == null) { 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的高度为行高*指定行数,因为没有padding等属性,所以不需要考虑,同时判断全文收起的按钮是否为空,为空就初始化控件,并添加到布局
点击事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| 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绘制的时候能不能获取到正确的行数,因为我测试过程中发现在onGlobalLayout中获取的每行文字数量会多次变化,但是最后一次获取到的是正确的,所以想知道为啥获取每行字数的时候会有误差,希望知道的解答一下,当然我自己也会查询资料了解,同时附上本控件源码和demo github链接