約束布局
介紹 :
在2016年的Google I/O大會(huì)上 , Google 發(fā)布了Android Studio 2.2預(yù)覽版,同時(shí)也發(fā)布了Android 新的布局方案 ConstraintLayout , 但是最近的一年也沒有大規(guī)模的使用。2017年Google發(fā)布了 Android Studio 2.3 正式版,在 Android Studio 2.3 版本中新建的Module中默認(rèn)的布局就是 ConstraintLayout ,ConstraintLayout向下兼容 API 9;
優(yōu)點(diǎn) :
1、可以極大程度的降低布局的嵌套層級(jí),集合了幾大布局的優(yōu)點(diǎn);
使用 :
1、AndroidStudio 2.2 需要自己添加 ConstraintLayout依賴 ;
compile 'com.android.support.constraint:constraint-layout:1.0.1';
同時(shí)還需要添加倉庫,因?yàn)榧s束布局是google()mevan倉庫中的
maven { url 'https://maven.google.com' }
我們公司的的項(xiàng)目需要兩者都加,如果沒有加倉庫,可能本地不會(huì)報(bào)錯(cuò),但是在jekens上會(huì)編譯不過
2、AndoroidStudio 2.3 之后,默認(rèn)添加了布局依賴
添加依賴后直接在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>
常用屬性 :
設(shè)置控件大小
layout_width // 設(shè)置控件的寬
layout_height // 設(shè)置控件的高
以上兩個(gè)屬性是用于設(shè)置控件的大小,相比較之前的布局的大小略有不同
wrap_content // 控件的大小為自適應(yīng)大小,按內(nèi)容填充
match_parent // 控件的大小,父布局大小一樣大,就算添加了各邊的約束也一樣
0dp // 控件的大小,與為設(shè)置的約束大小,大小就為四邊的約束大小
除了以上方式還有以下兩種方式用來設(shè)置控件的大小
layout_constraintHeight_percent // 設(shè)置控件的高度相對(duì)父布局的百分比 大小在0~1.0 中
layout_constraintWidth_percent // 設(shè)置控件的寬度相對(duì)父布局的百分比 大小在0~1.0 中
上述這種方式,設(shè)置的寬度和高度都必須為0dp 不然的話屬性無法起作用
layout_constraintDimensionRatio="4:3"
還有一種方式為通過設(shè)置寬高比來設(shè)置控件的大小 ,但是這種方式有一點(diǎn)需要注意,需要知道寬大小或者高度的大小,只有一方已知大小后,另一方的大小為0dp,才能生效,因?yàn)榇笮⌒枰鶕?jù)比值計(jì)算得出
替換RelativeLayout
layout_constraintTop_toTopOf // 將所需視圖的頂部與另一個(gè)視圖的頂部對(duì)齊。
layout_constraintTop_toBottomOf // 將所需視圖的頂部與另一個(gè)視圖的底部對(duì)齊。
layout_constraintBottom_toTopOf // 將所需視圖的底部與另一個(gè)視圖的頂部對(duì)齊。
layout_constraintBottom_toBottomOf // 將所需視圖的底部與另一個(gè)視圖的底部對(duì)齊。
layout_constraintLeft_toLeftOf // 將所需視圖的左邊與另一個(gè)視圖的左邊對(duì)齊。
layout_constraintLeft_toRightOf // 將所需視圖的左邊與另一個(gè)視圖的右邊對(duì)齊。
layout_constraintRight_toLeftOf // 將所需視圖的右邊與另一個(gè)視圖的左邊對(duì)齊。
layout_constraintRight_toRightOf // 將所需視圖的右邊與另一個(gè)視圖的右邊對(duì)齊。
當(dāng)我們將一個(gè)控件四個(gè)方向都約束,如下所示
<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>

