美文网首页程序员Android知识安卓控件
LinearLayout weight 子控件不重新测量的解决及

LinearLayout weight 子控件不重新测量的解决及

作者: sunrain_ | 来源:发表于2017-02-08 16:51 被阅读332次

    需求描述

    LinearLayout 中横向排列两个子控件 TextView 和 Button.
    Button 在 TextView 的右侧并根据文本长度动态改变位置。当 LinearLayout 到达最大宽度后,控制 TextView 的宽度避免 Button 被挤出屏幕。

    问题重现

    根据以上需求实现如下:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:orientation="horizontal">
    
        <TextView
            android:id="@+id/tv"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:ellipsize="end"
            android:maxLines="1"
            />
    
        <Button
            android:id="@+id/btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="update text"
            />
    </LinearLayout>
    

    Activity 部分代码:

    public class MainActivity extends Activity implements View.OnClickListener {
    
        private static final String sTestText = "This is a very long test text, used to test the project, it is more than the length of the screen width of the phone.";
    
        private Button mBtn;
        private TextView mTv;
    
        private void initView() {
            mBtn = (Button) findViewById(R.id.btn);
            mTv = (TextView) findViewById(R.id.tv);
        }
    
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.btn:
                    updateText();
                    break;
            }
        }
    
        private void updateText() {
            mTv.setText(sTestText.substring(0, getTextLength()));
        }
    
        private int getTextLength() {
            int length = mTv.getText().length() + 20;
            if (length > 70) {
                length = 20;
            }
            return length;
        }
    }
    

    但是实际使用时,TextView 首次显示文字后就不再改变宽度了(开启了显示布局边界功能)。

    TextView未改变宽度

    推测是 TextView 的 onMeasure() 方法没有执行。
    新建 LogTextView 打印日志后可以看到文字显示后再调用 setText() 方法时只调用了 onDraw() 方法。

    public class LogTextView extends TextView {
    
        private static final String TAG = "LogTextView";
        ...
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            Log.i(TAG, "onDraw");
        }
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            super.onLayout(changed, left, top, right, bottom);
            Log.i(TAG, "onLayout");
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            Log.i(TAG, "onMeasure");
        }
    }
    

    Log输出:

    I/LogTextView: onMeasure
    I/LogTextView: onMeasure
    I/LogTextView: onLayout
    I/LogTextView: onDraw
    I/MainActivity: button click
    I/LogTextView: onMeasure
    I/LogTextView: onLayout
    I/LogTextView: onDraw
    I/MainActivity: button click
    I/LogTextView: onDraw
    I/MainActivity: button click
    I/LogTextView: onDraw
    I/MainActivity: button click
    I/LogTextView: onDraw

    解决方案

    知道了导致问题的原因后,只要让 TextView 主动调用 onMeasure() 方法即可。
    在 setText() 之后,再调用 requestLayout() .
    修改 Activity#updateText() 方法如下:

    private void updateText() {
        mTv.setText(sTestText.substring(0, getTextLength()));
        mTv.requestLayout();
    }
    

    运行截图


    TextView正常改变宽度

    问题分析

    是什么导致 setText() 之后 onMeasure() 没有调用?

    查看源码可以看到 TextView 的 setText() 方法中又会调用 checkForRelayout() 方法。

    private void setText(CharSequence text, BufferType type, boolean notifyBefore, int oldlen) {
        ···
        if (mLayout != null) {
            checkForRelayout();
        }
        ···
    }
    

    这里结合上边的例子来分析 checkForRelayout() 方法。
    以下注释中的 TextView 都指上边 xml 中定义的 TextView.

    private void checkForRelayout() {
        /*
         * TextView宽为0dp,所以mLayoutParams.width = 0
         * TextView没有设置Hint,所以mHint = null
         * TextView没有设置padding或Drawable,所以 getCompoundPaddingLeft() = getCompoundPaddingLeft() = 0 
         * 当TextView无内容时,mRight = mLeft = 0
         * 当TextView有内容时,mRight > mLeft
         *
         * 结合上述分析,if可以简化为:
         * TextView无内容时:if ((true || 无所谓) && (true) && (false)) = false
         * TextView有内容时:if ((true || 无所谓) && (true) && (true)) = true
         *
         * 需要注意,TextView有没有内容要取决于方法执行到这里时的状态,
         * 比如说给一个无内容TextView设置文字,执行到这里的时候,
         * 还没有调用requestLayout()方法,TextView依旧是无内容的。
         */
        if ((mLayoutParams.width != ViewGroup.LayoutParams.WRAP_CONTENT || 
                (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) &&
                (mHint == null || mHintLayout != null) &&
                (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
    
            int oldht = mLayout.getHeight();
            int want = mLayout.getWidth();
            int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
    
            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
                    mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
                    false);
    
            // TextView Ellipsize 为 END
            // 进入if
            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
                // mLayoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
                // 不进入if
                if (mLayoutParams.height != ViewGroup.LayoutParams.WRAP_CONTENT &&
                        mLayoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) {
                    invalidate();
                    return;
                }
    
                // TextView行数最多为一行,所以高度没有发生改变
                // 没有设置Hint,mHintLayout = null
                // 进入if
                if (mLayout.getHeight() == oldht &&
                        (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
                    // 只进行了重绘
                    invalidate();
                    return;
                }
            }
    
            requestLayout();
            invalidate();
        } else {
            nullLayouts();
            // 第一次点击按钮时,会调用requestLayout()方法
            requestLayout();
            invalidate();
        }
    }
    

    从上边的代码分析可以看出

    • TextView 没有内容时,点击按钮会执行 requestLayout() 方法,重新测量,显示文字。
    • TextView 有内容时,点击按钮只进行了重绘,并没有改变大小。

    至此,setText() 之后 onMeasure() 没有调用的问题分析完毕。
    欢迎留言探讨,谢谢。

    相关文章

      网友评论

        本文标题:LinearLayout weight 子控件不重新测量的解决及

        本文链接:https://www.haomeiwen.com/subject/ssnvittx.html