美文网首页Android进阶Android开发技巧
ConstraintLayout动态添加View,改变约束

ConstraintLayout动态添加View,改变约束

作者: leilifengxingmw | 来源:发表于2019-06-01 16:25 被阅读0次

    DEMO源码

    使用的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>
    
    
    初始布局.jpg

    上面的布局文件中呈现的效果如图所示,接下来我们用代码的方式动态添加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>
    
    
    31573289245_.pic.jpg

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

    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的约束该怎么做呢?比如我们想把上面的布局样式改成下图所示。


    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 动态修改约束分四步。

    1. 首先要声明一下ConstraintSet对象
      val constraintSet = ConstraintSet()
    
    1. 复制一份现有的约束关系,这一步不是必须的。
    //从一个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
    
    
    1. 设置控件之间的约束
    2. 应用新的约束。在应用约束的时候,为了让约束改变的时候不是那么突兀,我们可以设置一个动画,来让约束改变平滑一点。
    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)
        }
    
    

    效果如下所示

    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的方向的控件的约束无法正确指定,所以会报错。如果遇到类似的问题请仔细检查,是否正确的设置了约束。

    参考链接

    1. Android开发笔记(一百四十九)约束布局ConstraintLayout
    2. ConstraintLayout 之 ConstraintSet 动态修改约束(动画)
    3. ConstraintSet

    相关文章

      网友评论

        本文标题:ConstraintLayout动态添加View,改变约束

        本文链接:https://www.haomeiwen.com/subject/pxnotctx.html