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 | --------------------------------------------------------------------------------