美文网首页
ExpandableTextView的源码解读

ExpandableTextView的源码解读

作者: Elder | 来源:发表于2016-12-08 14:07 被阅读137次

这个自定义TextView可以实现内部文字的折叠和扩展显示,效果如下:

previewpreview
有兴趣的同学可以自行点击下载源码,里面的注释已经写的很完善了。这里我将我的代码整理分享出来:
首先需要定义ExpandableTextView的属性资源文件attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="TextViewExpandable">
        <attr name="tvea_expandLines" format="integer"/>
        <attr name="tvea_shrinkBitmap" format="reference" />
        <attr name="tvea_expandBitmap" format="reference" />
        <attr name="tvea_textStateColor" format="color" />
        <attr name="tvea_textContentColor" format="color" />
        <attr name="tvea_textContentSize" format="dimension" />
        <attr name="tvea_textShrink" format="string" />
        <attr name="tvea_textExpand" format="string" />
    </declare-styleable>
</resources>

各属性的意义通过名称大家应该也都能理解出来,论学好英语的重要性啊......扯远了,接下来需要给ExpandableTextView指定布局文件layout_textview_expand_animation.xml代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/ll_text_expand_animation_parent"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_expand_text_view_animation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textColor="@color/color_gray_light_content_text"
        android:textSize="@dimen/sp_txt_size_content"
        tools:text="@string/tips" />

    <RelativeLayout
        android:id="@+id/rl_expand_text_view_animation_toggle_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:visibility="gone">

        <ImageView
            android:id="@+id/iv_expand_text_view_animation_toggle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            />

        <TextView
            android:id="@+id/tv_expand_text_view_animation_hint"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_toLeftOf="@+id/iv_expand_text_view_animation_toggle"
            android:textColor="@color/colorPrimary"
            android:textSize="@dimen/sp_txt_size_content"
            tools:text="全部" />

        <View
            android:layout_width="match_parent"
            android:layout_height="0.1dp"
            android:layout_centerVertical="true"
            android:layout_marginLeft="10dp"
            android:layout_toLeftOf="@id/tv_expand_text_view_animation_hint"
            android:background="@color/color_divider_line_gray" />
    </RelativeLayout>
</LinearLayout>

好接下来就是我们的主题了,ExpandableTextView类的代码编写了。代码如下,我已经将需要注释的地方都进行了注释。

public class TextViewExpandable extends LinearLayout implements View.OnClickListener{
    /**
     * TextView
     */
    private TextView textView;

    /**
     * 收起/全部TextView
     * <br>shrink/expand TextView
     */
    private TextView tvState;

    /**
     * 点击进行折叠/展开的图片
     * <br>shrink/expand icon
     */
    private ImageView ivExpandOrShrink;

    /**
     * 底部是否折叠/收起的父类布局
     * <br>shrink/expand layout parent
     */
    private RelativeLayout rlToggleLayout;

    /**
     * 提示折叠的图片资源
     * <br>shrink drawable
     */
    private Drawable drawableShrink;
    /**
     * 提示显示全部的图片资源
     * <br>expand drawable
     */
    private Drawable drawableExpand;

    /**
     * 全部/收起文本的字体颜色
     * <br>color of shrink/expand text
     */
    private int textViewStateColor;
    /**
     * 展开提示文本
     * <br>expand text
     */
    private String textExpand;
    /**
     * 收缩提示文本
     * <br>shrink text
     */
    private String textShrink;

    /**
     * 是否折叠显示的标示
     * <br>flag of shrink/expand
     */
    private boolean isShrink = false;

    /**
     * 是否需要折叠的标示
     * <br>flag of expand needed
     */
    private boolean isExpandNeeded = false;

    /**
     * 是否初始化TextView
     * <br>flag of TextView Initialization
     */
    private boolean isInitTextView = true;

    /**
     * 折叠显示的行数
     * <br>number of lines to expand
     */
    private int expandLines;

    /**
     * 文本的行数
     * <br>Original number of lines
     */
    private int textLines;

    /**
     * 显示的文本
     * <br>content text
     */
    private CharSequence textContent;

    /**
     * 显示的文本颜色
     * <br>content color
     */
    private int textContentColor;

    /**
     * 显示的文本字体大小
     * <br>content text size
     */
    private float textContentSize;

    /**
     * 动画线程
     * <br>thread
     */
    private Thread thread;

    /**
     * 动画过度间隔
     * <br>animation interval
     */
    private int sleepTime = 22;

    /**
     * handler信号
     * <br>handler signal
     */
    private final int WHAT = 2;
    /**
     * 动画结束信号
     * <br>animation end signal of handler
     */
    private final int WHAT_ANIMATION_END = 3;

    /**
     * 动画结束,只是改变图标,并不隐藏
     * <br>animation end and expand only,but not disappear
     */
    private final int WHAT_EXPAND_ONLY = 4;

    public TextViewExpandable(Context context) {
        this(context, null);
    }

