Enlarging single item in React Native ScrollView - javascript

I have a ScrollView like so:
<ScrollView>
{this.state.businessMerchants.map(merchant => (
<Business
merchant={merchant}
key={merchant.id}
/>
))}
</ScrollView>
Usually with 2-4 items in it.
I want to highlight the currently top item and make it take up more room (maybe 10% more?). This "top item" would switch as one scrolls through the list.
Is there a more standardized way of doing this, or will I have to do something like use scrollEventThrottle and a custom function like so?
Pseudocode below:
if((findHeightOfItem + padding) * multiple === itemPos) {
addSizeHere()
}
Quite curious about what's the best/most performative way of doing this in React Native.

Steps:
Decide the height of each component itemHeight
Apply onScroll and get current scrolled height scrolledHeight
Have one local state called currentItemIndex which will be decided in onScroll function and will be calculated as Math.floor(scrolledHeight / itemHeight)
Send isCurrentItem as prop to Business component
Apply necessary styles to Business component depending on value of boolean isCurrentItem
I would suggest you to use Animated view with transform scale
handleScroll = (e) => {
const currentHeight = e.nativeEvent.contentOffset.y;
const currentItemIndex = Math.floor(currentHeight / 100);
this.setState({ currentItemIndex });
}
<ScrollView onScroll>
{this.state.businessMerchants.map((merchant, index) => (
<Business
merchant={merchant}
isCurrentItem={index === this.state.currentItemIndex}
key={merchant.id}
/>
))}
</ScrollView>

Related

Changing content dynamically inside react-native-pager-view (or any other)

i'm working on a project where i'm going to be displaying details and information about a certain book page by page inside a pager view as page components, the book contains 500+ pages so i can't just create 500 page components like that and insert them into the pager..what i thought is i can get a specific page, render its component, alongside the previous, and the next page only..and when the user swipes to the next/previous page i would change the component state, and have it re-render with the new 3 pages, the current one, the previous, and the next one. the logic in my head makes perfect sense, but it just won't work when i try to apply it.
can anyone help me, guide me to certain videos that explain this principal more? i feel like i'm missing something.
the code goes like this:
first i have the PagesContainer, here i will create the PagesDetails component(s) based on the current page, and having these pages in react-native-pager-view (you can suggest me a better option). for testing purpose only, i set the swipe end callback (onPageSelected) to increment the current page number state, which would then cause the component to re-render and create the new page component(s), that happens only when the user swipes to new page of course:
function PagesContainer({ currentPageNumber, setCurrentPageNumber }) {
const [pageComponents, setPageComponents] = useState([]);
useEffect(() => {
let compArr = [];
compArr.push(<PageDetails key="current" pageNumber={currentPageNumber} />);
if (currentPageNumber > 1) {
compArr.unshift(<PageDetails key="previous" pageNumber={currentPageNumber - 1} />)
}
if (currentPageNumber <= 500) {
compArr.push(<PageDetails key="next" pageNumber={currentPageNumber + 1} />)
}
setPageComponents(compArr);
}, [currentPageNumber])
return (<PagerView style={{ flex: 1 }}
initialPage={currentPageNumber == 1 ? 0 : 1}
layoutDirection={"rtl"}
onPageSelected={(PageSelectedEvent)=>{setCurrentPageNumber(currentPageNumber + 1)}}
>
{pageComponents.map(page => {
return page;
})}
</PagerView>)
}
and then here i have my PageDeatails component where i simply display texts and details of the page, i take the data from the bookData object which is imported at the very top of the code file:
function PageDetails({ pageNumber }) {
const [pageContent, setPageContent] = useState(null);
useEffect(() => {
setPageContent(bookData[pageNumber]["pageContent"]);
}, []);
return (
<View>
{pageContent && <View>
{pageContent.map(item => {
return (<Text>item</Text>)
})}
</View>
}
</View>
)
}
The logic makes perfect sense in my head but it just doesn't work when i test it..what am i missing? what am i doing wrong?
use the PagerView reference using useRef also store the page index and pass to initialPage get current page index from onPageSelected callback
like:
initialPage={currentPageIndex}

How to get the element ID into scroll view to mark category in navbar

