Android自定义控件入门到精通--View树的布局

《Android自定义控件入门到精通》文章索引 ☞ https://blog.csdn.net/Jhone_csdn/article/details/118146683

《Android自定义控件入门到精通》所有源码 ☞ https://gitee.com/zengjiangwen/Code

文章目录

  • View树的布局
    • LayoutParams
    • 流式标签布局

View树的布局

经过前面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) {

}

LayoutParams

我们能用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跟Parent相关的布局属性是放在LayoutParams中的,比如Child的width、height、margin
  • Child的padding是跟Parent无关的,它属于width/height的一部分,通过child.getPadding就可以获取

那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);
    }
}

我们直接结合测量和布局,用案例来加深学习。

流式标签布局

思路:

  • 测量出一行能放下多少child
  • 将ViewGroup的宽度减去每行children的宽度剩下的宽度平均分配给child的左右padding,以实现左右端对齐

Android自定义控件入门到精通--View树的布局_第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);
        }
    }
}

你可能感兴趣的:(android)