从源码学习自定义View(二):LayoutParams

LayoutParams

  在前面的measure过程中我们知道,当父View给子View构建MeasureSpec的时候不是直接构建的,而是根据父View的宽高模式和子View的LayoutParams共同的作用下生成MeasureSpec。
  这里就关乎到了LayoutParams,从名字可以看出这是布局参数,也就是子View所携带的布局参数,通常是在xml中设置的,如我们常用的android:layout_width等,最终在解析为View的时候都会将某些参数封装到LayoutParams中由View携带。

  这个LayoutParms代表的是子View本身的意愿,父View对子View进行measure和layout的时候都会参考到它的意愿。
  另外,上一章谈到测量子View的时候最好使用measureChildWithMargins方法进行测量,因为这样会使得控件更加友好。但是,在该方法中,ViewGroup将LayoutParams强转成了MarginLayoutParams,而强转很容易抛出类型转换异常,而这样是否会出现问题则需要看child.getLayoutParams()的返回值具体是什么,即child的LayoutParams是如何生成的。
  由于LayoutParams中存放的是View的属性,View的属性我们通常是在xml中设置的,那么这里又将涉及到View从xml到对象的解析过程,这里涉及的比较复杂,而这里又只是为了分析LayoutParams的生成,就简单的过一遍这个流程吧。(非通过布局添加的View的LayoutParams在后面也会有提及)

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
	}
}

  上面是我们在Activity加载xml布局的方法,通过setContentView进行加载。而继续追踪setContentView可以看到该方法是在AppCompatActivity的方法。

public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
        TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {
    ...
    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }
    
    @NonNull
    public AppCompatDelegate getDelegate() {
        if (mDelegate == null) {
            mDelegate = AppCompatDelegate.create(this, this);
        }
        return mDelegate;
    }
    ...
}

在AppCompatActivity,依旧没有处理布局而是交给了AppCompatDelegate,而AppCompatDelegate只是个抽象方法,具体实现在AppCompatDelegateImpl中。

@Override
public void setContentView(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mOriginalWindowCallback.onContentChanged();
}

  从上面我们可以看出布局文件实际上是通过LayoutInflater加载的,而在它的方法inflate中,则是具体的解析xml的过程的地方。而inflate方法最终会调用下面这个重载方法进行设置LayoutParams。

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
	...
    ViewGroup.LayoutParams params = null;
    ...
    params = root.generateLayoutParams(attrs);
    if (!attachToRoot) {
        temp.setLayoutParams(params);
    }
    ...             
    if (root != null && attachToRoot) {
          root.addView(temp, params);
    }
    ...
}

  绕了一大圈最后发现是在LayoutInflater中通过调用root.generateLayoutParams(attrs)创建的,并通过addView添加进父View中。也就是说,子View本身所携带的LayoutParams是由父View根据子View的布局属性来生成的。 而generateLayoutParams方法则是定义在ViewGroup中,追踪这么多其实就是想引出generateLayoutParams而已。
  在了解这些方法之前,还需要对LayoutParams有所了解。在ViewGroup中,通过静态内部类定义了两个LayoutParams。一个就是LayoutParams而另一个则是继承LayoutParams的MarginLayoutParams,后者就是在前者的基础属性之上又封装了margin属性等。

public static class LayoutParams {
    public static final int FILL_PARENT = -1;
    public static final int MATCH_PARENT = -1;
    public static final int WRAP_CONTENT = -2;

    public int width;
    public int height;
    ...
    public LayoutParams(Context c, AttributeSet attrs) {
        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
        // 获取宽高
        setBaseAttributes(a,
                R.styleable.ViewGroup_Layout_layout_width,
                R.styleable.ViewGroup_Layout_layout_height);
        a.recycle();
    }
    protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
        width = a.getLayoutDimension(widthAttr, "layout_width");
        height = a.getLayoutDimension(heightAttr, "layout_height");
    }
    ...
}


public static class MarginLayoutParams extends ViewGroup.LayoutParams {
    public int leftMargin;
    public int topMargin;
    public int rightMargin;
    public int bottomMargin;
        
