您的位置:首页 > 移动开发 > Android开发

Android自定义布局

2015-08-19 16:43 471 查看
首先我们观察Android API:

View.java

// 注意final修饰,该方法永远不会被覆盖,整个布局结构 measure方法唯一

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

onMeasure(widthMeasureSpec, heightMeasureSpec);

}

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}

//注意final修饰,该方法永远不会被覆盖,整个布局结构layout方法唯一

public final void layout(int l, int t, int r, int b) {

boolean changed = setFrame(l, t, r, b);

if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {

onLayout(changed, l, t, r, b);

}

}

protected void onLayout(boolean changed, int left, int top, int right, int bottom) { } 空方法

ViewGroup.java extends View.java

@Override

protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

// 测量该ViewGroup所包含的所有布局

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {}

protected void measureChild(View child, int parentWidthMeasureSpec,

int parentHeightMeasureSpec) {}

//我会单讲mChildren数组mChildren中的View是如何来的。

public View getChildAt(int index) {  return mChildren[index];  }

public int getChildCount() {  return mChildrenCount; }


RelativeLayout.java extends ViewGroup.java

//当继承RelativeLayout布局时,我们应当覆盖该方法,以实现测量该布局包含的View,//此处的实现并不能测量所有的View

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}

protected void onLayout(boolean changed, int l, int t, int r, int b) {}

private void measureChild(View child, LayoutParams params, int myWidth, int myHeight) {}

//还包含一个重要的内部类,代表RelativeLayout所包含的每一个view大小及位置信息

public static class LayoutParams extends ViewGroup.MarginLayoutParams{

private int mLeft, mTop, mRight, mBottom;

}


下面我要自定义一个布局,定义布局的目的肯定是为了向其内部放置View

CustomGridLayout.java extends RelativeLayout.java

初学者会问,我们到底需要继承RelativeLayout类的哪个方法呢!!

抛去一切,我们自己想象,布局控件需要

第一:控件(View)的大小

第二:控件(View)的位置

第三:知道要放置多少个View


通过熟读文档,我们应该知道:

onMeasure方法负责测量将要放在CustomGridLayout内部的View的大小。

onLayout方法负责分配尺寸及位置给将要放在CustomGridLayout内部的View。

所以很明显,需要我们继承的方法是

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}

功能:测量该布局所包含的所有View的大小(会在框架层循环取得每一个View,然后测量其大小),该方法会被View.java中的measure方法调用。而measure方法会被Window对象的DecorView调用

protected void onLayout(boolean changed, int l, int t, int r, int b) {}

功能:在相应的位置放置相应的View,该方法会被View.java中的layout方法调用,而layout方法会被谁调用呢?

(1) 调用requestLayout()方法. 该方法内部会执行Object.layout(…)

(2) 直接调用 Object.layout(…)

(3) 调用addView(View child, …)时,

调用addView(…)之前一般需要先调用android.view.View.setLayoutParams(LayoutParams params)



也许有人会问当调用addView时,会和框架层的layout,onLayout,measure, onMeasure等几个布局方法有什么关系,或者说后者几个方法是怎么被触发的,别着急,看见addView(…)的底层实现了吗?

public void addView(View child, int index, LayoutParams params) {

// 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();

addViewInner(child, index, params, false);

}


很明显,addView在底层调用了requestLayout方法,该方法如时序图所示,会依次触发我们的onMeasure,onLayout方法。

示例1-子控件横向滚动的容器控件

