1

I was under the impression that the value returned by View.getWidth() includes padding in it. I don't think I'm hallucinating things - a common line of code that Android developers write is something like myView.run { width - (paddingLeft + paddingRight) }, whose goal is to get the width of a View without the padding (the "base width", so to speak).

This is something I personally have done at my previous jobs, and is also corroborated by numerous other examples online (ref: SourceGraph code search).

However, after trying to use this pattern today, I was shocked to see that the value returned View.getWidth() does not include padding in it. To illustrate, I ran two experiments:

Experiment 1: programmatically added padding

activity_main.xml:

<HorizontalScrollView
 android:id="@+id/container"
 android:background="#ff00c0cb"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:layout_marginBottom="8dp"
 android:clipToPadding="false"
 android:clipChildren="false"
 app:layout_constraintStart_toStartOf="parent"
 app:layout_constraintEnd_toEndOf="parent"
 app:layout_constraintBottom_toBottomOf="parent">
 <LinearLayout
 android:id="@+id/inner"
 android:background="#ffffc0cb"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="horizontal" />
</HorizontalScrollView>

MainActivity.kt:

myList.forEach { s ->
 val squareItemView = SquareItemView(this)
 // irrelevant boilerplate omitted
 innerContents.addView(squareItemView)
 innerContents.post {
 val baseWidth = innerContents.run { width - (paddingLeft + paddingRight) }
 Log.i(
 "TAG",
 "innerContents.measuredWidth=${innerContents.measuredWidth}, " +
 "innerContents.width=${innerContents.width}, " +
 "baseWidth=${baseWidth}, " +
 "innerContents.paddingLeft=${innerContents.paddingLeft}"
 )
 Log.i("TAG", "will add 10px px on either side")
 innerContents.apply { setPadding(paddingLeft + 10, paddingTop, paddingRight + 10, paddingBottom) }
 }
}

Here is the log output:

23:42:49.923 I innerContents.measuredWidth=693, innerContents.width=693, baseWidth=693, innerContents.paddingLeft=0
23:42:49.923 I will add 10px px on either side
23:42:49.924 I innerContents.measuredWidth=693, innerContents.width=693, baseWidth=673, innerContents.paddingLeft=10
23:42:49.924 I will add 10px px on either side
23:42:49.924 I innerContents.measuredWidth=693, innerContents.width=693, baseWidth=653, innerContents.paddingLeft=20
23:42:49.924 I will add 10px px on either side

In this demo, notice that each time we add 10px of padding to left and right:

  1. the plain getMeasuredWidth and getWidth numbers are identical and remain constant
  2. the "base width" calculation decreases by the amount of the padding

Experiment 2: hard-coded padding (less variables to think about)

activity_main.xml:

<TextView
 android:id="@+id/texty"
 android:layout_width="300dp"
 android:layout_height="300dp"
 android:padding="10dp" (note that I tested including and omitting this attribute)
 android:layout_gravity="top"
 android:text="Texty (300x300)"
 android:background="#ff00f0fe" />

MainActivity.kt:

findViewById<View>(R.id.texty).let {
 it.doOnNextLayout {
 Log.i("TAG", "texty.height=${it.height}, texty.paddingTop=${it.paddingTop}")
 Log.i("TAG", "texty.width=${it.width}, texty.paddingLeft=${it.paddingLeft}")
 }
}

Log output when the padding is omitted:

16:49:57.414 I texty.height=788, texty.paddingTop=0
16:49:57.414 I texty.width=788, texty.paddingLeft=0

Log output when the padding is set to 10dp:

16:50:39.012 I texty.height=788, texty.paddingTop=26
16:50:39.012 I texty.width=788, texty.paddingLeft=26

