以编程方式修改 ConstraintSet 链未按预期工作

Posted

技术标签:

【中文标题】以编程方式修改 ConstraintSet 链未按预期工作【英文标题】:Modifying ConstraintSet chains programmatically not working as expected 【发布时间】:2020-01-29 20:37:20 【问题描述】:

由于某种原因,当以编程方式修改ConstraintLayoutConstraintSet 以更改视图位置(属于链)时,结果与预期不符。

在下面的示例中,我构建了一个 带有图标视图的按钮,其中图像可以定位在按钮的开头或结尾。当图标位于末尾时,一切都很好。但是当它被设置在按钮的开头时,它的内容会无缘无故地向左对齐。

我不知道如何解决这个问题。我已经在代码中尝试了几处修改,但都没有奏效。

如何解决?


将图标设置为位于按钮开头时的错误行为。它以某种方式与按钮的左侧对齐


ButtonWithIconView.kt

package com.example.buttonwithimageexample

import android.content.Context
import android.content.res.Resources
import android.graphics.Color
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.content.res.getIntOrThrow

class ButtonWithIconView : ConstraintLayout 

    private val iconView by lazy  findViewById<ImageView>(R.id.icon) 
    private val textView by lazy  findViewById<TextView>(R.id.text) 

    /**
     * Acceptable values: Gravity.START and Gravity.END
     */
    private var iconGravity = Gravity.START

    constructor(context: Context?) : super(context) 
        commonInit(context, null)
    

    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) 
        commonInit(context, attrs)
    

    constructor(
        context: Context?,
        attrs: AttributeSet?,
        defStyleAttr: Int
    ) : super(context, attrs, defStyleAttr) 
        commonInit(context, attrs)
    

    private fun commonInit(context: Context?, attrs: AttributeSet?) 
        if (context == null) 
            return
        

        this.setBackgroundColor(Color.LTGRAY)
        this.setPadding(
            BUTTON_PADDING,
            BUTTON_PADDING,
            BUTTON_PADDING,
            BUTTON_PADDING
        )

        View.inflate(context, R.layout.button_with_icon_view, this)

        if (attrs != null) 
            applyAttrs(attrs)
        

        if (isInEditMode) 
            return
        
    

    private fun applyAttrs(attrs: AttributeSet) 
        val typedArray = context.obtainStyledAttributes(
            attrs,
            R.styleable.ButtonWithIconView,
            0,
            0
        )

        if (typedArray.hasValue(R.styleable.ButtonWithIconView_button_text)) 
            textView.text = typedArray.getText(R.styleable.ButtonWithIconView_button_text)
        

        if (typedArray.hasValue(R.styleable.ButtonWithIconView_button_icon_position)) 
            when (typedArray.getIntOrThrow(R.styleable.ButtonWithIconView_button_icon_position)) 
                ATTR_BUTTON_ICON_POS_START -> setIconPosition(Gravity.START)
                ATTR_BUTTON_ICON_POS_END -> setIconPosition(Gravity.END)
            
        

        typedArray.recycle()
    

    private fun getACopyOfTheCurrentConstraintSet(): ConstraintSet 
        return ConstraintSet().apply 
            this.clone(this@ButtonWithIconView)
        
    

    private fun onBeforeMovingIcon(constrainSet: ConstraintSet) 
        constrainSet.removeFromHorizontalChain(textView.id)
        constrainSet.removeFromHorizontalChain(iconView.id)

        constrainSet.clear(iconView.id, ConstraintSet.LEFT)
        constrainSet.clear(iconView.id, ConstraintSet.TOP)
        constrainSet.clear(iconView.id, ConstraintSet.RIGHT)
        constrainSet.clear(iconView.id, ConstraintSet.BOTTOM)
        constrainSet.clear(iconView.id, ConstraintSet.START)
        constrainSet.clear(iconView.id, ConstraintSet.END)

        when (iconGravity) 
            Gravity.START -> 
                constrainSet.clear(
                    textView.id,
                    ConstraintSet.START
                )

                constrainSet.connect(
                    textView.id,
                    ConstraintSet.START,
                    ConstraintSet.PARENT_ID,
                    ConstraintSet.START,
                    0
                )
            
            Gravity.END -> 
                constrainSet.clear(
                    textView.id,
                    ConstraintSet.END
                )

                constrainSet.connect(
                    textView.id,
                    ConstraintSet.END,
                    ConstraintSet.PARENT_ID,
                    ConstraintSet.END,
                    0
                )
            
        
    

    private fun moveIconToLeftOfTheText() 
        val newConstraintSet = getACopyOfTheCurrentConstraintSet()

        onBeforeMovingIcon(newConstraintSet)

        newConstraintSet.clear(
            textView.id,
            ConstraintSet.START
        )

        newConstraintSet.connect(
            iconView.id,
            ConstraintSet.END,
            textView.id,
            ConstraintSet.START,
            HALF_DISTANCE_BETWEEN_ICON_AND_TEXT
        )

        /**
         *  When this line is set, the resulting layout becomes bugged. Instead of the chain
         * being centralized in the parent, it is to the start of it =,/.
         *  Without that function call, everything works as expected, but it shouldn't, because
         * it as a chain (<left to right of> and <right to left of> are required).
         */
        newConstraintSet.connect(
            textView.id,
            ConstraintSet.START,
            iconView.id,
            ConstraintSet.END,
            HALF_DISTANCE_BETWEEN_ICON_AND_TEXT
        )

        newConstraintSet.connect(
            iconView.id,
            ConstraintSet.START,
            ConstraintSet.PARENT_ID,
            ConstraintSet.START,
            0
        )

        newConstraintSet.connect(
            iconView.id,
            ConstraintSet.TOP,
            ConstraintSet.PARENT_ID,
            ConstraintSet.TOP,
            0
        )

        newConstraintSet.connect(
            iconView.id,
            ConstraintSet.BOTTOM,
            ConstraintSet.PARENT_ID,
            ConstraintSet.BOTTOM,
            0
        )

        newConstraintSet.createHorizontalChain(
            ConstraintSet.PARENT_ID,
            ConstraintSet.LEFT,
            ConstraintSet.PARENT_ID,
            ConstraintSet.RIGHT,
            intArrayOf(
                iconView.id,
                textView.id
            ),
            null,
            ConstraintSet.CHAIN_PACKED
        )

        newConstraintSet.applyTo(this)
        iconGravity = Gravity.START
    

    private fun moveIconToTheRightOfTheText() 
        val newConstraintSet = getACopyOfTheCurrentConstraintSet()

        onBeforeMovingIcon(newConstraintSet)

        newConstraintSet.clear(
            textView.id,
            ConstraintSet.END
        )

        newConstraintSet.connect(
            iconView.id,
            ConstraintSet.START,
            textView.id,
            ConstraintSet.END,
            HALF_DISTANCE_BETWEEN_ICON_AND_TEXT
        )

        newConstraintSet.connect(
            textView.id,
            ConstraintSet.END,
            iconView.id,
            ConstraintSet.START,
            HALF_DISTANCE_BETWEEN_ICON_AND_TEXT
        )

        newConstraintSet.connect(
            iconView.id,
            ConstraintSet.TOP,
            ConstraintSet.PARENT_ID,
            ConstraintSet.TOP,
            0
        )

        newConstraintSet.connect(
            iconView.id,
            ConstraintSet.END,
            ConstraintSet.PARENT_ID,
            ConstraintSet.END,
            0
        )

        newConstraintSet.connect(
            iconView.id,
            ConstraintSet.BOTTOM,
            ConstraintSet.PARENT_ID,
            ConstraintSet.BOTTOM,
            0
        )

        newConstraintSet.createHorizontalChain(
            ConstraintSet.PARENT_ID,
            ConstraintSet.LEFT,
            ConstraintSet.PARENT_ID,
            ConstraintSet.RIGHT,
            intArrayOf(
                textView.id,
                iconView.id
            ),
            null,
            ConstraintSet.CHAIN_PACKED
        )

        newConstraintSet.applyTo(this)
        iconGravity = Gravity.END
    

    /**
     * @param gravity may be Gravity.START or Gravity.END (from the text)
     */
    fun setIconPosition(gravity: Int) 
        when (gravity) 
            Gravity.START -> moveIconToLeftOfTheText()
            Gravity.END -> moveIconToTheRightOfTheText()
            else -> throw IllegalArgumentException("Invalid gravity: $gravity")
        
    

    companion object 
        private val BUTTON_PADDING = TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP,
            16f,
            Resources.getSystem().displayMetrics
        ).toInt()
        private val HALF_DISTANCE_BETWEEN_ICON_AND_TEXT = TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP,
            4f,
            Resources.getSystem().displayMetrics
        ).toInt()

        private const val ATTR_BUTTON_ICON_POS_START = 0
        private const val ATTR_BUTTON_ICON_POS_END = 1
    


