开源项目ExpandableTextView使用和源码分析
2016-08-13 00:34
519 查看
ExpandableTextView,可展开和收起的TextView,就像GooglePlay里面显示应用的描述那样。
项目地址:
https://github.com/Manabu-GT/ExpandableTextView
其中包含项目源码和示例代码。
运行效果图:
在国内很多应用中,也可以看到这种效果的使用场景。如豌豆荚的详情页:
一、项目使用
(1).在工程的build.gradle文件中添加项目引用。
该项目较简单,java代码部分只有ExpandableTextView.java一个文件。
注意,TextView的id只能定义为expandable_text,ImageButton的id只能定义为expand_collapse。
二、源码分析
实现原理:
该View扩展自LinearLayout,由一个TextView和一个ImageButton组成,垂直排列。初始时,限定TextView的maxHeight使内容未完全显示,此时处于“收起”状态。当被点击时,为View添加自定义动画,在动画执行过程中,不断的修改TextView的maxHeight和整个View的height,达到“展开”和“收起”的效果。
下面进入代码部分。其核心在于自定义动画的实现,和onMeasure()、onClick()方法。
(1).类的定义。
ExpandableTextView继承自LinearLayout类,实现了OnClickListener接口,监听自身的点击事件,处理“展开”和“收起”。
(2).成员变量介绍。
(3).构造方法。
在构造方法中,调用init()方法,执行初始化操作。包括获取布局文件中的自定义属性,设置默认按钮图标,设置方向,限定布局的方向只能为垂直。
(4).onFinishInflate()方法。
当xml布局文件加载完成之后,后执行onFinishInflate()方法,在这里完成View的初始化。
(5).核心之onMeasure()方法。
首先是条件判断,当View没有设置内容或内容为空,或者内容本身较短无需处理展开和收起,此时隐藏箭头按钮,直接return代码不再往下执行。
然后,通过setMaxLines()缩小TextView的显示行数,计算TextView显示全部内容时的高度、整个LinearLayout的高度、TextView的bottomMargin值,并保存起来再之后执行动画时使用。
(6).核心之自定义动画的实现。
继承Animation类实现动画,主要是重写Animation类的applyTransformation(float interpolatedTime, Transformation t)方法来实现自定义动画效果。要点:动画在执行过程中会反复的调用applyTransformation()方法,每次调用时参数interpolatedTime都在变化,从0逐渐递增为1,当该参数为1时表明动画结束。利用interpolatedTime参数,我们可以逐步改变TextView的高度、TextView的透明度和整个LinearLayout的高度,来达到展开和收起的效果。
(7).核心之onClick()方法。
触发点击时,Animation开始执行,主要是ExpandCollapseAnimation在构造时startHeight和endHeight的计算。执行“收起”动画时,从当前高度开始,降低到收起状态下的高度mCollapsedHeight;执行“展开”动画时,在当前高度的基础上,增加TextView变化的高度mTextHeightWithMaxLines - mTv.getHeight()。这里有个小细节,动画开始时标记mAnimating为true,结束时标记为false,标记mAnimating变量是为了在onInterceptTouchEvent()方法中使用。这样,在动画执行期间,可以拦截掉触摸事件,不传递给子View。
(8).其它方法之setText()。
一般情况下,只需调用setText(text)方法即可。
在ListView等存在item复用的情况下,需要调用setText(text,collapsedStatus,position)方法,这里使用了SparseBooleanArray记录当前位置的展开收起状态。以ListView为例,在实现BaseAdapter类的代码中,定义成员变量SparseBooleanArray collapsedStatus = new SparseBooleanArray(),当在getView()中调用setText(text,collapsedStatus,position)时,传入成员变量collapsedStatus和当前的position。
(9).其它方法之setOnExpandStateChangeListener()。
设置状态(“展开”、“收起”)改变的监听,可以根据实际需求决定是否使用该方法。
(10).其它方法之setOrientation()。
在setOrientation()方法中,限定了当前布局的方向只能为垂直。
项目地址:
https://github.com/Manabu-GT/ExpandableTextView
其中包含项目源码和示例代码。
运行效果图:
在国内很多应用中,也可以看到这种效果的使用场景。如豌豆荚的详情页:
一、项目使用
(1).在工程的build.gradle文件中添加项目引用。
该项目较简单,java代码部分只有ExpandableTextView.java一个文件。
dependencies { compile 'com.ms-square:expandableTextView:0.1.4' }(2).在布局中添加如下代码。
注意,TextView的id只能定义为expandable_text,ImageButton的id只能定义为expand_collapse。
<com.ms.square.android.expandabletextview.ExpandableTextView android:id="@+id/expand_text_view" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@id/expandable_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:layout_marginLeft="10dp" android:layout_marginRight="10dp" android:textSize="16sp" android:textColor="#666666" /> <ImageButton android:id="@id/expand_collapse" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="16dp" android:layout_gravity="right|bottom" android:background="@android:color/transparent"/> </com.ms.square.android.expandabletextview.ExpandableTextView>ExpandableTextView支持使用自定义属性。对应的attrs.xml文件及属性释义如下。
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="ExpandableTextView"> <!-- 默认折叠状态下,文字最多显示的行数 --> <attr name="maxCollapsedLines" format="integer"/> <!-- 动画时长 --> <attr name="animDuration" format="integer"/> <!-- 动画启动时文字的透明度 --> <attr name="animAlphaStart" format="float"/> <!-- 将文字展开的图标(向下的箭头) --> <attr name="expandDrawable" format="reference"/> <!-- 将文字收起的图标(向上的箭头) --> <attr name="collapseDrawable" format="reference"/> </declare-styleable> </resources>(3).添加Java代码。
ExpandableTextView expandableTextView = (ExpandableTextView) rootView.findViewById(R.id.expand_text_view); // 设置显示内容 expandableTextView.setText(""); // 设置状态监听 expandableTextView.setOnExpandStateChangeListener(new ExpandableTextView.OnExpandStateChangeListener() { @Override public void onExpandStateChanged(TextView textView, boolean isExpanded) { } });
二、源码分析
实现原理:
该View扩展自LinearLayout,由一个TextView和一个ImageButton组成,垂直排列。初始时,限定TextView的maxHeight使内容未完全显示,此时处于“收起”状态。当被点击时,为View添加自定义动画,在动画执行过程中,不断的修改TextView的maxHeight和整个View的height,达到“展开”和“收起”的效果。
下面进入代码部分。其核心在于自定义动画的实现,和onMeasure()、onClick()方法。
(1).类的定义。
ExpandableTextView继承自LinearLayout类,实现了OnClickListener接口,监听自身的点击事件,处理“展开”和“收起”。
public class ExpandableTextView extends LinearLayout implements View.OnClickListener { }
(2).成员变量介绍。
// 默认行数 private static final int MAX_COLLAPSED_LINES = 8; // 默认动画时长 private static final int DEFAULT_ANIM_DURATION = 300; // 动画启动时TextView的默认透明度 private static final float DEFAULT_ANIM_ALPHA_START = 0.7f; // 显示内容的TextView protected TextView mTv; // 箭头按钮ImageButton protected ImageButton mButton; // 是否需要重新布局。当调用了setText()方法后,该值置为true private boolean mRelayout; // 默认TextView处于收起状态 private boolean mCollapsed = true; // 收起状态下的整个View的高度 private int mCollapsedHeight; // TextView整个文本的高度 private int mTextHeightWithMaxLines; // 收起状态下的最大显示行数 private int mMaxCollapsedLines; // TextView的bottomMargin private int mMarginBetweenTxtAndBottom; // 箭头按钮的展开图标 private Drawable mExpandDrawable; // 箭头按钮的收起图标 private Drawable mCollapseDrawable; // 动画执行时长 private int mAnimationDuration; // 动画启动时显示内容的透明度 private float mAnimAlphaStart; // 是否正在执行动画 private boolean mAnimating; // 状态改变的监听 private OnExpandStateChangeListener mListener; // 如果是在ListView中,需要使用到mCollapsedStatus和mPosition,保存当前position的展开或收起状态 private SparseBooleanArray mCollapsedStatus; private int mPosition;
(3).构造方法。
在构造方法中,调用init()方法,执行初始化操作。包括获取布局文件中的自定义属性,设置默认按钮图标,设置方向,限定布局的方向只能为垂直。
public ExpandableTextView(Context context) { this(context, null); } public ExpandableTextView(Context context, AttributeSet attrs) { super(context, attrs); init(attrs); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public ExpandableTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(attrs); } private void init(AttributeSet attrs) { TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.ExpandableTextView); mMaxCollapsedLines = typedArray.getInt(R.styleable.ExpandableTextView_maxCollapsedLines, MAX_COLLAPSED_LINES); mAnimationDuration = typedArray.getInt(R.styleable.ExpandableTextView_animDuration, DEFAULT_ANIM_DURATION); mAnimAlphaStart = typedArray.getFloat(R.styleable.ExpandableTextView_animAlphaStart, DEFAULT_ANIM_ALPHA_START); mExpandDrawable = typedArray.getDrawable(R.styleable.ExpandableTextView_expandDrawable); mCollapseDrawable = typedArray.getDrawable(R.styleable.ExpandableTextView_collapseDrawable); if (mExpandDrawable == null) { mExpandDrawable = getDrawable(getContext(), R.drawable.ic_expand_more_black_12dp); } if (mCollapseDrawable == null) { mCollapseDrawable = getDrawable(getContext(), R.drawable.ic_expand_less_black_12dp); } typedArray.recycle(); // 强制把方向设置为垂直 setOrientation(LinearLayout.VERTICAL); // 默认不显示 setVisibility(GONE); }
(4).onFinishInflate()方法。
当xml布局文件加载完成之后,后执行onFinishInflate()方法,在这里完成View的初始化。
@Override protected void onFinishInflate() { findViews(); } private void findViews() { mTv = (TextView) findViewById(R.id.expandable_text); mTv.setOnClickListener(this); mButton = (ImageButton) findViewById(R.id.expand_collapse); mButton.setImageDrawable(mCollapsed ? mExpandDrawable : mCollapseDrawable); mButton.setOnClickListener(this); }
(5).核心之onMeasure()方法。
首先是条件判断,当View没有设置内容或内容为空,或者内容本身较短无需处理展开和收起,此时隐藏箭头按钮,直接return代码不再往下执行。
然后,通过setMaxLines()缩小TextView的显示行数,计算TextView显示全部内容时的高度、整个LinearLayout的高度、TextView的bottomMargin值,并保存起来再之后执行动画时使用。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 如果没有改变显示内容,或者显示内容为空,执行super.onMeasure()并返回 if (!mRelayout || getVisibility() == View.GONE) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); return; } mRelayout = false; // 先隐藏箭头按钮,将文字最大显示行数设置到最大,后面再根据测量情况修改 mButton.setVisibility(View.GONE); mTv.setMaxLines(Integer.MAX_VALUE); // 测量 super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 如果内容未超出限定的最大显示行数,直接返回。在这种情况下,无需展开和收起。 if (mTv.getLineCount() <= mMaxCollapsedLines) { return; } // 计算TextView的真实高度并保存 mTextHeightWithMaxLines = getRealTextViewHeight(mTv); // 限定TextView的最大行数,并将箭头按钮显示出来 if (mCollapsed) { mTv.setMaxLines(mMaxCollapsedLines); } mButton.setVisibility(View.VISIBLE); // 再次重新测量 super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mCollapsed) { mTv.post(new Runnable() { @Override public void run() { // TextView的bottomMargin mMarginBetweenTxtAndBottom = getHeight() - mTv.getHeight(); } }); // 保存收起状态下的整个View的高度 mCollapsedHeight = getMeasuredHeight(); } } // 获取TextView的真实高度,即内容展开时的高度 private static int getRealTextViewHeight(@NonNull TextView textView) { int textHeight = textView.getLayout().getLineTop(textView.getLineCount()); int padding = textView.getCompoundPaddingTop() + textView.getCompoundPaddingBottom(); return textHeight + padding; }
(6).核心之自定义动画的实现。
继承Animation类实现动画,主要是重写Animation类的applyTransformation(float interpolatedTime, Transformation t)方法来实现自定义动画效果。要点:动画在执行过程中会反复的调用applyTransformation()方法,每次调用时参数interpolatedTime都在变化,从0逐渐递增为1,当该参数为1时表明动画结束。利用interpolatedTime参数,我们可以逐步改变TextView的高度、TextView的透明度和整个LinearLayout的高度,来达到展开和收起的效果。
// 实现了展开/收起功能的动画 class ExpandCollapseAnimation extends Animation { private final View mTargetView; private final int mStartHeight; private final int mEndHeight; public ExpandCollapseAnimation(View view, int startHeight, int endHeight) { mTargetView = view; mStartHeight = startHeight; mEndHeight = endHeight; setDuration(mAnimationDuration); } @Override protected void applyTransformation(float interpolatedTime, Transformation t) { // 计算在执行动画时,逐渐变化产生的整个View的新高度 final int newHeight = (int)((mEndHeight - mStartHeight) * interpolatedTime + mStartHeight); // 修改TextView的最大高度 mTv.setMaxHeight(newHeight - mMarginBetweenTxtAndBottom); // 如果初始设置了透明度,修改TextView的透明度,直到alpha==1完全不透明 if (Float.compare(mAnimAlphaStart, 1.0f) != 0) { applyAlphaAnimation(mTv, mAnimAlphaStart + interpolatedTime * (1.0f - mAnimAlphaStart)); } // 修改整个View的高度 mTargetView.getLayoutParams().height = newHeight; mTargetView.requestLayout(); } } // 改变View的透明度 @TargetApi(Build.VERSION_CODES.HONEYCOMB) private static void applyAlphaAnimation(View view, float alpha) { if (isPostHoneycomb()) { // setAlpha()方法是Android3.0以上才有的api view.setAlpha(alpha); } else { // 在Android3.0以下,使用AlphaAnimation并将Duration设置为0来实现 AlphaAnimation alphaAnimation = new AlphaAnimation(alpha, alpha); alphaAnimation.setDuration(0); alphaAnimation.setFillAfter(true); view.startAnimation(alphaAnimation); } } private static boolean isPostHoneycomb() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; }
(7).核心之onClick()方法。
触发点击时,Animation开始执行,主要是ExpandCollapseAnimation在构造时startHeight和endHeight的计算。执行“收起”动画时,从当前高度开始,降低到收起状态下的高度mCollapsedHeight;执行“展开”动画时,在当前高度的基础上,增加TextView变化的高度mTextHeightWithMaxLines - mTv.getHeight()。这里有个小细节,动画开始时标记mAnimating为true,结束时标记为false,标记mAnimating变量是为了在onInterceptTouchEvent()方法中使用。这样,在动画执行期间,可以拦截掉触摸事件,不传递给子View。
@Override public void onClick(View view) { // 文字内容太短,无需折叠时,箭头按钮是隐藏的。这时直接返回。 if (mButton.getVisibility() != View.VISIBLE) { return; } // 将变量mCollapsed和mButton显示的图片赋值为相反 mCollapsed = !mCollapsed; mButton.setImageDrawable(mCollapsed ? mExpandDrawable : mCollapseDrawable); // 将当前位置的状态保存起来,在ListView中需要使用 if (mCollapsedStatus != null) { mCollapsedStatus.put(mPosition, mCollapsed); } // 标记动画开始 mAnimating = true; Animation animation; if (mCollapsed) { // “收起”动画,从当前高度开始,降低到收起状态下的高度mCollapsedHeight animation = new ExpandCollapseAnimation(this, getHeight(), mCollapsedHeight); } else { // “展开”动画,在当前整个View高度的基础上,增加TextView变化的高度(mTextHeightWithMaxLines - mTv.getHeight()) animation = new ExpandCollapseAnimation(this, getHeight(), getHeight() + mTextHeightWithMaxLines - mTv.getHeight()); } animation.setFillAfter(true); animation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { // 动画开始,改变TextView的透明度 applyAlphaAnimation(mTv, mAnimAlphaStart); } @Override public void onAnimationEnd(Animation animation) { // 清除动画,防止applyTransformation()重复执行 clearAnimation(); // 标记动画结束 mAnimating = false; // 通知listener if (mListener != null) { mListener.onExpandStateChanged(mTv, !mCollapsed); } } @Override public void onAnimationRepeat(Animation animation) { } }); // 清除之前的动画,开启新的动画 clearAnimation(); startAnimation(animation); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // 动画执行期间,拦截掉触摸事件,不传递给子View return mAnimating; }
(8).其它方法之setText()。
一般情况下,只需调用setText(text)方法即可。
在ListView等存在item复用的情况下,需要调用setText(text,collapsedStatus,position)方法,这里使用了SparseBooleanArray记录当前位置的展开收起状态。以ListView为例,在实现BaseAdapter类的代码中,定义成员变量SparseBooleanArray collapsedStatus = new SparseBooleanArray(),当在getView()中调用setText(text,collapsedStatus,position)时,传入成员变量collapsedStatus和当前的position。
// 设置显示内容 public void setText(@Nullable CharSequence text) { mRelayout = true; mTv.setText(text); // 内容不为空时,才会显示 setVisibility(TextUtils.isEmpty(text) ? View.GONE : View.VISIBLE); } // 设置显示内容,在ListView等存在item复用的情况下需要使用该方法 public void setText(@Nullable CharSequence text, @NonNull SparseBooleanArray collapsedStatus, int position) { mCollapsedStatus = collapsedStatus; mPosition = position; boolean isCollapsed = collapsedStatus.get(position, true); clearAnimation(); mCollapsed = isCollapsed; mButton.setImageDrawable(mCollapsed ? mExpandDrawable : mCollapseDrawable); setText(text); getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; requestLayout(); }
(9).其它方法之setOnExpandStateChangeListener()。
设置状态(“展开”、“收起”)改变的监听,可以根据实际需求决定是否使用该方法。
public void setOnExpandStateChangeListener(@Nullable OnExpandStateChangeListener listener) { mListener = listener; } // 状态改变的监听器 public interface OnExpandStateChangeListener { /** * 当展开或收起的动画执行完毕之后,该方法被回调 * @param textView - 展开或收起的TextView * @param isExpanded - 如果文字被展开,该值为true;否则为false */ void onExpandStateChanged(TextView textView, boolean isExpanded); }
(10).其它方法之setOrientation()。
在setOrientation()方法中,限定了当前布局的方向只能为垂直。
@Override public void setOrientation(int orientation){ if(LinearLayout.HORIZONTAL == orientation){ throw new IllegalArgumentException("ExpandableTextView only supports Vertical Orientation."); } super.setOrientation(orientation); }
相关文章推荐
- 【Android开源项目分析】自定义圆形头像CircleImageView的使用和源码分析
- 开源项目GridPasswordView使用和源码分析
- 开源项目GridPasswordView使用和源码分析
- 【Android开源项目分析】自定义圆形头像CircleImageView的使用和源码分析
- 【Android开源项目分析】自定义圆形头像CircleImageView的使用和源码分析
- 【Android开源项目分析】自定义圆形头像CircleImageView的使用和源码分析
- 开源项目ViewPagerIndicator源码分析
- 二维码识别开源项目zxing的使用和源码分析
- 开源项目android-process-button使用和源码分析
- Android部分开源项目源码分析之---ViewBadge(View 上面动态的自定义的添加view)
- 【Android开源项目分析】TAB导航栏PagerSlidingTabStrip的使用和源码分析
- 我的mqtt协议和emqttd开源项目个人理解(5) - hook的使用,源码分析
- BaseRecyclerViewAdapterHelper开源项目之BaseSectionQuickAdapter 实现Expandable And collapse效果的源码学习
- 开源项目GridViewWithHeaderAndFooter使用和源码分析
- ExpandableTextView源码分析
- BaseRecyclerViewAdapterHelper开源项目之BaseSectionQuickAdapter 实现Expandable And collapse效果的源码学习
- 用开源项目ExpandableTextView打造可以下拉扩展的TextView
- 二维码识别开源项目zxing的使用和源码分析
- 【Android开源项目解析】背景有波浪效果的TextView——从Titanic项目学习BitmapShader的使用
- 用开源项目ExpandableTextView打造可以下拉扩展的TextView