动态调整ConstaintLayout元素位置

背景

最近一个需求需要动态添加删除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
}

分析

ConstraintConstraintSet的内部静态类,包括75个属性,是个工具类。这些属性都是ConstraintLayout在布局时设置的xml设置项。
clone方法复制根布局的ConstraintSet值,将所有view的Constraints.LayoutParams都拷贝到ConstraintSetmConstraints对象里,这是一个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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值