Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ti.Scrollwatch

Continuous scroll position for Titanium's Android ListView and TableView.

Why this exists

On Android, Titanium's ListView tells you where it is scrolled to at exactly two moments: when a gesture starts (scrolling) and when it comes to rest (scrollend). Nothing is reported while the list is actually moving. Fling a long list and your JavaScript hears about the starting position, then silence for as long as the fling lasts, then the final position.

iOS has no such gap — scrolling fires continuously there — so this is one of those bugs that only shows up when someone finally tests the Android build.

That gap is fine if you only need to know where the user landed. It is not fine if you are drawing anything that has to track the list as it moves:

  • sticky section headers — a pinned day/category bar above the list
  • chrome that reacts to scrolling — collapsing toolbars, shadows, scroll-to-top buttons, progress indicators
  • loading more rows before the user reaches the bottom

The usual workarounds all fail in their own way. setMarker/addMarker fires when a row enters the bottom of the viewport, which tells you nothing reliable about what is at the top — on a tall screen showing several sections at once, a marker-driven header changes to the wrong section while the top of the list has not moved. Guessing from item heights breaks the moment a row wraps to two lines. Polling on a timer is jittery and burns battery.

This module removes the guesswork. It attaches a real RecyclerView.OnScrollListener — the same callback the platform uses internally — to the RecyclerView backing the Titanium view, and forwards the visible range to JavaScript whenever it changes. The position is the platform's own answer, not an estimate.

Install

Download ti.scrollwatch-android-1.0.0.zip from Releases and unzip it into your project root (the zip contains a modules/ directory, so it lands in the right place):

unzip ti.scrollwatch-android-1.0.0.zip -d /path/to/your/app

Then add it to tiapp.xml:

<modules>
    <module platform="android">ti.scrollwatch</module>
</modules>

Requires Titanium SDK 13.0.0.GA or later.

Usage

const scrollwatch = require('ti.scrollwatch');

scrollwatch.watch($.myListView, function (e) {
    // e.firstVisiblePosition            first row visible, even partially
    // e.lastVisiblePosition             last row visible, even partially
    // e.firstCompletelyVisiblePosition  first fully visible row
    // e.dx, e.dy                        pixels scrolled since the last event
    Ti.API.info('top row is ' + e.firstVisiblePosition);
});

Positions are flat adapter rows

The reported positions are flat row indexes across the whole list, not {section, item} pairs. Two things to know when mapping them back to sections:

  1. Titanium's Android ListView does not give section headers a row of their own. A flat position is a running count of items only.
  2. So section n starts at the sum of the item counts of the sections before it.
// Build this once, when you build the sections.
const sectionStarts = [];
let flat = 0;
sections.forEach(function (section, i) {
    sectionStarts[i] = flat;
    flat += section.items.length;   // no +1 — headers take no row
});

function sectionForPosition(position) {
    let i = sectionStarts.length - 1;
    while (i > 0 && sectionStarts[i] > position) { i--; }
    return i;
}

If you are unsure whether your list matches this model, verify it against the ListView's own firstVisibleSectionIndex in a scrollend handler — it is authoritative, just too late to drive an animation.

The view has to exist first

The RecyclerView does not exist until the window has been laid out, so watch() returns false if you call it too early. Retry briefly:

function attach(attempt) {
    if (scrollwatch.watch($.myListView, onScroll)) { return; }
    if (attempt < 10) {
        setTimeout(function () { attach(attempt + 1); }, 200);
    }
}
attach(0);

scrollwatch.isReady($.myListView) answers the same question without attaching.

Stop watching

scrollwatch.unwatch($.myListView);

Not strictly required — the listener dies with the RecyclerView — but it is good manners if you keep the view around and no longer care.

API

Method Returns Notes
watch(view, callback) Boolean false if the view is not laid out yet. Watching an already-watched view replaces the callback.
unwatch(view) Boolean false if the view was not being watched.
isReady(view) Boolean Whether watch() would succeed right now.

Callback event properties: firstVisiblePosition, lastVisiblePosition, firstCompletelyVisiblePosition, dx, dy.

The callback fires only when the visible range changes, not on every frame of a fling, so it does not flood the JS bridge.

Building from source

./build.sh          # -> dist/ti.scrollwatch-android-1.0.0.zip

Needs the Titanium CLI, an Android SDK, and NDK r25 or r26 — Titanium's ndk-build toolchain fails on r27+, where lld dropped a flag it still passes.

Two quirks are worth knowing if you build other Titanium modules:

  • The Titanium CLI refuses to build a module that lives inside an app project, which is why build.sh copies the source to a temp directory first.
  • On some Linux distributions the SDK's module Android.mk template passes a bare -L$(SYSROOT)/usr/lib. SYSROOT is empty under modern ndk-build, so the host's /usr/lib ends up on the link line and lld chokes on the system's linker scripts — reported as the thoroughly misleading --fix-cortex-a53-843419 is only supported on AArch64 targets. Removing that path from the template fixes it.

The shipped binaries are linked with -Wl,-z,max-page-size=16384, so they are 16 KB page-size compatible for Android 15+ and GrapheneOS.

License

GPL-3.0-or-later. Copyright (c) 2026 Centreville Tech LLC. See LICENSE.

About

Continuous scroll position for Titanium's Android ListView and TableView — the RecyclerView onScrolled callback Titanium doesn't expose.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages