B4A Library [B4X][B4A] B4XDaisyPageScroll - Automate Your ScrollView Heights & Layout Boilerplate!

Hello Fam!

If you are tired of manually calculating the total height of your ScrollViews every time you add a new view or rotate the screen, I want to share a solution: B4XDaisyPageScroll.
B4XDaisyPageScroll is a reusable custom view that acts as a page container. It encapsulates a standard ScrollView and a transparent host panel to completely centralize all the layout boilerplate code you normally write.

Key Features:
  • AutoFit Magic: Automatically calculates content boundaries based on the components you add, and dynamically updates the scroll height.
  • Built-in Properties: Easily set standard spacing with PagePadding and YGap directly from the designer or in code.
  • Helper Methods: Includes built-in methods like AddSectionTitle and AddDivider to quickly generate standard UI elements while automatically tracking the Y-coordinate for your next view.
  • Easy Resizing: Automatically handles layout stretching. Just pass the new Width and Height to Base_Resize() during the B4XPage_Resize event.

1782327092656.png
1782327048312.png
1782327119908.png


Layout Example: The "Nav Scroll Dock" The component makes complex responsive layouts a breeze. For example, you can create a layout with three zones:
  1. A Navbar pinned to the top (added to Root).
  2. A Dock pinned to the bottom (added at Root.Height - DOCK_H).
  3. The B4XDaisyPageScroll component layered directly between them.
Because the ScrollView calculates its own height, all you do is inset the scroll container by the navbar height and dock height, giving you a perfect scrollable middle zone that respects screen rotation.




Let me know what you think or if you have any questions!
 

Mashiane

Expert
Licensed User
Longtime User
Lets take a peek at the NavBar, Scroll, Dock Source...

B4X:
B4A=true
Group=Default Group\Pages
ModulesStructureVersion=1
Type=Class
Version=13.4
@EndOfDesignText@
#IgnoreWarnings: 12
'/**
' * @class B4XPageNavScrollDock
' * @description
' *  Demonstrates a full-screen layout with three fixed zones:
' *    - A Navbar pinned to the top of the screen.
' *    - A scrollable PageScroll area that fills the space between them.
' *    - A Dock pinned to the bottom of the screen.
' *  The Navbar and Dock are added directly to Root so they never scroll.
' *  The PageScroll is inset (top = navbar height, bottom = dock height) so
' *  its content area is exactly the visible space between the two bars.
' */
#Region Variables
' ELI15: Class_Globals is our page's inventory box. 
' Any variables declared here exist as long as the page is in memory and can be accessed/modified
' by any Sub (method) on this page. Local variables inside a Sub vanish when that Sub ends.
' ELI15: Class_Globals is our page's inventory box.
' Any variables declared here exist as long as the page is in memory and can be accessed/modified
' by any Sub (method) on this page. Local variables inside a Sub vanish when that Sub ends.
Sub Class_Globals
    ' ELI15: Root is the main parent panel (canvas) that represents this entire screen.
    Private Root As B4XView
    
    ' ELI15: xui is a translation helper that lets us write color and drawing code once, 
    ' and have it compile natively on Android (B4A), iOS (B4i), and Desktop (B4J).
    Private xui As XUI
    ' ELI15: navbar is the header component pinned at the very top of our screen.
    Private navbar As B4XDaisyNavbar
    ' ELI15: pageScroll is the custom helper that creates the scrollable area in the middle.
    Private pageScroll As B4XDaisyPageScroll
    
    ' ELI15: pnlHost is the actual scrollable canvas inside pageScroll where our form inputs sit.
    Private pnlHost As B4XView
    ' ELI15: dock is the footer navigation bar pinned at the very bottom of our screen.
    Private dock As B4XDaisyDock
    ' ELI15: Height values in dip (Density Independent Pixels) to keep sizing uniform on low and high resolution screens.
    Private NAVBAR_H As Int
    Private DOCK_H As Int
    
    ' ELI15: Declared as global so we can request focus on it inside B4XPage_Appear (when the page slides into view).
    Private inp2 As B4XDaisyInput
