自定义布局系列之自定义RelativeLayout
众所周知 , Android碎片化非常严重 , 全球那么多机型 , 数以千计的分辨率 , 给我们的开发带来的不小的难度 , 那么机型都需要配置 , 谷歌也做了一些努力 , 退出了与分辨率无关的dp , sp等等单位 , 但这些单位在不同的机型上面还是显示各异 , 谷歌在15年的时候推出了百分比布局 , 可以在一定程度上 , 解决我们的布局差异问题 , 但却只有两种百分比布局 , PercentRelativeLayout 和 PercentFrameLayout 。
下面我们就深究其理 , 写一个我们自己的百分比布局 。
在写自定义布局之前 , 我们首先要弄明白的一点是 , 怎样进行百分比布局 , 只有明白其中原理 , 代码就可以随之写出了 。我们知道 , View最重要的三个方法是onMeasure
,onLayout
, onDraw
这三个方法决定了View的宽高,在什么位置 , 以及呈现的形态 , 这和我们绘画是一致的 , 都需要明确 , 画什么(确定宽高属性) , 在什么地方画 (确定绘画的位置), 怎样画(用什么颜色的画笔,尺子等)等问题 。
明确了上述问题 , 接下来就是分析我们的百分比布局 , 用到了哪些方法 。 我们的百分比布局 , 主要改变的是子控件的宽高,以及外边距的属性 , 也就是画什么 , 需要多宽多高 , 所以我们需要在onMeasure
方法里面做工作 , 去改变子控件的属性值 。
我们都写过LayoutInflater.infalte()
这个方法 , 将我们的xml文件布局inflate成一个View对象 , 我们在写自定义控件的时候 , 也都需要实现一个带属性参数的构造函数,PercentRelativeLayout(Context context, AttributeSet attrs)
,如果没有 , 则在xml中使用中会报错 。 在xml文件中写的View控件标签 , 最后都会inflate成一个View对象 ,而inflate里面 , 使用的pull解析 , 将xml标签解析成一个个View对象 。
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
在解析View标签的时候 , 必然也会将标签的属性一并解析 , 并将标签属性设置到LayoutParams对象中 , 所以我们才可以通过子控件拿到布局属性 。
// 拿到根布局的View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// 创建一个布局参数对象
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
/**
* Returns a new set of layout parameters based on the supplied attributes set.
*
* @param attrs the attributes to build the layout parameters from
*
* @return an instance of {@link android.view.ViewGroup.LayoutParams} or one
* of its descendants
*/
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
知道了上面两点 , 我们就可以自定义一个我们自己的布局参数 , 通过generateLayoutParams()
返回我们的自定义布局参数对象 。之后我们就可以通过判断是否是我们自定义的布局参数 , 来进行子控件的宽高改变 。
首先我们将自定义的布局属性得到 , 并实例化自定义布局属性对象:
/**
* 自定义布局参数类 , 因为只是拓展RelativeLayout属性 , 所以继承自RelativeLayout.LayoutParams类
* ,保留原有的RelativeLayout属性 。
*/
static class PercentLayoutParams extends RelativeLayout.LayoutParams {
public PercentLayoutInfo info = null;
public PercentLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
/* 初始化布局自定义布局属性信息对象 */
info = info != null ? info : new PercentLayoutInfo();
/* 得到自定义布局属性值 */
TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.PercentLayout);
info.setPercentHeight(typedArray.getFloat(R.styleable.PercentLayout_layout_percentHeight, 0));
info.setPercentWidth(typedArray.getFloat(R.styleable.PercentLayout_layout_percentWidth, 0));
info.setPercentMarginOfWidth(typedArray.getFloat(R.styleable.PercentLayout_layout_percentMarginOfWidth, 0));
info.setPercentMarginOfHeight(typedArray.getFloat(R.styleable.PercentLayout_layout_percentMarginOfHeight, 0));
info.setPercentMarginLeft(typedArray.getFloat(R.styleable.PercentLayout_layout_percentMarginLeft, 0));
info.setPercentMarginTop(typedArray.getFloat(R.styleable.PercentLayout_layout_percentMarginTop, 0));
info.setPercentMarginRight(typedArray.getFloat(R.styleable.PercentLayout_layout_percentMarginRight, 0));
info.setPercentMarginBottom(typedArray.getFloat(R.styleable.PercentLayout_layout_percentMarginBottom, 0));
typedArray.recycle();
}
}
自己的LayoutParams类 , 是继承自RelativeLayout的 , 因为我们只是做扩展 , 所有保留原有的RelativeLayout的属性。接下来我们将PercentLayoutParams
对象 ,作为返回值返给我们的generateLayoutParams
方法 , 这样我们就可以在测量的时候 , 进行LayoutParams对象的判断了。
/*
* 生成布局参数对象
* */
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
/* 将返回的布局参数对象设置成自定义的参数对象 */
return new PercentLayoutParams(getContext(), attrs);
}
测量的时候 , 我们对LayoutParams
对象进行判断 , 然后将我们自定义的属性 , 设置到子控件相应的属性 :
/* 测量View的宽高 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
/* 测量RelativeLayout的宽高 */
int viewGroupWidth = MeasureSpec.getSize(widthMeasureSpec);
int viewGroupHeight = MeasureSpec.getSize(heightMeasureSpec);
/* 得到RelativeLayout中的子控件个数 */
int childCount = this.getChildCount();
/* 循环取得子控件 */
for (int i = 0; i < childCount; i++) {
View child = this.getChildAt(i);
/* 得到子控件的布局参数 */
ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
/* 判断LayoutParams对象是不是自定义的Params对象 */
if (layoutParams instanceof PercentRelativeLayout.PercentLayoutParams) {
/* 得到布局参数信息对象 */
PercentLayoutInfo info = ((PercentLayoutParams) layoutParams).info;
if (info != null) {
/* 得到自定义的宽高比 */
float percentWidth = info.getPercentWidth();
float percentHeight = info.getPercentHeight();
/* 如果宽高比大于0 ,则与父容器宽高进行计算 , 并将结果赋值给子控件 */
if (percentHeight > 0) {
layoutParams.height = (int) (percentHeight * viewGroupHeight);
}
if (percentWidth > 0) {
layoutParams.width = (int) (percentWidth * viewGroupWidth);
}
/* 设置外边距百分比 */
setLayoutPercentMargin(layoutParams, info, viewGroupWidth, viewGroupHeight);
}
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
设置外边距百分比:
/**
* 设置子控件外边距百分比
* @param layoutParams 子控件的布局参数
* @param info 自定义属性对象
* @param parentWidth 父容器的宽度
* @param parentHeight 父容器的高度
*/
private void setLayoutPercentMargin(ViewGroup.LayoutParams layoutParams, PercentLayoutInfo info, int parentWidth, int parentHeight) {
/* 得到自定义属性的值 */
float percentMarginOfWidth = info.getPercentMarginOfWidth();
float percentMarginOfHeight = info.getPercentMarginOfHeight();
float percentMarginLeft = info.getPercentMarginLeft();
float percentMarginTop = info.getPercentMarginTop();
float percentMarginRight = info.getPercentMarginRight();
float percentMarginBottom = info.getPercentMarginBottom();
/* 判断子控件是否设置了外边距的参数 */
if (layoutParams instanceof MarginLayoutParams) {
if (percentMarginOfWidth > 0) {
setLayoutMarginParams(layoutParams, percentMarginOfWidth, parentWidth);
}
if (percentMarginOfHeight > 0 ) {
setLayoutMarginParams(layoutParams, percentMarginOfHeight, parentHeight);
}
if (percentMarginLeft > 0) {
((MarginLayoutParams) layoutParams).leftMargin = (int) (percentMarginLeft * parentWidth);
}
if (percentMarginTop > 0) {
((MarginLayoutParams) layoutParams).topMargin = (int) (percentMarginTop * parentHeight);
}
if (percentMarginRight > 0) {
((MarginLayoutParams) layoutParams).rightMargin = (int) (percentMarginRight * parentWidth);
}
if (percentMarginBottom > 0) {
((MarginLayoutParams) layoutParams).bottomMargin = (int) (percentMarginBottom * parentHeight);
}
}
}
/**
* 设置子控件的外边距 , 根据父容器的某个宽度和高度的百分比
* @param layoutParams 子控件的布局参数对象
* @param percent 自定义属性百分比
* @param parent 父容器的宽度或高度
*/
private void setLayoutMarginParams(ViewGroup.LayoutParams layoutParams, float percent, int parent) {
((MarginLayoutParams) layoutParams).leftMargin = (int) (percent * parent);
((MarginLayoutParams) layoutParams).topMargin = (int) (percent * parent);
((MarginLayoutParams) layoutParams).rightMargin = (int) (percent * parent);
((MarginLayoutParams) layoutParams).bottomMargin = (int) (percent * parent);
}
控件属性XML:
自定义RelativeLayout就到这里 , 其他的百分比布局 , 原理都是类似的 , 这里就不赘述了 , 如果写多个百分比布局 , 可以将那些属性处理 , 属性设置 , 抽取一个帮助类出来 。