《Android自定义控件入门到精通》文章索引 ☞ https://blog.csdn.net/Jhone_csdn/article/details/118146683
《Android自定义控件入门到精通》所有源码 ☞ https://gitee.com/zengjiangwen/Code
经过前面View树的绘制流程和View树的测量流程的学习,相信大家自己分析View树的布局流程已经没有什么难度了
ViewRootImpl.java
//ViewRootImpl.java
private void performTraversals() {
//测量流程
measureHierarchy(...)-->performMeasure(...);
//布局流程
performLayout(...);
//绘制流程
performDraw();
}
布局的操作是针对ViewGroup而言,且直接在onLayout()方法中实现就行了:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
我们能用view.getLayoutParams()获取到布局参数LayoutParams,那么,这个对象是怎么生成的?
ViewGroup.java
//ViewGroup.java
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);
}
//生成Child的布局参数对象LayoutParams
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
可以看到,当ViewGroup添加Child的时候,是通过generateDefaultLayoutParams()来生成Child的LayoutParams对象的
当时翻看LayoutParams类的时候发现,它里面只有width、height两个属性
public static class LayoutParams {
public int width;
public int height;
}
我们知道:
那Child的margin值,我们怎么获取到呢
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nYh58Pe6-1624353180372)(…/img/image-20210621103312840.png)]
在ViewGroup中,还提供了一个MarginLayoutParams
public static class MarginLayoutParams extends ViewGroup.LayoutParams {
public int leftMargin;
public int topMargin;
public int rightMargin;
public int bottomMargin;
}
那么,我们在自己自定义的ViewGroup中,返回MarginLayoutParams不就可以了吗
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
然后通过xml等方式设置的margin值,就可以被MarginLayoutParams解析了
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);
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;
}
......
}
我们还可以自己实现LayoutParams,来应用我们自己的属性需求,比如:
public static class MyLayoutParams extends ViewGroup.LayoutParams {
public int radius;
public int cx;
public int cy;
public int bgColor;
public MyLayoutParams(Context c, AttributeSet attrs) {
super();
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.MyLayoutParams_MyLayout);
int radius = a.getDimensionPixelSize(R.styleable.MyLayoutParams_MyLayout_radius, -1);
int cx = a.getDimensionPixelSize(R.styleable.MyLayoutParams_MyLayout_cx, -1);
int cy = a.getDimensionPixelSize(R.styleable.MyLayoutParams_MyLayout_cy, -1);
int bgColor = a.getDimensionPixelSize(R.styleable.MyLayoutParams_MyLayout_bgColor, -1);
}
}
我们直接结合测量和布局,用案例来加深学习。
思路:
xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:orientation="vertical">
<cn.code.code.wiget.FlowViewGroup
android:id="@+id/flowViewGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp"
android:background="#222222"
/>
</LinearLayout>
Activity
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
FlowViewGroup flowViewGroup=findViewById(R.id.flowViewGroup);
String[] labs=new String[]{"自定义View","热点","关注新闻","理财小知识","最牛百度人","好","运动","健康监控","远程办公","轻快","牛","小火车上街","北上广深","房地产","房地产是灰犀牛","搞研究","买房致富","加餐","回家睡觉","十年之前的事瞒不住了"};
flowViewGroup.setLabs(labs);
}
FlowViewGroup
public class FlowViewGroup extends ViewGroup {
private final List<List<View>> mAllChildViews;
public FlowViewGroup(Context context) {
this(context, null);
}
public FlowViewGroup(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public FlowViewGroup(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mAllChildViews = new ArrayList<>();
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = getPaddingLeft();
int top = getPaddingTop();
int lineNumber = mAllChildViews.size();
for (int i = 0; i < lineNumber; i++) {
List<View> lineViews = mAllChildViews.get(i);
for (int j = 0; j < lineViews.size(); j++) {
View child = lineViews.get(j);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int lc = left + lp.leftMargin;
int tc = top + lp.topMargin;
int rc = lc + child.getMeasuredWidth();
int bc = tc + child.getMeasuredHeight();
child.layout(lc, tc, rc, bc);
left += child.getMeasuredWidth() + lp.rightMargin
+ lp.leftMargin;
}
left = getPaddingLeft();
View child=lineViews.get(0);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
top += child.getMeasuredHeight()+lp.topMargin+lp.bottomMargin;
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
//如果宽度是wrap_cotent,直接运行异常,因为宽度必须指定才符合我们的流式标签布局要求
if (widthMode == MeasureSpec.AT_MOST) {
throw new RuntimeException("layout_width不能设置为wrap_content!");
}
//一下根据子View的摆放,求得FlowViewGroup的高
int height = getPaddingTop() + getPaddingBottom();
// 行宽
int lineWidth = 0;
// 行高
int lineHeight = 0;
int childCount = getChildCount();
mAllChildViews.clear();
List<View> lineViews = new ArrayList<>();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
//measureChild之后,才能获取到Child的宽高等布局属性
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
//处理margin
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
// 换行
if (childWidth + lineWidth > (widthSize - getPaddingRight() - getPaddingLeft())) {
//行剩余没有用上的空间平均分给子View的padding
int space = widthSize - lineWidth;
int perPadding = space / (lineViews.size() * 2);
for (View lineView : lineViews) {
int l = lineView.getPaddingLeft() + perPadding;
int r = lineView.getPaddingRight() + perPadding;
int t = lineView.getPaddingTop();
int b = lineView.getPaddingBottom();
lineView.setPadding(l, t, r, b);
}
height += lineHeight;
lineWidth = childWidth;
// 添加一行
mAllChildViews.add(lineViews);
lineViews = new ArrayList<View>();
lineViews.add(childView);
} else { // 不换行
//所有的child的高都是一样的
lineHeight = childHeight;
lineWidth += childWidth;
lineViews.add(childView);
}
//添加最后一行
if (i == childCount - 1) {
height += lineHeight;
mAllChildViews.add(lineViews);
}
}
//由于我们的Child的宽度layout_width=wrap_content,所以平均分配padding后,宽度会改变,需要重新测量Children
measureChildren(widthMeasureSpec,heightMeasureSpec);
//保存(设置)计算好的宽高尺寸
setMeasuredDimension(widthSize, height);
}
public void setLabs(String[] labs) {
TextView textView;
for (String lab : labs) {
textView = new TextView(getContext());
addView(textView);
textView.setTextSize(14);
textView.setTextColor(Color.WHITE);
textView.setGravity(Gravity.CENTER);
textView.setText(lab);
textView.setBackgroundColor(Color.BLUE);
textView.setPadding(10,10,10,10);
MarginLayoutParams lp = (MarginLayoutParams) textView.getLayoutParams();
lp.setMargins(10, 10, 10, 10);
}
}
}