Phenomenon of the problem

We were customizing the UI of a video component and noticed an unusual effect. Let me summarize:

  • Textview and Seekbar are usually displayed at the bottom of the video controller
  • This effect is usually achieved by setting a scheduled task to ressettext at regular intervals.

The effect is as follows:

The testers then discovered a certain anomaly: the video’s progress bar, known as seekbar, would always fall back a few seconds before the video started. Then the progress bar can be displayed properly.

How to fix this problem

After some effort, we found that the solution to this problem was to change the width property of the TextView from wrap_content to a fixed XXDP value. The solution seems relatively simple, but the logic behind it is not clear. Why do we fix the seekbar problem by changing the textView properties?

Restore the problem site

To get to the root of what happened, we did a minimal reduction. That is, build a clean project, eliminate other problems, and see what went wrong?

Let’s take a look at the layout, which is similar to the one we bugged in the beginning.

 <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@color/colorPrimary"
        android:orientation="horizontal"
        tools:ignore="MissingConstraints">

        <TextView
            android:id="@+id/tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:gravity="center"
            android:text="Hello World!" />

        <SeekBar
            android:id="@+id/sb"
            android:layout_width="100dp"
            android:layout_height="match_parent"
            android:layout_marginLeft="10dp">

        </SeekBar>
    </LinearLayout>
Copy the code

Then take a look at the code for our key recurrence problem:

 button = findViewById(R.id.bt);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread() {
                    @Override
                    public void run() {// Refresh the interface once in a whilewhile (true) {
                            try {
                                Thread.sleep(3000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
    
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {// This is the problem,setTv.settext (system.currentTimemillis () +)""); }}); } } }.start(); }});Copy the code

Source code analysis

First let’s see why textView setText causes a lot of problems with wrap_content.

Check out the setText source code:

So let’s post this whole thing up

 /**
     * Check whether entirely new text requires a new view layout
     * or merely a new text layout.
     */
    @UnsupportedAppUsage
    private void checkForRelayout() {
        // If we have a fixed width, we can just swap in a new text layout
        // if the text height stays the same or if the view height is fixed.

        if((mLayoutParams.width ! = LayoutParams.WRAP_CONTENT || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) && (mHint == null || mHintLayout ! = null) && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) { // Static width, so try making a new text layout. int oldht = mLayout.getHeight(); int want = mLayout.getWidth(); int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth(); /* * No need to bring the text into view, since the size is not * changing (unless wedo the requestLayout(), in which case it
             * will happen at measure).
             */
            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
                          mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
                          false);

            if(mEllipsize ! = TextUtils.TruncateAt.MARQUEE) { // In a fixed-height view, so use our new text layout.if(mLayoutParams.height ! = LayoutParams.WRAP_CONTENT && mLayoutParams.height ! = LayoutParams.MATCH_PARENT) { autoSizeText(); invalidate();return;
                }

                // Dynamic height, but height has stayed the same,
                // so use our new text layout.
                if (mLayout.getHeight() == oldht
                        && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
                    autoSizeText();
                    invalidate();
                    return;
                }
            }

            // We lose: the height has changed and we have a dynamic height.
            // Request a new view layout using our new text layout.
            requestLayout();
            invalidate();
        } else{ // Dynamic width, so we have no choice but to request a new // view layout with a new text layout. nullLayouts(); requestLayout(); invalidate(); }}Copy the code

RequestLayout will not trigger as long as certain conditions are met. A quick look at the source code shows that the width should not be a wrAP_content attribute, and should not be a horselight attribute. Of course, there are certain height requirements, but the most important one is the wrap_content attribute of Width (we use less of the other attributes), otherwise we would not be in the requestLayout process.

Has the cause really been analyzed?

Think again, in a view tree, does a redrawing of a child view necessarily cause the entire view tree to be redrawn?

Take a look at the source of the View itself

So let’s translate this code right here

MeasureSpec = MeasureSpec = MeasureSpec = MeasureSpec = MeasureSpec = MeasureSpec = MeasureSpec = MeasureSpec

The setText will trigger requestLayout if width equals Wrap Content, but this does not necessarily cause the sibling of the TextView to measure each time. This is why it appears that only the first setText will cause the Seekbar to be redrawn, but subsequent setText will not cause the Seekbar to be redrawn.

conclusion

For textView control, setText is an implicit trigger interface redraw operation, we periodically call textView setText method, must be considered, best to set the width of the TextView to a fixed value or match, Avoid causing all kinds of weird bugs, or reduce the efficiency of the interface rendering. Especially for our animation function writing, be sure to look at the API, see if this API will trigger requestLayout, so as to improve efficiency.