End Sub
#End Region
#Region Initialization
' ELI15: The constructor. Prepares this class object so it can be added to the B4XPages manager.
Public Sub Initialize As Object
    Return Me
End Sub
' ELI15: Automatically runs exactly once when this page is loaded into memory.
' This is where we build our UI layout structure.
Private Sub B4XPage_Created(Root1 As B4XView)
    ' Save the main screen parent container.
    Root = Root1
    ' Define standard heights for our top and bottom bars.
    NAVBAR_H = 56dip
    DOCK_H   = 64dip
    ' ELI15: CRITICAL ORDER: In B4A, views added later are stacked on top of views added earlier.
    ' We build the scrollable content container FIRST so it lies at the bottom of the stack,
    ' then we build the navbar and dock so they float on top and stay visible while scrolling.
    '
    ' ELI15: Imagine you are laying transparent sheets on a table. The first sheet goes at the bottom.
    ' If you put a photo on top, you can still see the bottom sheet around the edges.
    ' Here, the scrollable page content is the bottom sheet, and the navbar/dock are photos on top.
    BuildScroll
    BuildNavbar
    BuildDock
    ' ELI15: BringToFront/SendToBack are our insurance policy. Even if another part of the code
    ' adds more views later, these commands force the navbar to stay on top and the page scroll
    ' to stay behind, so the header and footer are never hidden by scrolling text or buttons.
    ' Draw all form inputs inside the scroll panel.
    RenderContent
End Sub
#End Region
#Region Layout Builders
' ELI15: Builds the top header navbar and stretches it across the screen width.
Private Sub BuildNavbar
    navbar.Initialize(Me, "navbar")
    navbar.AddToParent(Root, 0, 0, Root.Width, NAVBAR_H)
    ' ELI15: BringToFront lifts this view above every other view in the same parent.
    ' Think of it like taking a piece of paper and placing it on top of the pile.
    ' This guarantees the navbar header is never covered by scrolling form content.
    navbar.getView.BringToFront
    navbar.Title = "Nav Scroll Dock"
    navbar.Variant = "primary"
    navbar.BackVisible = True
End Sub
' ELI15: Builds the bottom footer dock and pins it to the screen's bottom edge.
Private Sub BuildDock
    dock.Initialize(Me, "dock")
    dock.Size = "md"
    dock.ActiveIndex = 0
    ' Y coordinate starts at Root.Height minus the dock's height so it stays at the very bottom.
    dock.AddToParent(Root, 0, Root.Height - DOCK_H, Root.Width, DOCK_H)
    ' ELI15: The dock is added after the scroll view, so it naturally sits on top.
    ' We position it at Root.Height - DOCK_H so its bottom edge lines up with the screen bottom.
    dock.AddItem("home",     "Home",     "dock-home.svg")
    dock.AddItem("inbox",    "Inbox",    "dock-inbox.svg")
    dock.AddItem("settings", "Settings", "dock-settings.svg")
End Sub
' ELI15: Builds the scrollable container. We place it directly between the navbar and the dock.
Private Sub BuildScroll
    Dim scrollTop As Int = NAVBAR_H
    ' Height is calculated as: Total Screen Height - Top Bar Height - Bottom Bar Height.
    Dim scrollH   As Int = Root.Height - NAVBAR_H - DOCK_H
    pageScroll.Initialize(Me, "pageScroll")
    pageScroll.AddToParent(Root, 0, scrollTop, Root.Width, scrollH)
    ' ELI15: SendToBack pushes this view to the very bottom of the pile.
    ' Even though we added it first, this command makes absolutely sure the scrollable content
    ' stays behind the navbar and dock, so the user can scroll without the bars blocking clicks.
    pageScroll.mBase.SendToBack
    pnlHost = pageScroll.Panel
End Sub
#End Region
#Region Content Rendering
' ELI15: Draws the form controls inside the scrollable content canvas.
Private Sub RenderContent
    ' Clear any previous elements to avoid duplication.
    pageScroll.Clear
    ' Fetch dimensions relative to screen padding and setup a vertical stack coordinate 'y'.
    Dim maxW   As Int = pageScroll.UsableWidth
    Dim pad    As Int = pageScroll.PagePadding
    Dim gap    As Int = pageScroll.YGap
    Dim y      As Int = pad
    ' -- Section: Text Inputs --
    ' We call AddSectionTitle with False for the third parameter so the title aligns Left.
    y = pageScroll.AddSectionTitle("Text Inputs", y, False)
    Dim inp1 As B4XDaisyInput
    inp1.Initialize(Me, "inp1")
    inp1.AddToParent(pnlHost, pad, y, maxW, 40dip)
    inp1.LabelAbove = "Full Name"
    inp1.Placeholder = "Enter your name..."
    ' Move our vertical coordinate down: current Y + height of input + vertical gap.
    y = y + inp1.GetComputedHeight + gap
    inp2.Initialize(Me, "inp2")
    inp2.AddToParent(pnlHost, pad, y, maxW, 40dip)
    inp2.LabelAbove = "Email Address"
    inp2.Placeholder = "[email protected]"
    inp2.InputType = "email"
    y = y + inp2.GetComputedHeight + gap
    Dim inp3 As B4XDaisyInput
    inp3.Initialize(Me, "inp3")
    inp3.AddToParent(pnlHost, pad, y, maxW, 40dip)
    inp3.LabelAbove = "Password"
    inp3.Placeholder = "••••••••"
    inp3.InputType = "password"
    y = y + inp3.GetComputedHeight + gap
    ' -- Section: Select --
    y = pageScroll.AddSectionTitle("Dropdown Select", y, False)
    Dim sel1 As B4XDaisySelect
    sel1.Initialize(Me, "sel1")
    sel1.AddToParent(pnlHost, pad, y, maxW, 40dip)
    sel1.LabelAbove = "Country"
    sel1.Placeholder = "Pick a country..."
    sel1.Items = CreateMap("za": "South Africa", "us": "United States", "gb": "United Kingdom", "de": "Germany")
    y = y + sel1.GetComputedHeight + gap
    ' -- Section: Toggles & Checkboxes --
    y = pageScroll.AddSectionTitle("Toggles & Checkboxes", y, False)
    Dim tgl1 As B4XDaisyToggle
    tgl1.Initialize(Me, "tgl1")
    tgl1.AddToParent(pnlHost, pad, y, maxW, 40dip)
    tgl1.Text = "Enable notifications"
    y = y + tgl1.GetComputedHeight + gap
    Dim tgl2 As B4XDaisyToggle
    tgl2.Initialize(Me, "tgl2")
    tgl2.AddToParent(pnlHost, pad, y, maxW, 40dip)
    tgl2.Text = "Dark mode"
    tgl2.Checked = True
    y = y + tgl2.GetComputedHeight + gap
    Dim chk1 As B4XDaisyCheckbox
    chk1.Initialize(Me, "chk1")
    chk1.AddToParent(pnlHost, pad, y, maxW, 40dip)
    chk1.Text = "I agree to the terms and conditions"
    y = y + chk1.GetComputedHeight + gap
    ' -- Section: Range Slider --
    y = pageScroll.AddSectionTitle("Range Slider", y, False)
    Dim rng1 As B4XDaisyRange
    rng1.Initialize(Me, "rng1")
    rng1.AddToParent(pnlHost, pad, y, maxW, 24dip)
    rng1.Value = 40
    y = y + rng1.GetComputedHeight + gap
    ' -- Section: Rating --
    y = pageScroll.AddSectionTitle("Star Rating", y, False)
    Dim rat1 As B4XDaisyRating
    rat1.Initialize(Me, "rat1")
    rat1.AddToParent(pnlHost, pad, y, maxW, 32dip)
    rat1.Value = 4
    y = y + rat1.GetComputedHeight + gap
    ' -- Section: Action Buttons --
    y = pageScroll.AddSectionTitle("Actions", y, False)
    Dim btnW As Int = (maxW - 8dip) / 2
    Dim btnSave As B4XDaisyButton
    btnSave.Initialize(Me, "btnSave")
    btnSave.AddToParent(pnlHost, pad, y, btnW, 44dip)
    btnSave.Text = "Save"
    btnSave.Variant = "primary"
    Dim btnCancel As B4XDaisyButton
    btnCancel.Initialize(Me, "btnCancel")
    btnCancel.AddToParent(pnlHost, pad + btnW + 8dip, y, btnW, 44dip)
    btnCancel.Text = "Cancel"
    btnCancel.Variant = "secondary"
    y = y + btnSave.GetComputedHeight + gap
    Dim btnDelete As B4XDaisyButton
    btnDelete.Initialize(Me, "btnDelete")
    btnDelete.AddToParent(pnlHost, pad, y, maxW, 44dip)
    btnDelete.Text = "Delete Account"
    btnDelete.Variant = "error"
    y = y + btnDelete.GetComputedHeight + gap
    ' ELI15: Tell the ScrollView to calculate the total height of all views inside it,
    ' so the user can scroll all the way down to see the final Delete button without it getting cut off.
    pageScroll.AutoFit
