ConstraintLayout Chains and Text Ellipsis + Image on the Right

AndroidAndroid StudioAndroid ConstraintlayoutConstraint Layout-Chains

Android Problem Overview


UPDATE JULY 2020:

Added information in the answer below to explain in more detail why/what does constrainedWidth/Height do and when it's applicable to use them.

UPDATE JULY 2018:

If you are using ConstraintLayout 1.1.0, the correct property to use is app:layout_constrainedWidth="true" in place of the old app:layout_constraintWidth_default="wrap" (and the height counterpart). See updated answer.

UPDATE NOVEMBER 2017:

If you are using ConstraintLayout 1.0.0 stable (or above) (1.0.2 at this time), see the updated Answer for a simpler solution without the need to nest layouts.

Original Question:

Using ConstraintLayouts-Beta3 released on Nov 3rd 2016. (source)

I'm trying to do the following:

|                                        |
|<-[TextView]<->[ImageView] -----------> |
|                                        |

Which I have achieved with a layout like so:

  <TextView
      android:id="@+id/textView"
      android:layout_height="wrap_content"
      android:layout_width="wrap_content"

      app:layout_constraintHorizontal_chainStyle="packed"

      app:layout_constraintLeft_toLeftOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      app:layout_constraintRight_toLeftOf="@+id/caret"
      app:layout_constraintHorizontal_bias="0.0"

      android:text="Some Text"
      android:textAlignment="viewStart"
      android:gravity="start" />

  <ImageView
      android:id="@+id/caret"
      android:layout_width="wrap_content"
      android:layout_height="8dp"
      app:layout_constraintDimensionRatio="1:1"

      app:layout_constraintLeft_toRightOf="@+id/textView"
      app:layout_constraintRight_toRightOf="parent"

      app:layout_constraintTop_toTopOf="@+id/textView"
      app:layout_constraintBottom_toBottomOf="@+id/textView"


      app:layout_constraintHorizontal_bias="0.0"

      android:contentDescription=""
      app:srcCompat="@drawable/ic_selection"
      android:layout_marginStart="8dp"/>

This looks ok, but when the text is longer than the available space…

|                                        |
|<-[TextView Larger Than The Space Avail]|
|                                        |

The text view has a style that specifies these:

<item name="android:lines">1</item>
<item name="android:maxLines">1</item>
<item name="android:ellipsize">end</item>

So it should work, but I'm not sure what constraints I need to have the image slide until the right and then stop there and let the text view understand there's no more space.

What am I missing?

Note: If I set the textview's width to the 0dp, it works, but then the image is always on the right (horizontal Bias seems to be ignored for it)

Note2: I have tried this with beta2 as well, in fact, it seems like Beta3 has a bug in the visual editor.

UPDATE: I tried to replicate this in Xcode/AutoLayout:

This is how it looks with a Short Text

Short Text

Now the same layout, I just type a long text in the text view…

Long Text

As you can see the trail (right) constraint for the image view says: you're 8 or more points from the right margin.

It's also pinned to the left to the label (textView).

From what I have just learned from Twitter, this may not be possible on Android's ConstraintLayout at the moment: Source

Android Solutions


Solution 1 - Android

UPDATE JULY 2020: What does constrainedWidth/Height do?

A lot of people kept asking me what exactly does this constrainedWidth/Height do when set to true (defaults to false). I finally have an answer (from a Google employee), so in lieu of clearing up all the doubts people coming to this post keep having, here's what I gathered (some are my words, some are direct quotes from the original Google issue quote.

ConstraintLayout needs to determine the dimension of every view involved, and depending on how said view is constrained, it has to perform different calculations.

Given a view:

  1. if it's a fixed dimension, CL will just use that dimension.
  2. if it's match_parent, CL will use the exact dimension of parent
  3. if it's wrap_content, CL will ask the widget for its size, but then use it as a fixed dimension
  4. if it's 0dp, CL will apply constraints to the dimension
1) Fixed Dimension

