继续创造,加速成长!这是我参加「日新计划 10 月更文挑战」的第11天,点击查看活动详情

前言

之前的文章咱们都讲到了WX盆友圈动态列表的效果,九宫格控件的完成【传送门】。而且讲到了发布动态中论题的处理【传送门】。那么在动态列表中咱们怎样显现咱们发布的论题数据和一些圈子数据呢?

大致完成效果如下:(本地测验环境,无其他意义)

记录实现一个动态列表中展示话题、圈子、网址的可展开收起的特殊TextView

TextView的特别文本处理

咱们在把服务器回来的文本设置给自定义折叠的TextView之前,咱们先对文本进行Span的预处理。

 /**
     * 露出办法-替换原文本中的论题数据,变色处理
     *
     * @param topics  服务器回来的论题数据
     * @param content 服务器回来的原始文本数据
     */
    public CharSequence replaceTopicSpan(List<RemoteTopicBean> topics, String content, OnTopicClickListener listener) {
        if (!CheckUtil.isEmpty(topics) && !CheckUtil.isEmpty(content)) {
            CharSequence topicCharSequece = content;
            int startPosition = 0;
            int endPosition = 0;
            for (RemoteTopicBean bean : topics) {
                startPosition = content.indexOf(bean.topic_name, startPosition);
                endPosition = startPosition + bean.topic_name.length();
                if (startPosition == -1)
                    break;
                topicCharSequece = SpanUtils.getInstance()
                        .toClickSpan(topicCharSequece, startPosition, endPosition, CommUtils.getColor(R.color.app_blue), false, charSequence -> {
                            //论题的点击(路由直接跳转搜索结果展现)
                            listener.onTopicClick(charSequence.toString());
                        });
                startPosition = endPosition;
            }
            return topicCharSequece;
        }
        return "";
    }

其实便是对多个论题进行遍历,找到start和end,然后运用Span的工具类,把一般的文本转为可点击和变色的Span。并回调出去外界运用。关键是要回来处理之后的文本 CharSequece 回来外部去设置。