    private int startMargin = DEFAULT_MARGIN_RELATIVE;
    private int endMargin = DEFAULT_MARGIN_RELATIVE;
    public static final int DEFAULT_MARGIN_RELATIVE = Integer.MIN_VALUE;
    ...
    public MarginLayoutParams(Context c, AttributeSet attrs) {
        super();

        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
        // 获取宽高
        setBaseAttributes(a,
                R.styleable.ViewGroup_MarginLayout_layout_width,
                R.styleable.ViewGroup_MarginLayout_layout_height);
		// 先获取margin
        int margin = a.getDimensionPixelSize(
                com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
        if (margin >= 0) {
            leftMargin = margin;
            topMargin = margin;
            rightMargin= margin;
            bottomMargin = margin;
        // 未设置margin则获取marginRight等
        } else {
            int horizontalMargin = a.getDimensionPixelSize(
                    R.styleable.ViewGroup_MarginLayout_layout_marginHorizontal, -1);
            int verticalMargin = a.getDimensionPixelSize(
                    R.styleable.ViewGroup_MarginLayout_layout_marginVertical, -1);
            if (horizontalMargin >= 0) {
                leftMargin = horizontalMargin;
                rightMargin = horizontalMargin;
            } else {
                leftMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
                        UNDEFINED_MARGIN);
                if (leftMargin == UNDEFINED_MARGIN) {
                    mMarginFlags |= LEFT_MARGIN_UNDEFINED_MASK;
                    leftMargin = DEFAULT_MARGIN_RESOLVED;
                }
                rightMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginRight,
                        UNDEFINED_MARGIN);
                if (rightMargin == UNDEFINED_MARGIN) {
                    mMarginFlags |= RIGHT_MARGIN_UNDEFINED_MASK;
                    rightMargin = DEFAULT_MARGIN_RESOLVED;
                }
            }
            startMargin = a.getDimensionPixelSize(
                    R.styleable.ViewGroup_MarginLayout_layout_marginStart,
                    DEFAULT_MARGIN_RELATIVE);
            endMargin = a.getDimensionPixelSize(
                    R.styleable.ViewGroup_MarginLayout_layout_marginEnd,
                    DEFAULT_MARGIN_RELATIVE);
            if (verticalMargin >= 0) {
                topMargin = verticalMargin;
                bottomMargin = verticalMargin;
            } else {
                topMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginTop,
                        DEFAULT_MARGIN_RESOLVED);
                bottomMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginBottom,
                        DEFAULT_MARGIN_RESOLVED);
            }
			...
        }
      ...
        a.recycle();
    }
    ...
}

  上面就是LayoutParams和MarginLayoutParams的定义,其中只列出了部分,但是我们可以看出它其实就是用来存放一些布局属性的。并且,对应布局文件的那个构造方法会读取相应的属性。在LayoutParams中,只获取了宽高属性。而在MarginLayoutParams中则获取了所有的margin属性。
  并且,获取margin属性的时候会有覆盖。根据上面的构造方法可以看出,若是设置了layout_margin则会设置上下左右的margin都是该值,并且不再读取其他margin属性。若是没有设置,则进一步读取layout_marginVertical和layout_marginHorizontal。若是设置了,则会覆盖上下margin和左右margin,若是都没有设置才会去读取layout_marginLeft等属性。

那么接下来继续看generateLayoutParams方法。

public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}

  从LayoutInflater中我们知道LayoutParams是通过generateLayoutParams方法生成的,当attachToRoot为false并且root非空时就直接将param设置到View中,否则的话通过root.addView(temp, params)添加进生成的View和param添加进父View。
   这里也能看出来,当inflate一个布局的时候,若是未传进root父布局,则会丢失本身所携带的属性参数。
那么继续看addView方法。

@Override
public void addView(View child, LayoutParams params) {
    addView(child, -1, params);
}

public void addView(View child, int index, LayoutParams params) {
    ...
    requestLayout();
    invalidate(true);
    addViewInner(child, index, params, false);
}

private void addViewInner(View child, int index, LayoutParams params,
    boolean preventRequestLayout) {
    ...
    if (!checkLayoutParams(params)) {
        params = generateLayoutParams(params);
    }
	...
    if (preventRequestLayout) {
        child.mLayoutParams = params;
    } else {
        child.setLayoutParams(params);
    }
	...
}

  addView一路调用到了addViewInner,然后调用方法checkLayoutParams进行判断Params是否满足条件,不满足的话则根据已有的params重新生成一个LayoutParams,然后才会调用setLayoutParams进行设置Params。

