Theory
The rules nobody could read
The Garba Night details screen grew: poster, description, timing, dress code, 12 rules. On your emulator it fit. On a classmate's smaller phone, everything after rule 4 simply did not exist: no error, no hint, just clipped.
Android layouts do not scroll by themselves; content past the screen's edge is silently invisible. The fix is a wrapper whose whole job is scrolling: ScrollView: plus its sideways twin, and one strict law about children.
Theory
The two scrollers, formally
ScrollView scrolls its content vertically: the reading direction: long forms, detail pages, rules.
HorizontalScrollView is a separate widget for sideways travel: photo strips, sponsor logos, a row of filter chips.
One widget, one axis: vertical ScrollView does not scroll sideways, and there is no both-axes scroller in the classic toolkit (nest one inside the other for the rare map-like need).
Sizing habit: the scroller gets match_parent height; its content wraps: wrap_content.
Formula
The one-child law
A ScrollView (and its horizontal twin) may hold exactly ONE direct child.
Two TextViews placed directly inside it will not even inflate: IllegalStateException: ScrollView can host only one direct child.
The pattern, always: put ONE layout inside (a vertical LinearLayout for ScrollView, a horizontal one for HorizontalScrollView), and stack everything inside THAT. One wrapper, one child, unlimited grandchildren.
Practical
event_details.xml: the scrolling page + sponsor strip
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- the ONE direct child -->
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView android:id="@+id/poster"
android:layout_width="match_parent"
android:layout_height="220dp" />
<TextView android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<!-- sideways strip inside the vertical page -->
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<!-- sponsor ImageViews line up here -->
</LinearLayout>
</HorizontalScrollView>
<TextView android:id="@+id/rules"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</ScrollView>
This example runs in Gri-Learn on the web, where you can edit it and see the output.
Quiz
A layout places 2 TextViews directly inside a ScrollView. What happens when the screen opens?
- Both show; the ScrollView stacks them vertically like a LinearLayout
- The app crashes at inflate time: ScrollView can host only one direct child
- Only the first TextView shows; the second is ignored with a warning
- They overlap, both anchored to the top
Show the answer
The app crashes at inflate time: ScrollView can host only one direct child
The one-child law is enforced with an IllegalStateException the moment the XML inflates: the screen never appears. ScrollView is a scrolling WRAPPER, not a stacking layout: arranging children is a LinearLayout's job, which is exactly why the canonical structure is ScrollView holding one LinearLayout holding everything. Option A grants ScrollView abilities it deliberately lacks; options C and D describe mercies (silent ignoring, overlap) that other containers show but ScrollView refuses. One wrapper, one child: the grandchildren are unlimited.
Think first
The scroller that ate the ListView
A classmate wraps last lesson's event ListView inside the details ScrollView so the whole page scrolls together. Two distinct things go wrong. Reason them out from how each widget scrolls, then tap.
Show the answer
Gesture war: both widgets claim vertical swipes; the touch goes to one and the other never scrolls properly: the list becomes a stunted strip. Recycling dies: inside a ScrollView, the ListView is measured to its FULL content height (the scroller must know its child's size), so all 10000 rows inflate at once: the thali system from the ListView lesson collapses into 10000 plates. The rule: a ListView scrolls ITSELF, never wrapped in a scroller; long mixed pages either put extra content in header/footer views of the list, or avoid the mix.
Watch out
Scroll traps
The invisible clipping: no scroller means content past the screen edge silently vanishes: test on a SMALL screen, not just your emulator.
Wrong axis: text in a HorizontalScrollView will not wrap and walk off sideways; reading content wants the vertical one.
Short content, ugly gap: a ScrollView whose content is shorter than the screen leaves dead space; android:fillViewport="true" stretches the child to fill.
Scroller inside scroller (same axis): the ListView story again: any two same-axis scrollers fight.
Theory
Scrolling is a wrapper, not a property
The design insight worth keeping: in Android, scrolling is not a checkbox on a layout: it is a CONTAINER you deliberately add, one axis at a time, around exactly one child. (Flutter, waiting in Unit 3, makes the same move with SingleChildScrollView: the name confesses the same law.) Next lesson the search box learns to think: AutoCompleteTextView returns from BCA305-02 for a deeper second pass, with TextWatcher's full machinery.
Summary
Key takeaways
- ScrollView scrolls vertically; HorizontalScrollView is the separate sideways widget: one axis each.
- Each may hold exactly ONE direct child: wrap content in a single LinearLayout inside it.
- Two direct children = IllegalStateException at inflate: the screen never opens.
- Never wrap a ListView in a ScrollView: gestures fight and recycling breaks.
- fillViewport="true" stretches short content to fill the screen.
- Content past the screen edge without a scroller is silently clipped: test small screens.
- Memory hook: one wrapper, one axis, one child.