使用的ConstraintLayout版本

implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

如果不使用androidx的话可以使用下面的版本

implementation 'com.android.support.constraint:constraint-layout:1.1.3'

注意:使用不同的ConstraintLayout版本可能会有坑,如果在使用过程中发现实现不了想要添加的约束,可以尝试改变ConstraintLayout的版本如上所示。

1. 动态添加View

第一种情况:所有的View都是动态添加

举个例子

xmlns:app="http://schemas.android.com/apk/res-auto"

xmlns:tools="http://schemas.android.com/tools"

android:id="@+id/clRoot"

android:layout_width="match_parent"

android:layout_height="match_parent"

tools:context=".MainActivity">

android:id="@+id/ivLeft"

android:layout_width="100dp"

android:layout_height="0dp"

android:layout_marginStart="16dp"

android:layout_marginTop="16dp"

android:scaleType="centerCrop"

android:src="@drawable/ic_lake"

app:layout_constraintDimensionRatio="h,16:9"

app:layout_constraintLeft_toLeftOf="parent"

app:layout_constraintTop_toTopOf="parent" />

android:id="@+id/tvRight"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginStart="16dp"

android:layout_marginTop="16dp"

android:text="@string/lake_tahoe_title"

android:textSize="30sp"

app:layout_constraintLeft_toRightOf="@+id/ivLeft"

app:layout_constraintTop_toTopOf="parent" />

android:id="@+id/tvBottom"

android:layout_width="0dp"

android:layout_height="wrap_content"

android:layout_marginStart="8dp"

android:layout_marginTop="24dp"

android:layout_marginEnd="8dp"

android:text="@string/lake_discription"

app:layout_constraintLeft_toLeftOf="parent"

app:layout_constraintRight_toRightOf="parent"

app:layout_constraintTop_toBottomOf="@+id/ivLeft" />

16e34f919e1a

初始布局.jpg

上面的布局文件中呈现的效果如图所示,接下来我们用代码的方式动态添加View,实现上面的效果。

首先在res/values文件夹下新建一个ids.xml,在ids.xml中声明我们要添加的View的控件id。

ids.xml

然后开始写代码

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

addViewUseLayoutParams()

}

使用ConstraintLayout.LayoutParams

private fun addViewUseLayoutParams() {

val constraintLayout = ConstraintLayout(this)

constraintLayout.id = R.id.clRoot

constraintLayout.layoutParams = ViewGroup.LayoutParams(

ViewGroup.LayoutParams.MATCH_PARENT,

ViewGroup.LayoutParams.MATCH_PARENT

)

//先设置根布局

setContentView(constraintLayout)

val ivLeft = ImageView(this)

ivLeft.id = R.id.ivLeft

ivLeft.scaleType = ImageView.ScaleType.CENTER_CROP

ivLeft.setImageResource(R.drawable.ic_lake)

val ivLeftLayoutParams: ConstraintLayout.LayoutParams = ConstraintLayout.LayoutParams(

ScreenUtil.dpToPx(this, 100), 0

)

ivLeftLayoutParams.leftToLeft = R.id.clRoot

ivLeftLayoutParams.marginStart = ScreenUtil.dpToPx(this, 8)

ivLeftLayoutParams.topToTop = R.id.clRoot

ivLeftLayoutParams.topMargin = ScreenUtil.dpToPx(this, 8)

ivLeftLayoutParams.dimensionRatio = "h,16:9"

ivLeft.layoutParams = ivLeftLayoutParams

val tvRight = TextView(this)

tvRight.id = R.id.tvRight

tvRight.text = getString(R.string.lake_tahoe_title)

tvRight.textSize = 30F

val tvRightLayoutParams: ConstraintLayout.LayoutParams = ConstraintLayout.LayoutParams(

ConstraintLayout.LayoutParams.WRAP_CONTENT,

ConstraintLayout.LayoutParams.WRAP_CONTENT

)

tvRightLayoutParams.startToEnd = R.id.ivLeft

tvRightLayoutParams.topToTop = R.id.clRoot

tvRightLayoutParams.marginStart = ScreenUtil.dpToPx(this, 16)

tvRightLayoutParams.topMargin = ScreenUtil.dpToPx(this, 16)

tvRight.layoutParams = tvRightLayoutParams

val tvBottom = TextView(this)

tvBottom.id = R.id.tvBottom

tvBottom.text = getString(R.string.lake_discription)

val tvBottomLayoutParams: ConstraintLayout.LayoutParams = ConstraintLayout.LayoutParams(

0,

ConstraintLayout.LayoutParams.WRAP_CONTENT

)

tvBottomLayoutParams.startToStart = R.id.clRoot

tvBottomLayoutParams.marginStart = ScreenUtil.dpToPx(this, 8)

tvBottomLayoutParams.endToEnd = R.id.clRoot

tvBottomLayoutParams.marginEnd = ScreenUtil.dpToPx(this, 8)

tvBottomLayoutParams.topToBottom = R.id.ivLeft

tvBottomLayoutParams.topMargin = ScreenUtil.dpToPx(this, 24)

tvBottom.layoutParams = tvBottomLayoutParams

constraintLayout.addView(ivLeft)

constraintLayout.addView(tvRight)

constraintLayout.addView(tvBottom)

}