// 检查params是否满足我们的要求,满足则返回true
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    return  p != null;
}
// 根据已有的Params生成LayoutParams
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    return p;
}    

  但是在ViewGroup中,判断param是否满足条件的方法中只是简答的判断params是否为空,而重新生成param的方法更是什么也没做,而是简单粗暴地再将原param再返回来,因此当我们自定义ViewGroup的时候,需要重写这两个方法,以满足通过addView动态添加子View时未传递params的情况。
  分析到这里,我们可以知道在View添加到ViewGroup中的时候,布局参数只是ViewGroup.LayoutParams,因此,若是我们不进行处理的话,在使用measureChildWithMargins方法进行测量子View的时候,将会在强转为MarginLayoutParams的时候抛出异常。因此,自定义ViewGroup若要使用margin属性,则必须重写generateLayoutParams(attr)方法,让其返回MarginLayoutParams对象。(对应于通过布局文件声明的ViewGroup)
  上述讨论的是在xml布局中给自定义ViewGroup添加子View并生成LayoutParams的过程,还有另一种情况,我们并不是通过布局文件给我们自定义的ViewGroup添加子View的,而是直接调用构造方法生成的子View然后调用VIewGroup的addView添加进去的。而这里addView 有多个重载方法,对应不同的情况,但是所有这些情况都是针对于是否含有LayoutParams的。

// 1
public void addView(View child) {
    addView(child, -1);
}
//2
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();
    // 若是添加的子View没有LayoutParams,则构建一个默认的params
    if (params == null) {
        params = generateDefaultLayoutParams();
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}
//3
public void addView(View child, LayoutParams params) {
    addView(child, -1, params);
}
//4
public void addView(View child, int width, int height) {
	// 带宽高参数的方法,构建一个默认的LayoutParams然后修改宽高
    final LayoutParams params = generateDefaultLayoutParams();
    params.width = width;
    params.height = height;
    addView(child, -1, params);
}
//5
public void addView(View child, int index, LayoutParams params) {
   ...
    requestLayout();
    invalidate(true);
    addViewInner(child, index, params, false);
}

  从上面我们看到,一共有5种添加子View的方法,其中我们常用的是1,3,4这三种方法。而这些的方法实际上的区别就是是否有LayoutParams参数,当不存在的时候则会通过generateDefaultLayoutParams方法构建一个默认的LayoutParams。并且这几种方法到最后都是调用了方法5,也就是通过xml布局生成params调用的方法,而方法5我们上面已经说过了。

// 构建默认的LayoutParams
protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

  从generateDefaultLayoutParams我们可以看到默认情况下生成的LayoutParams宽高的模式都是wrap_content。前面提过,inflate布局时未设置父View,则会丢失最外层的布局属性,而没有外层属性的时候,则会直接生成默认的params,宽高都是wrap_content。

  那么总结一下,这一趟下来我们接触了3个generateLayoutParams的重载方法,1个checkLayoutParams方法。这四个方法是涉及到LayoutParams的。

// 方法1
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {}
// 方法2
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {}
// 方法3
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {}
// 方法4
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {}

  那么总结一下这四个方法的关系,首先判断子View是否有LayoutParams,没有的话则通过方法1生成默认的LayoutParams,然后方法4判断这个LayoutParams是否满足我们的要求,不满足的话通过方法2重新生成我们需要的LayoutParams,最后给子View添加params。方法3是使用xml方式添加子View生成LayoutParams,添加进子View的时候也要经历上面的步骤。
  知道了它们的关系,那么我们也就知道了在自定义ViewGroup时如何处理LayoutParams了。也就是需要重写这四个方法,让其返回的LayoutParams是我们需要的。通常若是没有别的需求我们应该给子View添加MarginLayoutParams,这样就可以使用measureChildWithMargins方法来测量子View了。若是有其他需求的话,我们应该自定义LayoutParams,然后在重写的方法中构建我们自定义的LayoutParams返回。同样的,我们自定义的LayoutParams应该继承MarginLayoutParams,原因与上面一样。

总结

  若是想要响应子View的某些属性,如margin,gravity等,则在自定义ViewGroup的时候应当做如下操作。

   1,根据需求决定是否需要定义LayoutParams
    1.1,需求即是否需要响应子View除了宽高margin以外的属性
    1.2,定义LayoutParams时应当继承ViewGroup.LayoutParams或者MarginLayoutParams
   2,重写generateDefaultLayoutParams生成我们需要的默认的LayoutParams
   3,重写checkLayoutParams判断是否是我们所需的LayoutParams
   4,重写两个generateLayoutParams方法,生成对应的LayoutParams


相关文章目录:

  从源码学习自定义View(一):Measure过程
  从源码学习自定义View(二):LayoutParams
  从源码学习自定义View(三):Layout和Draw过程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值