背景
最近一个需求需要动态添加删除ConstraintLayout
里的元素,一时不知道如何处理。虽然ConstraintLayout
确实减少了层级,提升了绘制效率,但对于动态增删却一直没有尝试过。借着这个需求也好好调研了下ConstraintLayout
的一些相关属性。
按照以往的经验,增删view应该也跟RelativityLayout
或者LinearLayout
一样,直接添加就行了。不过在查阅了开发文档和Stack Overflow之后,发现并不是这么简单。这里有个核心的类ConstraintSet
,控制了元素的位置。相当于原来RelativityLayout
里的addRule
方法,但确实另外一个类来处理这些。
实操
需求如下:
点击button_addtop
时添加一个textview在image
之下button_addtop
之上。
点击button_addbottom
时添加一个textview在button_addbottom
之下。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:background="@color/colorAccent">
<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/dice_1"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<Button
android:id="@+id/button_addtop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="addtop"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/image"/>
<Button
android:id="@+id/button_addbottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="addbottom"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_addtop"/>
</androidx.constraintlayout.widget.ConstraintLayout>
代码如下:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root : ConstraintLayout = inflater.inflate(R.layout.fragment_fourth, container, false) as ConstraintLayout
root.findViewById<Button>(R.id.button_addtop).setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
textView = TextView(context)
textView.text = "Naruto"
textView.id = View.generateViewId()
textView.background = ColorDrawable(resources.getColor(R.color.colorPrimaryDark, null))
root.addView(textView)
val set = ConstraintSet()
set.clone(root)
set.connect(textView.id, ConstraintSet.TOP, R.id.image, ConstraintSet.BOTTOM, 500)
set.connect(textView.id, ConstraintSet.LEFT, ConstraintSet.PARENT_ID, ConstraintSet.LEFT, 200)
set.connect(textView.id, ConstraintSet.BOTTOM, R.id.button_addtop, ConstraintSet.TOP, 200)
set.applyTo(root)
val setaddtop = ConstraintSet()
setaddtop.clone(root)
setaddtop.connect(R.id.button_addtop, ConstraintSet.TOP, textView.id, ConstraintSet.BOTTOM)
setaddtop.applyTo(root)
}
})
root.findViewById<Button>(R.id.button_addbottom).setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
textView = TextView(context)
textView.text = "Kagawa"
textView.id = View.generateViewId()
textView.background = ColorDrawable(resources.getColor(R.color.colorPrimaryDark, null))
root.addView(textView)
val set = ConstraintSet()
set.clone(root)
set.connect(textView.id, ConstraintSet.TOP, R.id.button_addbottom, ConstraintSet.BOTTOM)
set.applyTo(root)
}
})
return root
}
分析
Constraint
是ConstraintSet
的内部静态类,包括75个属性,是个工具类。这些属性都是ConstraintLayout
在布局时设置的xml设置项。
clone
方法复制根布局的ConstraintSet
值,将所有view的Constraints.LayoutParams
都拷贝到ConstraintSet
的mConstraints
对象里,这是一个HashMap
。
public void clone(ConstraintLayout constraintLayout) {
// 获取当前布局的子View数量
int count = constraintLayout.getChildCount();
// 清空当前ConstraintSet的mConstraints
this.mConstraints.clear();
// 循环拷贝当前布局的子view param到mConstraints中
for(int i = 0; i < count; ++i) {
View view = constraintLayout.getChildAt(i);
LayoutParams param = (LayoutParams)view.getLayoutParams();
int id = view.getId();
if (id == -1) {
throw new RuntimeException("All children of ConstraintLayout must have ids to use ConstraintSet");
}
if (!this.mConstraints.containsKey(id)) {
this.mConstraints.put(id, new ConstraintSet.Constraint());
}
ConstraintSet.Constraint constraint = (ConstraintSet.Constraint)this.mConstraints.get(id);
constraint.fillFrom(id, param);
constraint.visibility = view.getVisibility();
if (VERSION.SDK_INT >= 17) {
constraint.alpha = view.getAlpha();
constraint.rotation = view.getRotation();
constraint.rotationX = view.getRotationX();
constraint.rotationY = view.getRotationY();
constraint.scaleX = view.getScaleX();
constraint.scaleY = view.getScaleY();
float pivotX = view.getPivotX();
float pivotY = view.getPivotY();
if ((double)pivotX != 0.0D || (double)pivotY != 0.0D) {
constraint.transformPivotX = pivotX;
constraint.transformPivotY = pivotY;
}
constraint.translationX = view.getTranslationX();
constraint.translationY = view.getTranslationY();
if (VERSION.SDK_INT >= 21) {
constraint.translationZ = view.getTranslationZ();
if (constraint.applyElevation) {
constraint.elevation = view.getElevation();
}
}
}
if (view instanceof Barrier) {
Barrier barrier = (Barrier)view;
constraint.mBarrierAllowsGoneWidgets = barrier.allowsGoneWidget();
constraint.mReferenceIds = barrier.getReferencedIds();
constraint.mBarrierDirection = barrier.getType();
}
}
}
connect
方法则根据不同的属性对应的整型值将需要修改的属性进行赋值。
public void connect(int startID, int startSide, int endID, int endSide)
public void connect(int startID, int startSide, int endID, int endSide, int margin)
以带margin的方法为例:
public void connect(int startID, int startSide, int endID, int endSide, int margin) {
// 查找View.getId对应的限制是否存在,不存在则创建一个value
if (!mConstraints.containsKey(startID)) {
mConstraints.put(startID, new Constraint());
}
// 找到对应的值
Constraint constraint = mConstraints.get(startID);
switch (startSide) {
case LEFT: // 设置LEFT侧属性,endID是对应的约束布局对象
if (endSide == LEFT) {
constraint.layout.leftToLeft = endID;
constraint.layout.leftToRight = Layout.UNSET;
} else if (endSide == RIGHT) {
constraint.layout.leftToRight = endID;
constraint.layout.leftToLeft = Layout.UNSET;
} else {
throw new IllegalArgumentException("Left to " + sideToString(endSide) + " undefined");
}
// 设置距left依赖的margin
constraint.layout.leftMargin = margin;
break;
case RIGHT: // 设置RIGHT侧属性,endID是对应的约束布局对象
if (endSide == LEFT) {
constraint.layout.rightToLeft = endID;
constraint.layout.rightToRight = Layout.UNSET;
} else if (endSide == RIGHT) {
constraint.layout.rightToRight = endID;
constraint.layout.rightToLeft = Layout.UNSET;
} else {
throw new IllegalArgumentException("right to " + sideToString(endSide) + " undefined");
}
// 设置距right依赖的margin
constraint.layout.rightMargin = margin;
break;
case TOP: // 设置TOP侧属性,endID是对应的约束布局对象
if (endSide == TOP) {
constraint.layout.topToTop = endID;
constraint.layout.topToBottom = Layout.UNSET;
constraint.layout.baselineToBaseline = Layout.UNSET;
} else if (endSide == BOTTOM) {
constraint.layout.topToBottom = endID;
constraint.layout.topToTop = Layout.UNSET;
constraint.layout.baselineToBaseline = Layout.UNSET;
} else {
throw new IllegalArgumentException("right to " + sideToString(endSide) + " undefined");
}
// 设置距top依赖的margin
constraint.layout.topMargin = margin;
break;
case BOTTOM: // 设置BOTTOM侧属性,endID是对应的约束布局对象
if (endSide == BOTTOM) {
constraint.layout.bottomToBottom = endID;
constraint.layout.bottomToTop = Layout.UNSET;
constraint.layout.baselineToBaseline = Layout.UNSET;
} else if (endSide == TOP) {
constraint.layout.bottomToTop = endID;
constraint.layout.bottomToBottom = Layout.UNSET;
constraint.layout.baselineToBaseline = Layout.UNSET;
} else {
throw new IllegalArgumentException("right to " + sideToString(endSide) + " undefined");
}
// 设置距bottom依赖的margin
constraint.layout.bottomMargin = margin;
break;
case BASELINE: // 设置BASELINE属性,endID是对应的约束布局对象
if (endSide == BASELINE) {
constraint.layout.baselineToBaseline = endID;
constraint.layout.bottomToBottom = Layout.UNSET;
constraint.layout.bottomToTop = Layout.UNSET;
constraint.layout.topToTop = Layout.UNSET;
constraint.layout.topToBottom = Layout.UNSET;
} else {
throw new IllegalArgumentException("right to " + sideToString(endSide) + " undefined");
}
break;
case START: // 设置START侧属性,endID是对应的约束布局对象
if (endSide == START) {
constraint.layout.startToStart = endID;
constraint.layout.startToEnd = Layout.UNSET;
} else if (endSide == END) {
constraint.layout.startToEnd = endID;
constraint.layout.startToStart = Layout.UNSET;
} else {
throw new IllegalArgumentException("right to " + sideToString(endSide) + " undefined");
}
// 设置距start依赖的margin
constraint.layout.startMargin = margin;
break;
case END: // 设置END侧属性,endID是对应的约束布局对象
if (endSide == END) {
constraint.layout.endToEnd = endID;
constraint.layout.endToStart = Layout.UNSET;
} else if (endSide == START) {
constraint.layout.endToStart = endID;
constraint.layout.endToEnd = Layout.UNSET;
} else {
throw new IllegalArgumentException("right to " + sideToString(endSide) + " undefined");
}
// 设置距end依赖的margin
constraint.layout.endMargin = margin;
break;
default:
throw new IllegalArgumentException(
sideToString(startSide) + " to " + sideToString(endSide) + " unknown");
}
}
applyto
方法将属性设置给布局
public void applyTo(ConstraintLayout constraintLayout) {
this.applyToInternal(constraintLayout);
constraintLayout.setConstraintSet((ConstraintSet)null);
}
实际干活的是applyToInternal
。
void applyToInternal(ConstraintLayout constraintLayout, boolean applyPostLayout) {
int count = constraintLayout.getChildCount();
HashSet<Integer> used = new HashSet<Integer>(mConstraints.keySet());
for (int i = 0; i < count; i++) {
View view = constraintLayout.getChildAt(i);
int id = view.getId();
if (!mConstraints.containsKey(id)) {
Log.w(TAG, "id unknown " + Debug.getName(view));
continue;
}
if (mForceId && id == -1) {
throw new RuntimeException("All children of ConstraintLayout must have ids to use ConstraintSet");
}
if (id == -1) {
continue;
}
if (mConstraints.containsKey(id)) {
used.remove(id);
Constraint constraint = mConstraints.get(id);
if (view instanceof Barrier) {
constraint.layout.mHelperType = BARRIER_TYPE;
}
if (constraint.layout.mHelperType != UNSET) {
switch (constraint.layout.mHelperType) {
case BARRIER_TYPE:
Barrier barrier = (Barrier) view;
barrier.setId(id);
barrier.setType(constraint.layout.mBarrierDirection);
barrier.setMargin(constraint.layout.mBarrierMargin);
barrier.setAllowsGoneWidget(constraint.layout.mBarrierAllowsGoneWidgets);
if (constraint.layout.mReferenceIds != null) {
barrier.setReferencedIds(constraint.layout.mReferenceIds);
} else if (constraint.layout.mReferenceIdString != null) {
constraint.layout.mReferenceIds = convertReferenceString(barrier,
constraint.layout.mReferenceIdString);
barrier.setReferencedIds(constraint.layout.mReferenceIds);
}
break;
}
}
ConstraintLayout.LayoutParams param = (ConstraintLayout.LayoutParams) view
.getLayoutParams();
param.validate();
constraint.applyTo(param);
if (applyPostLayout) {
ConstraintAttribute.setAttributes(view, constraint.mCustomConstraints);
}
view.setLayoutParams(param);
if (constraint.propertySet.mVisibilityMode == VISIBILITY_MODE_NORMAL) {
view.setVisibility(constraint.propertySet.visibility);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
view.setAlpha(constraint.propertySet.alpha);
view.setRotation(constraint.transform.rotation);
view.setRotationX(constraint.transform.rotationX);
view.setRotationY(constraint.transform.rotationY);
view.setScaleX(constraint.transform.scaleX);
view.setScaleY(constraint.transform.scaleY);
if (constraint.transform.transformPivotTarget != UNSET) {
View layout = (View) view.getParent();
View center = layout.findViewById(constraint.transform.transformPivotTarget);
if (center != null) {
float cy = (center.getTop() + center.getBottom()) / 2.0f;
float cx = (center.getLeft() + center.getRight()) / 2.0f;
if (view.getRight() - view.getLeft() > 0 && view.getBottom() - view.getTop() > 0) {
float px = (cx - view.getLeft());
float py = (cy - view.getTop());
view.setPivotX(px);
view.setPivotY(py);
}
}
} else {
if (!Float.isNaN(constraint.transform.transformPivotX)) {
view.setPivotX(constraint.transform.transformPivotX);
}
if (!Float.isNaN(constraint.transform.transformPivotY)) {
view.setPivotY(constraint.transform.transformPivotY);
}
}
view.setTranslationX(constraint.transform.translationX);
view.setTranslationY(constraint.transform.translationY);
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
view.setTranslationZ(constraint.transform.translationZ);
if (constraint.transform.applyElevation) {
view.setElevation(constraint.transform.elevation);
}
}
}
} else {
Log.v(TAG, "WARNING NO CONSTRAINTS for view " + id);
}
}
for (Integer id : used) {
Constraint constraint = mConstraints.get(id);
if (constraint.layout.mHelperType != UNSET) {
switch (constraint.layout.mHelperType) {
case BARRIER_TYPE:
Barrier barrier = new Barrier(constraintLayout.getContext());
barrier.setId(id);
if (constraint.layout.mReferenceIds != null) {
barrier.setReferencedIds(constraint.layout.mReferenceIds);
} else if (constraint.layout.mReferenceIdString != null) {
constraint.layout.mReferenceIds = convertReferenceString(barrier,
constraint.layout.mReferenceIdString);
barrier.setReferencedIds(constraint.layout.mReferenceIds);
}
barrier.setType(constraint.layout.mBarrierDirection);
barrier.setMargin(constraint.layout.mBarrierMargin);
ConstraintLayout.LayoutParams param = constraintLayout
.generateDefaultLayoutParams();
barrier.validateParams();
constraint.applyTo(param);
constraintLayout.addView(barrier, param);
break;
}
}
if (constraint.layout.mIsGuideline) {
Guideline g = new Guideline(constraintLayout.getContext());
g.setId(id);
ConstraintLayout.LayoutParams param = constraintLayout.generateDefaultLayoutParams();
constraint.applyTo(param);
constraintLayout.addView(g, param);
}
}
}
实际上是把所有子view的param再赋值给对应的view,并刷新布局。
参考
https://www.zoftino.com/adding-views-&-constraints-to-android-constraint-layout-programmatically
https://stackoverflow.com/questions/45263159/constraintlayout-change-constraints-programmatically