ConstraintLayout

236 阅读5分钟
1. Chain Style

Chain Style可以将水平或垂直方向的多个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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:fillViewport="true"
    >
    <View
        android:id="@+id/v1"
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:background="@color/purple_200"
        android:layout_marginStart="40dp"
        android:layout_marginTop="40dp"
        />
    <TextView
        android:id="@+id/tv1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_green_light"
        app:layout_constraintStart_toEndOf="@id/v1"
        android:layout_marginStart="12dp"
        android:text="111111"
        app:layout_constraintTop_toTopOf="@id/v1"
        app:layout_constraintVertical_chainStyle="packed"
        app:layout_constraintBottom_toTopOf="@id/tv2"
        />
    <TextView
        android:id="@+id/tv2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_blue_bright"
        app:layout_constraintStart_toEndOf="@id/v1"
        app:layout_constraintTop_toBottomOf="@id/tv1"
        app:layout_constraintBottom_toBottomOf="@id/v1"
        android:layout_marginStart="12dp"
        android:layout_marginTop="4dp"
        android:text="22222222222222"
        />
</androidx.constraintlayout.widget.ConstraintLayout>

垂直排列的2个View当做整体 相对于左边View垂直居中.png

  • tv1 和 tv2 垂直排列,当做一个整体,相对于左边的v1垂直居中.
  • 即使tv1或tv2有1个被隐藏/gone, 剩下的Viwe依然相对于v1垂直居中.
  • 可以使用bias来调节整体的位置,即使View被隐藏也生效.

screenshot-20230330-151047.png

2. layout_constraintWidth_percent

percent可以控制View的宽度/高度占外部ConstraintLayout的百分比.

  • 0dp
  • default="percent"
  • percent="0.X"

screenshot-20230330-153429.png

3. Barrier

Barrier类似GuideLine,但是更加灵活,其引用一组控件id,以尺寸最大的控件的边缘作为自身的位置. screenshot-20230330-155006.png

4. Layer

Layer用于给一组View设置公共的背景,或者控制1组View同时执行1个动画.

  • alpha动画针对的是Layer自身,不是其控制的View
  • rotate, translate, scale 针对的是其控制的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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".constraintlayout.ConstraintLayoutActivity">
    <androidx.constraintlayout.helper.widget.Layer
        android:id="@+id/layer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:constraint_referenced_ids="v1,v2,v3"
        android:background="@color/purple_500"
        />
    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/bt1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:text="展示Layer动画"
        android:onClick="exeLayerAnim"
        />
    <TextView
        android:id="@+id/v1"
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_margin="20dp"
        android:text="111"
        />
    <TextView
        android:id="@+id/v2"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_margin="20dp"
        app:layout_constraintTop_toBottomOf="@id/v1"
        app:layout_constraintStart_toStartOf="parent"
        android:text="222"
        />
    <TextView
        android:id="@+id/v3"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:layout_margin="20dp"
        app:layout_constraintTop_toBottomOf="@id/v1"
        app:layout_constraintStart_toEndOf="@id/v2"
        android:text="333"
        />
</androidx.constraintlayout.widget.ConstraintLayout>

Layer为多个View设置共同的背景: screenshot-20230330-170556.png Layer让一组View同时执行1个动画:

fun exeLayerAnim(view: View) {
    val layer: Layer = findViewById(R.id.layer)
    //alpha是针对Layer自身的,不是针对其控制的View集合
    val anim = ValueAnimator.ofFloat(0F, 1.0F)
    anim.duration = 3000
    anim.addUpdateListener {
        layer.alpha = it.animatedValue as Float
    }
    anim.start()

    //rotate动画是针对其控制的View集合
    //旋转过程中,其背景不会跟随旋转
    /*val anim = ValueAnimator.ofFloat(0F, 360.0F)
    anim.duration = 3000
    anim.addUpdateListener {
        layer.rotation = it.animatedValue as Float
    }
    anim.start()*/

    //translation动画是针对其控制的View集合
    //移动过程中,其背景不会跟随移动
    /*val anim = ValueAnimator.ofFloat(0F, 360.0F, 0F)
    anim.duration = 3000
    anim.addUpdateListener {
        layer.translationX = it.animatedValue as Float
    }
    anim.start()*/

    //scale动画是针对其控制的View集合
    //缩放过程中,其背景不会跟随缩放
    /*val anim = ValueAnimator.ofFloat(0F, 2.0F, 1.0F)
    anim.duration = 3000
    anim.addUpdateListener {
        layer.scaleX = it.animatedValue as Float
        layer.scaleY = it.animatedValue as Float
    }
    anim.start()*/
}
5. ImageFilterView

