Implement smooth text transition in React Native - react-native

I try to implement component that takes takes text property and depend on previous value shows smooth transition:
import React, { useEffect, useRef } from 'react'
import { Animated, StyleSheet } from 'react-native'
const usePrevious = (value) => {
const ref = useRef()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
export default function AnimatedText({ style, children }) {
const fadeInValue = new Animated.Value(0)
const fadeOutValue = new Animated.Value(1)
const prevChildren = usePrevious(children)
useEffect(() => {
if (children != prevChildren) {
animate()
}
}, [children])
const animate = () => {
Animated.parallel([
Animated.timing(fadeInValue, {
toValue: 1,
duration: 1000,
useNativeDriver: true
}),
Animated.timing(fadeOutValue, {
toValue: 0,
duration: 1000,
useNativeDriver: true
})
]).start()
}
return (
<>
<Animated.Text style={[ style, { opacity: fadeInValue }]}>{children}</Animated.Text>
{
prevChildren &&
<Animated.Text style={[ style, styles.animatedText, { opacity: fadeOutValue }]}>{prevChildren}</Animated.Text>
}
</>
)
}
const styles = StyleSheet.create({
animatedText: {
position: 'absolute',
top: 0,
left: 0,
}
})
As the result I got smooth transition between component rendering with different children arguments. But there is a flickering due to some reasons related to animated value updates. Is there any way to avoid this problem or better solution to implement such component?

Found the solution with react-native-reanimated. It's not elegant implementation but it seems to work correctly without flickering:
import React, { useEffect, useRef } from 'react'
import { StyleSheet } from 'react-native'
import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated'
const usePrevious = (value) => {
const ref = useRef()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
export default function AnimatedText({ style, children }) {
const fadeValue1 = useSharedValue(0)
const fadeValue2 = useSharedValue(1)
const toggleFlagRef = useRef(false)
const animatedTextStyle1 = useAnimatedStyle(() => {
return {
opacity: withTiming(fadeValue1.value, { duration: 1000 })
}
})
const animatedTextStyle2 = useAnimatedStyle(() => {
return {
opacity: withTiming(fadeValue2.value, { duration: 1000 })
}
})
const prevChildren = usePrevious(children)
useEffect(() => {
if (children != prevChildren) {
animate()
}
}, [children])
const animate = () => {
if (toggleFlagRef.current) {
fadeValue1.value = 0
fadeValue2.value = 1
} else {
fadeValue1.value = 1
fadeValue2.value = 0
}
toggleFlagRef.current = !toggleFlagRef.current
}
return (
<>
<Animated.Text style={[ style, animatedTextStyle1 ]}>{toggleFlagRef.current ? prevChildren : children}</Animated.Text>
{
prevChildren &&
<Animated.Text style={[ style, styles.animatedText, animatedTextStyle2 ]}>{toggleFlagRef.current ? children : prevChildren}</Animated.Text>
}
</>
)
}
const styles = StyleSheet.create({
animatedText: {
position: 'absolute',
top: 0,
left: 0,
}
})

Related

Reanimated 2 reusable animation in custom hook

How can I create a reusable React hook with animation style with Reanimated 2? I have an animation that is working on one element, but if I try to use the same animation on multiple elements on same screen only the first one registered is animating. It is too much animation code to duplicate it everywhere I need this animation, so how can I share this between multiple components on the same screen? And tips for making the animation simpler is also much appreciated.
import {useEffect} from 'react';
import {
cancelAnimation,
Easing,
useAnimatedStyle,
useSharedValue,
withRepeat,
withSequence,
withTiming,
} from 'react-native-reanimated';
const usePulseAnimation = ({shouldAnimate}: {shouldAnimate: boolean}) => {
const titleOpacity = useSharedValue(1);
const isAnimating = useSharedValue(false);
useEffect(() => {
if (shouldAnimate && !isAnimating.value) {
isAnimating.value = true;
titleOpacity.value = withRepeat(
withSequence(
withTiming(0.2, {duration: 700, easing: Easing.inOut(Easing.ease)}),
withTiming(
1,
{duration: 700, easing: Easing.inOut(Easing.ease)},
() => {
if (!shouldAnimate) {
cancelAnimation(titleOpacity);
}
},
),
),
-1,
false,
() => {
if (titleOpacity.value < 1) {
titleOpacity.value = withSequence(
withTiming(0.2, {
duration: 700,
easing: Easing.inOut(Easing.ease),
}),
withTiming(
1,
{duration: 700, easing: Easing.inOut(Easing.ease)},
() => {
isAnimating.value = false;
},
),
);
} else {
titleOpacity.value = withTiming(
1,
{
duration: 700,
easing: Easing.inOut(Easing.ease),
},
() => {
isAnimating.value = false;
},
);
}
},
);
} else {
isAnimating.value = false;
cancelAnimation(titleOpacity);
}
}, [shouldAnimate, isAnimating, titleOpacity]);
const pulseAnimationStyle = useAnimatedStyle(() => {
return {
opacity: titleOpacity.value,
};
});
return {pulseAnimationStyle, isAnimating: isAnimating.value};
};
export default usePulseAnimation;
And I am using it like this inside a component:
const {pulseAnimationStyle} = usePulseAnimation({
shouldAnimate: true,
});
return (
<Animated.View
style={[
{backgroundColor: 'white', height: 100, width: 100},
pulseAnimationStyle,
]}
/>
);
The approach that I've taken is to write my Animations as wrapper components.
This way you can build up a library of these animation components and then simply wrap whatever needs to be animated.
e.g.
//Wrapper component type:
export type ShakeProps = {
// Animation:
children: React.ReactNode;
repeat?: boolean;
repeatEvery?: number;
}
// Wrapper component:
const Shake: FC<ShakeProps> = ({
children,
repeat = false,
repeatEvery = 5000,
}) => {
const shiftY = useSharedValue(0);
const animatedStyles = useAnimatedStyle(() => ({
//Animation properties...
}));
const shake = () => {
//Update shared values...
}
// Loop every X seconds:
const repeatAnimation = () => {
shake();
setTimeout(() => {
repeatAnimation();
}, repeatEvery);
}
// Start Animations on component Init:
useEffect(() => {
// Run animation continously:
if(repeat){
repeatAnimation();
}
// OR ~ call once:
else{
shake();
}
}, []);
return (
<Animated.View style={[animatedStyles]}>
{children}
</Animated.View>
)
}
export default Shake;
Wrapper Component Usage:
import Shake from "../../util/animated-components/shake";
const Screen: FC = () => {
return (
<Shake repeat={true} repeatEvery={5000}>
{/* Whatever needs to be animated!...e.g. */}
<Text>Hello World!</Text>
</Shake>
)
}
From their docs:
CAUTION
Animated styles cannot be shared between views.
To work around this you can generate multiple useAnimatedStyle in top-level loop (number of iterations must be static, see React's Rules of Hooks for more information).
https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

Onscroll native event update all same type wrapper components

I have a wrapper flat list component used in react navigation library.
This component is in different stacknavigation tab to handle the header's animation.
import React, { Component } from "react";
import { Constants } from 'expo';
// import PropTypes from "prop-types";
import {
Animated,
Dimensions,
// PanResponder,
// Platform,
// ScrollView,
StyleSheet,
FlatList,
// ScrollView,
// StatusBar,
// Text,
// TouchableWithoutFeedback,
// View
} from "react-native";
// import Icon from "react-native-vector-icons/Ionicons";
// Get screen dimensions
const { width, height } = Dimensions.get("window");
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
const HEADER_HEIGHT= 40;
const FILTER_HEIGHT= 50;
const STATUS_BAR_HEIGHT = Constants.statusBarHeight;
const NAVBAR_HEIGHT = HEADER_HEIGHT+FILTER_HEIGHT-2;
const scrollAnim = new Animated.Value(0);
const offsetAnim = new Animated.Value(0);
export default class AnimatedFlatListComp extends React.PureComponent {
// Define state
state = {
scrollAnim,
offsetAnim,
clampedScroll: Animated.diffClamp(
Animated.add(
scrollAnim.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolateLeft: 'clamp',
}),
offsetAnim,
),
0,
// NAVBAR_HEIGHT - STATUS_BAR_HEIGHT,
HEADER_HEIGHT //i mede this one cuz the code abode not work is the value 40
),
};
componentWillUnmount() {
console.log('smontoooo');
// this._isMounted = false;
// Don't forget to remove the listeners!
// this.state.scrollAnim.removeAllListeners();
// this.state.offsetAnim.removeAllListeners();
this._disableListener();
}
componentDidMount() {
this._clampedScrollValue = 0;
this._offsetValue = 0;
this._scrollValue = 0;
this._enableLister()
this._handleScroll()
}
_onMomentumScrollBegin = () => {
console.log('_onMomentumScrollBegin');
clearTimeout(this._scrollEndTimer);
}
_onScrollEndDrag = () => {
this._scrollEndTimer = setTimeout(this._onMomentumScrollEnd, 250);
}
_onMomentumScrollEnd = () => {
console.log('_onMomentumScrollEnd');
console.log(this._scrollValue, NAVBAR_HEIGHT, this._clampedScrollValue, (NAVBAR_HEIGHT - STATUS_BAR_HEIGHT) / 2);
const toValue = this._scrollValue > NAVBAR_HEIGHT &&
this._clampedScrollValue > (NAVBAR_HEIGHT - STATUS_BAR_HEIGHT) / 2
? this._offsetValue + NAVBAR_HEIGHT
: this._offsetValue - NAVBAR_HEIGHT;
Animated.timing(this.state.offsetAnim, {
toValue,
duration: 350,
useNativeDriver: true,
}).start();
}
_handleScroll = () => this.props._handleScroll(this.state.clampedScroll)
// _handleScroll = event => {
// const { y } = event.nativeEvent.contentOffset;
// // // console.log(y);
// this.setState({ scrollOffset: y }, () => {
// this.props._handleScroll(this.state.clampedScroll)
// });
//
// };
_scrollToTop = () => {
console.log('_scrollToTop');
if (!!this.flatListRef) {
// this.flatListRef.getNode().scrollTo({ y: 0, animated: true });
this.flatListRef.getNode().scrollToOffset({ offset: 0, animated: true });
}
};
_enableLister = () => {
// this._firstMountFunction();
this.state.scrollAnim.addListener(({ value }) => {
// This is the same calculations that diffClamp does.
const diff = value - this._scrollValue;
this._scrollValue = value;
this._clampedScrollValue = Math.min(
Math.max(this._clampedScrollValue + diff, 0),
NAVBAR_HEIGHT - STATUS_BAR_HEIGHT,
);
});
this.state.offsetAnim.addListener(({ value }) => {
this._offsetValue = value;
});
}
_disableListener = () => {
this.state.scrollAnim.removeAllListeners();
this.state.offsetAnim.removeAllListeners();
}
_keyExtractor = (item, index) => index.toString();
// _onScroll = event => {
//
// }
render() {
return (
<AnimatedFlatList
{...this.props}
ref={(ref) => { this.flatListRef = ref; }}
showsVerticalScrollIndicator={false}
onScroll={Animated.event(
[{nativeEvent: {contentOffset: {y: this.state.scrollAnim}}}],
{
useNativeDriver: true,
// listener: this._handleScroll
},
)}
// onScroll={this._onScroll}
removeClippedSubviews={true}
keyExtractor={this._keyExtractor}
onMomentumScrollBegin={this._onMomentumScrollBegin}
onMomentumScrollEnd={this._onMomentumScrollEnd}
onScrollEndDrag={this._onScrollEndDrag}
scrollEventThrottle={1}
/>
);
}
}
this is the parent
_handleScroll = clampedScroll => this.setState({ clampedScroll: clampedScroll })
render(){
const { clampedScroll } = this.state;
//
const navbarTranslate = clampedScroll.interpolate({
inputRange: [0, NAVBAR_HEIGHT - STATUS_BAR_HEIGHT],
outputRange: [0, -(NAVBAR_HEIGHT - STATUS_BAR_HEIGHT)],
extrapolate: 'clamp',
});
return (
<AnimatedFlatList
// debug={true}
ref={(ref) => { this.flatListRef = ref; }}
maxToRenderPerBatch={4}
contentContainerStyle={{
paddingTop: NAVBAR_HEIGHT+STATUS_BAR_HEIGHT,
}}
data={this.state.dataSource}
renderItem={
({item, index}) =>
<CardAgenda
item={JSON.parse(item.JSON)}
ChangeSelectedEvent={this.ChangeSelectedEvent}
colorTrail={JSON.parse(item.colorTrail)}
// _sendBackdata={this._getChildrenCategoryData}
searchData={JSON.parse(item.searchData)}
NumAncillary={item.NumAncillary}
indexItinerary={item.id}
index={index}
/>
}
ListEmptyComponent={this._emptyList}
ItemSeparatorComponent={() => <View style={{width: width-40, backgroundColor: 'rgba(0,0,0,0.1)', height: 1, marginTop: 20, marginLeft: 20, marginRight: 20}}/>}
_handleScroll={this._handleScroll}
/>
)}
Its working fine but onscroll event triggers the this.state.scrollAnim variable of ALL wrappers.
I mean if i scroll up the first AnimatedFlatList the header goes up but also the different one header in new navigation page goes up.
The correct behavior must be that all header must be independent to the own flatlist.
Thanks in advance
This is because you are setting up a reference to the state when creating animated Values obj. You should not keep them as constants outside your class boundary.
Try remove your following constants.
const scrollAnim = new Animated.Value(0);
const offsetAnim = new Animated.Value(0);
Then define them inside the constructor.
export default class AnimatedFlatListComp extends React.PureComponent {
constructor(props){
super(props);
this.scrollAnim = new Animated.Value(0);
this.offsetAnim = new Animated.Value(0);
// Define state
state = {
scrollAnim: this.scrollAnim,
offsetAnim: this.offsetAnim,
clampedScroll: Animated.diffClamp(
Animated.add(
this.scrollAnim.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolateLeft: 'clamp',
}),
this.offsetAnim,
),
0,
// NAVBAR_HEIGHT - STATUS_BAR_HEIGHT,
HEADER_HEIGHT //i mede this one cuz the code abode not work is
the value 40
),
};
}

