How to land on the image a user has clicked in image swipe? - react-native-reanimated

My React Native 0.64 app uses react-native-gesture-handler 1.16/react-native-reanimated 2.1 to swipe images left and right. The idea is when user clicks a image in image gallery, then the image was displayed occupying the full screen and the rest of images in the gallery can be viewed one by one by swipe left or right. Here is the full code which swipe works but with one catch: the image clicked is not presented but the first image in gallery is always shown first. How to land on the image a user has clicked?
import React, { useRef } from "react";
import { Dimensions, StyleSheet, View, Platform } from "react-native";
import FastImage from 'react-native-fast-image';
import Animated, {
useAnimatedStyle,
useSharedValue,
useAnimatedRef,
useAnimatedGestureHandler,
useAnimatedReaction,
withTiming,
withSpring,
useDerivedValue,
} from "react-native-reanimated";
import {
snapPoint,
} from "react-native-redash";
import {
PanGestureHandler,
PinchGestureHandler,
State,
TouchableOpacity,
} from "react-native-gesture-handler";
const { width, height } = Dimensions.get("window");
export default Swipe = ({route, navigation}) => {
const {images, initIndex} = route.params; //initIndex is the index of image clicked
console.log("route params swipe ", route.params);
const index = useSharedValue(initIndex);
const snapPoints = images.map((_, i) => i * -width);
const panRef = useAnimatedRef();
const pinchRef = useAnimatedRef();
const offsetX = initIndex ? useSharedValue(initIndex * -width) : useSharedValue(0); //The init offsetX is set according to initIndex a user clicked.
const translationX = useSharedValue(0);
const translateX = useSharedValue(0); //if translateX was assgined with offsetX, then swipe stops working
//for snap in swipe
const lowVal = useDerivedValue(() => {return (index.value + 1) * -width}); //shared value, with .value
const highVal = useDerivedValue(() => {return (index.value - 1) * -width}); //shared value, with .value
const snapPt = (e) => {
"worklet";
return snapPoint(translateX.value, e.velocityX, snapPoints);
};
const panHandler = useAnimatedGestureHandler(
{
onStart: () => {
translateX.value = offsetX.value; //As soon as a user swipe, this line allow the
//image is moved to initIndex-th image. But how to land on the image a user has clicked?
},
onActive: (event) => {
translateX.value = offsetX.value + event.translationX;
translationX.value = event.translationX;
},
onEnd: (event) => {
let val = snapPt(event);
let sp = Math.min(Math.max(lowVal.value, val), highVal.value); //clamp in RN redash
if (event.translationX !== 0) {
translateX.value = withTiming(sp);
offsetX.value = translateX.value;
};
//update index
index.value = Math.floor(translateX.value/-width);
},
}, []
)
const swipeStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: translateX.value}]
}
});
return (
<PanGestureHandler
onGestureEvent={panHandler}
ref={panRef}
minDist={10}
avgTouches
>
<Animated.View style={styles.container}>
<Animated.View
style={[{width: width * images.length, height, flexDirection: "row"}, swipeStyle]}
>
{images.map((img_source, i) => {
const isActive = useDerivedValue(() => {return index.value === i});
return (
<View key={i} style={styles.picture}>
<View style={[
styles.image,
]}>
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.button}>
<View style={styles.button}>
<FastImage
source={{uri:img_source.path}}
resizeMode={FastImage.resizeMode.contain}
style={styles.image}
/>
</View>
</TouchableOpacity>
</View>
</View>
);
})}
</Animated.View>
</Animated.View>
</PanGestureHandler>
);
}
const styles = StyleSheet.create({
container: {
...StyleSheet.absoluteFillObject,
backgroundColor: "black",
},
picture: {
width,
height,
overflow: "hidden",
},
image: {
//...StyleSheet.absoluteFillObject,
flex:1,
width: undefined,
height: undefined,
//resizeMode: "cover",
},
button: {
width: width,
height:height,
},
});
Another issue the code is that when touching the image before swipe the image jerks left or right 1/3-1/2 screen width. This makes the swipe un-smooth. I would like to know how to eliminate the image jerk and make the swipe smooth from beginning to the end.

To land on the image a user has clicked, a useEffect hook is added :
useEffect(() => {
translateX.value = offsetX.value;
}, [initIndex]);
The onStart needs to be removed completely.
The reason for image jerking is caused by misrepresenting of offsetX value. The offsetX shall be at the border of the screen (multiple of the screen width). However when the image width is less than the screen width, the offsetX may be at the border of the image which is a little less or more (depending on if swipe left or swipe right) and this causes the image jerking left or right when swiping initially. In this case, the offsetX needs to be set to the border of the screen (multiple of screen width).

Related

change the id inside an svg on scroll at viewport in React

So I have an svg with an animation, but I want the animation to start when I'm scolled on viewport. I cant quite get there tho.
My code:
export const useScrollHandler = () => {
const [scroll, setScroll] = useState(1);
useEffect(() => {
const onScroll = () => {
const scrollCheck = window.scrollY > 100;
setScroll(scrollCheck);
};
document.addEventListener("scroll", onScroll);
return () => {
document.removeEventListener("scroll", onScroll);
};
}, [scroll, setScroll]);
return scroll;
};
then I add it on my svg component ( i wont put the whole svg cause its too long):
const ImageEssentials = (props) => {
const scroll = useScrollHandler();
<svg ...>
<g id={scroll ? "" : "Box1"}>
</svg>
}
So basicly when I scroll i want the svg group to get the id "Box1" that it has the animation.

how to update the value of a react semantic UI progress bar by clicking it?

I would like to be able to click on a react semantic UI progress bar to update its value.
Let's say I would click the exact middle of the bar, and its value would update to 50%.
Is that possible ?
<Progress
percent={value}
/>
Thanks !
Well, I made this ProgressClickable component that wraps the Progress one.
Maybe it could be improved, but, hey, it works.
import React from "react";
import { Progress } from 'semantic-ui-react'
export const ProgressClickable = (props) => {
const [mousePercent,setmousePercent] = React.useState(0);
const updateMousePercent = (e) => {
var rect = e.target.getBoundingClientRect();
var width = rect.right - rect.left; //width of component
var mouseX = e.clientX - rect.left; //pixel position of mouse within component
var percent = mouseX / width * 100;
setmousePercent(percent);
}
const handleOnMouseUp = (e) => {
if (typeof props.onMouseUp === 'function') {
props.onMouseUp(e,mousePercent);
}
}
const cssStyle = {
pointerEvents: props.disabled ? 'none' : 'auto',
cursor: 'pointer'
}
return (
<Progress
percent={props.percent}
size={props.size}
onMouseMove={updateMousePercent}
onMouseUp={handleOnMouseUp}
disabled={props.disabled}
style={cssStyle}
/>
);
}

How set react-select width depending on biggest option width?

How set react-select input width depending on biggest option width? Options position is absolute and therefore their size not affect parent div width.
It is example: https://codesandbox.io/s/9o4rkklz14
I need to select width was longest option width.
I've answered this in a few issue threads here and here for more background about complexities and caveats, but here is the gist of the approach.
You need to set the width of the menu to be auto so it will stretch to fit its contents.
onMount, style the menu (height: 0, visibility: hidden) and open the menu with the internal ref method
With the onMenuOpen prop, use a function that waits 1ms, get the width of the listMenuRef, and close/reset the menuIsOpen.
With the calculated width, you can now set the width of the ValueContainer.
The result can be seen here at this codesandbox
Code posted below...
import React, { useState, useRef, useEffect } from "react";
import Select from "react-select";
const App = (props) => {
const selectRef = useRef();
const [menuIsOpen, setMenuIsOpen] = useState();
const [menuWidth, setMenuWidth] = useState();
const [isCalculatingWidth, setIsCalculatingWidth] = useState(false);
useEffect(() => {
if (!menuWidth && !isCalculatingWidth) {
setTimeout(() => {
setIsCalculatingWidth(true);
// setIsOpen doesn't trigger onOpenMenu, so calling internal method
selectRef.current.select.openMenu("first");
setMenuIsOpen(true);
}, 1);
}
}, [menuWidth, isCalculatingWidth]);
const onMenuOpen = () => {
if (!menuWidth && isCalculatingWidth) {
setTimeout(() => {
const width = selectRef.current.select.menuListRef.getBoundingClientRect()
.width;
setMenuWidth(width);
setIsCalculatingWidth(false);
// setting isMenuOpen to undefined and closing menu
selectRef.current.select.onMenuClose();
setMenuIsOpen(undefined);
}, 1);
}
};
const styles = {
menu: (css) => ({
...css,
width: "auto",
...(isCalculatingWidth && { height: 0, visibility: "hidden" })
}),
control: (css) => ({ ...css, display: "inline-flex " }),
valueContainer: (css) => ({
...css,
...(menuWidth && { width: menuWidth })
})
};
const options = [
{ label: "Option 1", value: 1 },
{ label: "Option 2", value: 2 },
{ label: "Option 3 is a reallly realllly long boi", value: 3 }
];
return (
<div>
<div> Width of menu is {menuWidth}</div>
<Select
ref={selectRef}
onMenuOpen={onMenuOpen}
options={options}
styles={styles}
menuIsOpen={menuIsOpen}
/>
</div>
);
};
export default App;

