约束布局ConstraintLayout详解与简单自定义布局
约束布局
介绍 :
在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);
}