以编程方式修改 ConstraintSet 链未按预期工作
Posted
技术标签:
【中文标题】以编程方式修改 ConstraintSet 链未按预期工作【英文标题】:Modifying ConstraintSet chains programmatically not working as expected 【发布时间】:2020-01-29 20:37:20 【问题描述】:由于某种原因,当以编程方式修改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_
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 - 使用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_
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中的“toStartOf”和“toTopOf”?
来自 ConstraintSet 的 clone 和 applyTo 使应用程序崩溃