tabris-js
tabris-js copied to clipboard
CollectionView "scroll" event deltas sometimes cannot be used to reliably track scroll offset
Problem description
CollectionView does not have an API like ScrollView's offsetY
. The only option to track the scroll offset is to manually sum deltaY
provided by the scroll event. However, on Android, in some cases deltaY
might not reflect the reality. The delta sum gets inaccurate particularly when calling CollectionView#reveal()
. Maybe some scroll
events are not getting triggered in this scenario.
Expected behavior
Fix deltaY
so that it can be used to track the CollectionView scroll offset in conjunction with reveal()
and/or provide an API like ScrollView's offsetY
in addition.
Environment
- Tabris.js version: nightly, 2.9.0
- OS: Android 9
Code snippet
Scroll to the bottom of the list manually and scroll to the top manually. The offset calculated by using the deltas will be almost 0 as expected.
Scroll to the bottom of the list manually and tap on the "scroll to top" button. The offset will be off by a small margin. Repeat this a few times and the error of the sum will increase.
import {$, CollectionView, contentView, TextView, Composite, Button} from 'tabris';
/** @param {tabris.Attributes<TextView>=} attributes */
const SectionCell = attributes =>
<TextView background='#aaaaaa' textColor='white' font='bold 24px' alignment='centerX' {...attributes}/>;
/** @param {tabris.Attributes<TextView>=} attributes */
const ItemCell = attributes =>
<TextView padding={[2, 5]} font='14px' alignment='left' {...attributes}/>;
const items = createItems();
contentView.append(
<$>
<CollectionView stretch
itemCount={items.length}
cellType={index => items[index].type}
cellHeight={(_, type) => type === 'section' ? 48 : 32}
createCell={type => type === 'section' ? SectionCell() : ItemCell()}
updateCell={(cell, index) => cell.text = items[index].name}
onScroll={handleScroll}/>
<TextView id='tracker' center />
<SectionCell stretchX height={48} id='floatingSection' text={items[0].name}/>
<Button centerX centerY={32} text='scroll to top' onTap={scrollToTop}/>
</$>
);
function scrollToTop() {
tabris.contentView.find(tabris.CollectionView).first().reveal(0);
}
let offset = 0;
/** @param {tabris.CollectionViewScrollEvent<CollectionView<TextView>>} ev */
function handleScroll({deltaY}) {
offset += deltaY;
contentView.find('#tracker').first(TextView).text = offset.toString();
}
function createItems() {
let itemCount = 1;
/** @type {Array<{name: string, type: 'section' | 'item'}>} */
const result = [];
for (let j = 1; j <= 10; j++) {
result.push({name: 'Section ' + itemCount++, type: 'section'});
for (let i = 0; i < 5; i++) {
result.push({name: 'Item ' + itemCount++, type: 'item'});
}
}
return result;
}
In such a case you could get the absolute position of the view in question this will be more stable. See https://docs.tabris.com/latest/layout.html#properties-bounds-and-absolutebounds
Thanks @cpetrov for reporting the issue.
After investigating the issue, we found that the total deltaY that we get from scroll event always is 1 pixel less than expected(actual) when we call CollectionView#reveal(0)
.
P.S. 1 px is converted to Dip which was approximately 0.399... in my testing device.
The issue occurs because of the unexpected behavior of native Android components mentioned above. So it seems we have nothing to do right now.
I would suggest a hacky solution that resets offset
value when it is less than 1
.
const APPROXIMATION_ERROR = 1;
...
function handleScroll({deltaY}) {
offset += deltaY;
if (offset < APPROXIMATION_ERROR) {
offset = 0;
}
contentView.find('#tracker').first(TextView).text = offset.toString();
}
We can get different values on different devices when we convert 1 pixel to Dip. So I think 1 is the average...