Модификация цепочек ConstraintSet программно не работает должным образом - PullRequest
0 голосов
/ 30 сентября 2019

По какой-то причине, при программном изменении ConstraintLayout ConstraintSet для изменения позиции вида (которая принадлежит цепочке), результат не соответствует ожидаемому.

В следующем примере Iвстроенная кнопка с иконкой , где изображение может быть расположено в начале или конце кнопки. Когда значок расположен в конце, все в порядке. Но когда он настроен на начало кнопки, его содержимое безо всякой причины выравнивается по левому краю.

Я не знаю, как решить эту проблему. Я уже пробовал несколько модификаций в коде, но ни одна из них не сработала.

Как это можно решить?


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

Поведение с ошибками, когда значок установлен в начале кнопки. Он каким-то образом выравнивается по левому краю кнопки


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

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_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.example.buttonwithimageexample.ButtonWithIconView
        android:id="@+id/left_button"
        android:layout_width="170dp"
        android:layout_height="wrap_content"
        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_width="170dp"
        android:layout_height="wrap_content"
        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>

1 Ответ

1 голос
/ 30 сентября 2019

Вместо программного воссоздания ограничений, установленных с нуля, у вас есть лучшие варианты. Ваше решение очень трудно читать и трудно изменить.

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
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...