您的位置:首页 > 移动开发 > Android开发

开源项目ExpandableTextView使用和源码分析

2016-08-13 00:34 519 查看
ExpandableTextView,可展开和收起的TextView,就像GooglePlay里面显示应用的描述那样。

项目地址:
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);
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