"Sticky" center element of horizontal React Native ListView - react-native

My complete source code for this issue is posted as an Expo app: https://exp.host/#kevinoldlifeway/swipe-scrollview-of-webviews as well as on Github https://github.com/kevinold/swipe-scrollview-of-webviews
I am building a book view in React Native. Using a ScrollView, I would like to swipe left and right to navigate through the pages of a title that could have several hundred to several thousand.
Since that is the case, my goal is to only the minimal amount of data so that the user is able to swipe between pages, seeing the immediately previous and next pages.
I am loading a 3 element array like so:
[Previous, Current, Next]
That would be updated in the state by Redux (not used here to keep simple) and would re-render and refocus the list.
My goal is that my ScrollView is always "centered" on the "Current" page.
Page scrolls to the previous and next page are handled by a handleScroll method which loads the appropriate precomputed array so that the current page stays in focus, but the previous and next pages (offscreen) are updated appropriately.
handleScroll (event) {
//const x = event.nativeEvent.contentOffset.x;
const { activeIndex, scrollTimes } = this.state;
const windowWidth = Dimensions.get('window').width;
const eventWidth = event.nativeEvent.contentSize.width;
const offset = event.nativeEvent.contentOffset.x;
console.log('event width: ', eventWidth);
console.log('event offset: ', offset);
console.log('scrollTimes: ', scrollTimes);
//if (scrollTimes <= 1 ) return;
if (windowWidth + offset >= eventWidth) {
//ScrollEnd, do sth...
console.log('scrollEnd right (nextPage)', offset);
const nextIndex = activeIndex + 1;
console.log('nextIndex: ', nextIndex);
// Load next page
this.loadMore()
} else if (windowWidth - offset <= eventWidth) {
//ScrollEnd, do sth...
console.log('scrollEnd left (prevPage)', offset);
// Load prev page
this.loadPrev()
}
this.setState({ scrollTimes: scrollTimes + 1 });
}
I have tried to balance the "current" page using a combination of:
contentOffset={{ x: width, y: 0 }} on ScrollView
And
componentDidMount() {
// Attempt to keep "center" element in array as focused "screen" in the horizontal list view
this.scrollView.scrollTo({ x: width, y: 0, animated: false });
}
I've also tried to scrollTo in the callback after this.setState, but have not had any luck.
I'm wondering if this "centering" could be accomplished by using Animated.

I gave this a shot but I'm not entirely sure I understood the problem, and I'm not sure how well this would hold up.
Basically I just simplified the handleScroll function significantly. First checking if we were on a scroll completion and if so determining if when we landed on that screen it was the "previous" screen or "next" - do nothing if it's already the middle screen.
I think in your code the issue was that it would fire and load data if it was the middle screen, not just the first or last. Therefore it would fire twice for each transition.
Here's the handleScroll that I think will work for you.
handleScroll (event) {
const offset = event.nativeEvent.contentOffset.x;
const mod = offset % width;
if (mod === 0) { // only transition on scroll complete
if (offset === width * 2) { // last screen
console.log('load more')
this.loadMore();
this.scrollView.scrollTo({ x: width, y: 0, animated: false });
} else if (offset !== width) { // first screen
console.log('load prev')
this.loadPrev();
this.scrollView.scrollTo({ x: width, y: 0, animated: false });
}
}
}
And a Snack demoing it.

Related

How to disable animations when the user scrolls (react-native-reanimated 2)