public class LandspaceLayout extends ComplexLayout {
private Rect mMaxChildSpace = new Rect();
private Rect mLastChildSpace = new Rect();

public LandspaceLayout(int w, int h) {
super(w, h);
}

@Override
public void reportChildLayout(Layout childLayout) {
// 子控件布局时,会调用父控件布局类的这个方法。在这里记录需要传递给父控件布局的子控件布局信息。
// 记录最大的子控件大小
final Rect prune = childLayout.getNativeSpace();
mMaxChildSpace.left = Math.max(mMaxChildSpace.left, prune.left);
mMaxChildSpace.right = Math.max(mMaxChildSpace.right, prune.right);
mMaxChildSpace.top = Math.max(mMaxChildSpace.top, prune.top);
mMaxChildSpace.bottom = Math.max(mMaxChildSpace.bottom, prune.bottom);
}

@Override
public void adjustSpace(Rect mySpace, Rect childSpace) {
// 该方法会在子控件都布局完后调用,根据子控件的布局调整当前控件的一些布局设置,在这里可以调整每个子控件的布局位置
// 未给定高度时,根据子控件大小改变当前控件高度
if (!fixHeight()) {
mySpace.bottom = mMaxChildSpace.bottom + mPaddingRect.bottom;
}

// 调整每个子控件的位置
Element element = null;
NodeList nodeList = mElement.getChildNodes();
final int size = nodeList.getLength();
for (int i = 0; i < size; i++) {
Node child = nodeList.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
element = (Element) child;
Layout layout = (Layout) element.getUserData(Entity.NODE_USER_STYLE);
if (layout != null && layout instanceof ComplexLayout) {
// 将子控件按顺序横向排列
ComplexLayout childComplexLayout = (ComplexLayout) layout;
if (childComplexLayout.fixLocation()) {
// 子控件固定位置,不作处理
continue;
}
if (childComplexLayout.fixX()) {
// 固定X值,Y值调整为0或上边距
int offsetY = mMarginRect.top;
childComplexLayout.setStyleByName(Entity.NODE_ATTRIBUTE_TOP, Integer.toString(offsetY) + "nat");
childComplexLayout.requstLayout();
} else if (childComplexLayout.fixY()) {
// 固定Y值,横向向后排列
int offsetX = 0;
if (mLastChildSpace.right == 0) {
offsetX = mMarginRect.left;
} else {
offsetX = mLastChildSpace.right + childComplexLayout.mSpacingX;
}
childComplexLayout.setStyleByName(Entity.NODE_ATTRIBUTE_LEFT, Integer.toString(offsetX) + "nat");
childComplexLayout.requstLayout();
} else {
int offsetX = 0;
if (mLastChildSpace.right == 0) {
offsetX = mMarginRect.left;
} else {
offsetX = mLastChildSpace.right +     childComplexLayout.mSpacingX;
}
childComplexLayout.setStyleByName(Entity.NODE_ATTRIBUTE_LEFT, Integer.toString(offsetX) + "nat");
int offsetY = mMarginRect.top;
childComplexLayout.setStyleByName(Entity.NODE_ATTRIBUTE_TOP, Integer.toString(offsetY) + "nat");
childComplexLayout.requstLayout();
}
mLastChildSpace.set(childComplexLayout.getDisplaySpace());
}
}
}
}
}


示例2-子控件按圆形布局的容器控件

public class CircleLayout extends ComplexLayout {

private Rect mMaxChildSpace = new Rect();
private int mChildNum = 0;
private int mRadius = 0;

public CircleLayout(int w, int h) {
super(w, h);
}

@Override
public void reportChildLayout(Layout childLayout) {
// 子控件布局时,会调用父控件布局类的这个方法。在这里记录需要传递给父控件布局的子控件布局信息。
// 记录子控件个数
mChildNum++;
// 记录最大的子控件大小
final Rect prune = childLayout.getNativeSpace();
mMaxChildSpace.left = Math.min(mMaxChildSpace.left, prune.left);
mMaxChildSpace.right = Math.max(mMaxChildSpace.right, prune.right);
mMaxChildSpace.top = Math.min(mMaxChildSpace.top, prune.top);
mMaxChildSpace.bottom = Math.max(mMaxChildSpace.bottom, prune.bottom);
}

@Override
public void adjustSpace(Rect mySpace, Rect childSpace) {
// 计算圆的半径
mRadius = (getWidth() - mMaxChildSpace.width()) / 2;

// 调整每个子控件的位置
int count = 0;
childSpace.setEmpty();
Element element = null;
NodeList nodeList = mElement.getChildNodes();
final int size = nodeList.getLength();
for (int i = 0; i < size; i++) {
Node child = nodeList.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
element = (Element) child;
Layout layout = (Layout) element.getUserData(Entity.NODE_USER_STYLE);
if (layout != null && layout instanceof ComplexLayout) {
// 按照子控件个数,将园N等分,从正上方开始,顺时针算出每个点的坐标,作为每个子控件中心点的位置
double angle = count * Math.PI * 2 / mChildNum - Math.PI / 2;
ComplexLayout childComplexLayout = (ComplexLayout) layout;
int offsetX = (int) (getWidth() / 2 + mRadius * Math.cos(angle));
offsetX -= childComplexLayout.mWidth / 2;
childComplexLayout.setStyleByName(Entity.NODE_ATTRIBUTE_LEFT, Integer.toString(offsetX) + "nat");
int offsetY = (int) (getWidth() / 2 + mRadius * Math.sin(angle));
offsetY -= childComplexLayout.mHeight / 2;
childComplexLayout.setStyleByName(Entity.NODE_ATTRIBUTE_TOP, Integer.toString(offsetY) + "nat");
childComplexLayout.requstLayout();

childSpace.union(childComplexLayout.getDisplaySpace());

count++;
}
}
}
// 未给定高度时,根据子控件大小改变当前控件高度
if (!fixHeight()) {
mySpace.bottom = childSpace.bottom + mPaddingRect.bottom;
}
}
}