This result is at odds with what we expect to see. Like I said, I don't remember this being the case a few months (weeks?) ago, but apparently it is so. Did Google change this behaviour on the library level or even the OS level? What's going on? :(

asked Nov 27, 2025 at 7:57
6
  • Can you show the code of this custom SquareItemView? Is there a possibility that it's somehow overriding the method with a custom implementation? Commented Nov 27, 2025 at 10:01
  • @Edric It's nothing special (literally just a blue square for debug purposes), and shouldn't have any effect on the ViewGroup that it sits in. Commented Nov 27, 2025 at 10:14
  • 3
    When is "Width" and measured width supposed to update? After adding padding you should wait for layout cycle before printing the values Commented Nov 27, 2025 at 12:58
  • @Pawel Does View.post() not ensure that it happens in that order? Since I'm doing it at the end of each loop, I believe the order of operations should be 1. measure, layout draw 2. padding callback #1 3. measure, layout, draw 4. padding callback #2 5. etc... Commented Nov 28, 2025 at 0:30
  • @Pawel I have included another demo app that does not involve programmatically changing the layout params. Commented Nov 28, 2025 at 0:57

1 Answer 1

1

The answer is yes, View.getHeight/getWidth includes padding.

The reason you are seeing the behaviour you're seeing is:

Experiment 1: Race condition

When you call setPadding(), the paddingLeft/paddingRight attributes immediately get updated, but getMeasuredWidth() does NOT get updated until after View.measure() is called, and getWidth() does NOT get updated until after View.layout() is called. The fact that you used View.post() doesn't really matter; the method just guarantees that the provided callback executes on the UI thread, it does not guarantee that measure-layout-draw has occurred before executing the callback.

It seems to me that what you are really trying to do is ensure that measure-layout-draw occurs after you set padding the nth time, and before you set padding the n+1th time - i.e. the order of operations is an alternating pattern of setPadding, measure-layout-draw, setPadding, measure-layout-draw, setPadding, ....

You have two options:

Option 1: manually call measure and layout

This is the simplest way that requires the least amount of work. Depending on the specific way your Views and their parent ViewGroups are set up, you might be able to get away with refactoring your code to use getMeasuredWidth() instead, in which case you only need to call View.measure().

Option 2: block on the natural timing of measure-layout-draw before executing the callbacks in sequence

Mind you, this requires you to manually keep track of concurrency. The following code is sloppy, most likely leaks memory, etc. - this is a proof-of-concept, bare minimum concurrency management you need to listen to the natural timing of the measure-layout-draw cycle. (If anyone here is better at RxJava than I am, then please feel free to improve this!)

 private var innerPtr: View? = null
 private var innerLayoutChangeListener: View.OnLayoutChangeListener? = null
 private val layoutChangesObservable = PublishSubject.create<Int>()
 private var layoutCounter = 0
 private val semaphore = Semaphore(1, /* fair= */ true)
 private val orders = PublishSubject.create<() -> Unit>()
 private var orderCounter = 0
 private var omnilistener: Disposable? = null
 init {
 omnilistener = orders.observeOn(Schedulers.io()).subscribe { callback ->
 Log.d(TAG, "received order")
 semaphore.acquire()
 layoutChangesObservable.take(1)
 .observeOn(AndroidSchedulers.mainThread())
 .subscribe { nthLayoutChange ->
 Log.d(TAG, "nth layout change: $nthLayoutChange")
 try {
 callback.invoke()
 } finally {
 semaphore.release()
 }
 Log.d(TAG, "processed order ${orderCounter++}")
 }
 }
 }
 fun tweakPadding(innerContents: View) {
 if (innerContents != innerPtr) {
 Log.d("TAG", "new button holder ptr")
 innerPtr?.removeOnLayoutChangeListener(innerLayoutChangeListener)
 innerLayoutChangeListener = View.OnLayoutChangeListener { _, left, _, right, _, oldLeft, _, oldRight, _ ->
 Log.d("TAG", "layout changed")
 layoutChangesObservable.onNext(layoutCounter++)
 }
 innerContents.addOnLayoutChangeListener(innerLayoutChangeListener)
 innerPtr = innerContents
 }
 // It is heavily implied that this triggers a layout change. I don't know what will happen if the following block does not trigger a layout change.
 orders.onNext {
 val baseWidth = innerContents.run { width - (paddingLeft + paddingRight) }
 Log.i(
 "TAG",
 "innerContents.measuredWidth=${innerContents.measuredWidth}, " +
 "innerContents.width=${innerContents.width}, " +
 "baseWidth=${baseWidth}, " +
 "innerContents.paddingLeft=${innerContents.paddingLeft}"
 )
 Log.i("TAG", "will add 10px px on either side")
 innerContents.apply { setPadding(paddingLeft + 10, paddingTop, paddingRight + 10, paddingBottom) }
 }
 }

While Option 2 is technically the most correct solution, if you had to ask me, I'd probably just go with Option 1 due to the far-superior readability and maintainability, even if it means you have to refactor your view hierarchy.

Experiment 2: Width is constant

This experiment was flawed and probably done in a sleep-deprived stupor. The question you ask really only makes sense when the width is wrap_content.

answered Nov 28, 2025 at 13:24
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.