    public TextViewExpandable(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TextViewExpandable(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initValue(context, attrs);
        initView(context);
        initClick();
    }

    private void initValue(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TextViewExpandable);
        expandLines = typedArray.getInteger(
                R.styleable.TextViewExpandable_tvea_expandLines, 6);
        drawableShrink = typedArray.getDrawable(
                R.styleable.TextViewExpandable_tvea_shrinkBitmap);
        drawableExpand = typedArray
                .getDrawable(R.styleable.TextViewExpandable_tvea_expandBitmap);
        // 设置右下角显示状态的文字颜色
        textViewStateColor = typedArray.getColor(
                R.styleable.TextViewExpandable_tvea_textStateColor, ContextCompat.getColor(context, R.color.colorPrimary));
        textShrink = typedArray.getString(R.styleable.TextViewExpandable_tvea_textShrink);
        textExpand = typedArray.getString(R.styleable.TextViewExpandable_tvea_textExpand);
        // 设置默认值
        if (drawableShrink == null) {
            // 支持包的获取Drawable资源的方法
            drawableShrink = ContextCompat.getDrawable(context, R.drawable.icon_green_arrow_up);
        }
        if (drawableExpand == null) {
            drawableExpand = ContextCompat.getDrawable(context, R.drawable.icon_green_arrow_down);
        }
        if (TextUtils.isEmpty(textShrink)) {
            textShrink = context.getString(R.string.shrink);
        }

        if (TextUtils.isEmpty(textExpand)) {
            textExpand = context.getString(R.string.expand);
        }

        textContentColor = typedArray.getColor(
                R.styleable.TextViewExpandable_tvea_textContentColor, ContextCompat.getColor(context, R.color.color_gray_light_content_text));
        textContentSize = typedArray.getDimension(R.styleable.TextViewExpandable_tvea_textContentSize, 14);
        typedArray.recycle();
    }

    private void initView(Context context) {
        // 得到系统的布局解析器
        LayoutInflater inflate = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View layout = inflate.inflate(R.layout.layout_textview_expand_animation, this);

        rlToggleLayout = (RelativeLayout) layout.findViewById(R.id.rl_expand_text_view_animation_toggle_layout);

        textView = (TextView) layout.findViewById(R.id.tv_expand_text_view_animation);
        textView.setTextColor(textContentColor);
        textView.getPaint().setTextSize(textContentSize);

        ivExpandOrShrink = (ImageView) layout.findViewById(R.id.iv_expand_text_view_animation_toggle);

        tvState = (TextView) layout.findViewById(R.id.tv_expand_text_view_animation_hint);
        tvState.setTextColor(textViewStateColor);
    }

    /**
     * 设置显示文本的TextView和显示底部标志的Layout设置监听
     */
    private void initClick() {
        textView.setOnClickListener(this);
        rlToggleLayout.setOnClickListener(this);
    }

