The effect

This article can help you achieve the following results. Fixed “See more” at the end, or whatever text you customize, and efficiently implemented an unfoldable animation.

Github.com/aesean/Acti…

Don’t care about the principle, just implement the reference here: TextViewExtensions

Use the sample: ShowMoreAnimationActivity, or refer to the following usage examples

Kotlin usage

    TextViewSuffixWrapper(textView).apply wrapper@{
            this.mainContent = getString(R.string.sample_text)
            this.suffix = "... Check out more"
            this.suffix? .apply { suffixColor("...".length, this.length, R.color.md_blue_500, listener = View.OnClickListener { view ->
                    toast("click ${this}")})}this.transition? .duration =5000
            sceneRoot = this.textView.parent.parent.parent as ViewGroup
            collapse(false)
            this.textView.setOnClickListener {
                toast("click view")
                this@wrapper.toggle()
            }
        }
Copy the code

Java version usage

        TextViewSuffixWrapper wrapper = new TextViewSuffixWrapper(textView);
        wrapper.setMainContent("mainContent");
        String suffix = "... Check out more";
        wrapper.setSuffix(suffix);
        wrapper.suffixColor("...".length(), suffix.length(), R.color.colorAccent, new View.OnClickListener() {
            @Override
            public void onClick(View v) {}}); wrapper.collapse(false);
        
        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) { wrapper.toggle(); }});Copy the code

A problem

Android’s TextView supports a maximum number of lines, for example if you want a TextView to have a maximum of 2 lines.

    textView.maxLines = 2
Copy the code

The textView will display at most two lines. At this point you can set ellipSize to use “… “when the text is longer than two lines. Replace the last words. Once set, the TextView will still have only two lines.

    textView.maxLines = 2
    textView.ellipsize = TextUtils.TruncateAt.END
Copy the code

That’s the question. I think… The prompt is not obvious, I want to change the similar view more such text how to do? And I want to add color to view more, what to do? For example, display as the first image at the beginning of the article. This requirement is actually quite common, Android is native and provides a similar implementation, so we have to do it ourselves.

To analyze problems

We’ll use the following text as the text for our textView to display. We’ll call this text mainContent

Ouyang Feng: [monologue] After I left The White Camel Mountain, I went to the desert and started another life. Ouyang Feng (monologue) : The first six days, awakening of Insects. Every year at this time, a man comes to me for a drink. His name is Huang Yaoshi. He was a strange man. He always came from the east. He had been doing this for years. This year, he brought me a handwritten note. Huang yaoshi: not long ago, I met a person who gave me a jar of wine. She said it was called “getting drunk and dreaming”. After drinking it, you can forget everything you have done. I wonder why there is such a wine. She said that the biggest trouble is a good memory, if everything can be forgotten, every day will be a new start, then how happy you are. This wine was meant for you, but it looks like we’re gonna split it. Ouyang Feng (soliloquy) : I always have a hard time accepting things that are too weird, so I haven’t drunk it. Maybe this wine really works, from that night on, Huang Yaoshi began to forget a lot of things.

To display the image below, we can break the problem into two parts.

  • frommainContentFind how many words to choose when, after the plus. To view moreIt could be exactly two lines, and one more word would result in the addition. To view moreWill a newline.
  • Change the lastTo view moreThe color is blue

To solve the problem

The problem has been abstracted. Let’s look at the second problem, changing the color of the text. This is easy to do with SpannableStringBuilder and ForegroundColorSpan. But the first problem, looking for the front exactly how many words, plus the suffix can just put two lines, how to deal with this?

If we have a way to know the number of TextView lines after several words, then we can find such an index from mainContent, which satisfies the following conditions

    textView.text = mainContent.substring(0, index) + "... Check out more"
    // textView.lineCount == 2

    textView.text = mainContent.substring(0, index+1) + "... Check out more"
    // textView.lineCount == 3
Copy the code

0 index, put it in TextView, TextView has 2 rows, 0 index plus 1, put it in TextView, TextView has 3 rows. So this index is actually where we’re going to truncate the mainContent. One word less or one word more is not right. Does the TextView provide a way to get the number of rows?

There are offers, we can go through the following methods

    textView.text = "111"
    val lineCount = textView.layout.lineCount // lineCount == 1
Copy the code

LineCount can be read from a setText without waiting for the next message loop as long as the layout is available. So we can simply find the index that fits into the space by using the following code.

    fun findIndex(textView: TextView, mainContent: CharSequence, suffix: CharSequence): Int {
        for (i in mainContent.length downTo 1) {
            textView.text = mainContent.subSequence(0, i).toString() + suffix.toString()
            val lineCount0 = textView.layout.lineCount
            textView.text = mainContent.subSequence(0, i + 1).toString() + suffix.toString()
            val lineCount1 = textView.layout.lineCount
            if (lineCount0 == 2 && lineCount1 == 3) {
                return i
            }
        }
        return - 1
    }
Copy the code

The logic is simple, try it one by one until you find one more index that you can’t put down.

To optimize the

The layout is not always available. After setText in onCreate, the layout is null. You need to wait for the onMeasure of the TextView to generate a layout. In addition, the layout process is very expensive. Can I put the layout process in the child thread? And this one text attempt is really a little silly, is there a better algorithm?

OnCreate getLayout is null after setText.

View TextView source code, we can find that the first time the layout is created in onMeasure, so we can not get the layout in onCreate setText. What I do is to add a LayoutChangeListener when the Layout is null, and then retrieve the Layout from the LayoutChangeListener.

The layout process consumes resources. Can we create a new layout and put the measurement process on the child thread? SetText affects TextChangedListener so many times.

If you have used the autoTextSize of the new TextView, if you have, you may have noticed that this feature is implemented in low version compatibility in AppCompatTextView. Inside a class called AppCompatTextViewAutoSizeHelper, through reflection and a series of methods, the new created a StaticLayout come out, and then by the newly created this StaticLayout binary search to calculate the most suitable size.

That we can imitate AppCompatTextViewAutoSizeHelper implementation, we also new a StaticLayout out? It’s ok to create a New StaticLayout, but the problem is that TextView doesn’t always use StaticLayout to layout text, so if we need to change to see more colors, and we need to be able to click to see more, we’ll use SpannableString, TextView will use DynamicLayout to layout the text. Different layouts may produce different results. So we need to not only new a StaticLayout, but also need to new a DynamicLayout when appropriate, which is a series of reasons to complicate the problem. Finally I gave up new and a new DynamicLayout came out. Instead, use TextView’s layout directly, which can greatly reduce the complexity of the problem, but the problem is that it cannot be measured in child threads, and because of multiple setText, it will result in multiple TextChangedListener callbacks.

It’s stupid to try it all out. How do you optimize?

Optimization is very simple, direct binary search. You can filter TextViewLayout in logcat. You can view the entire binary search process in debug logs.

Add animation

Now that you’ve figured out how to fold, you might want to expand as well. Expanding is very simple, just set all the text on it. What if you need animation? Can I use Animator? Or Animation?

Whether using Animator or Animation, what needs to be changed during Animation? It’s altitude, but the question is, how do I know how high I’m going to go before I move? This is a very tricky problem.

Fortunately, Google provides us with another way to implement animation, TransitionManager

    TransitionManager.beginDelayedTransition(sceneRoot, transition)
Copy the code

SceneRoot is the target View that you want to animate, and Transition is the animation mode, and usually AutoTransition is the default. So why does TransitionManager animate like this? This is basically the following line of code

    sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener)
Copy the code

The performance of this animation is very good. When it changes the height and width of animation, it only triggers the layout once, which can greatly reduce the performance consumption. More detailed principles will be analyzed later.

The end of the

This is the end of the whole process. If you have any other questions, please leave a comment.