addView()后子布局不居中问题总结与分析

来源:互联网 发布:原生js 双向绑定 编辑:程序博客网 时间:2024/05/21 10:07

题记

最近项目中遇到一个问题,一个自定义view使用addview加入父布局后,突然不居中了,原因是对父布局增加了一层嵌套;分析类似的问题前我们首先需要理解LayoutParams概念。


LayoutParams

Android SDK中的介绍如下:

LayoutParams are used by views to tell their parents how they want to be laid out. See ViewGroup Layout Attributes for a list of all child view attributes that this class supports.The base LayoutParams class just describes how big the view wants to be for both width and height. For each dimension, it can specify one of:    FILL_PARENT (renamed MATCH_PARENT in API Level 8 and higher), which means that the view wants to be as big as its parent (minus padding)    WRAP_CONTENT, which means that the view wants to be just big enough to enclose its content (plus padding)    an exact number There are subclasses of LayoutParams for different subclasses of ViewGroup. For example, AbsoluteLayout has its own subclass of LayoutParams which adds an X and Y value.

其实这个LayoutParams类是用于child view(子视图) 向 parent view(父视图)传达自己的意愿的一个东西(孩子想变成什么样向其父亲说明)其实子视图父视图可以简单理解成。需要注意的是LayoutParams只是ViewGroup的一个内部类,也就是ViewGroup里边这个LayoutParams类是 base class 基类,实际上每个不同的ViewGroup都有自己的LayoutParams子类,比如LinearLayout 也有自己的 LayoutParams;RelativeLayout也有自己的LayoutParams;


问题描述

代码功能为:在activity中动态添加一个TextView,使TextView文本居中;

居中代码如下(没有问题):
xml布局:

<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:id="@+id/main"    tools:context="demo.sunrise.com.viewdemo.MainActivity"></FrameLayout>

activity中处理

public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        View main = findViewById(R.id.main);        TextView text = new TextView(this);        text.setText("test");        text.setGravity(Gravity.CENTER_HORIZONTAL);        ((ViewGroup)(main)).addView(text);}

这段代码是textview中的文字是可以显示到布局的正中间;

不居中代码(有问题):
只是修改xml:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:id="@+id/main"    tools:context="demo.sunrise.com.viewdemo.MainActivity"></RelativeLayout>

问题分析

对比居中代码和不居中代码,两者的差别就是xml布局中一个使用FrameLayout,一个使用了RelativeLayout;那么为什么会出现这么截然不同的结果呢?根据我们对LayoutParams的理解,子view的如何显示,是由子view的LayoutParams来通知父view的;那么我们addview时,子view的LayoutParams到底是怎么样呢?

下面我们对addview的源码进行分析:
addview实现源码如下(源文件地址:frameworks/base/core/java/android/view/ViewGroup.java):

/**     * <p>Adds a child view. If no layout parameters are already set on the child, the     * default parameters for this ViewGroup are set on the child.</p>     *     * <p><strong>Note:</strong> do not invoke this method from     * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},     * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>     *     * @param child the child view to add     *     * @see #generateDefaultLayoutParams()     */    public void addView(View child) {        addView(child, -1);    }    /**     * Adds a child view. If no layout parameters are already set on the child, the     * default parameters for this ViewGroup are set on the child.     *     * <p><strong>Note:</strong> do not invoke this method from     * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},     * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>     *     * @param child the child view to add     * @param index the position at which to add the child     *     * @see #generateDefaultLayoutParams()     */    public void addView(View child, int index) {        if (child == null) {            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");        }        LayoutParams params = child.getLayoutParams();        if (params == null) {            params = generateDefaultLayoutParams();            if (params == null) {                throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");            }        }        addView(child, index, params);    }    /**     * Adds a child view with this ViewGroup's default layout parameters and the     * specified width and height.     *     * <p><strong>Note:</strong> do not invoke this method from     * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},     * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>     *     * @param child the child view to add     */    public void addView(View child, int width, int height) {        final LayoutParams params = generateDefaultLayoutParams();        params.width = width;        params.height = height;        addView(child, -1, params);    }    /**     * Adds a child view with the specified layout parameters.     *     * <p><strong>Note:</strong> do not invoke this method from     * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},     * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>     *     * @param child the child view to add     * @param params the layout parameters to set on the child     */    @Override    public void addView(View child, LayoutParams params) {        addView(child, -1, params);    }    /**     * Adds a child view with the specified layout parameters.     *     * <p><strong>Note:</strong> do not invoke this method from     * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},     * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>     *     * @param child the child view to add     * @param index the position at which to add the child or -1 to add last     * @param params the layout parameters to set on the child     */    public void addView(View child, int index, LayoutParams params) {        if (DBG) {            System.out.println(this + " addView");        }        if (child == null) {            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");        }        // addViewInner() will call child.requestLayout() when setting the new LayoutParams        // therefore, we call requestLayout() on ourselves before, so that the child's request        // will be blocked at our level        requestLayout();        invalidate(true);        addViewInner(child, index, params, false);    }

可以发现我们使用addview(View v)方法时,子view的LayoutParams 是这么获取的(当子view的LayoutParams没有传递及没有被赋值):params = generateDefaultLayoutParams();下面我们来看generateDefaultLayoutParams()的实现:我们查看FrameLayout和RelativeLayout中generateDefaultLayoutParams()的实现(这里也是java多态的一种表现形式);(由于FrameLayout和RelativeLayout继承至ViewGroup,所以实际调用的是FrameLayout和RelativeLayout的generateDefaultLayoutParams()方法)。

FrameLayout.java:(源文件地址frameworks/base/core/java/android/widget/FrameLayout.java)

    /**     * Returns a set of layout parameters with a width of     * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT},     * and a height of {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}.     */    @Override    protected LayoutParams generateDefaultLayoutParams() {        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);    }

RelativeLayout.java:(源文件地址frameworks/base/core/java/android/widget/RelativeLayout.java)

    /**     * Returns a set of layout parameters with a width of     * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT},     * a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and no spanning.     */    @Override    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);    }

至此我们已经可以得出结论:FrameLayout中addview时子view的布局是MATCH_PARENT,而RelativeLayout是WRAP_CONTENT,这也就解释了为何修改过后的代码textview的文本是不居中的了。


解决方案

  1. 使用FrameLayout布局作为父布局,默认是MATCH_PARENT。
  2. 使用addView(View child, LayoutParams params)方法进行添加子view(添加时附带子view想要的显示参数),代码如下
public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        View main = findViewById(R.id.main);        //构建子view想要显示的参数        RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT                ,ViewGroup.LayoutParams.WRAP_CONTENT);        lp.addRule(RelativeLayout.CENTER_HORIZONTAL);        TextView text = new TextView(this);        text.setText("test");        text.setGravity(Gravity.CENTER_HORIZONTAL);        ((ViewGroup)(main)).addView(text,lp);    }}

总结

当动态添加布局时,尽量的将子view的LayoutParams定义好,然后使用addView(View child, LayoutParams params)方法进行添加,避免由于不同布局的默认参数不同,产生结果的不统一;另外使用LayoutParams时,应该是父布局是什么类型的ViewGroup,那么就是使用什么类型的LayoutParams。也就是父布局是FrameLayout,那么就是用FrameLayout.LayoutParams,因为不同ViewGroup的LayoutParams的参数和实现是不同的,务必保持一致