详细富文本的转换办法如下:

    /**
     * 可点击-带下划线
     */
    public CharSequence toClickSpan(CharSequence charSequence, int start, int end, int color, boolean needUnderLine, OnSpanClickListener listener) {
        SpannableString spannableString = new SpannableString(charSequence);
        ClickableSpan clickableSpan = new ClickableSpan() {
            @Override
            public void onClick(@NonNull View widget) {
                if (listener != null) {
                    //避免重复点击
                    if (System.currentTimeMillis() - mLastClickTime >= TIME_INTERVAL) {
                        //to do
                        listener.onClick(charSequence.subSequence(start, end));
                        mLastClickTime = System.currentTimeMillis();
                    }
                }
            }
            @Override
            public void updateDrawState(@NonNull TextPaint ds) {
                ds.setColor(color);
                ds.setUnderlineText(needUnderLine);
            }
        };
        spannableString.setSpan(
                clickableSpan,
                start,
                end,
                Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
        return spannableString;
    }

运用的时候:

        //打开文本设置
        ExpandTextView tvContent = helper.getView(R.id.tv_feed_news_content);
        String content = item.contentDesc;
        CharSequence topicCharSequece  =  tvContent.replaceTopicSpan(item.topics, content, new ExpandTextView.OnTopicClickListener() {
            @Override
            public void onTopicClick(String topic) {
                YYRouterService.newsFeedComponentService.startSearchResultActivity(mActivity, topic, true);
            }
        });
        tvContent.setVisibility(View.VISIBLE);
        tvContent.initWidth(mTvWidth);
        tvContent.setMaxLines(3);
        tvContent.setTypeface(TypefaceUtil.getSFLight(mContext));
        tvContent.setCloseText(topicCharSequece);

假如自己想显现的控件文本需求显现一些自定义字体,那么咱们需求在设置文本之前就设置字体。

setCloseText 办法便是详细的完成打开收起进口办法,咱们看看它是怎样完成的。

TextView的打开收起功用

关于TextView的打开收起,都离不开 StaticLayout 这个神器。

咱们主要需求用到它的两个办法 :

  • 经过 StaticLayout 的 getLineCount() 办法知道文本是否会超出咱们设置的maxLines,
  • 经过 getLineEnd(int line) 办法能够找到终究一行的终究一个字符在文本中的方位。

因为咱们的需求是[打开]与[收起]的标签是紧接着文章后边而不是换行展现,所以咱们需求循环遍历才干找到最佳的方位。

setCloseText 的办法如下:

    private String TEXT_EXPAND = "  [More]";
    private String TEXT_CLOSE = "  [Show Less]";
 /**
     * 露出的办法-默许设置文本办法(假如需求折叠就会默许折叠)
     * 假如有特别的Span如论题之类的,需求处理完毕之后再调用此办法。
     */
    public void setCloseText(CharSequence text) {
        if (SPAN_CLOSE == null) {
            initCloseEnd();
        }
        boolean appendShowAll = false; // 需求打开收起功用,先运用flag阻拦,等丈量完毕之后再setText显现真实的文本
        originText = text;
        int maxLines = getMaxLines();
        CharSequence workingText = originText;
        if (maxLines >= 0) {
            //创建出一个StaticLayout主要是为了核算行数
            Layout layout = createStaticLayout(workingText);
            //核算全部打开的文本高度
            mOpenHeight = layout.getHeight() + getPaddingTop() + getPaddingBottom();
            if (layout.getLineCount() > maxLines) {
                //获取一行显现字符个数,然后截取字符串数, 收起状况原始文本截取展现的部分
                workingText = originText.subSequence(0, layout.getLineEnd(maxLines - 1));
                //再对加上[收起]标签的文本进行丈量
                String showText = originText.subSequence(0, layout.getLineEnd(maxLines - 1)) + "..." + SPAN_CLOSE;
                Layout layout2 = createStaticLayout(showText);
                // 对workingText进行-1截取,直到展现行数==最大行数,而且添加 SPAN_CLOSE 后刚好占满终究一行
                while (layout2.getLineCount() > maxLines) {
                    int lastSpace = workingText.length() - 1;
                    if (lastSpace == -1) {
                        break;
                    }
                    workingText = workingText.subSequence(0, lastSpace);
                    layout2 = createStaticLayout(workingText + "..." + SPAN_CLOSE);
                }
                //核算收起的文本高度
                mCLoseHeight = layout2.getHeight() + getPaddingTop() + getPaddingBottom();
                appendShowAll = true;
            }
        }
        setText(workingText);
        if (appendShowAll) {
            // 有必要运用append,不能在上面运用+连接,否则会失效
            append("...");
            append(SPAN_CLOSE);
        }
        setMovementMethod(LinkMovementMethod.getInstance());
        replaceUrlSpan();
    }
    /**
     * 收起的案牍(色彩处理)初始化
     */
    private void initCloseEnd() {
        //设置打开的文本
        SPAN_CLOSE = new SpannableString(TEXT_EXPAND);
        ButtonSpan span = new ButtonSpan(getContext(), new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ExpandTextView.super.setMaxLines(Integer.MAX_VALUE);
                setExpandText(originText);
                if (mCallback != null) mCallback.isExpand(1);
            }
        }, R.color.color_expand_span);
        SPAN_CLOSE.setSpan(span, 0, TEXT_EXPAND.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        SPAN_CLOSE.setSpan(new MyTypefaceSpan(TypefaceUtil.getSFRegular(getContext())), 0, TEXT_EXPAND.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
    }

其实只需求这两个办法就能够展现一个折叠起来的文本了。那么怎样切换打开与收起的状况呢?

多种方式的完成打开

第一种办法是直接修改setMaxLine的方式,设置最大答应展现行的方式。

/**
     * 打开的案牍(色彩处理)初始化
     */
    private void initExpandEnd() {
        //设置封闭的文本
        SPAN_EXPAND = new SpannableString(TEXT_CLOSE);
        ButtonSpan span = new ButtonSpan(getContext(), new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ExpandTextView.super.setMaxLines(mMaxLines);
                setCloseText(originText);
                if (mCallback != null) mCallback.isExpand(0);
            }
        }, R.color.color_expand_span);
        SPAN_EXPAND.setSpan(span, 0, TEXT_CLOSE.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        SPAN_EXPAND.setSpan(new MyTypefaceSpan(TypefaceUtil.getSFRegular(getContext())), 0, TEXT_CLOSE.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
    }
     /**
     * 设置打开的文本展现-后边加上[收起]的文本标签
     */
    private void setExpandText(CharSequence text) {
        if (SPAN_EXPAND == null) {
            initExpandEnd();
        }
        //创建出一个StaticLayout主要是为了核算行数
        Layout layout1 = createStaticLayout(text);
        Layout layout2 = createStaticLayout(text + TEXT_CLOSE);
        //判断- 当展现全部原始内容时 假如 TEXT_CLOSE 需求换行才干显现完好,则直接将TEXT_CLOSE展现鄙人一行
        if (layout2.getLineCount() > layout1.getLineCount()) {
            setText(originText + "\n");
        } else {
            setText(originText);
        }
        //加上[收起]的标签
        append(SPAN_EXPAND);
        setMovementMethod(LinkMovementMethod.getInstance());
        replaceUrlSpan();
    }

咱们在[打开]和[收起]的标签中先设置他们为可点击的标签,然后再回调的Click办法中咱们是设置切换 ExpandTextView.super.setMaxLines(mMaxLines); 的方式来完成的。

当然假如觉得这样的切换比较僵硬,想用动画来完成也是能够的。

另一种办法是记录打开与收起的高度,然后做特点动画直接改动layoutParams的height,然后改动高度,完成对应打开收起的状况切换。

之前在 setCloseText 办法中,咱们预丈量文本布局的时候已经记录了打开与收起的高度记录。

    private int mOpenHeight;   //打开的文本高度
    private int mCLoseHeight;  //收起的文本高度

那么我就能用动画来封装一下完成

class ExpandCollapseAnimation extends Animation {
    private final View mTargetView;//动画履行view
    private final int mStartHeight;//动画履行的开始高度
    private final int mEndHeight;//动画结束后的高度
    ExpandCollapseAnimation(View target, int startHeight, int endHeight) {
        mTargetView = target;
        mStartHeight = startHeight;
        mEndHeight = endHeight;
        setDuration(400);
    }
    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        //核算出每次应该显现的高度,改动履行view的高度,完成动画
        mTargetView.getLayoutParams().height = (int) ((mEndHeight - mStartHeight) * interpolatedTime + mStartHeight);
        mTargetView.requestLayout();
    }
}