效果和上面是一样的,就不截图了。在上面的方法中,我们是使用ConstraintLayout.LayoutParams来实现添加view并指定约束的。接下来,我们换一种方式,使用ConstraintSet来添加view并指定约束。关于ConstraintSet的介绍请参考 ConstraintSet。

使用ConstraintSet

private fun addViewUseConstraintSet() {

val constraintLayout = ConstraintLayout(this)

constraintLayout.id = R.id.clRoot

constraintLayout.layoutParams = ViewGroup.LayoutParams(

ViewGroup.LayoutParams.MATCH_PARENT,

ViewGroup.LayoutParams.MATCH_PARENT

)

//先设置根布局

setContentView(constraintLayout)

val constraintSet = ConstraintSet()

val ivLeft = ImageView(this)

ivLeft.id = R.id.ivLeft

ivLeft.scaleType = ImageView.ScaleType.CENTER_CROP

ivLeft.setImageResource(R.drawable.ic_lake)

constraintSet.constrainWidth(R.id.ivLeft, ScreenUtil.dpToPx(this, 100))

constraintSet.constrainHeight(R.id.ivLeft, 0)

constraintSet.setDimensionRatio(R.id.ivLeft, "h,16:9")

//layout_constraintTop_toTopOf

constraintSet.connect(

R.id.ivLeft, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP,

ScreenUtil.dpToPx(this, 16)

)

constraintSet.connect(

R.id.ivLeft, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START,

ScreenUtil.dpToPx(this, 16)

)

val tvRight = TextView(this)

tvRight.id = R.id.tvRight

tvRight.text = getString(R.string.lake_tahoe_title)

tvRight.textSize = 30F

constraintSet.constrainHeight(R.id.tvRight, ConstraintLayout.LayoutParams.WRAP_CONTENT)

constraintSet.constrainWidth(R.id.tvRight, ConstraintLayout.LayoutParams.WRAP_CONTENT)

constraintSet.connect(

R.id.tvRight, ConstraintSet.START, R.id.ivLeft, ConstraintSet.END,

ScreenUtil.dpToPx(this, 16)

)

constraintSet.connect(

R.id.tvRight, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP,

ScreenUtil.dpToPx(this, 16)

)

val tvBottom = TextView(this)

tvBottom.id = R.id.tvBottom

tvBottom.text = getString(R.string.lake_discription)

//设置高度

constraintSet.constrainHeight(R.id.tvBottom, ConstraintLayout.LayoutParams.WRAP_CONTENT)

constraintSet.connect(

R.id.tvBottom, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START,

ScreenUtil.dpToPx(this, 8)

)

constraintSet.connect(

R.id.tvBottom, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END,

ScreenUtil.dpToPx(this, 8)

)

constraintSet.connect(

R.id.tvBottom, ConstraintSet.TOP, R.id.ivLeft, ConstraintSet.BOTTOM,

ScreenUtil.dpToPx(this, 24)

)

constraintLayout.addView(ivLeft)

constraintLayout.addView(tvRight)

constraintLayout.addView(tvBottom)

TransitionManager.beginDelayedTransition(constraintLayout)

constraintSet.applyTo(constraintLayout)

}

效果也是一样的。

第二种情况,动态添加个别View,感觉这种场景应该不多