I have a navbar with a list of categories, and I wanted to set the selected category according to what the user is looking at, but for that I need to get the element ID, but these IDs are dynamically set. Currently I already have a function to set the selected category, but it sets the selected category as the click, and I would also like to mark the selected one as the user scrolls.
My code
function onHandleClick(props){
setCurrentCat(props)
document.getElementById(props).scrollIntoView();
}
return (
<div className = {Position ? 'navbarContainer navbarContainerScroll':'navbarContainer'}>
{Categorias.map(item =>(
<ul>
<li className = {item.Cod === currentCat ? 'navbarContainer liSelect' : 'navbarContainer liNormal'}
onClick = {() => onHandleClick(item.Cod)
}>{item.Nome}</li>
</ul>
))}
</div>
)
I currently move the screen to the category are clicked on the navigation bar, how can I set the selected category when the screen focus is on top of it
I have tried to answer your problem as I understand it. I have filled in some extra pieces to help give a full solution. I hope bits and pieces of this are relevant to your issue!
So if I understand you correctly you want to:
be able to automatically update the active tab in your navbar when a user scrolls the page. Also, I assume they are scrolling vertically for my answer.
As per the problem and code you provided, I infer you have a setup somewhat like this:
A Section or Category component containing content for a given item
A Page component that would render a number of Cateogory components and NavBar
Given these two things, I have put the following code together that could help you achieve the functionality you are looking for:
import React, { useEffect, useRef, useState } from "react";
/** Check if an HTML element is within the main focus region of the page */
function isElementInMainFocusArea(
element,
verticalConstrainFactor = 0,
horizontalConstrainFactor = 0
) {
const elementRect = element.getBoundingClientRect();
const documentHeight =
window.innerHeight || document.documentElement.clientHeight;
const documentWidth =
window.innerWidth || document.documentElement.clientWidth;
// Vertical focus region
const topFocusPos = documentHeight * verticalConstrainFactor;
const bottomFocusPos = documentHeight * (1 - verticalConstrainFactor);
// Horizontal focus region
const leftFocusPos = documentWidth * horizontalConstrainFactor;
const rightFocusPos = documentWidth * (1 - horizontalConstrainFactor);
return (
elementRect.top >= topFocusPos &&
elementRect.bottom <= bottomFocusPos &&
elementRect.left >= leftFocusPos &&
elementRect.right <= rightFocusPos
);
}
/** Navigation bar component which will taken in a list of refs.
Each ref must be assigned to or given as a prop to some other section on the page that we want to scroll to. */
function NavBar({ pageSectionRefs, categories }) {
const [currentCat, setCurrentCat] = useState();
/** Set up the scroll event listener to update the currentCat whenever we scroll*/
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
/** Check all the refs we are to watch to see which one of them is in focus */
function handleScroll() {
for (ref in pageSectionRefs.reverse()) {
if (isElementInMainFocusArea(ref.current)) {
setCurrentCat(index);
return; // if two elements are in focus, only the first will be chosen. Could cause a glitch
}
}
}
// Returns what you were previously returning for this component
return (
<div
className={
Position ? "navbarContainer navbarContainerScroll" : "navbarContainer"
}>
{categories.map((item) => (
<ul>
<li
className={
item.Cod === currentCat
? "navbarContainer liSelect"
: "navbarContainer liNormal"
}
onClick={() => onHandleClick(item.Cod)}>
{item.Nome}
</li>
</ul>
))}
</div>
);
}
/** The top level component that will be rendering the NavBar and page sections. */
function MyPage() {
const categories = [];
const pageSectionRefs = categories.map(() => useRef());
return (
<div>
<NavBar pageSectionRefs={pageSectionRefs} categories={categories} />
{categories.map((cat, idx) => {
return (
<div key={idx} ref={pageSectionRefs[idx]}>
Section for category {idx}
</div>
);
})}
</div>
);
}
In case I don't have a full picture of your problem, kindly comment things I may have missed so I update my answer!
References:
The isElementInFocusArea function was adapted from here: How can I tell if a DOM element is visible in the current viewport?
Inspiration for handling list of refs from here: How can I use multiple refs for an array of elements with hooks?

React Native Tab view always has the height equal to height of the highest tab