大致的完成如下:


private void executeOpenAnim() {
    if (mOpenAnim == null) {
        mOpenAnim = new ExpandCollapseAnimation(this, mCLoseHeight, mOpenHeight);
        mOpenAnim.setFillAfter(true);
        mOpenAnim.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
                ExpandableTextView.super.setMaxLines(Integer.MAX_VALUE);
                setText(mOpenSpannableStr);
            }
            @Override
            public void onAnimationEnd(Animation animation) {
                getLayoutParams().height = mOpenHeight;
                requestLayout();
                animating = false;
            }
            @Override
            public void onAnimationRepeat(Animation animation) {
            }
        });
    }
    if (animating) {
        return;
    }
    animating = true;
    clearAnimation();
    startAnimation(mOpenAnim);
}
private void executeCloseAnim() {
    if (mCloseAnim == null) {
        mCloseAnim = new ExpandCollapseAnimation(this, mOpenHeight, mCLoseHeight);
        mCloseAnim.setFillAfter(true);
        mCloseAnim.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
            }
            @Override
            public void onAnimationEnd(Animation animation) {
                animating = false;
                ExpandableTextView.super.setMaxLines(mMaxLines);
                setText(mCloseSpannableStr);
                getLayoutParams().height = mCLoseHeight;
                requestLayout();
            }
            @Override
            public void onAnimationRepeat(Animation animation) {
            }
        });
    }
    if (animating) {
        return;
    }
    animating = true;
    clearAnimation();
    startAnimation(mCloseAnim);
}

