自定義布局系列之自定義RelativeLayout
眾所周知 , Android碎片化非常嚴(yán)重 年枕, 全球那么多機(jī)型 炫欺, 數(shù)以千計(jì)的分辨率 , 給我們的開發(fā)帶來的不小的難度 画切, 那么機(jī)型都需要配置 , 谷歌也做了一些努力 囱怕, 退出了與分辨率無關(guān)的dp , sp等等單位 霍弹, 但這些單位在不同的機(jī)型上面還是顯示各異 , 谷歌在15年的時(shí)候推出了百分比布局 娃弓, 可以在一定程度上 典格, 解決我們的布局差異問題 , 但卻只有兩種百分比布局 台丛, PercentRelativeLayout 和 PercentFrameLayout 耍缴。
下面我們就深究其理 , 寫一個(gè)我們自己的百分比布局 挽霉。
在寫自定義布局之前 防嗡, 我們首先要弄明白的一點(diǎn)是 , 怎樣進(jìn)行百分比布局 侠坎, 只有明白其中原理 蚁趁, 代碼就可以隨之寫出了 。我們知道 实胸, View最重要的三個(gè)方法是onMeasure
,onLayout
, onDraw
這三個(gè)方法決定了View的寬高他嫡,在什么位置 , 以及呈現(xiàn)的形態(tài) 庐完, 這和我們繪畫是一致的 钢属, 都需要明確 , 畫什么(確定寬高屬性) 门躯, 在什么地方畫 (確定繪畫的位置)淆党, 怎樣畫(用什么顏色的畫筆,尺子等)等問題 。
明確了上述問題 宁否, 接下來就是分析我們的百分比布局 窒升, 用到了哪些方法 。 我們的百分比布局 慕匠, 主要改變的是子控件的寬高饱须,以及外邊距的屬性 , 也就是畫什么 台谊, 需要多寬多高 蓉媳, 所以我們需要在onMeasure
方法里面做工作 , 去改變子控件的屬性值 锅铅。
我們都寫過LayoutInflater.infalte()
這個(gè)方法 酪呻, 將我們的xml文件布局inflate成一個(gè)View對(duì)象 , 我們?cè)趯懽远x控件的時(shí)候 盐须, 也都需要實(shí)現(xiàn)一個(gè)帶屬性參數(shù)的構(gòu)造函數(shù)玩荠,PercentRelativeLayout(Context context, AttributeSet attrs)
,如果沒有 贼邓, 則在xml中使用中會(huì)報(bào)錯(cuò) 阶冈。 在xml文件中寫的View控件標(biāo)簽 , 最后都會(huì)inflate成一個(gè)View對(duì)象 塑径,而inflate里面 女坑, 使用的pull解析 , 將xml標(biāo)簽解析成一個(gè)個(gè)View對(duì)象 统舀。
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標(biāo)簽的時(shí)候 匆骗, 必然也會(huì)將標(biāo)簽的屬性一并解析 , 并將標(biāo)簽屬性設(shè)置到LayoutParams對(duì)象中 誉简, 所以我們才可以通過子控件拿到布局屬性 碉就。
// 拿到根布局的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);
}
// 創(chuàng)建一個(gè)布局參數(shù)對(duì)象
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);
}
知道了上面兩點(diǎn) , 我們就可以自定義一個(gè)我們自己的布局參數(shù) 闷串, 通過generateLayoutParams()
返回我們的自定義布局參數(shù)對(duì)象 铝噩。之后我們就可以通過判斷是否是我們自定義的布局參數(shù) , 來進(jìn)行子控件的寬高改變 窿克。
首先我們將自定義的布局屬性得到 , 并實(shí)例化自定義布局屬性對(duì)象:
/**
* 自定義布局參數(shù)類 骏庸, 因?yàn)橹皇峭卣筊elativeLayout屬性 , 所以繼承自RelativeLayout.LayoutParams類
* 年叮,保留原有的RelativeLayout屬性 具被。
*/
static class PercentLayoutParams extends RelativeLayout.LayoutParams {
public PercentLayoutInfo info = null;
public PercentLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
/* 初始化布局自定義布局屬性信息對(duì)象 */
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的 只损, 因?yàn)槲覀冎皇亲鰯U(kuò)展 一姿, 所有保留原有的RelativeLayout的屬性七咧。接下來我們將PercentLayoutParams
對(duì)象 ,作為返回值返給我們的generateLayoutParams
方法 叮叹, 這樣我們就可以在測(cè)量的時(shí)候 艾栋, 進(jìn)行LayoutParams對(duì)象的判斷了。
/*
* 生成布局參數(shù)對(duì)象
* */
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
/* 將返回的布局參數(shù)對(duì)象設(shè)置成自定義的參數(shù)對(duì)象 */
return new PercentLayoutParams(getContext(), attrs);
}
測(cè)量的時(shí)候 蛉顽, 我們對(duì)LayoutParams
對(duì)象進(jìn)行判斷 蝗砾, 然后將我們自定義的屬性 , 設(shè)置到子控件相應(yīng)的屬性 :
/* 測(cè)量View的寬高 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
/* 測(cè)量RelativeLayout的寬高 */
int viewGroupWidth = MeasureSpec.getSize(widthMeasureSpec);
int viewGroupHeight = MeasureSpec.getSize(heightMeasureSpec);
/* 得到RelativeLayout中的子控件個(gè)數(shù) */
int childCount = this.getChildCount();
/* 循環(huán)取得子控件 */
for (int i = 0; i < childCount; i++) {
View child = this.getChildAt(i);
/* 得到子控件的布局參數(shù) */
ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
/* 判斷LayoutParams對(duì)象是不是自定義的Params對(duì)象 */
if (layoutParams instanceof PercentRelativeLayout.PercentLayoutParams) {
/* 得到布局參數(shù)信息對(duì)象 */
PercentLayoutInfo info = ((PercentLayoutParams) layoutParams).info;
if (info != null) {
/* 得到自定義的寬高比 */
float percentWidth = info.getPercentWidth();
float percentHeight = info.getPercentHeight();
/* 如果寬高比大于0 携冤,則與父容器寬高進(jìn)行計(jì)算 悼粮, 并將結(jié)果賦值給子控件 */
if (percentHeight > 0) {
layoutParams.height = (int) (percentHeight * viewGroupHeight);
}
if (percentWidth > 0) {
layoutParams.width = (int) (percentWidth * viewGroupWidth);
}
/* 設(shè)置外邊距百分比 */
setLayoutPercentMargin(layoutParams, info, viewGroupWidth, viewGroupHeight);
}
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
設(shè)置外邊距百分比:
/**
* 設(shè)置子控件外邊距百分比
* @param layoutParams 子控件的布局參數(shù)
* @param info 自定義屬性對(duì)象
* @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();
/* 判斷子控件是否設(shè)置了外邊距的參數(shù) */
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);
}
}
}
/**
* 設(shè)置子控件的外邊距 , 根據(jù)父容器的某個(gè)寬度和高度的百分比
* @param layoutParams 子控件的布局參數(shù)對(duì)象
* @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:
<declare-styleable name="PercentLayout">
<!-- 寬高比 -->
<attr name="layout_percentWidth" format="float"/>
<attr name="layout_percentHeight" format="float"></attr>
<!-- 外邊距百分比 -->
<!-- 按照父容器的寬度來設(shè)置子控件的四個(gè)外邊距的百分比 -->
<attr name="layout_percentMarginOfWidth" format="float"></attr>
<!-- 按照父容器的高度來設(shè)置子控件的四個(gè)外邊距的百分比 -->
<attr name="layout_percentMarginOfHeight" format="float"></attr>
<!-- 四個(gè)外邊距的百分比 -->
<attr name="layout_percentMarginLeft" format="float"></attr>
<attr name="layout_percentMarginRight" format="float"></attr>
<attr name="layout_percentMarginTop" format="float"></attr>
<attr name="layout_percentMarginBottom" format="float"></attr>
</declare-styleable>
自定義RelativeLayout就到這里 曾棕, 其他的百分比布局 扣猫, 原理都是類似的 , 這里就不贅述了 翘地, 如果寫多個(gè)百分比布局 申尤, 可以將那些屬性處理 , 屬性設(shè)置 衙耕, 抽取一個(gè)幫助類出來 昧穿。