在上面的例子中,我们假设tvBottom已经在布局中了。

xmlns:app="http://schemas.android.com/apk/res-auto"

android:id="@+id/clRoot"

android:layout_width="match_parent"

android:layout_height="match_parent">

android:id="@+id/tvBottom"

android:layout_width="0dp"

android:layout_height="wrap_content"

android:layout_marginStart="8dp"

android:layout_marginTop="24dp"

android:layout_marginEnd="8dp"

android:text="@string/lake_discription"

app:layout_constraintLeft_toLeftOf="parent"

app:layout_constraintRight_toRightOf="parent"

app:layout_constraintTop_toTopOf="parent" />

16e34f919e1a

31573289245_.pic.jpg

接下来,我们动态的把ivLeft和tvRight 添加到布局中去,实现和第一个例子中同样的效果。

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

//布局文件别忘了

setContentView(R.layout.activity_main)

addPartView()

}

private fun addPartView() {

val ivLeft = ImageView(this)

ivLeft.id = R.id.ivLeft

ivLeft.scaleType = ImageView.ScaleType.CENTER_CROP

ivLeft.setImageResource(R.drawable.ic_lake)

val ivLeftLayoutParams: ConstraintLayout.LayoutParams = ConstraintLayout.LayoutParams(

ScreenUtil.dpToPx(this, 100), 0

)

ivLeftLayoutParams.dimensionRatio = "h,16:9"

ivLeftLayoutParams.topMargin = ScreenUtil.dpToPx(this, 16)

ivLeftLayoutParams.marginStart = ScreenUtil.dpToPx(this, 16)

ivLeftLayoutParams.leftToLeft = R.id.clRoot

ivLeftLayoutParams.topToTop = R.id.clRoot

ivLeft.layoutParams = ivLeftLayoutParams

val tvRight = TextView(this)

tvRight.id = R.id.tvRight

tvRight.text = getString(R.string.lake_tahoe_title)

tvRight.textSize = 30F

val tvRightLayoutParams: ConstraintLayout.LayoutParams = ConstraintLayout.LayoutParams(

ConstraintLayout.LayoutParams.WRAP_CONTENT,

ConstraintLayout.LayoutParams.WRAP_CONTENT

)

tvRightLayoutParams.startToEnd = R.id.ivLeft

tvRightLayoutParams.topToTop = R.id.clRoot

tvRightLayoutParams.marginStart = ScreenUtil.dpToPx(this, 16)

tvRightLayoutParams.topMargin = ScreenUtil.dpToPx(this, 16)

tvRight.layoutParams = tvRightLayoutParams

val tvBottomLayoutParams: ConstraintLayout.LayoutParams = ConstraintLayout.LayoutParams(

0,

ConstraintLayout.LayoutParams.WRAP_CONTENT

)

tvBottomLayoutParams.startToStart = R.id.clRoot

tvBottomLayoutParams.marginStart = ScreenUtil.dpToPx(this, 8)

tvBottomLayoutParams.endToEnd = R.id.clRoot

tvBottomLayoutParams.marginEnd = ScreenUtil.dpToPx(this, 8)

tvBottomLayoutParams.topToBottom = R.id.ivLeft

tvBottomLayoutParams.topMargin = ScreenUtil.dpToPx(this, 24)

tvBottomLayoutParams.bottomMargin = ScreenUtil.dpToPx(this, 8)

//重新为布局中已经存在的tvBottom设置新的布局参数。

tvBottom.layoutParams = tvBottomLayoutParams

clRoot.addView(ivLeft)

clRoot.addView(tvRight)

}

这种方式要注意重新为布局中已经存在的控件设置新的布局参数。

动态改变约束

如果我们想动态改变布局中的View的约束该怎么做呢?比如我们想把上面的布局样式改成下图所示。

16e34f919e1a

Screenshot_1559374001.png

其实,在上面我们已经给tvBottom动态改变约束了,就是给View重新设置布局参数就好了。

给View重新设置布局参数

下面我们在代码中,重新改变View的布局参数。