两种办法都是能够的,我这儿的做法是第一种做法,直接设置maxLine的办法,没有整那么多动画。

内部Link链接的自定义处理

这儿的Demo,做了两种演示,其实我么能够直接经过工具类转换到咱们自定义的ClickSpan,也能够经过new 一个 ButtonSpan 来替换完成

例如运用ButtonSpan,咱们能够设置点击,设置自定义字体等等。

       SPAN_CLOSE = new SpannableString(TEXT_EXPAND);
        ButtonSpan span = new ButtonSpan(getContext(), new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ExpandTextView.super.setMaxLines(Integer.MAX_VALUE);
                setExpandText(originText);
                if (mCallback != null) mCallback.isExpand(1);
            }
        }, R.color.color_expand_span);
        SPAN_CLOSE.setSpan(span, 0, TEXT_EXPAND.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        SPAN_CLOSE.setSpan(new MyTypefaceSpan(TypefaceUtil.getSFRegular(getContext())), 0, TEXT_EXPAND.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);

而内部的网址点击,因为默许是跳转到浏览器,咱们想App自己处理,那么咱们就需求找到文本中的 URLSpan 目标,然后对他进行替换,换成咱们自己的 InterceptUrlSpan 目标,跳转到咱们自己的WebView。

    /**
     * 填充文本之后尝试替换URLSpan
     */
    private void replaceUrlSpan() {
        CharSequence text = getText();
        if (text instanceof Spannable) {
            int end = text.length();
            Spannable sp = (Spannable) text;
            URLSpan[] urls = sp.getSpans(0, end, URLSpan.class);
            SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text);
            if (urls.length > 0) {
                for (URLSpan urlSpan : urls) {
                    //阻拦点击,替换Span
                    InterceptUrlSpan interceptUrlSpan = new InterceptUrlSpan(urlSpan.getURL());
                    spannableStringBuilder.setSpan(interceptUrlSpan, sp.getSpanStart(urlSpan), sp.getSpanEnd(urlSpan), Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
                }
                //替换之后从头设置进去
                setText(spannableStringBuilder);
            }
        }
    }

一般是在咱们设置玩文本显现之后再调用,如 setCloseText setExpandText 办法。

效果:

记录实现一个动态列表中展示话题、圈子、网址的可展开收起的特殊TextView

结语

涉及到的一些知识,文本Span的转换,StaticLayout的运用,URLSpan的查找与替换等。

主要是和咱们的需求相互对应,假如是要打开标签要在文本后边显现就简略一点,假如换行展现就简略一点,总的来说其实也不是很难,明确需求之后分解为一步一步的小需求,然后一步一步的完成小需求,串联起来便是咱们终究的效果。

因为一些隐私问题就没有很方便的直接在我的Demo中完好贴出。假如我们对代码有需求的话,全部的代码其实都已经在文中贴出了,我们细心整合一下便是完好的代码了。

当然了,我这种方案或许也仅仅凭空捏造,还需求我们提提意见,假如你有更好的方案,或者优化的空间都也能够一起沟通一下。如有错漏的地方还请指出,假如有疑问也能够在评论区我们一起评论哦。

假如感觉本文对你有一点点的启发,还望你能 点赞 支撑一下,你的支撑是我最大的动力。

Ok,这一期就此结束。

记录实现一个动态列表中展示话题、圈子、网址的可展开收起的特殊TextView