Record button snapchat style with expo/react-native - react-native

I'm trying to create a record button component for an expo camera app.
I didn't eject my app and I would like to keep it like that if possible.
So I did a component that is working:
import React from 'react';
import {TouchableWithoutFeedback, Animated, StyleSheet, View} from 'react-native';
import { Svg } from 'expo';
const AnimatedPath = Animated.createAnimatedComponent(Svg.Path);
const PRESSIN_DELAY = 200
export default class RecordButton extends React.Component {
constructor(props) {
super(props);
this.state = {
progress: new Animated.Value(0),
}
this.stopping = false
}
componentWillMount(){
let R = this.props.radius;
let strokeWidth = 7
let dRange = [];
let iRange = [];
let steps = 359;
for (var i = 0; i<steps; i++){
dRange.push(this.describeArc(R+strokeWidth+2/2, R+strokeWidth+2/2, R, 0, i));
iRange.push(i/(steps-1));
}
var _d = this.state.progress.interpolate({
inputRange: iRange,
outputRange: dRange
})
this.animationData = {
R: R,
strokeWidth: strokeWidth,
_d: _d,
}
}
describeArc = (x, y, radius, startAngle, endAngle)=>{
var start = this.polarToCartesian(x, y, radius, endAngle);
var end = this.polarToCartesian(x, y, radius, startAngle);
var largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
var d = [
"M", start.x, start.y,
"A", radius, radius, 0, largeArcFlag, 0, end.x, end.y
].join(" ");
return d;
}
polarToCartesian(centerX, centerY, radius, angleInDegrees) {
var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
}
startAnimation = async ()=>{
this.Animation = Animated.timing(this.state.progress,{
toValue:1,
duration:this.props.duration,
})
this.props.onPressIn().then(res => {
if(res){
this.Animation.start(()=>{this.stopAnimation(false)})
}
})
}
stopAnimation = (abort=true) => {
if(!this.stopping){
this.stopping = true
abort && this.Animation.stop()
Animated.timing(this.state.progress,{
toValue: 0,
duration:0,
}).start()
this.props.onPressOut()
this.stopping = false
}
}
_HandlePressIn = async ()=>{
this.timerTimeStamp = Date.now()
this.timer = setTimeout(()=>{
this.startAnimation()
}, PRESSIN_DELAY)
}
_HandlePressOut = async ()=>{
let timeStamp = Date.now()
if(timeStamp - this.timerTimeStamp <= PRESSIN_DELAY){
clearTimeout(this.timer)
this.props.onJustPress()
}else{
this.stopAnimation()
}
}
render() {
const { R, _d, strokeWidth } = this.animationData
return (
<View style={this.props.style}>
<TouchableWithoutFeedback style={styles.touchableStyle} onPress={this.props.onPress} onPressIn={this._HandlePressIn} onPressOut={this._HandlePressOut}>
<Svg
style={styles.svgStyle}
height={R*2+13}
width={R*2+13}
>
<Svg.Circle
cx={R+strokeWidth+2/2}
cy={R+strokeWidth+2/2}
r={R}
stroke="grey"
strokeWidth={strokeWidth+1}
fill="none"
/>
<Svg.Circle
cx={R+strokeWidth+2/2}
cy={R+strokeWidth+2/2}
r={R}
stroke="white"
strokeWidth={strokeWidth}
fill="none"
/>
<AnimatedPath
d={_d}
stroke="red"
strokeWidth={strokeWidth}
fill="none"
/>
</Svg>
</TouchableWithoutFeedback>
</View>
);
}
}
const styles = StyleSheet.create({
svgStyle:{
flex:1,
},
touchableStyle: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
});
So everything is almost fine (not really performant but that's ok for now)
The problem is when the parent view trigger a re-render with just a setState for exemple. This is really time consuming, like 3-5 seconds. So I think it's because each time it need to re-do all the math ...
But I'm quite new to expo/react-native and even more with animation so I don't know how to optimise it. Or if there is a better way to do what I want.
Edit: I notice that it was slow when I tried to show the button after hiding it with a setState in the parent component.
So I found a workaround: I just create a method that is doing the same but inside the recordButton. Like that, the re-render doesn't do the math again, it's more performant. I think there's a better way to do it so I'm still aware of other solutions...

Related

React native svg heart size animation

I am trying to make a pulsing svg heart in ReactNative expo using an SVG image.
The only way I managed to make the heart to resize with an animated value is to change bind it to style: fontSize.
This seems to change size correctly, but the animation is really choppy.
Here is the code:
import React, { Component } from 'react';
import { Animated } from 'react-native';
import { SimpleLineIcons } from '#expo/vector-icons';
const AnimatedIcon = Animated.createAnimatedComponent(SimpleLineIcons);
const TARGET_FONT_SIZE = 16;
const GROWN_FONT_SIZE = 24;
class GrowingHeart extends Component<any, any> {
size = new Animated.Value(TARGET_FONT_SIZE);
constructor(props) {
super(props);
Animated.sequence([
Animated.timing(this.size, {
duration: 1000,
toValue: GROWN_FONT_SIZE
}),
Animated.timing(this.size, {
duration: 1000,
toValue: GROWN_FONT_SIZE
})
]).start();
}
render() {
return (
<AnimatedIcon
style={{ fontSize: this.size }}
size={20}
name="heart"
color="red"
/>
);
}
}
I tried also to bind width and height but they are also choppy + they change container size, rather than the icon.
Is there a better way of doing this? Thanks
Seems that Animated api is simply still rubbish (please correct me if I am getting this wrong)
I re-wrote this with reanimated and it works smooth. The code below is not complete but shows how heart is growing with no choppiness, but rather perfectly smooth.
import React, { Component } from 'react';
import { TouchableOpacity } from 'react-native-gesture-handler';
import Animated, { Easing } from 'react-native-reanimated';
import { SimpleLineIcons } from '#expo/vector-icons';
const {
createAnimatedComponent,
debug,
set,
get,
block,
eq,
Value,
cond,
greaterThan,
startClock,
timing,
Clock,
Code,
clockRunning,
stopClock
} = Animated;
const AnimatedIcon = createAnimatedComponent(SimpleLineIcons);
const TARGET_FONT_SIZE = 16;
const GROWN_FONT_SIZE = 20;
class GrowingHeart extends Component<any, any> {
size = new Value(TARGET_FONT_SIZE);
clock = new Clock();
updatingValue = new Value(0);
clockState = {
finished: new Value(0),
position: new Value(5),
time: new Value(0),
frameTime: new Value(0)
};
clockConfig = {
duration: new Value(500),
toValue: new Value(GROWN_FONT_SIZE),
easing: Easing.linear
};
constructor(props) {
super(props);
}
render() {
const { color } = this.props;
const { updatingValue, size, clock, clockConfig, clockState } = this;
return (
<>
<Code>
{() =>
block([
cond(
// animation not triggered
eq(0, updatingValue),
[],
[
cond(
clockRunning(clock),
[],
[
set(clockState.finished, 0),
set(clockState.time, 0),
set(clockState.position, TARGET_FONT_SIZE),
set(clockState.frameTime, 0),
set(clockConfig.toValue, GROWN_FONT_SIZE),
startClock(clock)
]
),
cond(
greaterThan(0, updatingValue),
// is decreasing
[debug('going down', updatingValue)],
// is growing
[
timing(clock, clockState, clockConfig),
set(size, clockState.position)
]
),
cond(clockState.finished, [stopClock(clock)])
]
)
])
}
</Code>
<TouchableOpacity
onPress={() => {
this.updatingValue.setValue(1);
}}
>
<AnimatedIcon
style={{ fontSize: this.size }}
name="heart"
color={color}
/>
</TouchableOpacity>
</>
);
}
}

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 create a speedometer in react native

I am creating charts using d3 and svg in react native in which i have created an progress bar chart. I would like to use this progress bar chart as speedometer by having a needle shows the current value.
Can anyone tell me how to create a speedometer in react native.
import React, { PureComponent } from 'react';
import {
View,
} from 'react-native';
import PropTypes from 'prop-types';
import * as shape from 'd3-shape';
import Path from './animated-path';
import Svg, { G } from 'react-native-svg';
export default class Gauge extends PureComponent {
state = {
height: 0,
width: 0,
}
_onLayout(event) {
const {
nativeEvent: {
layout: {
height,
width,
}
}
} = event;
this.setState({height, width});
}
render() {
const {
style,
progressColor,
backgroundColor,
strokeWidth,
startAngle,
endAngle,
animate,
animationDuration,
children,
cornerRadius,
} = this.props
let { progress } = this.props
const {
height, width
} = this.state
const outerDiameter = Math.min(width, height)
if (!isFinite(progress) || isNaN(progress)) {
progress = 0;
}
const data = [
{
key: 'rest',
value: 1 - progress,
color: backgroundColor,
},
{
key: 'progress',
value: progress,
color: progressColor,
}
]
const pieSlices = shape
.pie()
.value(d => d.value)
.sort((a) => a.key === 'rest' ? 1 : -1)
.startAngle(startAngle)
.endAngle(endAngle)
(data)
const arcs = pieSlices.map((slice, index) => (
{
...data[index],
...slice,
path: shape.arc()
.outerRadius(outerDiameter / 2)
.innerRadius((outerDiameter / 2) - strokeWidth)
.startAngle(index === 0 ? startAngle : slice.startAngle)
.endAngle(index === 0 ? endAngle : slice.endAngle)
.cornerRadius(cornerRadius)
(),
}
))
const extraProps = {
width,
height,
}
return (
<View style={ style } onLayout={event => this._onLayout(event)}>
{
height > 0 && width > 0 &&
<Svg style={{height, width}}>
<G x={width / 2}
y={height / 2}
>
{
React.Children.map(children, child => {
if (child && child.props.belowChart) {
return React.cloneElement(child, extraProps)
}
return null
})
}
{
arcs.map((shape, index) => {
return (
<Path
key={index}
fill={shape.color}
d={shape.path}
animate={animate}
animationDuration={animationDuration}
/>
)
})
}
{
React.Children.map(children, child => {
if (child && !child.props.belowChart) {
return React.cloneElement(child, extraProps)
}
return null
})
}
</G>
</Svg>
}
</View>
)
}
}
Gauge.propTypes = {
progress: PropTypes.number.isRequired,
style: PropTypes.any,
progressColor: PropTypes.any,
backgroundColor: PropTypes.any,
strokeWidth: PropTypes.number,
startAngle: PropTypes.number,
endAngle: PropTypes.number,
animate: PropTypes.bool,
cornerRadius: PropTypes.number,
animationDuration: PropTypes.number,
}
Gauge.defaultProps = {
progressColor: 'black',
backgroundColor: '#ECECEC',
strokeWidth: 5,
startAngle: 0,
endAngle: Math.PI * 2,
cornerRadius: 45,
}
App.js
<GaugeChartExample />
The output which is got is like as follows:

Finding out scroll direction in react-native listview/scrollview

Is there a way to find out the scroll direction of react-native's listview/scrollview components?
Native iOS components seem to be able to do it by calculating the offset from scrollViewWillBeginDragging and scrollViewDidScroll, but there seems to be no bindings for these.
You can use the onScroll event of a ScrollView and check the contentOffset in the event callback:
https://rnplay.org/apps/FltwOg
'use strict';
var React = require('react-native');
var {
AppRegistry,
StyleSheet,
ScrollView,
Text,
View,
} = React;
var SampleApp = React.createClass({
offset: 0,
onScroll: function(event) {
var currentOffset = event.nativeEvent.contentOffset.y;
var direction = currentOffset > this.offset ? 'down' : 'up';
this.offset = currentOffset;
console.log(direction);
},
render: function() {
return (
<View style={styles.container}>
<ScrollView style={styles.scroller} onScroll={this.onScroll}>
<Text>Scrollable content here</Text>
</ScrollView>
</View>
)
}
});
var styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 50,
},
scroller: {
height: 5000,
},
});
AppRegistry.registerComponent('SampleApp', () => SampleApp);
I had problems with alexp's solution to accurately specify the direction when the difference between the old and the new offset was very small. So I filtered them out.
_onScroll = event => {
const currentOffset = event.nativeEvent.contentOffset.y;
const dif = currentOffset - (this.offset || 0);
if (Math.abs(dif) < 3) {
console.log('unclear');
} else if (dif < 0) {
console.log('up');
} else {
console.log('down');
}
this.offset = currentOffset;
};

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>
);
}