1

I have three TextViews (Rating, VotesRating and Votes) in one line but only one of them can be visible at the same time. That's why I added a Barrier with ids of those TextViews movie_rating,movie_votes,movie_rating_votes. And then I use that Barrier to add another TextView below it (with description of a movie). But as you can see in the screenshot the description text can be above that Barrier. This is such a buggy library...

enter image description here

For most ViewHolders it works ok, this is crazy

implementation "androidx.constraintlayout:constraintlayout:2.1.2"

Full layout

<com.google.android.material.card.MaterialCardView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="@dimen/margin_0_5x"
    android:background="?android:attr/selectableItemBackground"
    android:clickable="true"
    android:focusable="true"
    android:onClick="@{(v) -> holder.onMovieClicked.invoke(movie)}"
    android:paddingHorizontal="@dimen/margin_2x"
    app:cardBackgroundColor="#2a2a2a">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/movie_image"
            android:layout_width="72dp"
            android:layout_height="0dp"
            android:scaleType="centerCrop"
            app:imageUrl="@{movie.imageUrl}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintDimensionRatio="67:98"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0"
            tools:background="@drawable/placeholder_movie" />

        <TextView
            android:id="@+id/movie_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginHorizontal="@dimen/margin_1x"
            android:layout_marginTop="@dimen/margin_0_25x"
            android:text="@{String.format(@string/movie_title, movie.number, movie.title, movie.year)}"
            android:textColor="@color/colorTextLight"
            android:textSize="16sp"
            app:layout_constrainedWidth="true"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0"
            app:layout_constraintStart_toEndOf="@id/movie_image"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="1. Red Notice (2021)" />

        <TextView
            android:id="@+id/movie_certificate_runtime_genre_rating"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginHorizontal="@dimen/margin_1x"
            android:layout_marginTop="@dimen/margin_0_25x"
            android:text="@{movie.certificateRuntimeGenrerating}"
            android:textColor="#c9c9c9"
            android:textSize="12sp"
            app:layout_constrainedWidth="true"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0"
            app:layout_constraintStart_toEndOf="@id/movie_image"
            app:layout_constraintTop_toBottomOf="@id/movie_title"
            app:visible="@{movie.certificateRuntimeGenrerating != null}"
            tools:text="PG-13 | 118 min | Action, Comedy, Crime" />

        <TextView
            android:id="@+id/movie_rating"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="@dimen/margin_1x"
            android:layout_marginTop="@dimen/margin_0_25x"
            android:layout_marginEnd="@dimen/margin_1x"
            android:drawablePadding="@dimen/margin_0_5x"
            android:gravity="bottom"
            android:text="@{movie.rating}"
            android:textColor="@color/colorTextLight"
            android:textSize="12sp"
            app:drawableStartCompat="@drawable/ic_star_rate"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0"
            app:layout_constraintStart_toEndOf="@id/movie_image"
            app:layout_constraintTop_toBottomOf="@id/movie_certificate_runtime_genre_rating"
            app:visible="@{movie.rating != null &amp;&amp; movie.votes == null}"
            tools:text="7.7" />

        <TextView
            android:id="@+id/movie_rating_votes"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="@dimen/margin_1x"
            android:layout_marginTop="@dimen/margin_0_25x"
            android:layout_marginEnd="@dimen/margin_1x"
            android:drawablePadding="@dimen/margin_0_5x"
            android:gravity="bottom"
            android:text="@{String.format(@string/ph_3_spaced, movie.rating, @string/text_separator, movie.votes)}"
            android:textColor="@color/colorTextLight"
            android:textSize="12sp"
            app:drawableStartCompat="@drawable/ic_star_rate"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0"
            app:layout_constraintStart_toEndOf="@id/movie_rating"
            app:layout_constraintTop_toBottomOf="@id/movie_certificate_runtime_genre_rating"
            app:visible="@{movie.rating != null &amp;&amp; movie.rating != null}"
            tools:text="7.7 | Votes: 20,215" />

        <TextView
            android:id="@+id/movie_votes"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="@dimen/margin_1x"
            android:layout_marginTop="@dimen/margin_0_5x"
            android:layout_marginEnd="@dimen/margin_1x"
            android:text="@{movie.votes}"
            android:textColor="@color/colorTextLight"
            android:textSize="12sp"
            app:layout_constrainedWidth="true"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0"
            app:layout_constraintStart_toEndOf="@id/movie_rating_votes"
            app:layout_constraintTop_toBottomOf="@id/movie_certificate_runtime_genre_rating"
            app:visible="@{movie.votes != null &amp;&amp; movie.rating == null}"
            tools:text="Votes: 20,215" />

        <androidx.constraintlayout.widget.Barrier
            android:id="@+id/movie_rating_votes_bottom_barrier"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:barrierDirection="bottom"
            app:constraint_referenced_ids="movie_rating,movie_rating_votes,movie_votes" />

        <TextView
            android:id="@+id/movie_desc"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginHorizontal="@dimen/margin_1x"
            android:layout_marginTop="@dimen/margin_0_25x"
            android:text="@{movie.description}"
            android:textColor="@color/colorTextDescLight"
            android:textSize="12sp"
            app:layout_constrainedWidth="true"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0"
            app:layout_constraintStart_toEndOf="@id/movie_image"
            app:layout_constraintTop_toBottomOf="@id/movie_rating_votes_bottom_barrier"
            app:visible="@{movie.description != null}"
            tools:text="When a single mom and her two kids arrive in a small town, they begin to discover their connection to the original Ghostbusters and the secret legacy their grandfather left behind." />

        <Space
            android:id="@+id/movie_bottom_space"
            android:layout_width="wrap_content"
            android:layout_height="@dimen/margin_0_5x"
            app:layout_constraintTop_toBottomOf="@id/movie_desc"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</com.google.android.material.card.MaterialCardView>