private fun changeLayoutParams() {

val ivLeftLayoutParams: ConstraintLayout.LayoutParams = ConstraintLayout.LayoutParams(

0, 0

)

ivLeftLayoutParams.leftMargin = ScreenUtil.dpToPx(this, 16)

ivLeftLayoutParams.rightMargin = ScreenUtil.dpToPx(this, 16)

ivLeftLayoutParams.topMargin = ScreenUtil.dpToPx(this, 16)

ivLeftLayoutParams.leftToLeft = R.id.clRoot

ivLeftLayoutParams.rightToRight = R.id.clRoot

ivLeftLayoutParams.dimensionRatio = "h,16:9"

ivLeftLayoutParams.topToTop = R.id.clRoot

//修改布局参数

ivLeft.layoutParams = ivLeftLayoutParams

val tvRightLayoutParams: ConstraintLayout.LayoutParams = ConstraintLayout.LayoutParams(

ConstraintLayout.LayoutParams.WRAP_CONTENT,

ConstraintLayout.LayoutParams.WRAP_CONTENT

)

tvRightLayoutParams.leftToLeft = R.id.clRoot

tvRightLayoutParams.topToBottom = R.id.ivLeft

tvRightLayoutParams.marginStart = ScreenUtil.dpToPx(this, 16)

tvRightLayoutParams.topMargin = ScreenUtil.dpToPx(this, 16)

tvRight.layoutParams = tvRightLayoutParams

val tvBottomLayoutParams: ConstraintLayout.LayoutParams = ConstraintLayout.LayoutParams(

0,

ConstraintLayout.LayoutParams.WRAP_CONTENT

)

tvBottomLayoutParams.startToStart = R.id.clRoot

tvBottomLayoutParams.marginStart = ScreenUtil.dpToPx(this, 8)

tvBottomLayoutParams.endToEnd = R.id.clRoot

tvBottomLayoutParams.marginEnd = ScreenUtil.dpToPx(this, 8)

tvBottomLayoutParams.topToBottom = R.id.tvRight

tvBottomLayoutParams.topMargin = ScreenUtil.dpToPx(this, 24)

tvBottomLayoutParams.bottomMargin = ScreenUtil.dpToPx(this, 8)

tvBottom.layoutParams = tvBottomLayoutParams

}

改变后的效果就不贴了。

使用ConstraintSet 动态修改约束

使用ConstraintSet 动态修改约束分四步。

首先要声明一下ConstraintSet对象

val constraintSet = ConstraintSet()

复制一份现有的约束关系,这一步不是必须的。

//从一个constraintLayout中复制约束

set.clone(constraintLayout: ConstraintLayout);

//从一个ConstraintSet中复制约束

set.clone(set: ConstraintSet);

//从一个布局文件中复制约束

set.clone(context: Context, constraintLayoutId: Int);

如果说你要改变布局中某些控件的约束,但是还要保存其他控件的约束关系,那么你就需要从已有的根布局中复制一份约束,然后只更改哪些需要改变的控件的约束关系。

注意复制约束关系的时候,布局中的每个控件必都有id,不然会报下面的错误。

java.lang.RuntimeException: All children of ConstraintLayout must have ids to use ConstraintSet

设置控件之间的约束

应用新的约束。在应用约束的时候,为了让约束改变的时候不是那么突兀,我们可以设置一个动画,来让约束改变平滑一点。

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

setContentView(R.layout.activity_dynamic_add_view)

ivLeft.setOnClickListener {

changeConstraintSet()

}

}