    /**
     *
     * @param view
     */
    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.tv_expand_text_view_animation:
                clickImageToggle();
                break;
            case R.id.rl_expand_text_view_animation_toggle_layout:
                clickImageToggle();
                break;
        }
    }

    private void clickImageToggle() {
        if (isShrink) {// 如果是折叠状态进行非折叠处理
            doAnimation(expandLines, textLines, WHAT_EXPAND_ONLY);
        } else {
            doAnimation(textLines, expandLines, WHAT_EXPAND_ONLY);
        }
        isShrink = !isShrink;
    }

    /**
     * 对外设置文本的方法
     * @param charSequence:是String的父接口,就是字符序列
     */
    public void setText(CharSequence charSequence) {
        textContent = charSequence;
        // 设置显示的TextView显示文本
        textView.setText(charSequence);
        // A view tree observer is used to register listeners that can be notified of global changes in the view tree.
        // 这是一个注册监听视图树的观察者(observer),在视图树种全局事件改变时得到通知。
        // 这里指的全局 事件包括而且不局限在以下几个:整个视图树的布局变化,开始绘制视图,触摸模式改变等等。
        ViewTreeObserver viewTreeObserver = textView.getViewTreeObserver();
        // Register a callback to be invoked when the view tree is about to be drawn.
        viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {// 返回true代表继续当前绘制,false代表取消
                // 判断该View是否应该初始化
                if (!isInitTextView) {
                    return true;
                }
                // 未初始化进行初始化
                isInitTextView = false;
                // 得到当前文本的总行数
                textLines = textView.getLineCount();
                // 设置是否需要显示扩展(总行数与当前显示的行数作比较)
                isExpandNeeded = textLines > expandLines;
                if (isExpandNeeded) {
                    // 是否启用折叠标志
                    isShrink = true;
                    // 调用动画
                    doAnimation(textLines, expandLines, WHAT_ANIMATION_END);
                } else {
                    isShrink = false;
                    doNotExpand();
                }
                return true;
            }
        });
    }

    /**
     * 处理消息
     */
    //Indicates that Lint should ignore the specified warnings for the annotated element.
    @SuppressLint("HandlerLeak")
    private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case WHAT:
                    // 不断修改当前TextView的最大行数
                    textView.setMaxLines(msg.arg1);
                    textView.invalidate();
                    break;
                case WHAT_EXPAND_ONLY:
                    changeExpandState(msg.arg1);
                    break;
                case WHAT_ANIMATION_END:
                    setExpandState(msg.arg1);
                    break;
            }
        }
    };

    /**
     * @param startLines 开始动画的起点行数 <br> start index of animation
     * @param endLines   结束动画的终点行数 <br> end index of animation
     * @param what       动画结束后的handler信号标示 <br> signal of animation end
     */
    private void doAnimation(final int startLines, final int endLines, final int what) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 如果起始行大于终止行往上折叠到终止行
                if (startLines > endLines) {
                    int count = startLines;
                    while (count-- > endLines) {
                        Message msg = handler.obtainMessage(WHAT, count, 0);
                        // 休眠一定时刻
                        try {
                            Thread.sleep(sleepTime);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        handler.sendMessage(msg);
                    }
                } else if (startLines < endLines) {
                    // 如果起始行小于终止行向下扩展到终止行
                    int count = startLines;
                    while (count++ < endLines) {
                        Message msg = handler.obtainMessage(WHAT, count, 0);

                        try {
                            Thread.sleep(sleepTime);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                        handler.sendMessage(msg);
                    }
                }
                // 动画结束后发送结束的信号
                // animation end,send signal
                Message msg = handler.obtainMessage(what, endLines, 0);
                handler.sendMessage(msg);
            }
        }).start();
    }

    /**
     * 负责改变折叠或展开状态的方法
     * @param endLines
     */
    private void changeExpandState(int endLines) {
        rlToggleLayout.setVisibility(VISIBLE);
        if (endLines > expandLines) {// 显示展开的状态
            ivExpandOrShrink.setBackground(drawableShrink);
            tvState.setText(textShrink);
        } else {// 显示折叠的状态
            ivExpandOrShrink.setBackground(drawableExpand);
            tvState.setText(textExpand);
        }
    }

    /**
     * 设置折叠或展开的状态方法
     * @param endLines
     */
    private void setExpandState(int endLines) {
        if (endLines < textLines) {// 小于总行数,设置为可以折叠或扩展状态
            isShrink = true;
            rlToggleLayout.setVisibility(VISIBLE);
            ivExpandOrShrink.setBackground(drawableExpand);
            textView.setOnClickListener(this);
            tvState.setText(textExpand);
        } else {// 设置为不显示折叠状态
            Log.e("xns", "not show shrink");
            isShrink = false;
            rlToggleLayout.setVisibility(GONE);
            ivExpandOrShrink.setBackground(drawableShrink);
            textView.setOnClickListener(null);
            tvState.setText(textShrink);
        }
    }

    /**
     * 无需折叠
     * do not expand
     */
    private void doNotExpand() {
        textView.setMaxLines(expandLines);
        rlToggleLayout.setVisibility(GONE);
        textView.setOnClickListener(null);
    }

    public Drawable getDrawableShrink() {
        return drawableShrink;
    }

    public void setDrawableShrink(Drawable drawableShrink) {
        this.drawableShrink = drawableShrink;
    }

    public Drawable getDrawableExpand() {
        return drawableExpand;
    }

    public void setDrawableExpand(Drawable drawableExpand) {
        this.drawableExpand = drawableExpand;
    }

    public int getExpandLines() {
        return expandLines;
    }

    public void setExpandLines(int newExpandLines) {
        int startLines = isShrink ? expandLines : textLines;
        int endLines = newExpandLines > textLines ? newExpandLines : textLines;
        doAnimation(startLines, endLines, WHAT_ANIMATION_END);
        expandLines = newExpandLines;
    }

    /**
     * 取得显示的文本内容
     * get content text
     *
     * @return content text
     */
    public CharSequence getTextContent() {
        return textContent;
    }

    public int getSleepTime() {
        return sleepTime;
    }

    public void setSleepTime(int sleepTime) {
        this.sleepTime = sleepTime;
    }
}
  • 整体设计的精髓即是通过handle接受Message后,不断的修改内部TextView的最大行数,然后调用该TextView的初始化方法,来完成动画的显示。
  • 这里还涉及到一个新的知识点即ViewTreeObserver类,简单来说它是一个监听ViewTree的观察者,在视图树中全局事件改变时得到通知。全局事件包括但不局限于如:整个视图树的布局变化,开始绘制视图,触摸模式改变等。
    ViewTreeObserver是不能被应用程序实例化的,因为它是由视图提供的,通过view.getViewTreeObserver()获取。
    网上有很多关于这个类的介绍,这里我推荐一篇博文,你可以大致了解这个类的一些基本知识。

其余都是一些逻辑上的代码,通过注释我相信大家都可以读懂它的实现方法。朝阳在前方,同志们继续前进吧。

相关文章

网友评论

      本文标题:ExpandableTextView的源码解读

      本文链接:https://www.haomeiwen.com/subject/unfsmttx.html