This is a view whose width/height are fixed, say 24dp. In this case, CL will simply use that value, no other calculation needed for that widget in regards of sizing.

2) match_parent

I always thought this was not valid for CL, but turns out that it behaves like it used to in previous versions, it grabs the dimensions of the parent and uses that as "fixed". Unlike #1 above, I assume this may be more computationally expensive since CL now needs to ensure the parent dimensions are known to be able to use them here. I don't have proof of this nor a lot of experience, since I always thought this wasn't really valid so never used it.

3) wrap_content

As expected, the view has to determine its "required size", so if it's say an ImageView it will ask the imageView for its dimensions based on its source. After said number is obtained, it's used as a Fixed Size, like #1.

4) 0dp

This is where CL shines, by applying the constrains to each dimension (width, and height), and letting the dimension's value be determined by the outcome of the algorithm.

So why is this needed (constrainedWidth/Height)?

The first thing to understand is that 0dp has a spread and wrap behavior (and percent); in order to wrap, the engine starts with the dimension of the view's wrap_content (#3 above) but waits for constrains to change it if/when needed. Say you use wrap for the width of a text view, and its constrains pin it to the edges of the screen (start/end to parent). Those can be pulling in different directions; the text view will likely want to be small to wrap the text and the constrains will pull the edges of the widget to reach the parent start/end. There's a battle here. (if the text is larger than the space, the battle still exists, but in the opposite direction).

The reason why this attribute exists, is because some widgets (_Like textView), take some shortcuts and when there's a 0dp, they may not always correctly update. It's important to note that LinearLayouts with 0dp + weights did the same thing (hence why this was an issue with LL too); by using constrainedWidth/Height, a widget like a TextView can correctly use 0dp with wrapping behavior when needed; it gives the view a chance to correctly remeasure itself.

This problem mostly manifests when you reuse TexViews (I don't know exactly which other views benefit from this, but I assume anything that has text is prone to have calculation shortcuts/hacks and may need this extra bit of info to correctly trigger a remeasure). Reusing a Widget with Text like a TextView, is where this is mostly needed, think of a RecyclerView where your ViewHolder is in a ConstraintLayout (quite common), when you scroll, the ViewHolder is reused and re-bound to another "data model" and without this attribute, the TextView will/may fail to recalculate its size for the new text that may be coming.

I hope this makes sense.

tl;dr: it's a workaround to fix potential issues with some widgets that fail to recompute their dimensions when reused, notably in a RecyclerView, but most likely not limited to it.

There you have it. :)

UPDATE JULY 2018:

If you are using ConstraintLayout 1.1.0, the correct property to use is app:layout_constrainedWidth="true" in place of the old app:layout_constraintWidth_default="wrap" (and the height counterpart)

UPDATE NOVEMBER 2017

I’m using Constraint Layouts 1.0.2 and I have found a less nested solution using app:layout_constraintWidth_default="wrap" (a property that got introduced in 1.0.0 but the Beta this post was using didn’t have).

Instead of the FrameLayout that contains a LinearLayout you can now remove all that and have it this way:

    <android.support.constraint.ConstraintLayout
      android:id="@+id/new_way_container"
      android:layout_height="wrap_content"
      android:layout_width="0dp" // THIS GUY USES ALL THE WIDTH.
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent">

      <TextView
        android:ellipsize="end"
        android:id="@+id/some_text"
        android:layout_height="wrap_content"
        android:layout_width="0dp" //NO WRAP CONTENT, USE CONSTRAINTS
        android:lines="1"
        android:maxLines="1"
        app:layout_constraintEnd_toStartOf="@+id/disclosure_arrow"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintHorizontal_chainStyle="packed" //CHAIN IT for biasing.
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintWidth_default="wrap" /> //THIS IS THE KEY THAT WILL CAUSE THIS TO WORK

      <ImageView
        android:id="@+id/disclosure_arrow"
        android:layout_height="wrap_content"
        android:layout_width="10dp"
        app:layout_constraintBottom_toTopOf="@id/some_text"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/some_text"
        app:layout_constraintTop_toBottomOf="@id/some_text"
        app:srcCompat="@drawable/your_vector_image" />
    </android.support.constraint.ConstraintLayout>

This effectively does exactly what I want, without hacks or guidelines or hardcoded sizes.

The TextView will use the size provided by the Constraints (which under normal circumstances would mean it would either be wrong or will grow beyond the ‘parent’), but thanks to the new attribute, those constraints are allowed to be bent/broken if the content is smaller/larger.

I have to say it works much better than iOS Priorities. (At least it’s a lot easier to grasp to me). Thumbs up for Google on this one :)

OLD ANSWER (in case you still need it).

Based upon Nicolas Roard's answer, I was going to create a custom container that would basically calculate the available space, and programmatically set the maxWidth on the TextView. Instead of adding another class, unit test, possible set of bugs, etc., to the project, I tried a slightly less efficient method of nesting a couple of layouts; considering we've been nesting layouts since the age of dawn and that this is not going to be on any scrolling list view or moving too much (or at all) and that I am using ConstraintLayouts to flatten most of the hierarchy (like never before!), then I don't think a little nesting until this is better supported is that bad.

So what I did was basically, use a FrameLayout, which is by design optimized (or thought) to have one child (tho it can contain more). This FrameLayout is the one that has the ConstraintLayout rules applied like so:

  <FrameLayout
      android:id="@+id/hostTextWithCaretContainer"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      app:layout_constraintLeft_toLeftOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      app:layout_constraintRight_toRightOf="parent">

      <!-- MY CONTENT GOES HERE -->

  </FrameLayout>

So in my real app, this FrameLayout is inside another ConstraintLayout that has an icon to its left and some other stuff, but for the sake of this example, imagine you have to "pin" the left/right of this FrameLayout to whatever space you want to occupy. In this example you can see I'm using parent in all constraints, but there could be other widgets left and right of this FrameLayout; thanks to ConstraintLayout's magic, this will occupy all that available space.

Now here comes the 2nd part of the trick… since ConstraintLayout guarantees that the FrameLayout will use "all the space" we have and never more (or less), I can now use a LinearLayout inside… like so…

     <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

      <TextView
          android:id="@+id/textView"
          android:layout_height="wrap_content"
          android:layout_width="0dp"
          tools:text="Some Text"
          android:text="Some Text"
          android:textAlignment="viewStart"
          android:layout_gravity="center_vertical"
          android:gravity="start"
          android:ellipsize="end"
          android:maxLines="1"
          android:layout_weight="1"/>

      <ImageView
          android:id="@+id/caret"
          android:layout_width="8dp"
          android:layout_height="8dp"
          app:srcCompat="@drawable/ic_selection"
          android:contentDescription=""
          android:layout_gravity="center_vertical"
          android:layout_marginStart="8dp"
          android:layout_marginEnd="8dp" />

    </LinearLayout>

Astute readers will notice that the LinearLayout has wrap_content in its width, that's very important for then the child TextView can have a width of 0dp and a weight of 1, meaning it will take all available free space after all the other widgets have calculated their width.

In this particular case the other child (ImageView) caret has no weight specified and a fixed width, therefore the TextView doesn't have to share/split the free space with anybody else and it can take it all (but only free space, remember its width is 0dp).

This less efficient approach, effectively achieves exactly what I wanted, albeit with less ConstraintLayout Magic if you will.

On the plus side, I didn't have to create a custom view, perform math and issue a requestLayout() after all my math was done; this less efficient approach will/should scale and until ConstraintLayout offers a valid alternative, it may be enough as it is.

Shoutout to the Google engineer(s) who replied on social media and eventually took the time to think about this. Maybe in the future, when they are writing tasks and story points about ConstraintLayout 1.1, they remember this and come up with a good solution

Solution 2 - Android

In my case Update from July 2018 from Martin won't work: > If you are using ConstraintLayout 1.1.0, the correct property to use is > app:layout_constrainedWidth="true" in place of the old > app:layout_constraintWidth_default="wrap" (and the height counterpart)