Here's a weekly mini calendar, that turns into a monthly mini calendar component.
When it turns from weekly to monthly we have some entering/exiting animations
So far so good.
Problem:
The problem is, that those animations (being entering/exiting animations) also take place while the user is scrolling.
As you can see in the gif, animations play when I scroll horizontally, which isn't what I want, I only want animations when the component changes from weekly to monthly (expands/collapses)
Code:
import Animated, {
FadeInDown,
FadeInUp,
SlideOutUp,
SlideOutDown,
} from 'react-native-reanimated';
const MiniCalendarItem = () => {
let animationEnter;
let animationExit;
if (this.props.itemRepresents === ITEM_REPRESENTS.MONTH) {
if (this.dayIsPartOfCurrentWeek(day)) {
animationEnter = FadeInUp;
} else {
animationEnter = FadeInUp.delay((weekIndex * 150)).duration(350)
}
animationExit = SlideOutDown.duration(400);
} else {
animationEnter = FadeInDown.duration(500);
animationExit = SlideOutUp.duration(400);
}
return (
<Animated.View
entering={animationEnter}
exiting={animationExit}
key={`dayData_${dayProps.id}`}
>
{...}
</Animated.View>
);
};
and here's the parent:
renderItem = () => {
return (
<MiniCalendarItem
animationsEnabled
key={itemKey}
mode={mode}
itemRepresents={visible ? ITEM_REPRESENTS.MONTH : ITEM_REPRESENTS.WEEK}
/>
)
}
}
Essentially the parent is a ScrollView (not a FlatList)
Question:
How can I stop react-native-reanimated#2 from playing any animations and when is it a good time to do that.
I added a animationsEnabled prop, but ideally I'd love to feed it with an Animated.Value(true) object. I'm just not sure how to conditionally disable animations based on that prop, from within the MiniCalendarItem.

React native dynamic flatlist height

I have a FlatList that has 2 potential heights on a screen depending on what is being shown. There are top and bottom bars that show by default and when the FlatList is scrolled down, the bars are hidden and the FlatList expands to take up the full screen.
The problem is when the FlatList shifts between the 2 different heights, the content inside will shift slightly. I have already figured out that I need to apply a scroll offset of the height of the top bar (54), however i'm having issues doing that.
const [scrollOffset, setScrollOffset] = useState(0);
const [scrollDir, setScrollDir] = useState("neutral");
<FlatList ...
onScroll={(event) => {
const screenHeight = Dimensions.get('window').height - SAFE_AREA_INSETS.top - SAFE_AREA_INSETS.bottom;
const currentOffset = event.nativeEvent.contentOffset.y;
const diff = currentOffset - (scrollOffset || 0);
if (Math.abs(diff) < 3 || currentOffset < 0 || currentOffset > (event.nativeEvent.contentSize.height - screenHeight)) {
setScrollDir("neutral");
}
else if (diff < 0 && currentOffset <= (event.nativeEvent.contentSize.height - screenHeight)) {
// Top and bottom bars are shown and flatlist height is adjusted ...
if(scrollDir !== 'up') {
flatlistRef.current.scrollToOffset({ animated:false, offset:currentOffset + 54 }); // Not working as intended
}
setScrollDir('up');
}
else {
// Top and bottom bars are hidden and flatlist height is adjusted ...
if(scrollDir !== 'down') {
flatlistRef.current.scrollToOffset({ animated:false, offset:currentOffset - 54 }); // Not working as intended
}
setScrollDir('down');
}
setScrollOffset(currentOffset);
}}
The scrollToOffset method seems to trigger onScroll to think its going in the opposite direction which causes oscillations, which from the code makes sense. Any ideas to make this work correctly?
Edit:
Here is a link to a video of this happening
When I scroll up/down to unhide/hide the top and bottom bar, you'll see the content in the center jump up/down. It makes sense why this is happening, the height of the center content changes which causes the jump. I want the content to stay scrolled in the same position when changing the height.

dc.js composite chart toggle legend loses its translucence upon filtering

I have used this solution to get a toggle legend for a composite line chart and it works perfectly fine.
However, after i added a range chart to this composite chart, the deselected legend loses its translucence and becomes normal.
How can i keep the deselected legend object in faded state while filtering?
Here are screenshots for reference:
Before filter:
After filter:
This is the code I'm using for charts:
multiLineChart
.width(1000)
.height(300)
.transitionDuration(1000)
.margins({top: 30, right: 50, bottom: 40, left: 40})
.x(d3.time.scale().domain([startDate,endDate]))
.yAxisLabel("Data (Scaled)")
.xAxisLabel("Date And Time")
.rangeChart(timeSlider)
.legend(dc.legend().x(800).y(20).itemHeight(13).gap(5))
.renderHorizontalGridLines(true)
//.dimension(DateDim)
.compose([
dc.lineChart(multiLineChart)
.dimension(DateDim)
.colors('red')
.group(Line1Grp, 'Line1'),
dc.lineChart(multiLineChart)
.dimension(DateDim)
.colors('blue')
.group(Line2Grp, 'Line2')
])
.brushOn(false)
.on('pretransition.hideshow', function(chart) {
chart.selectAll('g.dc-legend .dc-legend-item')
.on('click.hideshow', function(d, i) {
var subchart = chart.select('g.sub._' + i);
var visible = subchart.style('visibility') !== 'hidden';
subchart.style('visibility', function() {
return visible ? 'hidden' : 'visible';
});
d3.select(this).style('opacity', visible ? 0.2 : 1);
});
});
//.xAxis().tickFormat(d3.time.format("%b %d %H:%M"));
timeSlider
.width(1000)
.height(50)
.margins({top: 0, right: 50, bottom: 20, left: 40})
.dimension(DateDim)
.group(Line1Grp)
.x(d3.time.scale().domain([startDate, endDate]))
.on("filtered", function (chart) {
dc.events.trigger(function () {
multiLineChart.focus(chart.filter());
dc.redrawAll(chart.chartGroup());
});
})
.xAxis().tickFormat(d3.time.format("%b %d"));
Here is a fiddle for the same.
Any help is appreciated.
Thanks for pointing this out - there was a bad practice in my earlier answer, and I went back and corrected it.
It's always better style, and more robust, to separate event handling and drawing, and always draw everything based on the data, not some event that is in flight.
If you follow these practices, then the code looks more like this:
function drawLegendToggles(chart) {
chart.selectAll('g.dc-legend .dc-legend-item')
.style('opacity', function(d, i) {
var subchart = chart.select('g.sub._' + i);
var visible = subchart.style('visibility') !== 'hidden';
return visible ? 1 : 0.2;
});
}
function legendToggle(chart) {
chart.selectAll('g.dc-legend .dc-legend-item')
.on('click.hideshow', function(d, i) {
var subchart = chart.select('g.sub._' + i);
var visible = subchart.style('visibility') !== 'hidden';
subchart.style('visibility', function() {
return visible ? 'hidden' : 'visible';
});
drawLegendToggles(chart);
})
drawLegendToggles(chart);
}
multiLineChart
.on('pretransition.hideshow', legendToggle);
Now, whenever we redraw the composite chart and its legend - no matter what the cause - all of the items in the legend will be updated based on whether the corresponding child chart has been hidden.
And the event handler is only concerned with hiding and showing charts, not drawing.
Fork of your fiddle.

How to improve PanResponder efficiency in ReactNative?

I am using React Native for a game (perhaps not the best framework but not the point). I need user to be able to slide on a grid. On the pic, user is selecting MKEC without removing its finger from screen.
I am from the web, so I thought I would use onHover event. But it dossn't exist on mobile. So I created a parent component and a grid component. Parent component catch event with PanResponder, and I calculate where user finger is on the grid, and change state of the grid component.
However, if user is going too fast from on letter to another, it sometimes 'skip' a letter. If you go from M to G (first column) very fast, it seems that not event is triggered on H. Am I clear ? User feels latency.
componentWillMount() {
const onRelease = (evt, gestureState) => {
this.props.validateWord();
};
const onMove = (evt, gestureState: GestureState) => {
const obj = Object.assign({}, {
pageX: evt.nativeEvent.pageX,
pageY: evt.nativeEvent.pageY
});
const pos = {
x: evt.nativeEvent.pageX - this.position.pageX,
y: evt.nativeEvent.pageY - this.position.pageY
}
const whatSquare = whatSquareIsTouched(
pos,
defaultLetter.width, // size of letter
defaultLetter.width * 0.22 // padding for diagonale
);
const letter = this.props.gridReducer[whatSquare.y]
&& this.props.gridReducer[whatSquare.y][whatSquare.x] ?
this.props.gridReducer[whatSquare.y][whatSquare.x] : null;
// if letter => if touch is on a letter and not outside of grid comp
if (letter) {
this.props.letterTouched(letter, whatSquare.insidePadding);
}
}
this._panResponder = createResponder(onMove, onRelease);
}
How could I improve my code ? Or perhaps go another way to tackle this problem?
Update after comment :
I have not solved the problem yet. But i tried the following. I assumed that when user change direction, a new event is trigger. So I could calculate the finger path. If user starts at M and finish in G without changing direction, then H has been hovered.

Flatlist visible position

I'm trying to render an curved vertical list like this iOS component: https://github.com/makotokw/CocoaWZYCircularTableView
That component (written in Obj-c) iterates the visible cells when laying them out, and sets the frame (i.e. indent) using asin.
I know in React Native I can set the leftMargin style in the renderItem callback, but I can't figure out how to get the on-screen index of the item - all I have is the index into the source data. And also, at that point, I don't think I have access to the absolute position.
Any ideas?
The function you are looking for is
onViewableItemsChanged.
You can use it with viewabilityConfig which provides us with
minimumViewTime,viewAreaCoveragePercentThreshold,waitForInteraction
which can be set accordingly
const VIEWABILITY_CONFIG = {
minimumViewTime: 3000,
viewAreaCoveragePercentThreshold: 100,
waitForInteraction: true,
};
_onViewableItemsChanged = (info: {
changed: Array<{
key: string,
isViewable: boolean,
item: any,
index: ?number,
section?: any,
}>
}
){
//here you can have the index which is visible to you
}
<FlatList
renderItem={this.renderItem}
data={this.state.data}
onViewableItemsChanged={this._onViewableItemsChanged}
viewabilityConfig={VIEWABILITY_CONFIG}
/>
Thanks for both answers.
What I have ended up doing is deriving the visible items using the scroll offset of the list. This is simple because the list items all have the same height.
I do this in the onScroll handler, and at that point I calculate the horizontal offset for each item (and I use leftMargin / rightMargin to render this). It's not perfect, but it does give me an elliptical list.
_handleScroll = (event) => {
const topItemIndex = Math.floor(event.nativeEvent.contentOffset.y / LIST_ITEM_HEIGHT);
const topItemSpare = LIST_ITEM_HEIGHT-(event.nativeEvent.contentOffset.y % LIST_ITEM_HEIGHT);
const positionFromEllipseTop = (forIndex-topItemIndex)*LIST_ITEM_HEIGHT+topItemSpare;
const positionFromOrigin = Math.floor(Math.abs(yRadius - positionFromEllipseTop));
const angle = Math.asin(positionFromOrigin / yRadius);
if (orientation === 'Left') {
marginLeft = 0;
marginRight = ((xRadius * Math.cos(angle)))-LIST_ITEM_HEIGHT;
alignSelf = 'flex-end';
}
else if (orientation === 'Right') {
marginLeft = (xRadius * Math.cos(angle))-LIST_ITEM_HEIGHT;
marginRight = 0;
alignSelf = 'flex-start';
}
}
React-native's FlatList component has a prop called onLayout. You can get the position of the component on screen with this prop.
onLayout
Invoked on mount and layout changes with:
{nativeEvent: { layout: {x, y, width, height}}}
This event is fired immediately once the layout has been calculated,
but the new layout may not yet be reflected on the screen at the time
the event is received, especially if a layout animation is in
progress.