24 | * 两种模式,三种对齐选择和一个padding值
25 | */
26 | public class ExpandTextView extends TextView implements View.OnClickListener {
27 |
28 | private static final int MAX_COLLAPSED_LINES = 8;// The default number of lines 默认显示行数为8行
29 | private static final int DEFAULT_ANIM_DURATION = 300; // The default animation duration 默认动画时长为300ms
30 | private static final float DEFAULT_ANIM_ALPHA_START = 0.7f;// The default alpha value when the animation starts
31 |
32 | private int mMaxCollapsedLines = 8;//最大显示行数
33 | private int mAnimationDuration;
34 | private float mAnimAlphaStart;
35 | private Drawable mExpandDrawable;//展开前显示图片
36 | private Drawable mCollapseDrawable;//展开后图片
37 |
38 | private int mCollapsedHeight;
39 | private int mTextHeightWithMaxLines;
40 |
41 | private boolean mCollapsed = true; // Show short version as default.标示现在所处的折叠状态
42 | private boolean mAnimating = false;
43 | private boolean needCollapse = true; //标示是否需要折叠已显示末尾的图标
44 |
45 |
46 | private int mDrawableSize = 0;
47 |
48 |
49 | /**
50 | * 表示箭头对齐方式,靠左/上,右/下,还是居中
51 | */
52 | private static final int ALIGN_RIGHT_BOTTOM = 0;
53 | private static final int ALIGN_LEFT_TOP = 1;
54 | private static final int ALIGN_CENTER = 2;
55 | private int arrowAlign = ALIGN_RIGHT_BOTTOM;
56 |
57 | /**
58 | * 表示箭头显示位置,在文字右边还是在文字下边
59 | */
60 | private static final int POSITION_RIGHT = 0;
61 | private static final int POSITION_BELOW = 1;
62 | private int arrowPosition = POSITION_RIGHT;
63 |
64 | /**
65 | * 箭头图标和文字的距离
66 | */
67 | private int arrowDrawablePadding = 0;
68 |
69 | /* Listener for callback */
70 | private OnExpandStateChangeListener mListener;
71 |
72 |
73 | public ExpandTextView(Context context) {
74 | this(context, null);
75 | }
76 |
77 | public ExpandTextView(Context context, AttributeSet attrs) {
78 | this(context, attrs, 0);
79 | }
80 |
81 | public ExpandTextView(Context context, AttributeSet attrs, int defStyleAttr) {
82 | super(context, attrs, defStyleAttr);
83 |
84 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ExpandTextView, defStyleAttr, 0);
85 | mMaxCollapsedLines = typedArray.getInt(R.styleable.ExpandTextView_maxCollapsedLines, MAX_COLLAPSED_LINES);
86 | mAnimationDuration = typedArray.getInt(R.styleable.ExpandTextView_animDuration, DEFAULT_ANIM_DURATION);
87 | mAnimAlphaStart = typedArray.getFloat(R.styleable.ExpandTextView_animAlphaStart, DEFAULT_ANIM_ALPHA_START);
88 | mExpandDrawable = typedArray.getDrawable(R.styleable.ExpandTextView_expandDrawable);
89 | mCollapseDrawable = typedArray.getDrawable(R.styleable.ExpandTextView_collapseDrawable);
90 | arrowAlign = typedArray.getInteger(R.styleable.ExpandTextView_arrowAlign, ALIGN_RIGHT_BOTTOM);
91 | arrowPosition = typedArray.getInteger(R.styleable.ExpandTextView_arrowPosition, POSITION_RIGHT);
92 | arrowDrawablePadding = (int) typedArray.getDimension(R.styleable.ExpandTextView_arrowPadding, DensityUtil.dp2px(context, 2f));
93 |
94 | typedArray.recycle();
95 |
96 | if (mExpandDrawable == null) {
97 | mExpandDrawable = getDrawable(getContext(), R.drawable.ic_expand_small_holo_light);
98 | }
99 | if (mCollapseDrawable == null) {
100 | mCollapseDrawable = getDrawable(getContext(), R.drawable.ic_collapse_small_holo_light);
101 | }
102 |
103 | setClickable(true);
104 | setOnClickListener(this);
105 | }
106 |
107 | private boolean isDrawablePaddingResolved = false;
108 |
109 | @Override
110 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
111 | if (getVisibility() == GONE || mAnimating) {
112 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
113 | return;
114 | }
115 |
116 | //重置高度重新测量
117 | getLayoutParams().height = -2;//设置为wrap_content,重新measure
118 | setMaxLines(Integer.MAX_VALUE);
119 | //测量TextView总高度
120 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
121 | if (getLineCount() <= mMaxCollapsedLines) {
122 | needCollapse = false;
123 | return;
124 | }
125 |
126 | needCollapse = true;
127 |
128 | mTextHeightWithMaxLines = getRealTextViewHeight(this);
129 | if (mCollapsed) {
130 | setMaxLines(mMaxCollapsedLines);
131 | }
132 |
133 | mDrawableSize = mExpandDrawable.getIntrinsicWidth();
134 | if (!isDrawablePaddingResolved) {
135 | if (arrowPosition == POSITION_RIGHT) {
136 | setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight() + mDrawableSize + arrowDrawablePadding, getPaddingBottom());
137 | } else {
138 | setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom() + mExpandDrawable.getIntrinsicHeight() + arrowDrawablePadding);
139 | }
140 | isDrawablePaddingResolved = true;
141 | }
142 |
143 | //设置完成后重新测量
144 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
145 | if (mCollapsed) {
146 | mCollapsedHeight = getMeasuredHeight();
147 | }
148 |
149 | }
150 |
151 | @Override
152 | protected void onDraw(Canvas canvas) {
153 | super.onDraw(canvas);
154 |
155 | if (needCollapse) {
156 | int left, top;
157 | if (arrowPosition == POSITION_RIGHT) {
158 | left = getWidth() - getTotalPaddingRight() + arrowDrawablePadding;
159 | switch (arrowAlign) {
160 | case ALIGN_LEFT_TOP:
161 | top = getTotalPaddingTop();
162 | break;
163 | case ALIGN_CENTER:
164 | top = (getHeight() - mExpandDrawable.getIntrinsicHeight()) / 2;
165 | break;
166 | case ALIGN_RIGHT_BOTTOM:
167 | default:
168 | top = getHeight() - getTotalPaddingBottom() - mExpandDrawable.getIntrinsicHeight();
169 | break;
170 | }
171 | } else {
172 | top = getHeight() - getTotalPaddingBottom() + arrowDrawablePadding;
173 | switch (arrowAlign) {
174 | case ALIGN_LEFT_TOP:
175 | left = getTotalPaddingLeft();
176 | break;
177 | case ALIGN_CENTER:
178 | left = (getWidth() - mExpandDrawable.getIntrinsicWidth()) / 2;
179 | break;
180 | case ALIGN_RIGHT_BOTTOM:
181 | default:
182 | left = getWidth() - getTotalPaddingRight() - mExpandDrawable.getIntrinsicWidth();
183 | break;
184 | }
185 | }
186 | canvas.translate(left, top);
187 |
188 | if (mCollapsed) {
189 | mExpandDrawable.setBounds(0, 0, mExpandDrawable.getIntrinsicWidth(), mExpandDrawable.getIntrinsicHeight());
190 | mExpandDrawable.draw(canvas);
191 | } else {
192 | mCollapseDrawable.setBounds(0, 0, mCollapseDrawable.getIntrinsicWidth(), mCollapseDrawable.getIntrinsicHeight());
193 | mCollapseDrawable.draw(canvas);
194 | }
195 | }
196 | }
197 |
198 | @Override
199 | public void setText(CharSequence text, BufferType type) {
200 | setCollapsed(true);
201 | super.setText(text, type);
202 | }
203 |
204 | @Override
205 | public void onClick(View v) {
206 | if (!needCollapse) {
207 | return;//行数不足,不响应点击事件
208 | }
209 | mCollapsed = !mCollapsed;
210 |
211 | Bitmap collapseBM = Bitmap.createBitmap(mCollapseDrawable.getIntrinsicWidth(), mCollapseDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
212 | Canvas cv2 = new Canvas(collapseBM);
213 | mCollapseDrawable.setBounds(0, 0, mCollapseDrawable.getIntrinsicWidth(), mCollapseDrawable.getIntrinsicHeight());
214 | mCollapseDrawable.draw(cv2);
215 |
216 | ImageSpan isExpand = new ImageSpan(mExpandDrawable);
217 | ImageSpan isCollapse = new ImageSpan(getContext(), collapseBM);
218 |
219 | SpannableString spannableString = new SpannableString("icon");
220 | spannableString.setSpan(mCollapsed ? isExpand : isCollapse, 0, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
221 |
222 | // mark that the animation is in progress
223 | mAnimating = true;
224 |
225 | Animation animation;
226 | if (mCollapsed) {
227 | animation = new ExpandCollapseAnimation(this, getHeight(), mCollapsedHeight);
228 | } else {
229 | animation = new ExpandCollapseAnimation(this, getHeight(), mTextHeightWithMaxLines);
230 | }
231 |
232 | animation.setFillAfter(true);
233 | animation.setAnimationListener(new Animation.AnimationListener() {
234 | @Override
235 | public void onAnimationStart(Animation animation) {
236 | if (mListener != null) {
237 | mListener.onChangeStateStart(!mCollapsed);
238 | }
239 | applyAlphaAnimation(ExpandTextView.this, mAnimAlphaStart);
240 | }
241 |
242 | @Override
243 | public void onAnimationEnd(Animation animation) {
244 | // clear animation here to avoid repeated applyTransformation() calls
245 | clearAnimation();
246 | // clear the animation flag
247 | mAnimating = false;
248 |
249 | // notify the listener
250 | if (mListener != null) {
251 | mListener.onExpandStateChanged(ExpandTextView.this, !mCollapsed);
252 | }
253 | }
254 |
255 | @Override
256 | public void onAnimationRepeat(Animation animation) {
257 | }
258 | });
259 |
260 | clearAnimation();
261 | startAnimation(animation);
262 | }
263 |
264 | private class ExpandCollapseAnimation extends Animation {
265 | private final View mTargetView;
266 | private final int mStartHeight;
267 | private final int mEndHeight;
268 |
269 | public ExpandCollapseAnimation(View view, int startHeight, int endHeight) {
270 | mTargetView = view;
271 | mStartHeight = startHeight;
272 | mEndHeight = endHeight;
273 | setDuration(mAnimationDuration);
274 | }
275 |
276 | @Override
277 | protected void applyTransformation(float interpolatedTime, Transformation t) {
278 | final int newHeight = (int) ((mEndHeight - mStartHeight) * interpolatedTime + mStartHeight);
279 | mTargetView.getLayoutParams().height = newHeight;
280 | setMaxHeight(newHeight);
281 | if (Float.compare(mAnimAlphaStart, 1.0f) != 0) {
282 | applyAlphaAnimation(ExpandTextView.this, mAnimAlphaStart + interpolatedTime * (1.0f - mAnimAlphaStart));
283 | }
284 | }
285 |
286 | @Override
287 | public void initialize(int width, int height, int parentWidth, int parentHeight) {
288 | super.initialize(width, height, parentWidth, parentHeight);
289 | }
290 |
291 | @Override
292 | public boolean willChangeBounds() {
293 | return true;
294 | }
295 | }
296 |
297 |
298 | private Drawable getDrawable(Context context, int drawableResId) {
299 | Resources resources = context.getResources();
300 | if (isPostLolipop()) {
301 | return resources.getDrawable(drawableResId, context.getTheme());
302 | } else {
303 | return resources.getDrawable(drawableResId);
304 | }
305 | }
306 |
307 | @TargetApi(Build.VERSION_CODES.HONEYCOMB)
308 | private void applyAlphaAnimation(View view, float alpha) {
309 | if (isPostHoneycomb()) {
310 | view.setAlpha(alpha);
311 | } else {
312 | AlphaAnimation alphaAnimation = new AlphaAnimation(alpha, alpha);
313 | // make it instant
314 | alphaAnimation.setDuration(0);
315 | alphaAnimation.setFillAfter(true);
316 | view.startAnimation(alphaAnimation);
317 | }
318 | }
319 |
320 | private boolean isPostHoneycomb() {
321 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
322 | }
323 |
324 | private boolean isPostLolipop() {
325 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
326 | }
327 |
328 | private int getRealTextViewHeight(TextView textView) {
329 | int textHeight = textView.getLayout().getLineTop(textView.getLineCount());
330 | int padding = textView.getCompoundPaddingTop() + textView.getCompoundPaddingBottom();
331 | return textHeight + padding;
332 | }
333 |
334 |
335 | public void setCollapsed(boolean isCollapsed) {
336 | mCollapsed = isCollapsed;
337 | }
338 |
339 |
340 | public interface OnExpandStateChangeListener {
341 | void onChangeStateStart(boolean willExpand);
342 | /**
343 | * Called when the expand/collapse animation has been finished
344 | *
345 | * @param textView - TextView being expanded/collapsed
346 | * @param isExpanded - true if the TextView has been expanded
347 | */
348 | void onExpandStateChanged(TextView textView, boolean isExpanded);
349 | }
350 |
351 | public void setOnExpandStateChangeListener(OnExpandStateChangeListener listener) {
352 | mListener = listener;
353 | }
354 | }
355 |
--------------------------------------------------------------------------------
/extextview/src/main/res/drawable-xhdpi/ic_collapse_small_holo_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcodecorex/ExpandTextView/c50ab3bba30969994a470bd75063ec6a8f705737/extextview/src/main/res/drawable-xhdpi/ic_collapse_small_holo_light.png
--------------------------------------------------------------------------------
/extextview/src/main/res/drawable-xhdpi/ic_expand_small_holo_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcodecorex/ExpandTextView/c50ab3bba30969994a470bd75063ec6a8f705737/extextview/src/main/res/drawable-xhdpi/ic_expand_small_holo_light.png
--------------------------------------------------------------------------------
/extextview/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |