preface

Recently I got used to the new environment and gradually became busy. Some students are still concerned about the thunder before. What I want to say is that it has been extended over and over again. The result is quite clear.

Two days ago, I received a demand to complete the following effects.

  • 1. Contents that exceed the specified number of lines need to be folded;
  • 2, if there is a link in the content, you need to hide the link, display the link as “web link”, and click to jump to the web page;
  • 3. If the content contains @+ “Content”, you need to jump to the specified page with “Content”.
  • 4. It is possible to append “Expand” or “recall” to other content, such as the time string in demo

Demo effect implementation

The @user and the link are highlighted and clickable, including the expansion and reclamation functions. The following shows the display effect in different situations:

Demo Download Experience

Download the Demo

Download

Implementation approach

There are two main ideas: one is the curve to save the country, the other is the TextView directly masturbation.

First, the curve to save the country

Use two TextViews to display, the top is mainly responsible for displaying content, the bottom is responsible for expanding and retracting functions. The advantage of this way is that the implementation is relatively simple, the disadvantage is that it is difficult to do as shown in the figure at the end of the text to add expand and recover the two words, that is, it is difficult to restore the design draft; And for the content or need to deal with the additional @ user and link operations, not very convenient.

Idea 2: Masturbate directly to TextView

The so-called “directly against TextView” is the custom View inherit TextView, in the custom View to deal with all the logic, the advantage is convenient to use, but also as far as possible to restore the design draft. Here we use the second way, the first way to provide an idea, you can try yourself interested.

The specific implementation

A prior consideration

Before we start writing code, we need to consider a few points

  • First, how to ensure that “expand” or “recover” at the end of the text
  • How to identify @ users in text
  • How to identify links in text
  • 4. Handle highlighting and clicking events for @ users, links and “expand” or “recall”

To solve the problem

First, how to ensure that “expand” or “recover” at the end of the text

In fact, this problem is the most difficult implementation of the bar! I had a headache before, but then I came across DynamicLayout, which allows me to get the last position of the row, the start position of the row, the width of the row, and the number of rows that the specified content occupies.

		// To calculate the size of the content
        DynamicLayout mDynamicLayout =
                new DynamicLayout(mFormatData.formatedContent, mPaint, mWidth, Layout.Alignment.ALIGN_NORMAL, 1.2 f.0.0 f.true);
        // Get the number of rows
        int mLineCount = mDynamicLayout.getLineCount();
        int index = currentLines - 1;
        // Gets the last position of the specified line
        int endPosition = mDynamicLayout.getLineEnd(index);
        // Gets the start of the specified line
        int startPosition = mDynamicLayout.getLineStart(index);
        // Gets the width of the specified line
        float lineWidth = mDynamicLayout.getLineWidth(index);
Copy the code

The following diagram illustrates the above parameters briefly:

	 /** * Calculates the length of the original content to be clipped **@param endPosition
     * @param startPosition
     * @param lineWidth
     * @param endStringWith
     * @param offset
     * @return* /
    private int getFitPosition(int endPosition, int startPosition, float lineWidth,
                               float endStringWith, float offset, String aimContent) {
        // The number of words to add to the last line
        int position = (int) ((lineWidth - (endStringWith + offset)) * (endPosition - startPosition)/ lineWidth);

        if (position < 0) return endPosition;
		// Calculate the length of the text to display on the last line
        float measureText = mPaint.measureText(
                (aimContent.substring(startPosition, startPosition + position)));
		// If the length of the text displayed on the last line is shorter than the length of the last line minus the length of the "expand" text, it is ok otherwise add a space to continue counting
        if (measureText <= lineWidth - endStringWith) {
            return startPosition + position;
        } else {
            return getFitPosition(endPosition, startPosition, lineWidth, endStringWith, offset + mPaint.measureText("")); }}Copy the code

How to identify @ users in text

Match the original content with a regular expression, as follows:

@ [\ w \ p {InCJKUnifiedIdeographs} -] {1, 26}Copy the code

Make a note of the match to the content, and finally use SpannableStringBuilder to set clickable spans and other specific styles such as colors for the matched content. In the following code, we save the content and location of the matched information for later use. For the @ user section, we’ll see how to add highlighting and click events later.

	// Perform a regular match for @user
    Pattern pattern = Pattern.compile(regexp_mention, Pattern.CASE_INSENSITIVE);
    Matcher matcher = pattern.matcher(newResult.toString());
    List<FormatData.PositionData> datasMention = new ArrayList<>();
    while (matcher.find()) {
        // The matched content is processed statistically
        datasMention.add(new FormatData.PositionData(matcher.start(), matcher.end(), matcher.group(), LinkType.MENTION_TYPE));
    }
Copy the code

How to identify links in text

In the beginning, I found a lot of regular expressions that matched links in text, many of which turned out to be problematic. Think of TextView itself has support for link jump, think of TextView internal must have the relevant re to match, then check TextView source code, found there really is.

For links, how to add highlighting and click events will be covered later. Here is the code for matching links:

		List<FormatData.PositionData> datas = new ArrayList<>();
        // Perform a regular match on the link
        Pattern pattern = AUTOLINK_WEB_URL;
        Matcher matcher = pattern.matcher(content);
        StringBuffer newResult = new StringBuffer();
        int start = 0;
        int end = 0;
        int temp = 0;
        while (matcher.find()) {
            start = matcher.start();
            end = matcher.end();
            newResult.append(content.toString().substring(temp, start));
            // The matched content is processed statistically
            datas.add(new FormatData.PositionData(newResult.length() + 1, newResult.length() + 2 + TARGET.length(), matcher.group(), LinkType.LINK_TYPE));
            newResult.append("" + TARGET + "");
            temp = end;
        }
Copy the code

In addition to matching links, we also need to mask the identified links. How do I mask it? That is, replace the link in the original text with “web link”. So how do you replace it? In the above code we get the corresponding link and the location of the link, so we just need to replace the matching link with “web link”.

//newResult is the container of content that will eventually be displayed on the page
newResult.append(content.toString().substring(end, content.toString().length()));
Copy the code

4. Handle highlighting and clicking events for @ users, links and “expand” or “recall”

For @ users, linking and “expand” or “undo” implementations are ultimately handled using SpannableStringBuilder. Before, when parsing the original content, we stored the matched link or @ user, and stored their location (start, end) and type.

	// Enumeration types that define types
    public enum LinkType {
        // Plain links
        LINK_TYPE,
        / / @ users
        MENTION_TYPE
    }
Copy the code

With this collection of data, we just need to walk through the data and setSpan the data separately, setting the font color during setSpan, and the callback for the click event.

// Handle links or @ users
    private void dealLinksOrMention(FormatData formatData,SpannableStringBuilder ssb) {
        List<FormatData.PositionData> positionDatas = formatData.getPositionDatas();
        HH:
        for (FormatData.PositionData data : positionDatas) {
            if (data.getType().equals(LinkType.LINK_TYPE)) {
                int fitPosition = ssb.length() - getHideEndContent().length();
                if (data.getStart() < fitPosition) {
                    SelfImageSpan imageSpan = new SelfImageSpan(mLinkDrawable, ImageSpan.ALIGN_BASELINE);
                    // Set the link icon
                    ssb.setSpan(imageSpan, data.getStart(), data.getStart() + 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
                    // Set the link text style
                    int endPosition = data.getEnd();
                    if (fitPosition > data.getStart() + 1 && fitPosition < data.getEnd()) {
                        endPosition = fitPosition;
                    }
                    if (data.getStart() + 1 < fitPosition) {
                        ssb.setSpan(new ClickableSpan() {
                            @Override
                            public void onClick(View widget) {
                                if(linkClickListener ! =null)
                                    linkClickListener.onLinkClickListener(LinkType.LINK_TYPE, data.getUrl());
                            }

                            @Override
                            public void updateDrawState(TextPaint ds) {
                                ds.setColor(mLinkTextColor);
                                ds.setUnderlineText(false);
                            }
                        }, data.getStart() + 1, endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); }}}else {
                int fitPosition = ssb.length() - getHideEndContent().length();
                if (data.getStart() < fitPosition) {
                    int endPosition = data.getEnd();
                    if (fitPosition < data.getEnd()) {
                        endPosition = fitPosition;
                    }
                    ssb.setSpan(new ClickableSpan() {
                        @Override
                        public void onClick(View widget) {
                            if(linkClickListener ! =null)
                                linkClickListener.onLinkClickListener(LinkType.MENTION_TYPE, data.getUrl());
                        }

                        @Override
                        public void updateDrawState(TextPaint ds) {
                            ds.setColor(mLinkTextColor);
                            ds.setUnderlineText(false); } }, data.getStart(), endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); }}}}/** * set "expand" *@param ssb
     * @param formatData
     */
    private void setExpandSpan(SpannableStringBuilder ssb,FormatData formatData){
        int index = currentLines - 1;
        int endPosition = mDynamicLayout.getLineEnd(index);
        int startPosition = mDynamicLayout.getLineStart(index);
        float lineWidth = mDynamicLayout.getLineWidth(index);

        String endString = getHideEndContent();

        // Calculate the position subscript where the original content is intercepted
        int fitPosition =
                getFitPosition(endPosition, startPosition, lineWidth, mPaint.measureText(endString), 0);

        ssb.append(formatData.formatedContent.substring(0, fitPosition));

        // Add the expanded text after the truncated text
        ssb.append(endString);

        int expendLength = TextUtils.isEmpty(mEndExpandContent) ? 0 : 2 + mEndExpandContent.length();
        ssb.setSpan(new ClickableSpan() {
            @Override
            public void onClick(View widget) {
                action();
            }

            @Override
            public void updateDrawState(TextPaint ds) {
                super.updateDrawState(ds);
                ds.setColor(mExpandTextColor);
                ds.setUnderlineText(false);
            }
        }, ssb.length() - TEXT_EXPEND.length() - expendLength, ssb.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
    }
Copy the code

In handling the block when there is a need to pay attention to detail, that if at the end of the text after cutting is a a link, and the place to show “a” or “back”, the place to pay special attention to the scope of the link setSpan behind a do not pay attention to may along with the “a” or “inherit” and set up together, lead to wrong events. Handling “recall” is similar, so I won’t post the code. Finally, there is an additional function is to add a time string at the end of the function, in fact, in the “expand” and “recall” before a string, good judgment in this respect, the code has been done. For details, go to Github.

Project address and conclusion

Github address: ExpandableTextView

If the connection fails, just click the link! Github.com/MZCretin/Ex…

Your star is the greatest encouragement to me!

About me

I just like to use code to solve problems in my life and feel happy, hahaha. I also hope you can follow my simple book, Nuggets, Github and CSDN.

Jane books home page link is www.jianshu.com/u/123f97613…

Nuggets home page, link is juejin.cn/user/109916…

The Github homepage is linked to github.com/MZCretin

CSDN home page, link is blog.csdn.net/u010998327

I’m Cretin, a lovely little boy.