Изменение цепочек ConstraintSet программно не работает должным образом
По какой-то причине при изменении ConstraintLayout
с ConstraintSet
программно для изменения позиции просмотра (которая принадлежит цепочке) результат не такой, как ожидалось.
В следующем примере я создал кнопку с представлением значков, где изображение можно расположить в начале или в конце кнопки. Когда иконка стоит в конце, все нормально. Но когда он настроен на размещение в начале кнопки, его содержимое выравнивается по левому краю без всякой причины.
Я не знаю, как решить эту проблему. Я уже пробовал несколько модификаций в коде, но ни одна из них не сработала.
Как это решить?
Ошибочное поведение, когда значок настроен на размещение в начале кнопки. Он каким-то образом выравнивается слева от кнопки
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 - Создайте файлы макета для начальной и конечной гравитации и примените их внутри вашего 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
}