ScrollView
ScrollView
ScrollView is a focus-aware container for one page-like view tree that is larger than its viewport. It renders real child views, then translates an internal scrollable group as focus changes or as web mouse/touch input scrolls the surface.
Use ScrollView for long detail screens, settings pages, and forms that span several viewport heights. Use Repeater for a short, bounded run of similar controls. For data-driven or frequently changing lists, rails, grids, EPGs, and season or episode lists, use CollectionView because it virtualizes and pools rows and cells.
Do not embed ScrollView, ScrollContainer, or Repeater inside a CollectionView cell, custom cell, or supplementary/header view. Express that content as CollectionView rows and cells instead.
Example
ScrollerRig
Source: hosanna-ui-samples-public/src/hosanna-ui-examples/rigs/controls/ScrollerRig.ts@view('ScrollerRig')
export class ScrollerRigView extends BaseExampleScreenView<ScrollerRigState> {
protected override getViews(): ViewStruct<ViewState>[] {
const isMobile = this.isMobileRigLayout();
const contentWidth = isMobile ? this.getRigContentWidth() : 1920;
const heroHeight = isMobile ? 220 : 500;
const tabWidth = isMobile ? contentWidth : 1820;
const sideSpacer = isMobile ? 20 : 200;
const cellSize = isMobile ? Math.min(110, Math.round((contentWidth - sideSpacer - 40) / 3)) : 400;
const sectionSpacer = isMobile ? 40 : 200;
return [
ScrollView([
VGroup([
Rectangle({ id: 'hero', color: '#00ff00', width: contentWidth, height: heroHeight }),
Spacer({ height: isMobile ? 40 : 100 }),
Rectangle({ id: 'tab', color: '#00fff0', width: tabWidth, height: 100 }),
HGroup([
Button({ text: 'Episodes' }),
Button({ text: 'Extras' }),
Button({ text: 'Details' }),
])
.isInitialFocus()
.id('tabBar'),
Spacer({ height: sectionSpacer }),
HGroup([
Rectangle({ id: 'episodesList', color: '#ff0000', width: cellSize, height: cellSize, canReceiveFocus: true }),
Spacer({ width: sideSpacer }),
VGroup([
HGroup([
Rectangle({ color: '#ff00ff', width: cellSize, height: cellSize, canReceiveFocus: true }),
Spacer({ width: 20 }),
Rectangle({ color: '#ff00ff', width: cellSize, height: cellSize, canReceiveFocus: true }),
Spacer({ width: 20 }),
]).id('collectionViewRow1'),
HGroup([
Rectangle({ color: '#ff00ff', width: cellSize, height: cellSize, canReceiveFocus: true }),
Spacer({ width: 20 }),
Rectangle({ color: '#ff00ff', width: cellSize, height: cellSize, canReceiveFocus: true }),
Spacer({ width: 20 }),
]).id('collectionViewRow2'),
HGroup([
Rectangle({ color: '#ff00ff', width: cellSize, height: cellSize, canReceiveFocus: true }),
Spacer({ width: 20 }),
Rectangle({ color: '#ff00ff', width: cellSize, height: cellSize, canReceiveFocus: true }),
Spacer({ width: 20 }),
]).id('collectionViewRow3')
]).itemSpacing(20)
// .focusMap({
// left: 'episodesList'
// })
.id('collectionView')
])
])
])
.viewportWidth(contentWidth)
.translation([this.getRigLeftOffset(0), 0])
.verticalAlignment(ScrollVerticalAlignment.Top)
.horizontalAlignment(ScrollHorizontalAlignment.None)
// .onFocusedChildChange((event: FocusChildChangeEvent) => {
// this.focusId = event.childId;
// console.info('SCROLLABLE CHILD FOCUS CHANGE focus id');
// })
];
}
}Set .canReceiveFocus(true) on scrollable children and .isInitialFocus(true) on the first item you want focused. Auto-scroll follows the focused child.
Common API
viewportWidthandviewportHeight: explicit visible area. If omitted, ScrollView falls back to its own size, a clipping parent, calculated size, or the default design surface.autoScroll: whentrue, focused child changes move the scroll position.verticalAlignment:Top,Center,Bottom,Floating,None,Predefined,CustomPosition, orCustomCallback.horizontalAlignment:Left,Center,Right,Floating,None,Predefined,CustomPosition, orCustomCallback.customPositionXandcustomPositionY: offsets used by custom-position alignment.customVerticalPositionCallbackandcustomHorizontalPositionCallback: calculate positions from the focus-change event.scrollToChild: state field that animates a specific child into view.animationDuration: duration for focus-driven and programmatic scroll animations.
Programmatic Scrolling
const scrollView = this.getSubView<ScrollViewView>('settingsScroll');
scrollView?.scrollTo([0, -480], 250);
scrollView?.animateToChild(targetChild, 250);
scrollView?.reset();
scrollTo(position, duration) expects a scroll translation in design coordinates. animateToChild(child, duration) computes the translation needed to bring that child into view.
Mouse and Touch
On web, ScrollView exposes an external scroll API used by input adapters:
- Mouse wheel calls into a scroll controller and updates the scroll offset.
- Touch drag starts a drag session and applies momentum after release.
- Scroll extents can be hard-stopped or overscrolled by the controller.
- Browser coordinates are normalized into Hosanna design coordinates before hit testing.
Application code usually does not call setExternalScrollOffset; it should configure the ScrollView's viewport and focus behavior, then let adapters drive pointer scrolling.
When CollectionView is Better
CollectionView is not limited to catalog screens. It can also model detail screens and form-like flows when sections are data-driven or can grow. Its fragment-backed cells make presentation easier to reuse and update independently than bespoke nested scroll trees.