How to remove network request failed error screen and display message "No internet connection" in react-native

How to remove network request failed error screen and display message "No internet connection" for better user experience in react-native when there is no internet connection.
You can use NetInfo in React-Native to check network state. This is link:
https://facebook.github.io/react-native/docs/netinfo.html
This is example code:
NetInfo.getConnectionInfo().then((connectionInfo) => {
if (connectionInfo.type === 'none') {
alert("No internet connection")
} else {
// online
// do something
}
});
Here i wrote an component for handling internet status issues refer this:
import React, { Component } from "react";
import {
View,
NetInfo,
Animated,
Easing,
Dimensions,
Platform,
AppState
} from "react-native";
// import { colors, Typography, primaryFont } from "../../Config/StylesConfig";
import { connect } from "react-redux";
import { changeConnectionStatus } from "../../actions/authActions";
import CustomText from "./CustomText";
import firebaseHelper from "../../helpers/firebaseHelper";
const { width } = Dimensions.get("window");
class InternetStatusBar extends Component {
constructor(props) {
super(props);
this.state = {
isNetworkConnected: true
};
this._updateConnectionStatus = this._updateConnectionStatus.bind(this);
this.positionValue = new Animated.Value(-26);
this.colorValue = new Animated.Value(0);
this.isMount = true;
this.isOnline = true;
}
_handleAppStateChange = nextAppState => {
if (nextAppState.match(/inactive|background/) && this.isOnline) {
firebaseHelper.goOffline();
// console.log("offline");
this.isOnline = false;
} else if (nextAppState === "active" && this.isOnline === false) {
firebaseHelper.goOnline();
// console.log("online");
this.isOnline = true;
}
};
componentDidMount() {
AppState.addEventListener("change", this._handleAppStateChange);
// NetInfo.isConnected.fetch().done(isNetworkConnected => {
// this._updateConnectionStatus(isNetworkConnected);
// });
NetInfo.isConnected.addEventListener(
"connectionChange",
this._updateConnectionStatus
);
}
componentWillUnmount() {
AppState.removeEventListener("change", this._handleAppStateChange);
NetInfo.isConnected.removeEventListener(
"connectionChange",
this._updateConnectionStatus
);
}
_updateConnectionStatus(isNetworkConnected) {
// this.setState({ isNetworkConnected });
if (this.isMount) {
this.isMount = false;
} else {
if (isNetworkConnected) {
this.animateColorChange(isNetworkConnected);
setTimeout(() => {
this.animateErrorView(isNetworkConnected);
}, 1000);
} else {
this.animateErrorView(isNetworkConnected);
this.colorValue = new Animated.Value(0);
}
}
// this.props.changeConnectionStatus(isNetworkConnected);
}
// componentWillReceiveProps = nextProps => {
// if (
// nextProps.isInternetConnected &&
// nextProps.isInternetConnected != this.state.isInternetConnected
// ) {
// const date = new Date();
// Actions.refresh({ refreshContent: date.getTime() });
// }
// };
animateErrorView(connected) {
Animated.timing(this.positionValue, {
toValue: connected ? -40 : Platform.OS === "ios" ? 20 : 0,
easing: Easing.linear,
duration: 600
}).start();
}
animateColorChange(connected) {
Animated.timing(this.colorValue, {
toValue: connected ? 150 : 0,
duration: 800
}).start();
}
render() {
return (
<Animated.View
style={[
{
position: "absolute",
backgroundColor: this.colorValue.interpolate({
inputRange: [0, 150],
outputRange: ["rgba(0,0,0,0.6)", "rgba(75, 181, 67, 0.8)"]
}),
zIndex: 1,
width: width,
top: 0,
transform: [{ translateY: this.positionValue }]
}
]}
>
<View
style={[
{
padding: 4,
flexDirection: "row",
flex: 1
}
]}
>
<CustomText
style={{
fontSize: 12,
textAlign: "center",
flex: 1
}}
>
{this.state.isInternetConnected ? "Back online" : "No connection"}
</CustomText>
</View>
</Animated.View>
);
}
}
const mapStateToProps = state => {
return {
isInternetConnected: state.user.isInternetConnected
};
};
export default connect(mapStateToProps, { changeConnectionStatus })(
InternetStatusBar
);
A Snackbar is a good way to convey this. Have a look at this library :
https://github.com/9gag-open-source/react-native-snackbar-dialog
Easy and Simple with good user experience
use "react-native-offline-status"
Reference:
https://github.com/rgabs/react-native-offline-status

React native signed APK crash

Signed APK crash after launch, in logCat i got requiring unknown module 'React'
Debug application works fine, but in logCat i got >> Requiring module 'React' by name is only supported for debugging purposes and will BREAK IN PRODUCTION!
React v15.4.1, React native v0.39.2 ?
Sorry for my english
this is my index.android.js
import React from 'react';
import {AppRegistry} from 'react-native';
import myapp from './index_start.js';
AppRegistry.registerComponent('myapp', () => myapp);
and index_start.js
import React, { Component } from "react";
import {
StyleSheet,
AppRegistry,
Text,
Image,
View,
AsyncStorage,
NetInfo,
StatusBar,
Navigator,
Dimensions
} from 'react-native';
// Window dismensions
const { width, height } = Dimensions.get('window');
// Device infos
import DeviceInfo from 'react-native-device-info';
// Native SplashScreen
import SplashScreen from 'react-native-splash-screen';
// Spinner
import Spinner from 'react-native-spinkit';
// Models
import User from './model/UserModel';
// Json data for initial launch
var DB = require('./DB.json');
// Components
import Stage from './components/stage/stage.js'
import Player from './components/player/player.js'
import Settings from './components/settings/settings.js'
import House from './stages/house/house.js'
// LocalStorage key
var USER_KEY = 'user_key';
const routes = [
{name: 'loading'},
{name: 'stage', component: Stage},
{name: 'house', component: House},
{name: 'settings', component: Settings}
];
const _navigator = null;
export default class myapp extends Component {
constructor(props) {
super(props);
this.state = {
isConnected: false,
isLoading: true,
_navigator: null,
stages: null
}
}
componentWillMount() {
// check if connected
this._checkConnexionType();
}
componentDidMount() {
SplashScreen.hide();
this._loadInitialData();
}
componentDidUpdate() {
// console.log(this.state.stages)
if (!this.state.isLoading && this.state.stages !== null) {
_navigator.push({
name: 'stage',
passProps: {
data: this.state.stages
}
})
}
}
/**
* Load localStorage Data
*/
async _loadInitialData() {
// GET User LocalStorage
if (this.state.stages == null) {
var localData;
//AsyncStorage.removeItem(USER_KEY)
AsyncStorage.getItem(USER_KEY).then((data) => {
if (data !== null) {
var localData = JSON.parse(data);
// User.uuid = localData.uuid;
User.setStages(localData.stages)
this.setState({
'stages' : localData.stages
})
} else {
var storage = {};
storage.setUiid = DeviceInfo.getUniqueID();
storage.stages = DB.stages;
AsyncStorage.setItem(USER_KEY, JSON.stringify(storage));
this.setState({
'stages' : DB.stages
})
}
})
}
if (this.state.isConnected) {
// var rStages = this._loadRemoteStages();
// console.log(rStages);
}
// Change state
setTimeout((function() {
this.setState({
'isLoading': false
})
}).bind(this), 1500);
}
/**
* GET stages from remote DB
*/
async _loadRemoteStages() {
await fetch(API_URL)
.then((response) => response.json())
.then((responseJson) => {
console.log(responseJson)
return responseJson;
})
.catch((error) => {
console.error(error);
});
}
/**
* CHECK IF user is connected to Network
* SET bool to state isLoading
*/
_checkConnexionType() {
NetInfo.isConnected.fetch().then(response => {
this.setState({ isConnected: response})
})
}
_renderScene(route, navigator) {
_navigator = navigator;
if (route.name == 'loading') {
return (
<View style={styles.container}>
<StatusBar hidden={true} />
<Image
style={{width: width, height: height}}
source={require('./img/screen.jpg')}
/>
<View style={styles.loading}>
<Text style={styles.loadingText}>CHARGEMENT</Text>
<Spinner type="ThreeBounce" color={'#fff'}/>
</View>
</View>
)
} else if (route.name == 'stage') {
return (
<Stage navigator={_navigator} {...route.passProps}/>
)
} else if (route.name == 'player') {
return (
<House navigator={_navigator} {...route.passProps}}/>
)
} else if (route.name == 'settings') {
return (
<Settings navigator={_navigator} {...route.passProps}/>
)
}
}
render() {
return (
<Navigator
initialRoute={{name: 'loading'}}
configureScene={() => Navigator.SceneConfigs.FloatFromBottomAndroid}
renderScene={this._renderScene.bind(this)}
/>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
loading: {
flex: 1,
position: 'absolute',
bottom: 50,
left: 0,
right: 0,
alignItems: 'center',
},
loadingText:{
flex: 1,
fontFamily: 'CarterOne',
fontSize: 20,
color: '#fff'
}
});

Scrollable image with pinch-to-zoom in react-native

I'm trying to display an image in my React Native app (Android) and I want to give users an ability to zoom that image in and out.
This also requires the image to be scrollable once zoomed in.
How would I go about it?
I tried to use ScrollView to display a bigger image inside, but on Android it can either scroll vertically or horizontally, not both ways.
Even if that worked there is a problem of making pinch-to-zoom work.
As far as I understand I need to use PanResponder on a custom view to zoom an image and position it accordingly. Is there an easier way?
I ended up rolling my own ZoomableImage component. So far it's been working out pretty well, here is the code:
import React, { Component } from "react";
import { View, PanResponder, Image } from "react-native";
import PropTypes from "prop-types";
function calcDistance(x1, y1, x2, y2) {
const dx = Math.abs(x1 - x2);
const dy = Math.abs(y1 - y2);
return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
}
function calcCenter(x1, y1, x2, y2) {
function middle(p1, p2) {
return p1 > p2 ? p1 - (p1 - p2) / 2 : p2 - (p2 - p1) / 2;
}
return {
x: middle(x1, x2),
y: middle(y1, y2)
};
}
function maxOffset(offset, windowDimension, imageDimension) {
const max = windowDimension - imageDimension;
if (max >= 0) {
return 0;
}
return offset < max ? max : offset;
}
function calcOffsetByZoom(width, height, imageWidth, imageHeight, zoom) {
const xDiff = imageWidth * zoom - width;
const yDiff = imageHeight * zoom - height;
return {
left: -xDiff / 2,
top: -yDiff / 2
};
}
class ZoomableImage extends Component {
constructor(props) {
super(props);
this._onLayout = this._onLayout.bind(this);
this.state = {
zoom: null,
minZoom: null,
layoutKnown: false,
isZooming: false,
isMoving: false,
initialDistance: null,
initialX: null,
initalY: null,
offsetTop: 0,
offsetLeft: 0,
initialTop: 0,
initialLeft: 0,
initialTopWithoutZoom: 0,
initialLeftWithoutZoom: 0,
initialZoom: 1,
top: 0,
left: 0
};
}
processPinch(x1, y1, x2, y2) {
const distance = calcDistance(x1, y1, x2, y2);
const center = calcCenter(x1, y1, x2, y2);
if (!this.state.isZooming) {
const offsetByZoom = calcOffsetByZoom(
this.state.width,
this.state.height,
this.props.imageWidth,
this.props.imageHeight,
this.state.zoom
);
this.setState({
isZooming: true,
initialDistance: distance,
initialX: center.x,
initialY: center.y,
initialTop: this.state.top,
initialLeft: this.state.left,
initialZoom: this.state.zoom,
initialTopWithoutZoom: this.state.top - offsetByZoom.top,
initialLeftWithoutZoom: this.state.left - offsetByZoom.left
});
} else {
const touchZoom = distance / this.state.initialDistance;
const zoom =
touchZoom * this.state.initialZoom > this.state.minZoom
? touchZoom * this.state.initialZoom
: this.state.minZoom;
const offsetByZoom = calcOffsetByZoom(
this.state.width,
this.state.height,
this.props.imageWidth,
this.props.imageHeight,
zoom
);
const left =
this.state.initialLeftWithoutZoom * touchZoom + offsetByZoom.left;
const top =
this.state.initialTopWithoutZoom * touchZoom + offsetByZoom.top;
this.setState({
zoom,
left:
left > 0
? 0
: maxOffset(left, this.state.width, this.props.imageWidth * zoom),
top:
top > 0
? 0
: maxOffset(top, this.state.height, this.props.imageHeight * zoom)
});
}
}
processTouch(x, y) {
if (!this.state.isMoving) {
this.setState({
isMoving: true,
initialX: x,
initialY: y,
initialTop: this.state.top,
initialLeft: this.state.left
});
} else {
const left = this.state.initialLeft + x - this.state.initialX;
const top = this.state.initialTop + y - this.state.initialY;
this.setState({
left:
left > 0
? 0
: maxOffset(
left,
this.state.width,
this.props.imageWidth * this.state.zoom
),
top:
top > 0
? 0
: maxOffset(
top,
this.state.height,
this.props.imageHeight * this.state.zoom
)
});
}
}
_onLayout(event) {
const layout = event.nativeEvent.layout;
if (
layout.width === this.state.width &&
layout.height === this.state.height
) {
return;
}
const zoom = layout.width / this.props.imageWidth;
const offsetTop =
layout.height > this.props.imageHeight * zoom
? (layout.height - this.props.imageHeight * zoom) / 2
: 0;
this.setState({
layoutKnown: true,
width: layout.width,
height: layout.height,
zoom,
offsetTop,
minZoom: zoom
});
}
componentWillMount() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onStartShouldSetPanResponderCapture: () => true,
onMoveShouldSetPanResponder: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onPanResponderGrant: () => {},
onPanResponderMove: evt => {
const touches = evt.nativeEvent.touches;
if (touches.length === 2) {
this.processPinch(
touches[0].pageX,
touches[0].pageY,
touches[1].pageX,
touches[1].pageY
);
} else if (touches.length === 1 && !this.state.isZooming) {
this.processTouch(touches[0].pageX, touches[0].pageY);
}
},
onPanResponderTerminationRequest: () => true,
onPanResponderRelease: () => {
this.setState({
isZooming: false,
isMoving: false
});
},
onPanResponderTerminate: () => {},
onShouldBlockNativeResponder: () => true
});
}
render() {
return (
<View
style={this.props.style}
{...this._panResponder.panHandlers}
onLayout={this._onLayout}
>
<Image
style={{
position: "absolute",
top: this.state.offsetTop + this.state.top,
left: this.state.offsetLeft + this.state.left,
width: this.props.imageWidth * this.state.zoom,
height: this.props.imageHeight * this.state.zoom
}}
source={this.props.source}
/>
</View>
);
}
}
ZoomableImage.propTypes = {
imageWidth: PropTypes.number.isRequired,
imageHeight: PropTypes.number.isRequired,
source: PropTypes.object.isRequired
};
export default ZoomableImage;
There's a much easier way now.
Just make a ScollView with minimumZoomScale and maximumZoomScale:
import React, { Component } from 'react';
import { AppRegistry, ScrollView, Text } from 'react-native';
export default class IScrolledDownAndWhatHappenedNextShockedMe extends Component {
render() {
return (
<ScrollView minimumZoomScale={1} maximumZoomScale={5} >
<Text style={{fontSize:96}}>Scroll me plz</Text>
<Text style={{fontSize:96}}>If you like</Text>
<Text style={{fontSize:96}}>Scrolling down</Text>
<Text style={{fontSize:96}}>What's the best</Text>
<Text style={{fontSize:96}}>Framework around?</Text>
<Text style={{fontSize:80}}>React Native</Text>
</ScrollView>
);
}
}
// skip these lines if using Create React Native App
AppRegistry.registerComponent(
'AwesomeProject',
() => IScrolledDownAndWhatHappenedNextShockedMe);
In my case I have to add images inside Viewpager with Zoom functionality.
So I have used these two library.
import ViewPager from '#react-native-community/viewpager'
import PhotoView from 'react-native-photo-view-ex';
which you can install from.
npm i #react-native-community/viewpager
npm i react-native-photo-view-ex
So I have used this code.
class ResumeView extends React.Component {
render() {
preivewArray = this.props.showPreview.previewArray
var pageViews = [];
for (i = 0; i < preivewArray.length; i++) {
pageViews.push(<View style={style.page}>
<PhotoView
source={{ uri: preivewArray[i].filePath }}
minimumZoomScale={1}
maximumZoomScale={3}
// resizeMode='stretch'
style={{ width: a4_width, height: a4_height, alignSelf: 'center' }} />
</View>);
}
return (
<ViewPager
onPageScroll={this.pageScroll}
style={{ width: '100%', height: a4_height }}>
{pageViews}
</ViewPager>
)
}
pageScroll = (event) => {
console.log("onPageScroll")
}
}
You can simply use the react-native-image-zoom-viewer or react-native-image-pan-zoom library for that. Using this libraries you don't have to code manually.
npm i react-native-photo-view-ex
import PhotoView from 'react-native-photo-view-ex';
<PhotoView
style={{ flex: 1, width: '100%', height: '100%' }}
source={{ uri: this.state.filePath }} // you can supply any URL as well
minimumZoomScale={1} // max value can be 1
maximumZoomScale={2} // max value can be 3
/>
Don't go deep if you are working with react-native because things will go more and more complex as deep you go.
Give it a try...
npm i react-native-image-zoom-viewer --save
or
yarn add react-native-image-zoom-viewer
copy this code and put it in app.js and hit Run button.
import React from 'react';
import {View} from 'react-native';
import ImageViewer from 'react-native-image-zoom-viewer';
const image = [
{
url:
'https://static8.depositphotos.com/1020341/896/i/950/depositphotos_8969502-stock-photo-human-face-with-cracked-texture.jpg',
},
];
const App = () => {
return (
<View style={{flex: 1}}>
<ImageViewer imageUrls={image} />
</View>
);
};
export default App;
Features like zoom, pan, tap/swipe to switch image using react-native-gesture-handler,react-native-reanimated. Perfectly and smoothly running on android/ios.
USAGE
<ImageZoomPan
uri={'https://picsum.photos/200/300'}
activityIndicatorProps={{
color: COLOR_SECONDARY,
}}
onInteractionStart={onInteractionStart}
onInteractionEnd={onInteractionEnd}
onLongPressActiveInteration={onPressIn}
onLongPressEndInteration={onPressOut}
onSwipeTapForNext={onSwipeTapForNext}
onSwipeTapForPrev={onSwipeTapForPrev}
minScale={0.8}
onLoadEnd={start}
resizeMode={isFullScreen ? 'cover' : 'contain'} //'stretch'
/>
Image Zoom component
import React, {useRef, useState} from 'react';
import {ActivityIndicator,Dimensions, Image} from 'react-native';
import {
LongPressGestureHandler,
PanGestureHandler,
PinchGestureHandler,
State,
TapGestureHandler,
} from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import styles from './styles';
const clamp = (value, min, max) => {
'worklet';
return Math.min(Math.max(min, value), max);
};
const noop = () => {};
const getDeviceWidth = () => {
return Dimensions.get('window').width;
};
const AnimatedImage = Animated.createAnimatedComponent(Image);
export default function ImageZoom({
uri = '',
minScale = 1,
maxScale = 5,
minPanPointers = 2,
maxPanPointers = 2,
isPanEnabled = true,
isPinchEnabled = true,
onLoadEnd = noop,
onInteractionStart = noop,
onInteractionEnd = noop,
onPinchStart = noop,
onPinchEnd = noop,
onPanStart = noop,
onPanEnd = noop,
onLongPressActiveInteration = noop,
onLongPressEndInteration = noop,
onSwipeTapForNext = noop,
onSwipeTapForPrev = noop,
style = {},
containerStyle = {},
imageContainerStyle = {},
activityIndicatorProps = {},
renderLoader,
resizeMode = 'cover',
...props
}) {
const panRef = useRef();
const pinchRef = useRef();
const isInteracting = useRef(false);
const isPanning = useRef(false);
const isPinching = useRef(false);
const doubleTapRef = useRef(null);
const [isLoading, setIsLoading] = useState(true);
const [state, setState] = useState({
canInteract: false,
centerX: 0,
centerY: 0,
});
const {canInteract, centerX, centerY} = state;
const scale = useSharedValue(1);
const initialFocalX = useSharedValue(0);
const initialFocalY = useSharedValue(0);
const focalX = useSharedValue(0);
const focalY = useSharedValue(0);
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const onInteractionStarted = () => {
if (!isInteracting.current) {
isInteracting.current = true;
onInteractionStart();
}
};
const onInteractionEnded = () => {
if (isInteracting.current && !isPinching.current && !isPanning.current) {
isInteracting.current = false;
onInteractionEnd();
}
};
const onPinchStarted = () => {
onInteractionStarted();
isPinching.current = true;
onPinchStart();
};
const onPinchEnded = () => {
isPinching.current = false;
onPinchEnd();
onInteractionEnded();
};
const onPanStarted = () => {
onInteractionStarted();
isPanning.current = true;
onPanStart();
};
const onPanEnded = () => {
isPanning.current = false;
onPanEnd();
onInteractionEnded();
};
const panHandler = useAnimatedGestureHandler({
onActive: event => {
translateX.value = event.translationX;
translateY.value = event.translationY;
},
onFinish: () => {
translateX.value = withTiming(0);
translateY.value = withTiming(0);
},
});
const pinchHandler = useAnimatedGestureHandler({
onStart: event => {
initialFocalX.value = event.focalX;
initialFocalY.value = event.focalY;
},
onActive: event => {
// onStart: focalX & focalY result both to 0 on Android
if (initialFocalX.value === 0 && initialFocalY.value === 0) {
initialFocalX.value = event.focalX;
initialFocalY.value = event.focalY;
}
scale.value = clamp(event.scale, minScale, maxScale);
focalX.value = (centerX - initialFocalX.value) * (scale.value - 1);
focalY.value = (centerY - initialFocalY.value) * (scale.value - 1);
},
onFinish: () => {
scale.value = withTiming(1);
focalX.value = withTiming(0);
focalY.value = withTiming(0);
initialFocalX.value = 0;
initialFocalY.value = 0;
},
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{translateX: translateX.value},
{translateY: translateY.value},
{translateX: focalX.value},
{translateY: focalY.value},
{scale: scale.value},
],
}));
const onLayout = ({
nativeEvent: {
layout: {x, y, width, height},
},
}) => {
setState(current => ({
...current,
canInteract: true,
centerX: x + width / 2,
centerY: y + height / 2,
}));
};
const onImageLoadEnd = () => {
onLoadEnd();
setIsLoading(false);
};
const onLongPress = event => {
if (event.nativeEvent.state === State.ACTIVE) {
onLongPressActiveInteration();
}
if (
event.nativeEvent.state === State.END ||
event.nativeEvent.state === State.CANCELLED
) {
onLongPressEndInteration();
}
};
const onSingleTapEvent = event => {
let e = event.nativeEvent;
if (e.state === State.ACTIVE) {
if (e.x < getDeviceWidth() / 2) {
onSwipeTapForPrev();
} else {
onSwipeTapForNext();
}
}
};
return (
<PinchGestureHandler
ref={pinchRef}
simultaneousHandlers={[panRef]}
onGestureEvent={pinchHandler}
onActivated={onPinchStarted}
onCancelled={onPinchEnded}
onEnded={onPinchEnded}
onFailed={onPinchEnded}
enabled={isPinchEnabled && canInteract}>
<Animated.View style={[styles.container, containerStyle]}>
<PanGestureHandler
ref={panRef}
simultaneousHandlers={[pinchRef]}
onGestureEvent={panHandler}
onActivated={onPanStarted}
onCancelled={onPanEnded}
onEnded={onPanEnded}
onFailed={onPanEnded}
minPointers={minPanPointers}
maxPointers={maxPanPointers}
enabled={isPanEnabled && canInteract}>
<Animated.View
onLayout={onLayout}
style={[styles.content, imageContainerStyle]}>
<TapGestureHandler
waitFor={doubleTapRef}
onHandlerStateChange={onSingleTapEvent}>
<TapGestureHandler
ref={doubleTapRef}
onHandlerStateChange={() => null}
numberOfTaps={2}>
<LongPressGestureHandler
onHandlerStateChange={onLongPress}
minDurationMs={800}>
<AnimatedImage
style={[styles.container, style, animatedStyle]}
source={{uri}}
resizeMode={resizeMode}
onLoadEnd={onImageLoadEnd}
{...props}
/>
</LongPressGestureHandler>
</TapGestureHandler>
</TapGestureHandler>
{isLoading &&
(renderLoader ? (
renderLoader()
) : (
<ActivityIndicator
size="large"
style={styles.loader}
color="dimgrey"
{...activityIndicatorProps}
/>
))}
</Animated.View>
</PanGestureHandler>
</Animated.View>
</PinchGestureHandler>
);
}