LeoColman
LeoColman

Reputation: 7113

How to programmatically add views and constraints to a ConstraintLayout?

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?

Upvotes: 94

Views: 76945

Answers (4)

Torben
Torben

Reputation: 6827

Adding constraints to a programmatically generated view is also possible without creating a new constraint set, but simply by just setting the layout parameters of the view. Here is an example in Kotlin and Java:

val view = ImageView(this)
view.layoutParams = ConstraintLayout.LayoutParams(
    ConstraintLayout.LayoutParams.WRAP_CONTENT,
    ConstraintLayout.LayoutParams.WRAP_CONTENT
).apply {
    topToTop = R.id.mainConstraint
    topMargin = 60
}

val layout = findViewById<ConstraintLayout>(R.id.mainConstraint)
layout.addView(view)
ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(
    ConstraintLayout.LayoutParams.WRAP_CONTENT,
    ConstraintLayout.LayoutParams.WRAP_CONTENT
);
layoutParams.topToTop = R.id.mainConstraint;
layoutParams.topMargin = 60;

ImageView view = new ImageView(this);
view.setLayoutParams(layoutParams);

ConstraintLayout layout = findViewById(R.id.mainConstraint);
layout.addView(view);

It is also easy to modify the layout parameters after a view has already been added to a layout. Again an example in Kotlin and Java:

view.layoutParams = (tv.layoutParams as ConstraintLayout.LayoutParams).apply {
    topMargin = 120
}
ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) view.getLayoutParams();
layoutParams.topMargin = 120;
view.setLayoutParams(layoutParams);

Just don't forget to re-assign the modified layout parameters to the view. Otherwise the changes won't be applied.

Upvotes: 0

rerashhh
rerashhh

Reputation: 1711

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

Upvotes: 156

miredirex
miredirex

Reputation: 392

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)

Upvotes: 0

Phillip Kigenyi
Phillip Kigenyi

Reputation: 1489

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);
        }
    });

Upvotes: 2

Related Questions