I have to use android:width="wrap_content" for text view with app:layout_constrainedWidth="true". android:layout_width="0dp" (match_constraint) makes text view stretched for shorter strings in my case.

Another possibility to achieve the same result would be to use android:layout_width="0dp" with flag app:layout_constraintWidth_max="wrap" instead.

More about flags for constraint layout can be found in doc: https://developer.android.com/reference/android/support/constraint/ConstraintLayout

Solution 3 - Android

Actually, here is how you could do it for TextViews -- use android:maxWidth:

<TextView
    android:id="@+id/textView7"
    android:maxWidth="200dp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:maxLines="1"
    android:ellipsize="end"
    android:text="TextViewTextViewTextViewTextViewTextViewTextViewTextViewTextViewTextView"
    tools:layout_editor_absoluteY="72dp"
    android:layout_marginStart="16dp"
    app:layout_constraintLeft_toLeftOf="parent"
    android:layout_marginLeft="16dp" />

<Button
    android:id="@+id/button20"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Button"
    tools:layout_editor_absoluteY="56dp"
    android:layout_marginStart="8dp"
    app:layout_constraintLeft_toRightOf="@+id/textView7"
    android:layout_marginLeft="8dp" />

enter image description here

It's not exactly what you asked (you need to know and set the size to limit width) but that might help you until we support that in ConstraintLayout itself.

Solution 4 - Android

You can use relative layout in constraint layout with negative margin for ImageView. Please attention to android:layout_marginRight="26dp" and android:layout_marginLeft="-26dp"

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <RelativeLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">
    
        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_gravity="center_vertical"
            android:layout_marginRight="26dp"
            android:ellipsize="end"
            android:gravity="start"
            android:maxLines="1"
            android:text="Some text" />
    
        <ImageView
            android:id="@+id/imageView"
            android:layout_width="24dp"
            android:layout_height="24dp"
            android:layout_centerVertical="true"
            android:layout_marginLeft="-26dp"
            android:layout_toRightOf="@+id/textView"
            android:contentDescription=""
            android:src="@drawable/ic_browser" />
    </RelativeLayout>

</android.support.constraint.ConstraintLayout>

enter image description here

When text long: enter image description here

Solution 5 - Android

This answer is inspired by Martin Marconcini's answer (https://stackoverflow.com/a/40491128/1151701). In order to reduce the hierarchy, you can replace the constraint layout with a simple view.

      <TextView
        android:ellipsize="end"
        android:id="@+id/some_text"
        android:layout_height="wrap_content"
        android:layout_width="0dp"
        android:lines="1"
        android:maxLines="1"
        app:layout_constraintEnd_toStartOf="@+id/disclosure_arrow"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintHorizontal_chainStyle="packed"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintWidth_default="wrap" /> 

      <ImageView
        android:id="@+id/disclosure_arrow"
        android:layout_height="wrap_content"
        android:layout_width="10dp"
        app:layout_constraintBottom_toTopOf="@id/some_text"
        app:layout_constraintEnd_toEndOf="@id/view_guide"
        app:layout_constraintStart_toEndOf="@id/some_text"
        app:layout_constraintTop_toBottomOf="@id/some_text"
        app:srcCompat="@drawable/your_vector_image" />

      <View
        android:id="@+id/view_guide"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

btw, I'm using ConstraintLayout androidx.constraintlayout:constraintlayout:2.0.0-alpha3 but app:layout_constrainedWidth="true" is worked as expected in this case. So I've used app:layout_constraintWidth_default="wrap" here

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMartin MarconciniView Question on Stackoverflow
Solution 1 - AndroidMartin MarconciniView Answer on Stackoverflow
Solution 2 - AndroidMDikkiiView Answer on Stackoverflow
Solution 3 - AndroidNicolas RoardView Answer on Stackoverflow
Solution 4 - AndroidDmitryView Answer on Stackoverflow
Solution 5 - AndroidAli MehrpourView Answer on Stackoverflow