自定义viewgroup实现等分格子布局

一般的思路就是,直接写布局文件,用LinearLayout 嵌套多层子LinearLayout,然后根据权重layout_weight可以达到上面的效果

还有就是利用gridview了,但是这里的需求就是不能上下滑动,使用gridview的时候还要计算布局的高度,否则内容超出下滑;

开始我是用的第一种,直接在布局文件实现了,但是后来发现代码太多太恶心哦,所以我继承viewGroup,重写两个关键的方法:onLayout(),onMeasure()

我的大致思路:

1.计算当前视图宽度和高度,然后根据边距,算出每个布局的item需要分配的多少宽度和高度:

2.支持adapter的方式,动态添加每一项,还可以设置每一项点击事件

package com.allen.view;

import static android.view.View.MeasureSpec.EXACTLY;
import static android.view.View.MeasureSpec.makeMeasureSpec;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

import com.allen.mygridlayout.R;

/**
* @author allen
* @email jaylong1302@163.com
* @date 2013-11-26 下午1:19:35
* @company 富媒科技
* @version 1.0
* @description 格子布局(类似4.0中的gridlayout)
*/
public class MyGridLayout extends ViewGroup {
private final String TAG = "MyGridLayout";

int margin = 2;// 每个格子的水平和垂直间隔
int colums = 2;
private int mMaxChildWidth = 0;
private int mMaxChildHeight = 0;
int count = 0;

GridAdatper adapter;

public MyGridLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
if (attrs != null) {
TypedArray a = getContext().obtainStyledAttributes(attrs,
R.styleable.MyGridLayout);
colums = a.getInteger(R.styleable.MyGridLayout_numColumns, 2);
margin = (int) a.getInteger(R.styleable.MyGridLayout_itemMargin, 2);
}
}

public MyGridLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public MyGridLayout(Context context) {
this(context, null);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO Auto-generated method stub
mMaxChildWidth = 0;
mMaxChildHeight = 0;

int modeW = 0, modeH = 0;
if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED)
modeW = MeasureSpec.UNSPECIFIED;
if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.UNSPECIFIED)
modeH = MeasureSpec.UNSPECIFIED;

final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
MeasureSpec.getSize(widthMeasureSpec), modeW);
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
MeasureSpec.getSize(heightMeasureSpec), modeH);

count = getChildCount();
if (count == 0) {
super.onMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
return;
}
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

mMaxChildWidth = Math.max(mMaxChildWidth, child.getMeasuredWidth());
mMaxChildHeight = Math.max(mMaxChildHeight,
child.getMeasuredHeight());
}
setMeasuredDimension(resolveSize(mMaxChildWidth, widthMeasureSpec),
resolveSize(mMaxChildHeight, heightMeasureSpec));
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// TODO Auto-generated method stub
int height = b - t;// 布局区域高度
int width = r - l;// 布局区域宽度
int rows = count % colums == 0 ? count / colums : count / colums + 1;// 行数
if (count == 0)
return;
int gridW = (width - margin * (colums - 1)) / colums;// 格子宽度
int gridH = (height - margin * rows) / rows;// 格子高度

int left = 0;
int top = margin;

for (int i = 0; i < rows; i++) {// 遍历行
for (int j = 0; j < colums; j++) {// 遍历每一行的元素
View child = this.getChildAt(i * colums + j);
if (child == null)
return;
left = j * gridW + j * margin;
// 如果当前布局宽度和测量宽度不一样,就直接用当前布局的宽度重新测量
if (gridW != child.getMeasuredWidth()
|| gridH != child.getMeasuredHeight()) {
child.measure(makeMeasureSpec(gridW, EXACTLY),
makeMeasureSpec(gridH, EXACTLY));
}
child.layout(left, top, left + gridW, top + gridH);
// System.out
// .println("--top--" + top + ",bottom=" + (top + gridH));

}
top += gridH + margin;
}
}

public interface GridAdatper {
View getView(int index);

int getCount();
}

/** 设置适配器 */
public void setGridAdapter(GridAdatper adapter) {
this.adapter = adapter;
// 动态添加视图
int size = adapter.getCount();
for (int i = 0; i < size; i++) {
addView(adapter.getView(i));
}
}

public interface OnItemClickListener {
void onItemClick(View v, int index);
}

public void setOnItemClickListener(final OnItemClickListener click) {
if (this.adapter == null)
return;
for (int i = 0; i < adapter.getCount(); i++) {
final int index = i;
View view = getChildAt(i);
view.setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
// TODO Auto-generated method stub
click.onItemClick(v, index);
}
});
}
}

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android