Skip to content

Instantly share code, notes, and snippets.

@lucasbento
Last active February 12, 2018 20:04
Show Gist options
  • Save lucasbento/a159137da1c21cb52f06e39929c57707 to your computer and use it in GitHub Desktop.
Save lucasbento/a159137da1c21cb52f06e39929c57707 to your computer and use it in GitHub Desktop.
Pdf zoom
/**
* Copyright (c) 2017-present, Wonday (@wonday.org)
* All rights reserved.
*
* This source code is licensed under the MIT-style license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
import React, {Component} from 'react';
import {FlatList, View, StyleSheet} from 'react-native';
import PropTypes from 'prop-types';
import Zoomable from 'react-native-zoomable';
import PdfManager from './PdfManager';
import PdfPageView from './PdfPageView';
import DoubleTapView from './DoubleTapView';
// import PinchZoomView from './PinchZoomView';
const MAX_SCALE = 3;
const VIEWABILITYCONFIG = {minimumViewTime: 500, itemVisiblePercentThreshold: 60, waitForInteraction: false};
export default class PdfView extends Component {
static propTypes = {
...View.propTypes,
path: PropTypes.string,
password: PropTypes.string,
scale: PropTypes.number,
spacing: PropTypes.number,
fitPolicy: PropTypes.number,
horizontal: PropTypes.bool,
page: PropTypes.number,
currentPage: PropTypes.number,
onPageSingleTap: PropTypes.func,
onScaleChanged: PropTypes.func,
};
static defaultProps = {
path: "",
password: "",
scale: 1,
spacing: 10,
style: {},
fitPolicy: 0,
horizontal: false,
page: 1,
currentPage:-1,
onPageSingleTap: (page)=>{},
onScaleChanged: (scale)=>{},
};
constructor(props) {
super(props);
this.state = {
pdfLoaded: false,
fileNo: -1,
numberOfPages: 0,
page: -1,
currentPage: -1,
pageAspectRate: 0.5,
contentContainerSize: {width: 0, height: 0},
scale: this.props.scale,
contentOffset: {x:0, y:0},
scrollEnabled: true,
};
this.flatList = null;
this.scaleTimer = null;
}
componentWillMount() {
}
componentDidMount() {
PdfManager.loadFile(this.props.path, this.props.password)
.then((pdfInfo) => {
this.setState({
pdfLoaded: true,
fileNo: pdfInfo[0],
numberOfPages: pdfInfo[1],
pageAspectRate: pdfInfo[3] === 0 ? 1 : pdfInfo[2] / pdfInfo[3]
});
if (this.props.onLoadComplete) this.props.onLoadComplete(pdfInfo[1], this.props.path);
})
.catch((error) => {
this.props.onError(error);
});
}
componentWillReceiveProps(nextProps) {
if (nextProps.scale !== this.state.scale) {
this._onScaleChanged(nextProps.scale / this.state.scale);
}
}
componentWillUnmount() {
clearTimeout(this.scaleTimer);
}
_keyExtractor = (item, index) => index;
_getPageWidth = () => {
// if only one page, show whole page in center
if (this.state.numberOfPages===1) {
return this.state.contentContainerSize.width * this.state.scale;
}
switch (this.props.fitPolicy) {
case 0: //fit width
return this.state.contentContainerSize.width * this.state.scale;
case 1: //fit height
return this.state.contentContainerSize.height * this.state.pageAspectRate * this.state.scale;
case 2: //fit both
default: {
if ((this.state.contentContainerSize.width/this.state.contentContainerSize.height) > this.state.pageAspectRate) {
return this.state.contentContainerSize.height * this.state.scale * this.state.pageAspectRate;
} else {
return this.state.contentContainerSize.width * this.state.scale;
}
}
}
};
_getPageHeight = () => {
// if only one page, show whole page in center
if (this.state.numberOfPages===1) {
return this.state.contentContainerSize.height * this.state.scale;
}
switch (this.props.fitPolicy) {
case 0: //fit width
return this.state.contentContainerSize.width * (1 / this.state.pageAspectRate) * this.state.scale;
case 1: //fit height
return this.state.contentContainerSize.height * this.state.scale;
case 2: //fit both
default: {
if ((this.state.contentContainerSize.width/this.state.contentContainerSize.height) > this.state.pageAspectRate) {
return this.state.contentContainerSize.height * this.state.scale;
} else {
return this.state.contentContainerSize.width * (1 / this.state.pageAspectRate) * this.state.scale;
}
}
}
};
_renderSeparator = () => (
<View style={this.props.horizontal ? {
width: this.props.spacing*this.state.scale,
backgroundColor: 'transparent'
} : {
height: this.props.spacing*this.state.scale,
backgroundColor: 'transparent'
}}/>
);
_onItemSingleTap = (index) => {
this.props.onPageSingleTap(index+1);
};
_onItemDoubleTap = (index) => {
if (this.state.scale >= MAX_SCALE) {
this._onScaleChanged(1 / this.state.scale);
} else {
this._onScaleChanged(1.2);
}
};
_onScaleChanged = (scale, center) => {
let newScale = scale * this.state.scale;
newScale = newScale > MAX_SCALE ? MAX_SCALE : newScale;
newScale = newScale < 1 ? 1 : newScale;
if (this.flatList && this.state.contentOffset) {
this.flatList.scrollToOffset({
animated: false,
offset: (this.props.horizontal ? this.state.contentOffset.x : this.state.contentOffset.y) * scale
});
}
this.setState({scale: newScale, scrollEnabed: false});
this.props.onScaleChanged(newScale);
if (this.scaleTimer) {
clearTimeout(this.scaleTimer);
this.scaleTimer = setTimeout(() => {
this.setState({scrollEnabled: true});
}, 1000);
}
};
_onSingleTap = index => () => this._onItemSingleTap(index);
_onDoubleTap = index => ()=>this._onItemDoubleTap(index);
_renderItem = ({item, index}) => {
return (
<View style={{flexDirection: this.props.horizontal ? 'row' : 'column'}}
>
<PdfPageView
key={item.id}
fileNo={this.state.fileNo}
page={item.key + 1}
width={this._getPageWidth()}
height={this._getPageHeight()}
/>
{(index !== this.state.numberOfPages - 1) && this._renderSeparator()}
</View>
);
};
_onViewableItemsChanged = (viewableInfo) => {
for (let i = 0; i < viewableInfo.viewableItems.length; i++) {
this._onPageChanged(viewableInfo.viewableItems[i].index + 1, this.state.numberOfPages);
if (viewableInfo.viewableItems.length + viewableInfo.viewableItems[0].index<this.state.numberOfPages) break;
}
};
_onPageChanged = (page,numberOfPages) => {
if (this.props.onPageChanged && this.state.currentPage !== page) {
this.props.onPageChanged(page, numberOfPages);
this.setState({currentPage:page});
}
};
_getRef = (ref) => this.flatList = ref;
_getItemLayout = (data, index) => ({
length: this.props.horizontal ? this._getPageWidth() : this._getPageHeight(),
offset: ((this.props.horizontal ? this._getPageWidth() : this._getPageHeight()) + this.props.spacing*this.state.scale) * index,
index
});
_onScroll = (e) => {
this.state.scrollEnabled && this.setState({contentOffset: e.nativeEvent.contentOffset});
};
_renderList = () => {
let data = [];
for (let i = 0; i < this.state.numberOfPages; i++) {
data[i] = {key: i};
}
let page = (this.props.page) < 1 ? 1 : this.props.page;
page = page>this.state.numberOfPages ? this.state.numberOfPages : page;
if (this.state.page !== page) {
this.timer = setTimeout(() => {
if (this.flatList) {
this.flatList.scrollToIndex({animated: true, index: page-1});
this.state.page = page;
}
}, 200);
}
return (
<FlatList
ref={this._getRef}
style={this.props.style}
contentContainerStyle={this.props.horizontal ? {height: this.state.contentContainerSize.height * this.state.scale} : {width: this.state.contentContainerSize.width * this.state.scale}}
horizontal={this.props.horizontal}
data={data}
renderItem={this._renderItem}
keyExtractor={this._keyExtractor}
windowSize={11}
getItemLayout={this._getItemLayout}
maxToRenderPerBatch={1}
removeClippedSubviews
/*initialScrollIndex={this.props.page - 1}*/ /* not action? */
onViewableItemsChanged={this._onViewableItemsChanged}
viewabilityConfig={VIEWABILITYCONFIG}
// onScroll={this._onScroll}
scrollEnabled={this.state.scrollEnabled}
/>
);
};
_onLayout = (event) => {
this.setState({
contentContainerSize: {
width: event.nativeEvent.layout.width,
height: event.nativeEvent.layout.height
}
});
};
render() {
if (!this.state.pdfLoaded) {
return <View/>
}
return (
<Zoomable>
<View
style={styles.container}
onLayout={this._onLayout}
>
{this._renderList()}
</View>
</Zoomable>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1
}
});
import React, { PropTypes } from 'react';
import { ScrollView } from 'react-native';
class Zoomable extends React.Component {
state = {
lastTouchStartNativeEvent: {},
lastTouchEndTimestamp: 0,
lastZoomActionTimestamp: 0,
isZoomed: false,
};
onScroll = (e) => {
this.props.onScrollOrZoom(e);
this.setState({ isZoomed: e.nativeEvent.zoomScale > 1 });
};
onTouchStart = (e) => {
if (this.isMultiTouch(e)) return;
this.setState({ lastTouchStartNativeEvent: e.nativeEvent });
};
onTouchEnd = (e) => {
const { timestamp } = e.nativeEvent;
const { zoomInTrigger, zoomOutTrigger } = this.props;
const trigger = this.state.isZoomed ? zoomOutTrigger : zoomInTrigger;
const actionToPerform = this.state.isZoomed ? this.zoomOut : this.zoomIn;
if (this.isLongPress(e) || this.isMoving(e) || this.isMultiTouch(e)) return;
// switch (trigger) {
// case 'singletap':
// actionToPerform(e);
// break;
// case 'doubletap':
// if (this.isSecondTap(e)) actionToPerform(e);
// break;
// default:
// }
this.setState({ lastTouchEndTimestamp: timestamp });
};
zoomIn = (e) => {
const { locationX: x, locationY: y, timestamp } = e.nativeEvent;
const coords = { x, y, width: 0, height: 0 };
if (this.isAlreadyZooming(e)) return;
this.scrollView.scrollResponderZoomTo(coords);
this.setState({ lastZoomActionTimestamp: timestamp });
};
zoomOut = (e) => {
const { locationX: x, locationY: y, timestamp } = e.nativeEvent;
const coords = { x, y, width: 10000, height: 10000 };
if (this.isAlreadyZooming(e)) return;
this.scrollView.scrollResponderZoomTo(coords);
this.setState({ lastZoomActionTimestamp: timestamp });
};
isSecondTap = (e) => {
const { timestamp } = e.nativeEvent;
return timestamp - this.state.lastTouchEndTimestamp <= 300;
};
isLongPress = (e) => {
console.log('e', e.nativeEvent);
const { timestamp } = e.nativeEvent;
const { timestamp: lastTimestamp } = this.state.lastTouchStartNativeEvent;
return timestamp - lastTimestamp >= 300;
};
isMoving = (e) => {
const { locationX, locationY } = e.nativeEvent;
const { locationX: lastLocationX, locationY: lastLocationY } = this.state.lastTouchStartNativeEvent;
return locationX !== lastLocationX && locationY !== lastLocationY;
};
isMultiTouch = ({ nativeEvent: { touches } }) => touches.length > 1;
isAlreadyZooming = (e) => {
const { timestamp } = e.nativeEvent;
return timestamp - this.state.lastZoomActionTimestamp <= 500;
};
render() {
return (
<ScrollView
ref={(ref) => { this.scrollView = ref; }}
onScroll={this.onScroll}
onTouchStart={this.onTouchStart}
onTouchEnd={this.onTouchEnd}
scrollEventThrottle={100}
scrollsToTop={false}
alwaysBounceVertical={false}
alwaysBounceHorizontal={false}
automaticallyAdjustContentInsets={false}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
maximumZoomScale={this.props.zoomScale}
// centerContent
>
{this.props.children}
</ScrollView>
);
}
}
Zoomable.propTypes = {
children: PropTypes.element.isRequired,
onScrollOrZoom: PropTypes.func,
zoomScale: PropTypes.number,
zoomInTrigger: PropTypes.oneOf(['singletap', 'doubletap', 'longpress']),
zoomOutTrigger: PropTypes.oneOf(['singletap', 'doubletap', 'longpress']),
};
Zoomable.defaultProps = {
onScrollOrZoom: () => {},
zoomScale: 4,
zoomInTrigger: 'doubletap',
zoomOutTrigger: 'singletap',
};
export default Zoomable;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment