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

时间:2019-09-30 14:15:57

标签: android android-constraintlayout constraint-layout-chains constraintset

由于某种原因,以编程方式修改ConstraintLayout的{​​{1}}来更改视图位置(属于链)时,结果与预期不符。

在下面的示例中,我构建了带有图标视图的 Button ,其中图像可以放置在按钮的开头或结尾。当图标位于末尾时,一切都很好。但是,当将其设置为位于按钮的开头时,其内容将毫无理由地向左对齐。

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

如何解决?


The bugged behaviour when the icon is set to be positioned at the start of the button

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


ButtonWithIconView.kt

ConstraintSet

button_with_icon_view.xml

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
    }
}

attrs.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_height="wrap_content"
    tools:layout_width="wrap_content"
    tools:padding="8dp"
    tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">

    <ImageView
        android:id="@+id/icon"
        android:layout_width="16dp"
        android:layout_height="16dp"
        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_width="wrap_content"
        android:layout_height="wrap_content"
        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>

activity_main.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>

1 个答案:

答案 0 :(得分: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-使用Placeholder s来设置约束并简单地交换它们的内容:

button_with_icon_view.xml

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    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_width="wrap_content"
        android:layout_height="wrap_content"
        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_width="wrap_content"
        android:layout_height="wrap_content"
        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_width="16dp"
        android:layout_height="16dp"
        android:background="#FF0000" />

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        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
}