ExpandableTextView的源码解读

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


有兴趣的同学可以自行点击下载源码,里面的注释已经写的很完善了。这里我将我的代码整理分享出来:
首先需要定义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()获取。
    网上有很多关于这个类的介绍,这里我推荐一篇博文,你可以大致了解这个类的一些基本知识。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,066评论 4 62
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,373评论 0 17
  • 选择这个视频的初衷: 被标题吸引,我是一个相对感性的人,看到这样的标题,就很容易被电到 听完之后的总结: ...
    亲子思维导图讲师燕子阅读 241评论 2 0
  • 【清平乐.梦伤】 情深几许?泪酒凄杯续。旧事沉溟归梦里, 侧转经年不去。高楼怨曲残阳,随风透冷成寒...
    青山精神病患者阅读 294评论 0 2