94

I'm having a problem to programmatically add views to a ConstraintLayout, and set up all the constraints required for the layout to work.

What I have at the moment doesn't work:

ConstraintLayout layout = (ConstraintLayout) findViewById(R.id.mainConstraint);
ConstraintSet set = new ConstraintSet();
set.clone(layout);

ImageView view = new ImageView(this);
layout.addView(view,0);
set.connect(view.getId(), ConstraintSet.TOP, layout.getId(), ConstraintSet.TOP, 60);
set.applyTo(layout);

The ImageView doesn't even appear on the layout. When adding to a RelativeLayout, it works like a charm.

What can I do to create the constraints I need, so that my layout work again?

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
LeoColman
  • 6,950
  • 7
  • 34
  • 63

3 Answers3

152

I think you should clone the layout after adding your ImageView.

ConstraintLayout layout = (ConstraintLayout)findViewById(R.id.mainConstraint);
ConstraintSet set = new ConstraintSet();

ImageView view = new ImageView(this);
view.setId(View.generateViewId());  // cannot set id after add
layout.addView(view,0);
set.clone(layout);
set.connect(view.getId(), ConstraintSet.TOP, layout.getId(), ConstraintSet.TOP, 60);
set.applyTo(layout); // apply to layout
rerashhh
  • 1,671
  • 1
  • 13
  • 15
  • 15
    Why does this work? Some explanation would be appreciated. Thanks! – Yonah Karp May 03 '17 at 02:08
  • 10
    @YonahKarp You are first adding all of the children to the constraint layout before cloning it to the set, so all the views you've added are included. I'd like to point out that you need to set your id programmatically as well, otherwise all getId()s will return -1 – Allan W Jul 07 '17 at 03:21
  • 3
    I spent so many hours and the solution was too simple, there is some place where is this pointed in documentation? how do you know this? – Diego Fernando Murillo Valenci Apr 27 '18 at 13:26
  • 1
    @DiegoFernandoMurilloValenci it seems that applying a constraintset clears previously applied constraintsets and knowing that one could first extract the old constraintset using clone and then add the new constraint and apply it. In other words, you can add one thing to an existing constraintlayout but you can't add one constraint to the existing constraintset. This is just my inference based solely on this answer. I am very new to this stuff and Android in general. – user2297550 Jun 18 '18 at 06:56
  • 2
    It's giving me exception: java.lang.RuntimeException: All children of ConstraintLayout must have ids to use ConstraintSet – Ali Azaz Alam Sep 13 '21 at 12:26
2

Merging this How do I add elements dynamically to a view created with XML with https://stackoverflow.com/a/40527407/4991437

I have discovered an 'easier' solution. This works best for adding multiple consistent views that uses Viewbinding and ViewModel

  1. Create the fragment_home.xml
  2. Add a LinearLayout to the bottom of fragment_home.xml
  3. Create an dynamic_view.xml that you will inflate
  4. Get elements from ViewModel/Array or whatever source of data available
  5. Inflate and add elements

In this case I am creating a textview(title) with a recyclerview[there are libs for this, however, I found more control doing this]

Step 1,2

<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView 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=".ui.hometab.HomeFragment">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/constraint_parent"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- otherviews here -->

        <LinearLayout
            android:id="@+id/dynamic_linear_layout"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/margin_large"
            android:orientation="vertical"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/curated_recycler" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

Step 3

    <?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"
    android:id="@+id/dynamic_constraint"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_begin="@dimen/margin_large"
        app:layout_constraintStart_toStartOf="parent" />

    <View
        android:id="@+id/title_view"
        android:layout_width="0dp"
        android:layout_height="40dp"
        android:layout_gravity="center"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <TextView
        android:id="@+id/shop_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/diary"
        android:textAppearance="@style/TextAppearance.AppCompat.Medium"
        android:textColor="@android:color/black"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="@+id/title_view"
        app:layout_constraintStart_toStartOf="@id/guideline"
        app:layout_constraintTop_toTopOf="@+id/title_view" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/shop_recycler"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:clipToPadding="false"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@id/guideline"
        app:layout_constraintTop_toBottomOf="@+id/title_view" />

</androidx.constraintlayout.widget.ConstraintLayout>

Step 4,5

    binding = FragmentHomeBinding.inflate(getLayoutInflater());

    ShopViewModel shopViewModel = new ViewModelProvider(this).get(ShopViewModel.class);
    shopViewModel.getAllShops(false).observe(getViewLifecycleOwner(), shopEntities -> {
        List<ConstraintLayout> shopConstraints = new ArrayList<>();

        for (ShopEntity shopEntity : shopEntities) {
            // get an instance of the dynamic_view.xml
            DynamicViewBinding dynamicViewBinding = DynamicViewBinding.inflate(getLayoutInflater());
            // add text view content
            dynamicViewBinding.shopName.setText(shopEntity.getName());
            // initialize the recycler layout adapter
            dynamicViewBinding.shopRecycler.setLayoutManager(new ProductCardLayoutManager(getContext(), 1,
                    GridLayoutManager.HORIZONTAL, false, 80));

            // get all products and add them to recycler view
            productViewModel.getAllProductsByShop(shopEntity.getShopId(), 10).observe(getViewLifecycleOwner(), productEntities ->
                    dynamicViewBinding.shopRecycler.setAdapter(new ProductAdapter(getActivity(), productEntities)));

            // attach dynamic view to list of dynamic constraint layouts
            shopConstraints.add(dynamicViewBinding.getRoot());
        }

        // remove all previous views from the linear layout
        binding.dynamicLinearLayout.removeAllViews();

        // add the new views
        for (ConstraintLayout shopConstraint : shopConstraints) {
            binding.dynamicLinearLayout.addView(shopConstraint);
        }
    });
Phillip Kigenyi
  • 1,359
  • 14
  • 21
0

MotionLayout Solution

MotionLayout is trickier as it requires special handling due to it having multiple ConstraintSets. If you use the solution intended for ConstraintLayout, for some reason it will position your views somewhere in the bottom right. The correct way to add view in MotionLayout is to use updateState instead of applyTo:

/* ...up until this point the steps are the same: create constraint set,
set view's id, clone, connect, etc. */

layout.updateState(R.id.start, set)

In order to make your view visible in other defined ConstraintSets (in this case the end ConstraintSet, aka R.id.end), use the dedicated cloneConstraintSet method and set the view's size:

// Clone the constraint set using MotionLayout's `cloneConstraintSet`
val setEnd = layout.cloneConstraintSet(R.id.end)
// Set the view's width and height
setEnd.constrainWidth(view.id, ConstraintLayout.LayoutParams.WRAP_CONTENT)
setEnd.constrainHeight(view.id, ConstraintLayout.LayoutParams.WRAP_CONTENT)
// Update state
layout.updateState(R.id.end, setEnd)
miredirex
  • 310
  • 3
  • 10