private fun changeConstraintSet() {

val constraintSet = ConstraintSet()

//从根布局中克隆约束参数

constraintSet.clone(clRoot)

//清空控件原有的约束

constraintSet.clear(R.id.ivLeft)

constraintSet.clear(R.id.tvRight)

constraintSet.clear(R.id.tvBottom)

constraintSet.constrainWidth(R.id.ivLeft, 0)

constraintSet.constrainHeight(R.id.ivLeft, 0)

//设置ivLeft顶部和父布局顶部对齐

constraintSet.connect(

R.id.ivLeft, ConstraintSet.TOP, R.id.clRoot, ConstraintSet.TOP,

ScreenUtil.dpToPx(this, 16)

)

constraintSet.connect(

R.id.ivLeft, ConstraintSet.START, R.id.clRoot, ConstraintSet.START,

ScreenUtil.dpToPx(this, 16)

)

constraintSet.connect(

R.id.ivLeft, ConstraintSet.END, R.id.clRoot, ConstraintSet.END,

ScreenUtil.dpToPx(this, 16)

)

//设置宽高比

constraintSet.setDimensionRatio(R.id.ivLeft, "h,16:9")

constraintSet.constrainWidth(R.id.tvRight, ConstraintLayout.LayoutParams.WRAP_CONTENT)

constraintSet.constrainHeight(R.id.tvRight, ConstraintLayout.LayoutParams.WRAP_CONTENT)

constraintSet.connect(

R.id.tvRight, ConstraintSet.TOP, R.id.ivLeft, ConstraintSet.BOTTOM,

ScreenUtil.dpToPx(this, 24)

)

constraintSet.connect(

R.id.tvRight, ConstraintSet.START, R.id.clRoot, ConstraintSet.START,

ScreenUtil.dpToPx(this, 8)

)

constraintSet.constrainHeight(R.id.tvBottom, ConstraintLayout.LayoutParams.WRAP_CONTENT)

constraintSet.connect(

R.id.tvBottom, ConstraintSet.START, R.id.clRoot, ConstraintSet.START,

ScreenUtil.dpToPx(this, 8)

)

constraintSet.connect(

R.id.tvBottom, ConstraintSet.END, R.id.clRoot, ConstraintSet.END,

ScreenUtil.dpToPx(this, 8)

)

constraintSet.connect(

R.id.tvBottom, ConstraintSet.TOP, R.id.tvRight, ConstraintSet.BOTTOM,

ScreenUtil.dpToPx(this, 24)

)

constraintSet.applyTo(clRoot)

//设置一个动画效果,让约束改变平滑一点,这一步不是必须的

TransitionManager.beginDelayedTransition(clRoot)

}

效果如下所示

16e34f919e1a

1559465809770862.gif

遇到的一个问题

在测试的时候,我想添加一个水平方向上的Guideline,让它在父布局竖直方向比例为0.4的地方,然后在Guideline之上添加一个ImageView。代码如下

private fun addGuideLine() {

val constraintLayout = ConstraintLayout(this)

constraintLayout.id = R.id.clRoot

constraintLayout.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,

ViewGroup.LayoutParams.MATCH_PARENT)

//先设置根布局

setContentView(constraintLayout)

val guideline = Guideline(this)

guideline.id = R.id.guideline

val guideLayoutParams: ConstraintLayout.LayoutParams = ConstraintLayout.LayoutParams(

ConstraintLayout.LayoutParams.WRAP_CONTENT, ConstraintLayout.LayoutParams.WRAP_CONTENT)

guideLayoutParams.guidePercent = 0.4f

guideLayoutParams.topToTop = R.id.clRoot

guideLayoutParams.bottomToBottom = R.id.clRoot

//注意

guideLayoutParams.orientation = ConstraintLayout.LayoutParams.VERTICAL

guideline.layoutParams = guideLayoutParams

constraintLayout.addView(guideline)

val ivLeft = ImageView(this)

ivLeft.id = R.id.ivLeft

ivLeft.scaleType = ImageView.ScaleType.CENTER_CROP

ivLeft.setImageResource(R.drawable.lake)

val ivLeftLayoutParams: ConstraintLayout.LayoutParams = ConstraintLayout.LayoutParams(

0, 0)

ivLeftLayoutParams.dimensionRatio = "h,16:9"

ivLeftLayoutParams.bottomToTop = R.id.guideline

ivLeftLayoutParams.startToStart = R.id.clRoot

ivLeftLayoutParams.endToEnd = R.id.clRoot

ivLeft.layoutParams = ivLeftLayoutParams

constraintLayout.addView(ivLeft)

}

在测试的时候报了一个错误

java.lang.AssertionError: TOP at android.support.constraint.solver.widgets.Guideline.getAnchor(Guideline.java:159)

折腾了半天,发现是Guideline的方向写错了。

guideLayoutParams.orientation = ConstraintLayout.LayoutParams.VERTICAL

正确的写法

guideLayoutParams.orientation = ConstraintLayout.LayoutParams.HORIZONTAL

如果Guideline的方向写错了,会导致依赖Guideline的方向的控件的约束无法正确指定,所以会报错。如果遇到类似的问题请仔细检查,是否正确的设置了约束。

参考链接

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