Introduction
I have a FlatList that renders a Tab View in its footer. This Tab View let the user switch between one FlatList or an other. So, these last are sibling FlatLists.
Problem
The first FlatList "A" has a greater height than the second one "B". When I choose the second list, its height is the same as the "A" FlatList's one.
I have reproduced the problem in a snack where you can see a similar code. I recognize that the code is a little long but too simple, just focus only on the parent FlatList (in the App component) and the two FlatLists that are rendered in each tab (at the end of the code)
QUESTION
Any ideas how to solve this issue? I don't know if the problem is in the styles or if I have to do something else to make this work (all flatlists have to have their own height, not the greater).
Thank you, I would really appreciate your help.
UPDATE 2022
const renderScene = ({ route }) => {
//
// 📝 Note: We are hidding tabs in order to avoid the
// "FlexBox Equal Height Columns" typical problem
//
switch (route.key) {
case "bitcoin":
return (
<View style={index !== 0 && styles.hidden}>
<Bitcoin />
</View>
);
case "ethereum":
return (
<View style={index !== 1 && styles.hidden}>
<Etherum />
</View>
);
case "rose":
return (
<View style={index !== 2 && styles.hidden}>
<Rose />
</View>
);
default:
return null;
}
};
...
<TabView
renderTabBar={renderTabBar}
navigationState={{ index, routes }}
renderScene={renderScene}
onIndexChange={handleOnIndexChange}
initialLayout={{ width: layout.width }}
removeClippedSubviews={false}
swipeEnabled
swipeVelocityImpact={0.2}
gestureHandlerProps={{
activeOffsetX: [-30, 30], // To solve swipe problems on Android
}}
style={globalStyles.flexContainer}
/>
Styles:
hidden: { display: "none" }
I have updated the snack with the solution!
As in the snack I implemented my own TabView, I have decided to implement the same solution with the library "react-native-tab-view", as it is the best tab for react native for now.
Think that some people having this issue will be able to solve it.
Basically, what we need to do is to dinamically calculate the height of each tab scene and pass it to the style of the TabView using the onLayout prop.
Just like this:
const renderScene = ({ route }) => {
switch (route.key) {
case "inifiniteScrollFlatList":
return (
<FirstRoute />
);
case "rawDataFlatList":
return (
<View
onLayout={(event) => setTab1Height(event.nativeEvent.layout.height + TAB_HEIGHT)}
>
<SecondRoute />
</View>
);
case "otherRawDataFlatList":
return (
<View
onLayout={(event) => setTab2Height(event.nativeEvent.layout.height + TAB_HEIGHT)}
>
<ThirdRoute />
</View>
);
default:
return null;
}
};
<TabView
style={ index !== 0 && {
height: index === 1 ? tab1Height : tab2Height,
}}
renderTabBar={renderTabBar}
navigationState={{ index, routes }}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={initialLayout}
removeClippedSubviews={false} // Pd: Don't enable this on iOS where this is buggy and views don't re-appear.
swipeEnabled={true}
/>
Pd: You shouldn't do this with a tab that uses an infinite scroll with pagination. Instead, you will have to set the height to null to allow the parent FlatList to automatically get its height.
So, I haven't gone through the entire code, nor did I find a solution to your height problem.
But, what you can do is check out React navigation 5 -> createMaterialTopTabNavigator.
It lets you create tabs with separate flatlist for each tab. It will solve the height problem because what is being rendered are separate flatlists as per the active tab. And it will also, make your code much cleaner.
You won't have to use a Flatlist with header and footer components to render the tabs with nested Flatlists.
And if you want to hide the tabs on scroll, that is possible by passing props to the tab navigator that toggle visibility on scroll of the Flatlist using the onScroll event that is called when a Flatlist is scrolled. The same can be done for the visibility of the header as well. And with the proper animations it would look as if the header and the tabs were pushed up on scroll like it does right now.
I ended up removing the tabs content from the tab control altogether. It's hacky but worked for me...
render() {
const {index} = this.state;
return (
<ScrollView>
<TabView
renderPager={this._renderPager}
renderScene={() => null}
onIndexChange={index => this.setState({index})}
initialLayout={{height: 0, width: Dimensions.get('window').width}}
/>
{index === 0 && <Tab1Content />}
{index === 1 && <Tab2Content />}
</ScrollView>
);
Source:- https://github.com/satya164/react-native-tab-view/issues/290#issuecomment-447941998

Cells overlapping problems in react virtualized with cell measurer

I am using React Virtualized to window my items list which has the following functionality
1) onClicking on any item, the user will be prompted with a details panel where he will be able to update the details of the item. And when he goes back to the list, he will be able to see the details in the item cell in the list. He can add a lot of details to make it bigger or reduce the size by removing the details
2) He can delete the item, or add another item to the list with certain details or without details.
CellMeasurer serves my requirement as dynamic height is supported. But I am having following issues with it
1) initially when my list mounts for the first time, first few items are measured and shown correctly but as soon as I scroll to the end, items get overlapped with each other.(positining isnt correct, I am guessing the defaultHeight is being applied to the unmeasured cells). This works fine as soon as the list is rerendered again.
2) Also, when I am updating the details of an item the list doesnt update with the new height adjustments
I am sure that somewhere my implementation is incorrect but have spent a lot of time hammering my head for it. Please let me know what can be done here to fix this
ItemView = props => {
const {index, isScrolling, key, style} = props,
items = this.getFormattedItems()[index]
return (
<CellMeasurer
cache={this._cache}
columnIndex={0}
isScrolling={isScrolling}
key={key}
rowIndex={index}
parent={parent}>
{({measure, registerChild}) => (
<div key={key} style={style} onLoad={measure}>
<div
onLoad={measure}
ref={registerChild}
{...parentProps}>
<Item
onLoad={measure}
key={annotation.getId()}
{...childProps}
/>
</div>
</div>
)}
</CellMeasurer>
)
}
renderDynamicListOfItems(){
return (<div>
<List
height={500}
ref={listComponent => (_this.listComponent = listComponent)}
style={{
width: '100%'
}}
onRowsRendered={()=>{}}
width={1000}
rowCount={props.items.length}
rowHeight={this._cache.rowHeight}
rowRenderer={memoize(this.ItemView)}
// onScroll={onChildScroll}
className={listClassName}
overscanRowCount={0}
/>
</div>
)
}
Also, I am manually triggering the remeasurement of my item in its componentDidUpdate like follows()
Component Item
...
componentDidUpdate() {
console.log('loading called for ', this.props.annotation.getId())
this.props.onLoad()
}
...
In the main parent I am recomputing the heights of the list every time the list has updated and triggering a forceupdate as follows
Component ParentList
...
componentDidUpdate() {
console.log("calling this parent recomputing")
this.listComponent.recomputeRowHeights()
this.listComponent.forceUpdateGrid()
}
...
I just faced the same issue and it turned out to be some layout updates in my Item component, that changed the height of the item after it has been measured by the CellMeasurer.
Fix was to pass down the measure function and call it on every layout update.
In my case it was
images being loaded
and text being manipulated with Twemoji (which replaces the emojis with images).
I also had problems getting the correct height from the cache after prepending some new list items when scrolling to the top of the list. This could be fixed by providing a keyMapper to the CellMeasurerCache:
const [cache, setCache] = useState<CellMeasurerCache>();
useEffect(() => {
setCache(new CellMeasurerCache({
keyMapper: (rowIndex: number, columnIndex: number) => listItems[rowIndex].id,
defaultHeight: 100,
fixedWidth: true,
}));
}, [listItems]);