可以看到 對(duì)應(yīng)的控件上下左右中間都有一個(gè)點(diǎn)連著父布局的邊界 ,后面可以跟對(duì)應(yīng)的id,而上面那八組屬性就是控制這個(gè)連接的位置,這四個(gè)屬性不是每個(gè)每個(gè)屬性都必須,就拿上面那個(gè)例子,假設(shè)只有左跟上兩個(gè)屬性,那么控件就會(huì)自動(dòng)吸附到父布局左上角,有點(diǎn)類似相對(duì)布局,但是比起相對(duì)布局而言在有些方面簡(jiǎn)單很多 ,比如說控件的左右對(duì)齊,上下對(duì)齊,通過這幾個(gè)屬性,就很容易實(shí)現(xiàn);
假設(shè)通過直接設(shè)置到對(duì)應(yīng)控件的約束不好實(shí)現(xiàn)的話,可能在那個(gè)位置沒有控件,比如說,我需要將一個(gè)TextView 設(shè)置到橫向30%的位置,那么由于在30% 的位置,沒有對(duì)應(yīng)的控件來提供約束,那么該怎么實(shí)現(xiàn)呢
<android.support.constraint.Guideline
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.1"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
可以通過上述的Guideline 在指定位置畫一條輔助線,這樣就可以實(shí)現(xiàn)在對(duì)應(yīng)的位置繪制一條輔助線,就可以實(shí)現(xiàn)對(duì)應(yīng)的約束了,而且為了方便確定輔助線的位置 提供了如下的屬性
layout_constraintGuide_percent="0.1" // 設(shè)置基線到在橫向或者豎向的相對(duì)父布局的百分比
layout_constraintGuide_begin="xxdp" // 設(shè)置基線橫向或者豎向相對(duì)左上角的位置
layout_constraintGuide_end="xxdp" // 設(shè)置基線相對(duì)終點(diǎn)的位置
這樣就可以通過基線,準(zhǔn)確的將控件設(shè)置到對(duì)應(yīng)的位置,這些屬性是可以通過代碼更改的,優(yōu)先順序?yàn)閜ercent-> begin -> end;
layout_constraintHorizontal_bias //控件的水平偏移比例
layout_constraintVertical_bias //控件的垂直偏移比例
這樣連接后,默認(rèn)視圖會(huì)顯示到父布局的中間位置,四邊約束后,布局顯示的位置不靠中,則可以通過以上兩個(gè)屬性設(shè)置顯示的位置,因?yàn)檫@兩個(gè)屬性默認(rèn)值都為0.5,所以顯示在中間 ,如果更改其值,就可以顯示到不同的位置
layout_constraintVertical_chainStyle // 豎直鏈的設(shè)置模式
layout_constraintHorizontal_chainStyle // 水平鏈的設(shè)置模式
通過以上八組屬性就可以輕易的實(shí)現(xiàn)RelativeLayout的常用功能,但是這八組屬性有一個(gè)問題,設(shè)置負(fù)的值margin或padding值是無效的;
替換LinearLayout功能
在約束布局中,如何實(shí)現(xiàn)線性布局的效果呢,其實(shí)在上述的八個(gè)屬性,就可以使得控件橫向或者豎向排布,而LinearLayout 有一個(gè)特殊點(diǎn)是可以使布局按照權(quán)重來分配,其實(shí)在約束布局中也有相應(yīng)的屬性
layout_constraintHorizontal_weight // 在橫向?qū)崿F(xiàn)鏈的基礎(chǔ)上,使控件按照對(duì)應(yīng)的權(quán)重分配
layout_constraintVertical_weight // 在縱向?qū)崿F(xiàn)鏈的基礎(chǔ)上,使控件大小按照權(quán)重分配
那么以上的內(nèi)容都提到了一個(gè)鏈,那么什么才是鏈呢,如下圖所示:

