Android开发AndroidUI

约束布局ConstraintLayout详解与简单自定义布局

2018-09-16  本文已影响0人  111_222

约束布局

介绍 :

在2016年的Google I/O大会上 , Google 发布了Android Studio 2.2预览版,同时也发布了Android 新的布局方案 ConstraintLayout , 但是最近的一年也没有大规模的使用。2017年Google发布了 Android Studio 2.3 正式版,在 Android Studio 2.3 版本中新建的Module中默认的布局就是 ConstraintLayout ,ConstraintLayout向下兼容 API 9;

优点 :

1、可以极大程度的降低布局的嵌套层级,集合了几大布局的优点;

使用 :

1、AndroidStudio 2.2 需要自己添加 ConstraintLayout依赖 ;

    compile 'com.android.support.constraint:constraint-layout:1.0.1';

同时还需要添加仓库,因为约束布局是google()mevan仓库中的

    maven { url 'https://maven.google.com' }

我们公司的的项目需要两者都加,如果没有加仓库,可能本地不会报错,但是在jekens上会编译不过

2、AndoroidStudio 2.3 之后,默认添加了布局依赖

添加依赖后直接在xml布局引用即可

<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.constraintlayout.app.Main2Activity"> 

</android.support.constraint.ConstraintLayout>

常用属性 :

设置控件大小

layout_width    // 设置控件的宽

layout_height   // 设置控件的高

以上两个属性是用于设置控件的大小,相比较之前的布局的大小略有不同

wrap_content    // 控件的大小为自适应大小,按内容填充

match_parent    // 控件的大小,父布局大小一样大,就算添加了各边的约束也一样

0dp             // 控件的大小,与为设置的约束大小,大小就为四边的约束大小

除了以上方式还有以下两种方式用来设置控件的大小

layout_constraintHeight_percent // 设置控件的高度相对父布局的百分比 大小在0~1.0 中

layout_constraintWidth_percent  // 设置控件的宽度相对父布局的百分比 大小在0~1.0 中

上述这种方式,设置的宽度和高度都必须为0dp 不然的话属性无法起作用

layout_constraintDimensionRatio="4:3"

还有一种方式为通过设置宽高比来设置控件的大小 ,但是这种方式有一点需要注意,需要知道宽大小或者高度的大小,只有一方已知大小后,另一方的大小为0dp,才能生效,因为大小需要根据比值计算得出

替换RelativeLayout

layout_constraintTop_toTopOf // 将所需视图的顶部与另一个视图的顶部对齐。
layout_constraintTop_toBottomOf // 将所需视图的顶部与另一个视图的底部对齐。 

layout_constraintBottom_toTopOf // 将所需视图的底部与另一个视图的顶部对齐。 
layout_constraintBottom_toBottomOf // 将所需视图的底部与另一个视图的底部对齐。 

layout_constraintLeft_toLeftOf // 将所需视图的左边与另一个视图的左边对齐。 
layout_constraintLeft_toRightOf // 将所需视图的左边与另一个视图的右边对齐。 

layout_constraintRight_toLeftOf // 将所需视图的右边与另一个视图的左边对齐。 
layout_constraintRight_toRightOf // 将所需视图的右边与另一个视图的右边对齐。

当我们将一个控件四个方向都约束,如下所示

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/constraintLayout"
tools:context="com.constraintlayout.app.MainActivity"
>
 <Button
    android:id="@+id/button3"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Button"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    />
 </android.support.constraint.ConstraintLayout>
约束示例

可以看到 对应的控件上下左右中间都有一个点连着父布局的边界 ,后面可以跟对应的id,而上面那八组属性就是控制这个连接的位置,这四个属性不是每个每个属性都必须,就拿上面那个例子,假设只有左跟上两个属性,那么控件就会自动吸附到父布局左上角,有点类似相对布局,但是比起相对布局而言在有些方面简单很多 ,比如说控件的左右对齐,上下对齐,通过这几个属性,就很容易实现;

假设通过直接设置到对应控件的约束不好实现的话,可能在那个位置没有控件,比如说,我需要将一个TextView 设置到横向30%的位置,那么由于在30% 的位置,没有对应的控件来提供约束,那么该怎么实现呢

<android.support.constraint.Guideline
    android:orientation="horizontal"
    app:layout_constraintGuide_percent="0.1"
    android:layout_width="wrap_content"
    android:layout_height="match_parent" />

可以通过上述的Guideline 在指定位置画一条辅助线,这样就可以实现在对应的位置绘制一条辅助线,就可以实现对应的约束了,而且为了方便确定辅助线的位置 提供了如下的属性

 layout_constraintGuide_percent="0.1"   // 设置基线到在横向或者竖向的相对父布局的百分比

 layout_constraintGuide_begin="xxdp"    // 设置基线横向或者竖向相对左上角的位置

 layout_constraintGuide_end="xxdp"      // 设置基线相对终点的位置