button_with_icon_view.xml

<?xml version="1.0" encoding="utf-8"?>
<merge 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:orientation="vertical"
    tools:background="#CCCCCC"
    tools:layout_
    tools:layout_
    tools:padding="8dp"
    tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">

    <ImageView
        android:id="@+id/icon"
        android:layout_
        android:layout_
        android:layout_marginRight="4dp"
        android:background="#FF0000"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/text"
        app:layout_constraintHorizontal_chainStyle="packed"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/text"
        android:layout_
        android:layout_
        android:layout_marginLeft="4dp"
        android:includeFontPadding="false"
        android:text="Clicker"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/icon"
        app:layout_constraintTop_toTopOf="parent" />

</merge>

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ButtonWithIconView">
        <attr name="button_text" />
        <attr name="button_icon_position" format="enum">
            <enum name="start" value="0" />
            <enum name="end" value="1" />
        </attr>
    </declare-styleable>
</resources>

activity_main.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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_
    android:layout_
    tools:context=".MainActivity">

    <com.example.buttonwithimageexample.ButtonWithIconView
        android:id="@+id/left_button"
        android:layout_
        android:layout_
        app:button_icon_position="start"
        app:button_text="Left Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/right_button"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.example.buttonwithimageexample.ButtonWithIconView
        android:id="@+id/right_button"
        android:layout_
        android:layout_
        app:button_icon_position="end"
        app:button_text="Right Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/left_button"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

【问题讨论】:

你的代码在这里有点冗长,但你确定你的集合的最终约束符合你的需要吗?似乎您确实清除了链并将任何一个的开始(取决于“开始”上的哪个小部件)固定到父级并成为链 HEAD,但是在 *** 上很难“跟随”。这总是发生吗?还是有没有的情况?如果您将一个 ButtonWithIconView 放入布局中,这是否也会受到影响,或者布局传递是否会影响您的布局? 【参考方案1】:

您有更好的选择,而不是以编程方式从头开始重新创建约束集。您的解决方案很难阅读且不易修改。

1 - 为开始/结束重力创建布局文件并将其应用到您的 setGravity 方法中:

fun setIconPosition(gravity: Int) 
    val cs = ConstraintSet()
    cs.clone(context, when (gravity) 
        Gravity.START -> R.layout.button_with_icon_view_start
        Gravity.END -> R.layout.button_with_icon_view_end
        else -> throw IllegalArgumentException("Invalid gravity: $gravity")
    )
    setConstraintSet(cs)

现在您不再需要难以理解的代码块。但是,如果您想修改布局,则必须同时维护两个布局文件。所以我推荐以下方法:


2 - 使用Placeholders 设置约束并简单地交换它们的内容:

button_with_icon_view.xml

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_
    android:layout_
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">

    <androidx.constraintlayout.widget.Placeholder
        android:id="@+id/placeHolderStart"
        android:layout_
        android:layout_
        android:layout_marginEnd="4dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/placeHolderEnd"
        app:layout_constraintHorizontal_chainStyle="packed"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:content="@+id/icon"/>

    <androidx.constraintlayout.widget.Placeholder
        android:id="@+id/placeHolderEnd"
        android:layout_
        android:layout_
        android:layout_marginStart="4dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/placeHolderStart"
        app:layout_constraintTop_toTopOf="parent"
        tools:content="@+id/text"/>

    <ImageView
        android:id="@+id/icon"
        android:layout_
        android:layout_
        android:background="#FF0000" />

    <TextView
        android:id="@+id/text"
        android:layout_
        android:layout_
        android:includeFontPadding="false"
        android:text="Clicker" />
</merge>

替换视图:

fun setIconPosition(gravity : Int)
    when(gravity)
        Gravity.START -> 
            placeHolderStart.setContentId(iconView.id)
            placeHolderEnd.setContentId(textView.id)
        
        Gravity.END -> 
            placeHolderStart.setContentId(textView.id)
            placeHolderEnd.setContentId(iconView.id)
        
    
    this.iconGravity = gravity

【讨论】:

多么令人难以置信的解决方案!我不知道Placeholder 的存在,但现在我知道了它的力量。非常感谢您的回答,这对我来说非常重要:)。我只是让“问题”打开一会儿,看看是否有人知道我的代码行为不端的原因(这真的让我很烦恼,呵呵)。再次感谢 \o/

以上是关于以编程方式修改 ConstraintSet 链未按预期工作的主要内容,如果未能解决你的问题,请参考以下文章

通过 constraintSet 以编程方式设置约束会导致视图消失

使用 ConstraintSet 时测量的布局大小为零

什么相当于ConstraintSet中的“toStartOf”和“toTopOf”?

来自 ConstraintSet 的 clone 和 applyTo 使应用程序崩溃

Android ConstraintLayout ConstraintSet动态布局

在 iOS 中以编程方式对 UITableView 的约束