使用的ConstraintLayout版本
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
如果不使用androidx
的话可以使用下面的版本
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
注意:使用不同的ConstraintLayout版本可能会有坑,如果在使用过程中发现实现不了想要添加的约束,可以尝试改变ConstraintLayout的版本如上所示。
1. 动态添加View
第一种情况:所有的View都是动态添加
举个例子
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:id="@+id/clRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ImageView
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" />
<TextView
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" />
<TextView
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" />
</androidx.constraintlayout.widget.ConstraintLayout>
上面的布局文件中呈现的效果如图所示,接下来我们用代码的方式动态添加View,实现上面的效果。
首先在res/values
文件夹下新建一个ids.xml
,在ids.xml
中声明我们要添加的View的控件id。
ids.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="clRoot" type="id" />
<item name="ivLeft" type="id" />
<item name="tvRight" type="id" />
<item name="tvBottom" type="id" />
</resources>
然后开始写代码
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已经在布局中了。
<?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"
android:id="@+id/clRoot"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
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" />
</android.support.constraint.ConstraintLayout>
接下来,我们动态的把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的约束该怎么做呢?比如我们想把上面的布局样式改成下图所示。
其实,在上面我们已经给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)
}
效果如下所示
遇到的一个问题
在测试的时候,我想添加一个水平方向上的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
的方向的控件的约束无法正确指定,所以会报错。如果遇到类似的问题请仔细检查,是否正确的设置了约束。
参考链接