这样就可以通过基线,准确的将控件设置到对应的位置,这些属性是可以通过代码更改的,优先顺序为percent-> begin -> end;

layout_constraintHorizontal_bias //控件的水平偏移比例 

layout_constraintVertical_bias //控件的垂直偏移比例

这样连接后,默认视图会显示到父布局的中间位置,四边约束后,布局显示的位置不靠中,则可以通过以上两个属性设置显示的位置,因为这两个属性默认值都为0.5,所以显示在中间 ,如果更改其值,就可以显示到不同的位置

layout_constraintVertical_chainStyle // 竖直链的设置模式

layout_constraintHorizontal_chainStyle  // 水平链的设置模式

通过以上八组属性就可以轻易的实现RelativeLayout的常用功能,但是这八组属性有一个问题,设置负的值margin或padding值是无效的;

替换LinearLayout功能

在约束布局中,如何实现线性布局的效果呢,其实在上述的八个属性,就可以使得控件横向或者竖向排布,而LinearLayout 有一个特殊点是可以使布局按照权重来分配,其实在约束布局中也有相应的属性

layout_constraintHorizontal_weight  // 在横向实现链的基础上,使控件按照对应的权重分配

layout_constraintVertical_weight    // 在纵向实现链的基础上,使控件大小按照权重分配

那么以上的内容都提到了一个链,那么什么才是链呢,如下图所示:


chainDemo.jpg

假设btn1的 layout_constraintRight_toLeftOf 属性连接的btn2 而btn2 的layout_constraintLeft_toRightOf 这个属性连接的是btn1 ,那么这两个控件之间的结构就是链结构了,而如果你想使用上面的横向weight属性的话,那么必须在在横向每个控件之间都是链结构,并且实现了最左边控件的左约束,以及最右边控件的右约束,并且每个控件的width 都必须为0dp 与LinearLayout一样,就可以实现而LinearLayout这种效果了;

layout_constraintHorizontal_chainStyle

layout_constraintVertical_chainStyle

以上两个属性是设置对应链的模式,模式有以下三种:

spread : 它将平分间隙让多个 Views 布局到剩余空间;
    
spread_inside :它将会把两边最边缘的两个 View 到外向父组件边缘的距离去除,然后让剩余的 Views 在剩余的空间内平分间隙布局

packed :它将所有 Views 打包到一起不分配多余的间隙(当然不包括通过 margin 设置多个 Views 之间的间隙),然后将整个组件组在可用的剩余位置居中

以上就是约束布局实现LinearLayout效果了

常用新特性 :

1、圆形定位 :

ayout_constraintCircle:引用另一个控件的 id。

layout_constraintCircleRadius:到另一个控件中心的距离。

layout_constraintCircleAngle:控件的角度(顺时针,0 - 360 度)。

通过给控件设置如上三个属性,就可以实现控件相对控件实现圆形定位

2、组的概念
Group 的作用就是控制一组控件的可见性。

<android.support.constraint.Group 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content"
    android:visibility="gone" 
    app:constraint_referenced_ids="title, desc" />

使用过程中发现的问题

1、设置负边距无效

2、guideline 在代码中更改位置时,当改变的是对应precent,begin ,end 三种模式切换时,当你付对应的值并不会清空其他值,所以而且,其优先级为precent > begin > end,所以如果需要更改时需要将对应的值改为默认值才会生效,precent 默认值为-1 begin 和 end 默认值也为-1;

3、需要注意match_parent 属性,与LinearLayout 不一样,不是填充好对应的可用空间,而是父布局,当设置width或者height 为0dp时,如果是指定约束来设置大小,那么必须把对应横向所有的约束补全,否则不会生效;

4、如果需要设置某个布局背景,可以通过View 设置backGround 来实现;

以上就是今天约束布局部分的所有内容,比较简单,而且在有些方面比原有布局更方便,并且能极大程度减少布局嵌套,所以,建议大家做布局优化时可以试试;

自定义Layout

Layout在Android中主要起到对子View 进行测量和布局的作用,通常情况下 如下图所示:

布局测量过程.PNG

为什么要自定义Layout :

1、 由于有的常用布局无法解决项目需求 ,比如说 流式布局的等;
2、 可以通过自定义布局更了解自定义控件的一些内容;平常我们写自定义控件时往往是自定义一个View 着重于onDraw()这个方法,今天我们可以了解一下 onMeasure(),以及onLayout()的流程;

如何自定义布局 :