假設(shè)btn1的 layout_constraintRight_toLeftOf 屬性連接的btn2 而btn2 的layout_constraintLeft_toRightOf 這個(gè)屬性連接的是btn1 ,那么這兩個(gè)控件之間的結(jié)構(gòu)就是鏈結(jié)構(gòu)了,而如果你想使用上面的橫向weight屬性的話,那么必須在在橫向每個(gè)控件之間都是鏈結(jié)構(gòu),并且實(shí)現(xiàn)了最左邊控件的左約束,以及最右邊控件的右約束,并且每個(gè)控件的width 都必須為0dp 與LinearLayout一樣,就可以實(shí)現(xiàn)而LinearLayout這種效果了;
layout_constraintHorizontal_chainStyle
layout_constraintVertical_chainStyle
以上兩個(gè)屬性是設(shè)置對(duì)應(yīng)鏈的模式,模式有以下三種:
spread : 它將平分間隙讓多個(gè) Views 布局到剩余空間;
spread_inside :它將會(huì)把兩邊最邊緣的兩個(gè) View 到外向父組件邊緣的距離去除,然后讓剩余的 Views 在剩余的空間內(nèi)平分間隙布局
packed :它將所有 Views 打包到一起不分配多余的間隙(當(dāng)然不包括通過 margin 設(shè)置多個(gè) Views 之間的間隙),然后將整個(gè)組件組在可用的剩余位置居中
以上就是約束布局實(shí)現(xiàn)LinearLayout效果了
常用新特性 :
1、圓形定位 :
ayout_constraintCircle:引用另一個(gè)控件的 id。
layout_constraintCircleRadius:到另一個(gè)控件中心的距離。
layout_constraintCircleAngle:控件的角度(順時(shí)針,0 - 360 度)。
通過給控件設(shè)置如上三個(gè)屬性,就可以實(shí)現(xiàn)控件相對(duì)控件實(shí)現(xiàn)圓形定位
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" />
使用過程中發(fā)現(xiàn)的問題
1、設(shè)置負(fù)邊距無效
2、guideline 在代碼中更改位置時(shí),當(dāng)改變的是對(duì)應(yīng)precent,begin ,end 三種模式切換時(shí),當(dāng)你付對(duì)應(yīng)的值并不會(huì)清空其他值,所以而且,其優(yōu)先級(jí)為precent > begin > end,所以如果需要更改時(shí)需要將對(duì)應(yīng)的值改為默認(rèn)值才會(huì)生效,precent 默認(rèn)值為-1 begin 和 end 默認(rèn)值也為-1;
3、需要注意match_parent 屬性,與LinearLayout 不一樣,不是填充好對(duì)應(yīng)的可用空間,而是父布局,當(dāng)設(shè)置width或者h(yuǎn)eight 為0dp時(shí),如果是指定約束來設(shè)置大小,那么必須把對(duì)應(yīng)橫向所有的約束補(bǔ)全,否則不會(huì)生效;
4、如果需要設(shè)置某個(gè)布局背景,可以通過View 設(shè)置backGround 來實(shí)現(xiàn);
以上就是今天約束布局部分的所有內(nèi)容,比較簡(jiǎn)單,而且在有些方面比原有布局更方便,并且能極大程度減少布局嵌套,所以,建議大家做布局優(yōu)化時(shí)可以試試;
自定義Layout
Layout在Android中主要起到對(duì)子View 進(jìn)行測(cè)量和布局的作用,通常情況下 如下圖所示:
為什么要自定義Layout :
1、 由于有的常用布局無法解決項(xiàng)目需求 ,比如說 流式布局的等;
2、 可以通過自定義布局更了解自定義控件的一些內(nèi)容;平常我們寫自定義控件時(shí)往往是自定義一個(gè)View 著重于onDraw()這個(gè)方法,今天我們可以了解一下 onMeasure(),以及onLayout()的流程;
如何自定義布局 :
1、 重寫onMeasure()來計(jì)算內(nèi)部布局 :
1、調(diào)用每個(gè)子View的Measure(),讓子View自我測(cè)量;
2、根據(jù)子View的尺寸,得到子View的位置,并保存他們的位置和尺寸;
3、根據(jù)子View的位置和尺寸計(jì)算得出自己的尺寸,并用SetMeasureDimension()進(jìn)行保存;
2、 重寫onLayout()來擺放子View的位置:
1、調(diào)用子View的layout方法,將對(duì)應(yīng)的位置以參數(shù)的形式傳進(jìn)去即可;
實(shí)例FlowLayout:
FlowLayout介紹 :
FlowLaout源碼解析:
public class FlowLayout extends ViewGroup {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 獲取父容器對(duì)布局的期望寬高 ,根據(jù)期望寬高 分別獲取長(zhǎng)寬的模式和值
// widthMeasureSpec 其實(shí)是一個(gè)32位的數(shù) 其中前兩位代表模式 ,后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;
}
// 測(cè)量子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 ,當(dāng)為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,需要補(bǔ)償一個(gè)padding值
left = width - (currentLineWidth + getPaddingLeft()) - getPaddingRight();
// 適配了rtl,需要把lineViews里面的數(shù)組倒序排
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 對(duì)子View進(jìn)行測(cè)量
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);
}
// 具體的根據(jù)layoutparams 參數(shù)中的width和height 獲取對(duì)應(yīng)的 子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);
}