dimensions

<dimen name="margin_1x">8dp</dimen>
<dimen name="margin_0_25x">2dp</dimen>
<dimen name="margin_0_5x">4dp</dimen>
<dimen name="margin_0_75x">6dp</dimen>
<dimen name="margin_1_5x">12dp</dimen>
<dimen name="margin_2x">16dp</dimen>
<dimen name="margin_2_5x">20dp</dimen>
<dimen name="margin_3x">24dp</dimen>
<dimen name="margin_4x">32dp</dimen>
<dimen name="margin_5x">40dp</dimen>
<dimen name="margin_6x">48dp</dimen>

bindings

@JvmStatic
@BindingAdapter("visible")
fun visible(view: View, visible: Boolean?) {
    view.isVisible = visible == true
}

RecyclerView and ViewHolder classes

class MoviesAdapter(
    private val onMovieClicked: (movie: Movie) -> Unit
) : PagingDataAdapter<Movie, MoviesAdapter.MovieViewHolder>(MovieComparator) {

    override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
        getItem(position)?.let {
            holder.bind(it, position)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        return MovieViewHolder(
            LayoutItemMovieBinding.inflate(layoutInflater, parent, false),
            onMovieClicked
        )
    }

    inner class MovieViewHolder(
        binding: LayoutItemMovieBinding,
        val onMovieClicked: (movie: Movie) -> Unit
    ) : BaseViewHolder<LayoutItemMovieBinding, Movie>(binding) {

        init {
            binding.holder = this
        }

        override fun bind(data: Movie, position: Int) {
            binding.movie = data
        }
    }

    private object MovieComparator : DiffUtil.ItemCallback<Movie>() {
        override fun areItemsTheSame(oldItem: Movie, newItem: Movie) = oldItem.id == newItem.id

        override fun areContentsTheSame(oldItem: Movie, newItem: Movie) = oldItem == newItem
    }
}
user924
  • 8,146
  • 7
  • 57
  • 139
  • Does adding `app:barrierAllowsGoneWidgets="true"` to the barrier helps? How about posting the relevant parts of your adapter? This seems to happen inside a RecyclerView, how is the re-bind happening? Are you setting the other views as "Gone" or "Invisible"? When does this exactly happen? Can you provide a simple sample project where this is reproduced? You cannot possibly post a CL question, post a Layout full of custom stuff (dimensions, colors, assets), and expect SO users to find out what the problem is, all while cursing a library for being buggy. – Martin Marconcini Dec 06 '21 at 14:33
  • You should likely also add `app:layout_constrainedWidth="true"` and the height counterpart to all 3 TextViews, since they can disappear when you use one or the other. (why are you using three widgets is also a bit of a mystery to me), if this is a presentation issue, I don't think the decision should be placed in a viewHolder to decide what to do, and to add more _constrain_ (no pun intended) to the View system trying to measure and lay out a list, real-time, in sub 60ms. If anything, use a custom view and encapsulate all that. But that's a different story. – Martin Marconcini Dec 06 '21 at 14:37
  • Lastly, if the barrier fails to do its job, it's likely that you hide/shown the textView when the layout was already measured/laid out (or in the process of being), so the barrier assumed all three referenced Ids were gone and therefore did its job that way. By the time you made the decision to show one and set its value, it was already too late. This is also unknown for we haven't seen neither your adapter, nor when you set/fetch this data. – Martin Marconcini Dec 06 '21 at 14:39
  • @MartinMarconcini you can see whole code right now – user924 Dec 06 '21 at 14:41
  • @MartinMarconcini `app:layout_constrainedWidth="true"`, yes, I forgot to add for two textviews, fixed it, but still it has nothing with this issue – user924 Dec 06 '21 at 14:42
  • For what is worth, I'm absolutely unfamiliar with DataBinding (and I would like to remain as such). I tried it, used it once, and decided to never touch it again so I don't know *if* and what it has to do with this. – Martin Marconcini Dec 06 '21 at 14:42
  • Did you add `app:layout_constrainedHeight="true"` as well? Since the height is also wrapping. (not sure if this will help) but it will certainly signal CL that it needs to be _a bit more patient_ with those views, and ensure the text is correctly measured (or.. re-measured). – Martin Marconcini Dec 06 '21 at 14:43
  • `app:layout_constrainedHeight="true"` works only if you have both `top` and `bottom` constraints set, in this case there is only `top` constraint – user924 Dec 06 '21 at 14:45
  • `app:barrierAllowsGoneWidgets="true"`. didn't work either, also I even tried to remove `app:visible` for all views, still it wasn't the case... – user924 Dec 06 '21 at 14:47
  • I'm really running out of ideas. Do you have any concrete approximation on *when* this happens? Is it when you scroll? does it "randomly" breaks for some rows but not others? Does it always fail in the same rows given the same data? Anything else you have debugged that you can add? I assure you barriers _do work_ but each use-case is different. – Martin Marconcini Dec 06 '21 at 15:01
  • @MartinMarconcini I just open the screen with list of movies, do nothing, after loading I see that fifth movie has this bug all the time, seems it's not random – user924 Dec 06 '21 at 16:25
  • One thing that appears to be different is that "the 5th movie" does use one of the different textViews (as opposed to the others). Do you have a Visual Espresso test with mocked data to test if this is always the case? (Or can you mock the data so all the movies use the same type -the one that fails- to see if this happens all the time)? Also try the suggested `app:layout_optimizationLevel="none"` – Martin Marconcini Dec 07 '21 at 13:38

2 Answers2

0

It's hard to tell what may be happening without the entire layout, but try the following:

Remove app:layout_constraintBottom_toTopOf="@id/movie_bottom_space" from "movie_desc".

Remove app:layout_constraintBottom_toBottomOf="parent" from movie_bottom_space.

Add app:layout_constraintTop_toBottomOf="@id/movie_desc" to movie_bottom_space.

Make sure the ConstraintLayout has wrap_content as its height.

This will stack the views and let the overall height of the ConstraintLayout fluctuate with its contents.

Cheticamp
  • 61,413
  • 10
  • 78
  • 131
  • `ConstraintLayout has wrap_content` of course, one viewholder would take whole screen if wasn't `wrap_content.` Other stuff doesn't help, I've already tried to remove `movie_bottom_space` view and `app:layout_constraintBottom_toTopOf="@id/movie_bottom_space"`, nothing of that helps – user924 Dec 06 '21 at 14:09
  • I provided full layout – user924 Dec 06 '21 at 14:12
  • @user924 Thanks, that helps. It is a strange problem. I am wondering if it is related to [this reported defect](https://issuetracker.google.com/issues/161156064). The barrier seems to be out of place. – Cheticamp Dec 06 '21 at 15:16
  • @Cheticamp you might be right. It may be late here but I have an app layout container uses RecylcerView with horizontal barrier. I used it for almost 2 years with no problems. Until last week I enabled "R8 Full mode" in gradle and publish the update, then one of my users sent me a screenshot about the text overlapping. The issue is exactly the same to the screenshot. I have no clue to this, I can only suspect this is something related to R8 Full mode. Still looking for more info though. I've never had this issue with R8 normal mode. – Lynch Chen Nov 25 '22 at 18:45
0

This is a variant of this known bug: https://github.com/androidx/constraintlayout/issues/422 You can turn off the optimizer to fix it.

hoford
  • 4,918
  • 2
  • 19
  • 19