I have a <List /> with row items of unknown DOM height

Given a react-virtualized List with variable content in each row the DOM height needs to be calculated by a rowHeight function - however since that gets called before the row is rendered I am unsure how to actually get the row height.
The examples given for dynamic List row height basically go off a predefined number in the list item's props which doesn't help.
What I think I want to do is render the row on the page at a default height and then get the overflow height on the DOM and set that as the row height. How can I hook into an afterRowRender function or something like that? I imagine performance would suffer so maybe there is a better way of doing this that I am missing.
Check out the docs on CellMeasurer. You can see it in action here.
Basically you'll be looking for something like this:
import React from 'react';
import { CellMeasurer, CellMeasurerCache, Grid } from 'react-virtualized';
const cache = new CellMeasurerCache({
fixedWidth: true,
minHeight: 50,
});
function rowRenderer ({ index, isScrolling, key, parent, style }) {
const source // This comes from your list data
return (
<CellMeasurer
cache={cache}
columnIndex={0}
key={key}
parent={parent}
rowIndex={index}
>
{({ measure }) => (
// 'style' attribute required to position cell (within parent List)
<div style={style}>
// Your content here
</div>
)}
</CellMeasurer>
);
}
function renderList (props) {
return (
<List
{...props}
deferredMeasurementCache={cache}
rowHeight={cache.rowHeight}
rowRenderer={rowRenderer}
/>
);
}

Categories

Resources