React Native: Different styles applied on orientation change

I'm developing a React Native application to be deployed as a native application on iOS and Android (and Windows, if possible).
The problem is that we want the layout to be different depending on screen dimensions and its orientation.
I've made some functions that return the styles object and are called on every component render's function, so I am able to apply different styles at application startup, but if the orientation (or screen's size) changes once the app has been initialized, they aren't recalculated nor reapplied.
I've added listeners to the top rendered so it updates its state on orientation change (and it forces a render for the rest of the application), but the subcomponents are not rerendering (because, in fact, they have not been changed).
So, my question is: how can I make to have styles that may be completely different based on screen size and orientation, just as with CSS Media Queries (which are rendered on the fly)?
I've already tried react-native-responsive module without luck.
Thank you!
If using Hooks. You can refer to this solution: https://stackoverflow.com/a/61838183/5648340
The orientation of apps from portrait to landscape and vice versa is a task that sounds easy but may be tricky in react native when the view has to be changed when orientation changes. In other words, having different views defined for the two orientations can be achieved by considering these two steps.
Import Dimensions from React Native
import { Dimensions } from 'react-native';
To identify the current orientation and render the view accordingly
/**
* Returns true if the screen is in portrait mode
*/
const isPortrait = () => {
const dim = Dimensions.get('screen');
return dim.height >= dim.width;
};
/**
* Returns true of the screen is in landscape mode
*/
const isLandscape = () => {
const dim = Dimensions.get('screen');
return dim.width >= dim.height;
};
To know when orientation changes to change view accordingly
// Event Listener for orientation changes
Dimensions.addEventListener('change', () => {
this.setState({
orientation: Platform.isPortrait() ? 'portrait' : 'landscape'
});
});
Assembling all pieces
import React from 'react';
import {
StyleSheet,
Text,
Dimensions,
View
} from 'react-native';
export default class App extends React.Component {
constructor() {
super();
/**
* Returns true if the screen is in portrait mode
*/
const isPortrait = () => {
const dim = Dimensions.get('screen');
return dim.height >= dim.width;
};
this.state = {
orientation: isPortrait() ? 'portrait' : 'landscape'
};
// Event Listener for orientation changes
Dimensions.addEventListener('change', () => {
this.setState({
orientation: isPortrait() ? 'portrait' : 'landscape'
});
});
}
render() {
if (this.state.orientation === 'portrait') {
return (
//Render View to be displayed in portrait mode
);
}
else {
return (
//Render View to be displayed in landscape mode
);
}
}
}
As the event defined for looking out the orientation change uses this command ‘this.setState()’, this method automatically again calls for ‘render()’ so we don’t have to worry about rendering it again, it’s all taken care of.
Here's #Mridul Tripathi's answer as a reusable hook:
// useOrientation.tsx
import {useEffect, useState} from 'react';
import {Dimensions} from 'react-native';
/**
* Returns true if the screen is in portrait mode
*/
const isPortrait = () => {
const dim = Dimensions.get('screen');
return dim.height >= dim.width;
};
/**
* A React Hook which updates when the orientation changes
* #returns whether the user is in 'PORTRAIT' or 'LANDSCAPE'
*/
export function useOrientation(): 'PORTRAIT' | 'LANDSCAPE' {
// State to hold the connection status
const [orientation, setOrientation] = useState<'PORTRAIT' | 'LANDSCAPE'>(
isPortrait() ? 'PORTRAIT' : 'LANDSCAPE',
);
useEffect(() => {
const callback = () => setOrientation(isPortrait() ? 'PORTRAIT' : 'LANDSCAPE');
Dimensions.addEventListener('change', callback);
return () => {
Dimensions.removeEventListener('change', callback);
};
}, []);
return orientation;
}
You can then consume it using:
import {useOrientation} from './useOrientation';
export const MyScreen = () => {
const orientation = useOrientation();
return (
<View style={{color: orientation === 'PORTRAIT' ? 'red' : 'blue'}} />
);
}
You can use the onLayout prop:
export default class Test extends Component {
constructor(props) {
super(props);
this.state = {
screen: Dimensions.get('window'),
};
}
getOrientation(){
if (this.state.screen.width > this.state.screen.height) {
return 'LANDSCAPE';
}else {
return 'PORTRAIT';
}
}
getStyle(){
if (this.getOrientation() === 'LANDSCAPE') {
return landscapeStyles;
} else {
return portraitStyles;
}
}
onLayout(){
this.setState({screen: Dimensions.get('window')});
}
render() {
return (
<View style={this.getStyle().container} onLayout = {this.onLayout.bind(this)}>
</View>
);
}
}
}
const portraitStyles = StyleSheet.create({
...
});
const landscapeStyles = StyleSheet.create({
...
});
Finally, I've been able to do so. Don't know the performance issues it can carry, but they should not be a problem since it's only called on resizing or orientation change.
I've made a global controller where I have a function which receives the component (the container, the view) and adds an event listener to it:
const getScreenInfo = () => {
const dim = Dimensions.get('window');
return dim;
}
const bindScreenDimensionsUpdate = (component) => {
Dimensions.addEventListener('change', () => {
try{
component.setState({
orientation: isPortrait() ? 'portrait' : 'landscape',
screenWidth: getScreenInfo().width,
screenHeight: getScreenInfo().height
});
}catch(e){
// Fail silently
}
});
}
With this, I force to rerender the component when there's a change on orientation, or on window resizing.
Then, on every component constructor:
import ScreenMetrics from './globalFunctionContainer';
export default class UserList extends Component {
constructor(props){
super(props);
this.state = {};
ScreenMetrics.bindScreenDimensionsUpdate(this);
}
}
This way, it gets rerendered everytime there's a window resize or an orientation change.
You should note, however, that this must be applied to every component which we want to listen to orientation changes, since if the parent container is updated but the state (or props) of the children do not update, they won't be rerendered, so it can be a performance kill if we have a big children tree listening to it.
Hope it helps someone!
I made a super light component that addresses this issue.
https://www.npmjs.com/package/rn-orientation-view
The component re-renders it's content upon orientation change.
You can, for example, pass landscapeStyles and portraitStyles to display these orientations differently.
Works on iOS and Android.
It's easy to use. Check it out.
React Native also have useWindowDimensions hooks that returns the width and height of your device.
With this, you can check easily if the device is in 'Portrait' or 'Landscape' by comparing the width and height.
See more here
I had the same problem. After the orientation change the layout didn't change.
Then I understood one simple idea - layout should depend on screen width that should be calculated inside render function, i.e.
getScreen = () => {
return Dimensions.get('screen');
}
render () {
return (
<View style={{ width: this.getScreen().width }>
// your code
</View>
);
}
In that case, the width will be calculated at the moment of render.
** I am using this logic for my landscape and portrait Logic.**
** by this if I launch my app in landscape first I am getting the real height of my device. and manage the hight of the header accordingly.**
const [deviceOrientation, setDeviceOrientation] = useState(
Dimensions.get('window').width < Dimensions.get('window').height
? 'portrait'
: 'landscape'
);
const [deviceHeight, setDeviceHeight] = useState(
Dimensions.get('window').width < Dimensions.get('window').height
? Dimensions.get('window').height
: Dimensions.get('window').width
);
useEffect(() => {
const setDeviceHeightAsOrientation = () => {
if (Dimensions.get('window').width < Dimensions.get('window').height) {
setDeviceHeight(Dimensions.get('window').height);
} else {
setDeviceHeight(Dimensions.get('window').width);
}
};
Dimensions.addEventListener('change', setDeviceHeightAsOrientation);
return () => {
//cleanup work
Dimensions.removeEventListener('change', setDeviceHeightAsOrientation);
};
});
useEffect(() => {
const deviceOrientation = () => {
if (Dimensions.get('window').width < Dimensions.get('window').height) {
setDeviceOrientation('portrait');
} else {
setDeviceOrientation('landscape');
}
};
Dimensions.addEventListener('change', deviceOrientation);
return () => {
//cleanup work
Dimensions.removeEventListener('change', deviceOrientation);
};
});
console.log(deviceHeight);
if (deviceOrientation === 'landscape') {
return (
<View style={[styles.header, { height: 60, paddingTop: 10 }]}>
<TitleText>{props.title}</TitleText>
</View>
);
} else {
return (
<View
style={[
styles.header,
{
height: deviceHeight >= 812 ? 90 : 60,
paddingTop: deviceHeight >= 812 ? 36 : 10
}
]}>
<TitleText>{props.title}</TitleText>
</View>
);
}
I have, by far, had the most success with this library: https://github.com/axilis/react-native-responsive-layout
It does what you are asking for and a lot more. Simple Component implementation without hardly any logic like some of the more complex answers above. My project is using Phone, Tablet, and web via RNW - and the implementation is flawless. Additionally when resizing the browser it's truly responsive, and not just on initial rendering - handling phone orientation changes flawlessly.
Example code (Put any components as children of blocks):
<Grid>
<Section> {/* Light blue */}
<Block xsSize="1/1" smSize="1/2" />
<Block xsSize="1/1" smSize="1/2" />
<Block xsSize="1/1" smSize="1/2" />
</Section>
<Section> {/* Dark blue */}
<Block size="1/1" smSize="1/2" />
<Block size="1/1" smSize="1/2" />
<Block size="1/1" smSize="1/2" />
<Block size="1/1" smSize="1/2" />
<Block size="1/1" smSize="1/2" />
</Section>
</Grid>
To give this:
I have written a HoC solution for my expo SDK36 project, it support orientation change and pass props.orientation based on ScreenOrientation.Orientation value.
import React, { Component } from 'react';
import { ScreenOrientation } from 'expo';
export default function withOrientation(Component) {
class DetectOrientation extends React.Component {
constructor(props) {
super(props);
this.state = {
orientation: '',
};
this.listener = this.listener.bind(this);
}
UNSAFE_componentWillMount() {
this.subscription = ScreenOrientation.addOrientationChangeListener(this.listener);
}
componentWillUnmount() {
ScreenOrientation.removeOrientationChangeListener(this.subscription);
}
listener(changeEvent) {
const { orientationInfo } = changeEvent;
this.setState({
orientation: orientationInfo.orientation.split('_')[0],
});
}
async componentDidMount() {
await this.detectOrientation();
}
async detectOrientation() {
const { orientation } = await ScreenOrientation.getOrientationAsync();
this.setState({
orientation: orientation.split('_')[0],
});
}
render() {
return (
<Component
{...this.props}
{...this.state}
onLayout={this.detectOrientation}
/>
);
}
}
return (props) => <DetectOrientation {...props} />;
}
To achieve a more performant integration, I used the following as a superclass for each of my react-navigation screens:
export default class BaseScreen extends Component {
constructor(props) {
super(props)
const { height, width } = Dimensions.get('screen')
// use this to avoid setState errors on unmount
this._isMounted = false
this.state = {
screen: {
orientation: width < height,
height: height,
width: width
}
}
}
componentDidMount() {
this._isMounted = true
Dimensions.addEventListener('change', () => this.updateScreen())
}
componentWillUnmount() {
this._isMounted = false
Dimensions.removeEventListener('change', () => this.updateScreen())
}
updateScreen = () => {
const { height, width } = Dimensions.get('screen')
if (this._isMounted) {
this.setState({
screen: {
orientation: width < height,
width: width, height: height
}
})
}
}
Set any root components to extend from this component, and then pass the screen state to your leaf/dumb components from the inheriting root components.
Additionally, to keep from adding to the performance overhead, change the style object instead of adding more components to the mix:
const TextObject = ({ title }) => (
<View style={[styles.main, screen.orientation ? styles.column : styles.row]}>
<Text style={[styles.text, screen.width > 600 ? {fontSize: 14} : null ]}>{title}</Text>
</View>
)
const styles = StyleSheet.create({
column: {
flexDirection: 'column'
},
row: {
flexDirection: 'row'
},
main: {
justifyContent: 'flex-start'
},
text: {
fontSize: 10
}
}
I hope this helps anyone in the future, and you'll find it to be quite optimal in terms of overhead.
I'm using styled-components, and this is how I re-render the UI on orientation change.
import React, { useState } from 'react';
import { View } from 'react-native';
import { ThemeProvider } from 'styled-components';
import appTheme from 'constants/appTheme';
const App = () => {
// Re-Layout on orientation change
const [theme, setTheme] = useState(appTheme.getTheme());
const onLayout = () => {
setTheme(appTheme.getTheme());
}
return (
<ThemeProvider theme={theme}>
<View onLayout={onLayout}/>
{/* Components */}
</ThemeProvider>
);
}
export default App;
Even if you're not using styled-components, you can create a state and update it on onLayout to re-render the UI.
This is my solution:
const CheckOrient = () => {
console.log('screenHeight:' + Dimensions.get('screen').height + ', screenWidth: ' + Dimensions.get('screen').width);
}
return ( <
View onLayout = {
() => CheckOrient()
} >
............
<
/View>
Note for the case with a pure component. #mridul-tripathi answer works correctly, but if a pure component is used, then probably only parent/top-level component reacting to orientation change is not enough. You will also need to update a pure component separately on orientation change.
All you need is:
import { useWindowDimensions } from 'react-native';
export default function useOrientation() {
const window = useWindowDimensions();
return window.height >= window.width ? 'portrait' : 'landscape';
}
You need useWindowDimensions
This hook re-render component when dimension change and apply styles but Dimensions object can't re-render component and change style, it just work in first render
import { useWindowDimensions } from 'react-native';
then destructure it
const { height, width } = useWindowDimensions();
and final you can do like this
import React from "react";
import { View, StyleSheet, useWindowDimensions } from "react-native";
const App = () => {
const { height, width } = useWindowDimensions();
const isPortrait = height > width;
return (
<View style={isPortrait ? styles.portrait : styles.landscape}>
{/* something */}
</View>
);
};
const styles = StyleSheet.create({
portrait: {},
landscape: {},
});
export default App;
also you can use scale property
const { scale } = useWindowDimensions();
read this document
https://reactnative.dev/docs/usewindowdimensions

Simulate display: inline in React Native

React Native doesn't support the CSS display property, and by default all elements use the behavior of display: flex (no inline-flex either). Most non-flex layouts can be simulated with flex properties, but I'm flustered with inline text.
My app has a container that contains several words in text, some of which need formatting. This means I need to use spans to accomplish the formatting. In order to achieve wrapping of the spans, I can set the container to use flex-wrap: wrap, but this will only allow wrapping at the end of a span rather than the traditional inline behavior of wrapping at word breaks.
The problem visualized (spans in yellow):
(via http://codepen.io/anon/pen/GoWmdm?editors=110)
Is there a way to get proper wrapping and true inline simulation using flex properties?
You can get this effect by wrapping text elements in other text elements the way you would wrap a span in a div or another element:
<View>
<Text><Text>This writing should fill most of the container </Text><Text>This writing should fill most of the container</Text></Text>
</View>
You can also get this effect by declaring a flexDirection:'row' property on the parent along with a flexWrap: 'wrap'. The children will then display inline:
<View style={{flexDirection:'row', flexWrap:'wrap'}}>
<Text>one</Text><Text>two</Text><Text>Three</Text><Text>Four</Text><Text>Five</Text>
</View>
Check out this example.
https://rnplay.org/apps/-rzWGg
You can only nest text nodes without using flex to get the desired effect.
Like this: https://facebook.github.io/react-native/docs/text
<Text style={{fontWeight: 'bold'}}>
I am bold
<Text style={{color: 'red'}}>
and red
</Text>
</Text>
I haven't found a proper way to inline text blocks with other content. Our current "hackish" workaround is to split every single word in a text string into its own block so flexWrap wraps properly for each word.
I had the following use case:
I needed a text that can wrap with different sizes, and throughout that text, I wanted to underscore some of the words (to indicate that they are clickable).
It's quite simple expect for the case that you can't control the underline in any way (how close is it, what color is it, so on) - this led me through the rabbit hole, and eventually coming up with the solution of splitting every word, and wrapping it in separate Text component, wrapped with View.
I'll paste the code here:
import React from 'react';
import { StyleSheet, View, TouchableOpacity, Text } from 'react-native';
import Colors from '../../styles/Colors';
import Fonts from '../../styles/Fonts';
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
export default class SaltText extends React.Component {
getTheme (type) {
if (type === 'robomonoregular10gray') {
return {
fontSize: Fonts.SIZES.TEN,
fontFamily: Fonts.ROBOTOMONO_REGULAR,
color: Colors.getColorOpacity(Colors.GRAY, 70),
lineHeight: Fonts.SIZES.TEN + 10
};
}
throw new Error('not supported');
}
splitText (text) {
const parts = [];
const maps = [];
let currentPart = '';
let matchIndex = 0;
for (const letter of text) {
const isOpening = letter === '[';
const isClosing = letter === ']';
if (!isOpening && !isClosing) {
currentPart += letter;
continue;
}
if (isOpening) {
parts.push(currentPart);
currentPart = '';
}
if (isClosing) {
parts.push(`[${matchIndex}]`);
maps.push(currentPart);
currentPart = '';
matchIndex++;
}
}
const partsModified = [];
for (const part of parts) {
const splitted = part
.split(' ')
.filter(f => f.length);
partsModified.push(...splitted);
}
return { parts: partsModified, maps };
}
render () {
const textProps = this.getTheme(this.props.type);
const children = this.props.children;
const getTextStyle = () => {
return {
...textProps,
};
};
const getTextUnderlineStyle = () => {
return {
...textProps,
borderBottomWidth: 1,
borderColor: textProps.color
};
};
const getViewStyle = () => {
return {
flexDirection: 'row',
flexWrap: 'wrap',
};
};
const { parts, maps } = this.splitText(children);
return (
<View style={getViewStyle()}>
{parts.map((part, index) => {
const key = `${part}_${index}`;
const isLast = parts.length === index + 1;
if (part[0] === '[') {
const mapIndex = part.substring(1, part.length - 1);
const val = maps[mapIndex];
const onPressHandler = () => {
this.props.onPress(parseInt(mapIndex, 10));
};
return (
<View key={key} style={getTextUnderlineStyle()}>
<Text style={getTextStyle()} onPress={() => onPressHandler()}>
{val}{isLast ? '' : ' '}
</Text>
</View>
);
}
return (
<Text key={key} style={getTextStyle()}>
{part}{isLast ? '' : ' '}
</Text>
);
})}
</View>
);
}
}
and usage:
renderPrivacy () {
const openTermsOfService = () => {
Linking.openURL('https://reactnativecode.com');
};
const openPrivacyPolicy = () => {
Linking.openURL('https://reactnativecode.com');
};
const onUrlClick = (index) => {
if (index === 0) {
openTermsOfService();
}
if (index === 1) {
openPrivacyPolicy();
}
};
return (
<SaltText type="robomonoregular10gray" onPress={(index) => onUrlClick(index)}>
By tapping Create an account or Continue, I agree to SALT\'s [Terms of Service] and [Privacy Policy]
</SaltText>
);
}
this is the end result:
Try this, simple and clean.
<Text style={{ fontFamily: 'CUSTOM_FONT', ... }}>
<Text>Lorem ipsum</Text>
<Text style={{ color: "red" }}> dolor sit amet.</Text>
</Text>
Result:
Lorem ipsum dolor sit amet.

Resources