0

I have a ConstraintLayout. For the purposes of this example, we can have three views inside.

<android.support.constraint.ConstraintLayout ...>

   <TextView
    android:id="@+id/text"
    .../>

   <TextView
    android:id="@+id/value"
    .../>

   <View
    android:id="@+id/bar"
    android:layout_width="0dp"
    android:layout_height="8dp"
    android:background="@color/bar_color"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="@+id/text"
    app:layout_constraintEnd_toStartOf="@id/value"
    />
</android.support.constraint.ConstraintLayout>

At runtime, I want to set the width of bar to some value between the distance between text and value.

I already have a value between 0 and 100 which is a percentage of the distance between the two text views.

I've tried two methods so far but have gotten stuck.

Attempt 1

In order to do this, I feel I need the actual distance between text and value and a method to set the bar's width correctly.

I'm able to set the width of bar by doing the following:

val params: ConstraintLayout.LayoutParams = percentageBar.layoutParams as LayoutParams

params.matchConstraintMaxWidth = 300  // should be actual value rather than 300
percentageBar.layoutParams = params

This sets the width of the bar fine. But I need some way of figuring out what the actual number should be rather than 300. Something like (percent value * text-value-distance / 100).

Attempt 2

From looking at other answers on SO, I found out about the percent constraint.

...

<View
        android:id="@+id/bar"
        android:layout_width="0dp"
        android:layout_height="8dp"
        android:background="@color/bar_color"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/value"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="@id/text"
        app:layout_constraintWidth_default="percent"
        app:layout_constraintWidth_percent="1.0"/>

...

Then, in the code, I can just do params.matchConstraintPercentWidth = item.percentageOfMaxValue.toFloat()

The problem with this is that when the percent value is set high, it's taking the percentage of the parent and goes past the start of value.

Am I heading in the right direction?

I hope I've described the problem clearly enough.

Thanks in advance.

// Update

I've pushed a simplified version with what resembles the problem I'm having. BarProblem on GitHub

You can actually see the problem from the design tab of the layout editor.

// Update 2

Problem solved. GitHub repo now contains the code for the solution.

the_new_mr
  • 3,476
  • 5
  • 42
  • 57
  • "But I need some way of figuring out what the actual number should be rather than 300" -- you can find out where the `text` and `value` widgets are on the screen and do the math. For example, for a LTR layout, `getX()+getWidth()` should tell you the X position of the right edge of `text`, and `getX()` should tell you the X position of `value`. – CommonsWare Apr 24 '19 at 22:58
  • Yes, I tried this. But I'm getting 0s for all these values. I'm guessing because it's a constraint layout and the values are not actually set? – the_new_mr Apr 25 '19 at 00:27
  • 1
    You need to wait until the `ConstraintLayout` is laid out, such as via `addOnGlobalLayoutListener()`: https://stackoverflow.com/a/14298483/115145 – CommonsWare Apr 25 '19 at 10:55
  • Awesome! That worked a treat! Thanks :) N.B. I removed the listener through a reference to itself to prevent an infinite loop. Add it as an answer and I'll most certainly accept it. – the_new_mr Apr 25 '19 at 19:38
  • I recommend that you post your own answer to the question, showing your solution! BTW, if you are using Kotlin, I think one of the `-ktx` libraries offers a `doOnLayout` extension function that handles the one-time layout listener pattern for you, so you don't need to unregister your listener. – CommonsWare Apr 25 '19 at 19:45
  • Alrighty. Will do. And I am so I'll check that out too. Thanks again! – the_new_mr Apr 25 '19 at 20:22

2 Answers2

0

I good solution for your case is guidelines. Those helpers are anchors that won’t be displayed in your app, they are like one line of a grid above your layout and can be used to attach or constraint your widgets to it.

  • Thanks. I tried those but no dice unfortunately. The problem is that setting the width of the bar as a percentage of the parent makes it override the constraints and it pushes out past the item it is supposed to be constrained to. – the_new_mr Apr 23 '19 at 09:24
  • app:layout_constraintWidth_max="300dp" Try to avoid setting up XML limits through the code. –  Apr 23 '19 at 10:19
  • The problem is that I don't know what the limit should be as it's dynamic. – the_new_mr Apr 24 '19 at 02:23
0

The key was that we need to wait until the ConstraintLayout is laid out. We can do this by using a ViewTreeObserver.OnGlobalLayoutListener.

Example solution:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        text.text = "Text"
        value.text = "Value"

        val globalLayoutListener = MainActivityGlobalListener(item_view, text, value, percentage_bar)

        item_view.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
    }
}

class MainActivityGlobalListener(private val itemView: View,
                                 private val text: View,
                                 private val value: View,
                                 private val bar: View) : ViewTreeObserver.OnGlobalLayoutListener {

    override fun onGlobalLayout() {
        Log.i("yoyo", "in onGlobalLayout")

        val width = value.x - (text.x)
        Log.i("yoyo", "Width: $width")

        val params: ConstraintLayout.LayoutParams = bar.layoutParams as ConstraintLayout.LayoutParams

        params.matchConstraintMaxWidth = (1.0 * width).toInt()  // using 0.1 here - this value is dynamic in real life scenario
        bar.layoutParams = params

        // required to prevent infinite loop
        itemView.viewTreeObserver.removeOnGlobalLayoutListener(this)
    }
}

Note the need to remove the listener on the last line to prevent an infinite loop from occurring (listen for layout -> set bar width -> triggers re-layout etc)

If using Android KTX, we can simplify to the following:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        text.text = "Text"
        value.text = "Value"

        item_view.doOnLayout {
            Log.i("yoyo", "in onGlobalLayout")

            val width = value.x - (text.x)
            Log.i("yoyo", "Width: $width")

            val params: ConstraintLayout.LayoutParams = percentage_bar.layoutParams as ConstraintLayout.LayoutParams

            params.matchConstraintMaxWidth = (1.0 * width).toInt()  // using 0.1 here - this value is dynamic in real life scenario
            percentage_bar.layoutParams = params
        }
    }

Thanks to CommonsWare for the answer!

GitHub repo with code

the_new_mr
  • 3,476
  • 5
  • 42
  • 57