End Sub
#End Region
#Region Page Events
' ELI15: Runs whenever the device is rotated or resized (like split-screen mode).
' We recalculate and stretch the top navbar, bottom dock, and scroll panel to fit the new size.
Private Sub B4XPage_Resize(Width As Int, Height As Int)
    If navbar.IsInitialized Then
        navbar.getView.SetLayoutAnimated(0, 0, 0, Width, NAVBAR_H)
    End If
    If dock.IsInitialized Then
        dock.View.SetLayoutAnimated(0, 0, Height - DOCK_H, Width, DOCK_H)
    End If
    If pageScroll.IsInitialized Then
        pageScroll.Base_Resize(Width, Height - NAVBAR_H - DOCK_H)
    End If
    ' Redraw content to fit the new width.
    RenderContent
End Sub
' ELI15: Runs every single time the user navigates and opens this page on their screen.
' Excellent place to set keyboard focus onto the primary text input.
Private Sub B4XPage_Appear
    ' Notify the main loader that loading is complete.
    CallSubDelayed(B4XPages.MainPage, "Page_Ready")
    ' If our email input field is fully initialized, focus the typing cursor there.
    If inp2.IsInitialized Then
        inp2.Focus = True
    End If
End Sub
#End Region
#Region Event Handlers
' ELI15: Triggers when the user taps the back navigation arrow on the navbar.
Private Sub navbar_Back(Tag As Object)
    B4XPages.ShowPage("Dashboard")
End Sub
' ELI15: Triggers when the user taps one of the bottom dock navigation items.
Private Sub dock_ItemClick(ItemId As String)
    #If B4A
    ' Display a short native Android pop-up toast message at the bottom of the screen.
    ToastMessageShow("Dock: " & ItemId, False)
    #End If
End Sub
' ELI15: Triggers when the Save button is clicked.
Private Sub btnSave_Click(Tag As Object)
    #If B4A
    ToastMessageShow("Saved!", False)
    #End If
End Sub
' ELI15: Triggers when the Cancel button is clicked.
Private Sub btnCancel_Click(Tag As Object)
    B4XPages.ShowPage("Dashboard")
End Sub
' ELI15: Triggers when the Delete Account button is clicked.
Private Sub btnDelete_Click(Tag As Object)
    #If B4A
    ToastMessageShow("Delete tapped", False)
    #End If
End Sub
#End Region
 
Top