1、  重写onMeasure()来计算内部布局 : 
    1、调用每个子View的Measure(),让子View自我测量;
    2、根据子View的尺寸,得到子View的位置,并保存他们的位置和尺寸;
    3、根据子View的位置和尺寸计算得出自己的尺寸,并用SetMeasureDimension()进行保存;

2、 重写onLayout()来摆放子View的位置:
    1、调用子View的layout方法,将对应的位置以参数的形式传进去即可;

实例FlowLayout:

FlowLayout介绍 :

FlowLaout源码解析:

public class FlowLayout extends ViewGroup {

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 获取父容器对布局的期望宽高 ,根据期望宽高 分别获取长宽的模式和值
        // widthMeasureSpec 其实是一个32位的数 其中前两位代表模式 ,后30位代表具体的值
        // 模式分为3种
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        // wrap_content
        int width = 0;
        int height = 0;

        int lineWidth = 0;
        int lineHeight = 0;

        int cCount = getChildCount();

        for (int i = 0; i < cCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == View.GONE) {
                if (i == cCount - 1) {
                    width = Math.max(lineWidth, width);
                    height += lineHeight;
                }
                continue;
            }
            // 测量子View 
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            MarginLayoutParams lp = (MarginLayoutParams) child
                    .getLayoutParams();

            int childWidth = child.getMeasuredWidth() + lp.leftMargin+ lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;

            if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()) {
                width = Math.max(width, lineWidth);
                lineWidth = childWidth;
                height += lineHeight;
                lineHeight = childHeight;
            } else {
                lineWidth += childWidth;
                lineHeight = Math.max(lineHeight, childHeight);
            }
            if (i == cCount - 1) {
                width = Math.max(lineWidth, width);
                height += lineHeight;
            }
        }

        // 保存FlowLaout的width和height ,当为match_parent 以及指定大小是 mode 都为EXACTLY
        setMeasuredDimension(
                //
                modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width + getPaddingLeft() + getPaddingRight(),
                modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height + getPaddingTop() + getPaddingBottom()//
        );

    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mAllViews.clear();
        mLineHeight.clear();
        mLineWidth.clear();
        lineViews.clear();

        int width = getWidth();

        int lineWidth = 0;
        int lineHeight = 0;

        int cCount = getChildCount();

        for (int i = 0; i < cCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == View.GONE) continue;
            MarginLayoutParams lp = (MarginLayoutParams) child
                    .getLayoutParams();

            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            if (childWidth + lineWidth + lp.leftMargin + lp.rightMargin > width - getPaddingLeft() - getPaddingRight()) {
                mLineHeight.add(lineHeight);
                mAllViews.add(lineViews);
                mLineWidth.add(lineWidth);

                lineWidth = 0;
                lineHeight = childHeight + lp.topMargin + lp.bottomMargin;
                lineViews = new ArrayList<View>();
            }
            lineWidth += childWidth + lp.leftMargin + lp.rightMargin;
            lineHeight = Math.max(lineHeight, childHeight + lp.topMargin
                    + lp.bottomMargin);
            lineViews.add(child);

        }
        mLineHeight.add(lineHeight);
        mLineWidth.add(lineWidth);
        mAllViews.add(lineViews);


        int left = getPaddingLeft();
        int top = getPaddingTop();

        int lineNum = mAllViews.size();

        for (int i = 0; i < lineNum; i++) {
            lineViews = mAllViews.get(i);
            lineHeight = mLineHeight.get(i);

            // set gravity
            int currentLineWidth = this.mLineWidth.get(i);
            switch (this.mGravity) {
                case LEFT:
                    left = getPaddingLeft();
                    break;
                case CENTER:
                    left = (width - currentLineWidth) / 2 + getPaddingLeft();
                    break;
                case RIGHT:
                    //  适配了rtl,需要补偿一个padding值
                    left = width - (currentLineWidth + getPaddingLeft()) - getPaddingRight();
                    //  适配了rtl,需要把lineViews里面的数组倒序排
                    Collections.reverse(lineViews);
                    break;
            }

            for (int j = 0; j < lineViews.size(); j++) {
                View child = lineViews.get(j);
                if (child.getVisibility() == View.GONE) {
                    continue;
                }

                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.leftMargin
                        + lp.rightMargin;
            }
            top += lineHeight;
        }

    }
}



// MeasureChildren 对子View进行测量 
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

// 具体的根据layoutparams 参数中的width和height 获取对应的 子View的MeasureSpec
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size, but our size is not fixed.
            // Constrain child to not be bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent asked to see how big we want to be
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // Child wants a specific size... let him have it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size.... find out how
            // big it should be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上一篇下一篇

猜你喜欢

热点阅读