ImageFilterView,可以方便的实现圆角图片/圆角背景色,在任意容器中均可使用,不限于ConstraintLayout.

  • 可以使用两个属性来设置图片资源的圆角,分别是roundPercent和round
  • roundPercent接受的值类型是0-1的小数,根据数值的大小会使图片在方形和圆形之间按比例过度,round=可以设置具体圆角的大小

screenshot-20230330-173011.png

6. 自定义 ConstraintHelper

ConstraintHelper可以引用页面中多个控件,可以很方便的为控件设置统一的行为

  • Helper提供了view的onLayout()/onMeasure()等流程方法执行前后的回调
  • 可以为引用的控件设置动画
  • 多个ConstraintHelper可以引用同一个View控件
普通动画
class CustomConstraintHelper1 @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ConstraintHelper(context, attrs, defStyleAttr) {
    @JvmOverloads
    fun f1(p1:Int = 1, p2:String?, p3:Long = 100){
    }

    override fun updatePostLayout(container: ConstraintLayout?) {
        getViews(container).forEach {
            animate(it)
        }
    }

    private fun animate(view: View) {
        val alpha: ValueAnimator = ObjectAnimator.ofFloat(view, "alpha", 0.2F, 1.0F)
        alpha.duration = 3000
        val scale: ValueAnimator = ObjectAnimator.ofFloat(view, "scaleX", 2.0F, 1.0F)
        scale.duration = 3000
        alpha.start()
        scale.start()
    }
}

screenshot-20230404-181443.png

圆形揭露动画

使用ViewAnimationUtils的createCircularReveal函数,可以很方便的创建圆形揭露动画:

class CustomConstraintHelper1 @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintHelper(context, attrs, defStyleAttr) {
    override fun updatePostLayout(container: ConstraintLayout?) {
        getViews(container).forEach {
            ViewAnimationUtils.createCircularReveal(
                it, it.width / 2, it.height / 2, 0F, Math.hypot(
                    (it.width / 2).toDouble(), (it.height / 2).toDouble()
                ).toFloat()
            ).apply {
                duration = 3000
                start()
            }
        }
    }
}
Fling动画/View从屏幕四周回到其初始位置
class CustomConstraintHelper2 @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintHelper(context, attrs, defStyleAttr) {
    override fun updatePostLayout(container: ConstraintLayout?) {
        val centerX = (left + right) / 2
        val centerY = (top + bottom) / 2
        getViews(container).forEach {
            val translationX = if ((it.left + it.right) / 2 > centerX) 300F else -300F
            val translationY = if ((it.top + it.bottom) / 2 > centerY) 300F else -300F
            val animX = ObjectAnimator.ofFloat(it, "translationX", translationX, 0F)
            val animY = ObjectAnimator.ofFloat(it, "translationY", translationY, 0F)
            val anim: AnimatorSet = AnimatorSet()
            anim.duration = 1000
            anim.playTogether(animX, animY)
            anim.start()
        }
    }
}

引入ConstraintHelper前 image.png 引入ConstraintHelper后 image.png

7. ConstraintLayoutStates

ConstraintLayoutStates用于为Activity/Fragment切换不同的ConstraintLayout布局.

  • 不同的ConstraintLayout xml布局中要包含相同的View
  • 只有visibility,位置等属性可以不同
  • xml中创建ConstraintLayout实例的状态描述文件

R.xml.states_for_constraintlayout

<?xml version="1.0" encoding="utf-8"?>
<ConstraintLayoutStates xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <State
        android:id="@+id/state1"
        app:constraints="@layout/activity_constraint_layout_states_1" />
    <State
        android:id="@+id/state2"
        app:constraints="@layout/activity_constraint_layout_states_2" />
</ConstraintLayoutStates>

activity_constraint_layout_states_1.xml

<?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"
    android:id="@+id/root"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".constraintlayout.ConstraintLayoutStatesActivity">
    <View
        android:id="@+id/v1"
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        android:background="@color/purple_500"
        />
    <androidx.appcompat.widget.AppCompatTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/tv"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:text="State"
        />
</androidx.constraintlayout.widget.ConstraintLayout>

activity_constraint_layout_states_2.xml

<?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"
    android:id="@+id/root"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".constraintlayout.ConstraintLayoutStatesActivity">
    <View
        android:id="@+id/v1"
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        android:background="@color/purple_500"
        />
    <androidx.appcompat.widget.AppCompatTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/tv"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:text="State"
        />
</androidx.constraintlayout.widget.ConstraintLayout>

Activity

class ConstraintLayoutStatesActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_constraint_layout_states_1)
        val root: ConstraintLayout = findViewById(R.id.root)
        val v1: View = findViewById(R.id.v1)
        //1: 为ConstraintLayout实例关联 状态配置文件
        root.loadLayoutDescription(R.xml.states_for_constraintlayout)
        root.postDelayed({
            v1.setBackgroundColor(getColor(R.color.teal_200))
            //2: 设置ConstraintLayout实例当前状态/布局文件
            root.setState(R.id.state2, 0, 0)
        }, 4000)
    }
}
8. ConstraintSet

ConstraintSet用于通过代码动态修改ConstraintLayout中View的约束条件,也可以切换Activity/Fragment的布局.

class ConstraintSetActivity : AppCompatActivity() {
    lateinit var root: ConstraintLayout
    lateinit var iv1: View
    lateinit var iv2: View

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_constraint_set1)
        root = findViewById(R.id.parent)
        iv1 = findViewById(R.id.iv1)
        iv2 = findViewById(R.id.iv2)
        iv1.setOnClickListener {
            changePosition()
        }
        iv2.setOnClickListener {
            changeLayout2()
        }
    }

    //修改ConstraintLayout中View的约束条件
    private fun changePosition() {
        val constraintSet: ConstraintSet = ConstraintSet()
        constraintSet.clone(root)
        constraintSet.centerHorizontally(R.id.iv1, ConstraintSet.PARENT_ID)
        constraintSet.centerVertically(R.id.iv1, ConstraintSet.PARENT_ID)

        constraintSet.connect(R.id.iv2, ConstraintSet.TOP, R.id.iv1, ConstraintSet.BOTTOM)
        constraintSet.connect(
            R.id.iv2,
            ConstraintSet.BOTTOM,
            ConstraintSet.PARENT_ID,
            ConstraintSet.BOTTOM
        )
        constraintSet.setVerticalBias(R.id.iv2, 0.2F)

        //TransitionManager.beginDelayedTransition(root)
        val transition: AutoTransition = AutoTransition()
        transition.duration = 4000
        TransitionManager.beginDelayedTransition(root, transition)
        constraintSet.applyTo(root)
        (iv1.layoutParams as MarginLayoutParams).leftMargin = 0
    }

    //为当前Activity切换布局xml
    private fun changeLayout2() {
        val constraintSet = ConstraintSet()
        constraintSet.clone(this, R.layout.activity_constraint_set2)
        TransitionManager.beginDelayedTransition(root)
        constraintSet.applyTo(root)
    }
}

screenshot-20230406-173929.png

修改ConstraintLayout中View的约束条件
  1. 获取根布局的ConstraintLayout实例
    • val root:ConstraintLayout = findViewById(id)
  2. 创建ConstraintSet实例,并复制根布局的约束
    • val constraintSet : ConstraintSet()
    • constraintSet.clone(root)
  3. 修改其中View的约束条件
    • 如果要实现View在ConstraintLayout中水平居中,要注意布局xml中不要用start/end,要用"layout_constraintLeft_toLeftOf".
    • 如果要修改View的margin值,要在4结束后执行 : (iv1.layoutParams as MarginLayoutParams).leftMargin = 0
  4. 将修改结果应用到根布局
    • constraintSet.applyTo(root)
    • 提前执行 TransitionManager.beginDelayedTransition(root) 可以为变化添加动画,也可以自定义Transition的属性.
切换Activity/Fragment的布局
  1. 获取根布局的ConstraintLayout实例
    • val root:ConstraintLayout = findViewById(id)
  2. 创建ConstraintSet实例,并复制待切换的布局约束
    • val constraintSet : ConstraintSet()
    • constraintSet.clone(Context context, int targetLayoutId)
  3. 将修改结果应用到根布局
    • constraintSet.applyTo(root)
注意:
  1. 如果要切换xml布局文件,则多个布局文件需要有相同的View,相同id的View之间才能进行切换.
  2. TransitionManager.beginDelayedTransition(root),可以用于任意父容器(未必是根布局容器).不限于ConstraintLayout. 如 FragmeLayout中1个子控件,想让其水平位置是end,且宽高*2
    //一定要在子控件布局参数变化前调用
    TransitionManager.beginDelayedTransition(frameLayout)
    
    //在修改子View布局属性前,使用TransitionManager为其父容器布局变化添加动画
    val params:FrameLayout.LayoutParams = child.layoutParams as FrameLayout.LayoutParams
    params.width *= 2
    params.height *= 2
    params.gravity = Gravity.END
    v.layoutParams = params
    
  3. 使用ConstraintSet切换布局xml,控件的内容,比如EditText中的输入内容会保留,因为它并不会移除原始布局中的View, 仅仅是复制了最新布局xml中的布局参数,应用到原始布局.