diff --git a/README.md b/README.md
index 4b9a7ae..dfb4e84 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ Even though the demo shows the library used with images, it was initially design
- Drag one finger to pan
- Keep content inside container boundaries
- Configurable minimum and maximum scale
+- Methods for programmatically updating position and scale
Thanks to `react-native-reanimated` all animations are running on the UI thread, so no fps drops are experienced.
@@ -33,8 +34,8 @@ This library uses `react-native-reanimated` v2 and the new API of `react-native-
Before installing it, you need to install those two libraries and set them up in your project:
-- `react-native-reanimated`: [INSTALLATION](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/installation)
-- `react-native-gesture-handler`: [INSTALLATION](https://docs.swmansion.com/react-native-gesture-handler/docs/#installation)
+- `react-native-reanimated`: [Installation & Setup](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/installation)
+- `react-native-gesture-handler`: [Installation & Setup](https://docs.swmansion.com/react-native-gesture-handler/docs/#installation)
## ⚙️ Installation
@@ -96,8 +97,19 @@ const styles = StyleSheet.create({
| maxScale | Number? | `4` | Maximum value of scale. |
| initialScale | Number? | `1` | Initial value of scale. |
+## 🛠 Methods
+
+| Method | Params | Return | Description |
+|----------------|-----------------------------------------|--------|----------------------------------------------------------------------------------------------|
+| scaleTo | value: number, animated: boolean | void | Sets sharedValue `scale` to `value`,
if `animated` is **true** uses `withTiming` |
+| setContentSize | width: number, height: number | void | Updates sharedValue `contentSize` and overrides prop: `contentDimensions` |
+| translateTo | x: number, y: number, animated: boolean | void | Updates content `translateX` / `translateY`,
if `animated` is **true** uses `withTiming` |
+| setMinScale | value: number | void | Updates `minScale` value |
+| setMaxScale | value: number | void | Updates `maxScale` value |
+| getScale | | number | Returns current value of sharedValue `scale` |
You can also refer to the app inside `example/` for a running demo of this library.
+
## Contributing
See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
index 90f5563..14c2b14 100644
--- a/example/android/app/build.gradle
+++ b/example/android/app/build.gradle
@@ -76,7 +76,7 @@ import com.android.build.OutputFile
*/
project.ext.react = [
- enableHermes: false, // clean and rebuild if changing
+ enableHermes: true, // clean and rebuild if changing
entryFile: "index.tsx",
]
diff --git a/example/android/app/src/main/java/com/example/reactnativepanpinchview/MainApplication.java b/example/android/app/src/main/java/com/example/reactnativepanpinchview/MainApplication.java
index f33a000..368c29c 100644
--- a/example/android/app/src/main/java/com/example/reactnativepanpinchview/MainApplication.java
+++ b/example/android/app/src/main/java/com/example/reactnativepanpinchview/MainApplication.java
@@ -10,6 +10,8 @@
import com.facebook.soloader.SoLoader;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
+import com.facebook.react.bridge.JSIModulePackage;
+import com.swmansion.reanimated.ReanimatedJSIModulePackage;
public class MainApplication extends Application implements ReactApplication {
@@ -26,7 +28,7 @@ protected List getPackages() {
List packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for PanPinchViewExample:
// packages.add(new MyReactNativePackage());
-
+
return packages;
}
@@ -34,6 +36,10 @@ protected List getPackages() {
protected String getJSMainModuleName() {
return "index";
}
+ @Override
+ protected JSIModulePackage getJSIModulePackage() {
+ return new ReanimatedJSIModulePackage();
+ }
};
@Override
diff --git a/example/src/App.tsx b/example/src/App.tsx
index a5acee8..cfa2b0c 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -1,7 +1,16 @@
import * as React from 'react';
-import { Image, SafeAreaView, StatusBar, StyleSheet, View } from 'react-native';
+import {
+ Button,
+ Image,
+ SafeAreaView,
+ StatusBar,
+ StyleSheet,
+ View,
+} from 'react-native';
import PanPinchView from 'react-native-pan-pinch-view';
+import { useRef } from 'react';
+import type { PanPinchViewRef } from '../../src/types.js';
const CONTENT = {
width: 150,
@@ -14,11 +23,56 @@ const CONTAINER = {
};
export default function App() {
+ const panPinchViewRef = useRef(null);
+
+ const scaleTo = (value: number) => {
+ panPinchViewRef.current?.scaleTo(value);
+ };
+
+ const moveTo = (x: number, y: number) => {
+ panPinchViewRef.current?.translateTo(x, y);
+ };
+
return (
+
+
+
+
+ moveTo(
+ CONTAINER.width / 2 - CONTENT.width / 2,
+ CONTAINER.height / 2 - CONTENT.height / 2
+ )
+ }
+ />
+
+ moveTo(
+ CONTAINER.width - CONTENT.width,
+ CONTAINER.height - CONTENT.height
+ )
+ }
+ />
+
+ moveTo(
+ CONTAINER.width / 2 - CONTENT.width / 2,
+ CONTAINER.height - CONTENT.height
+ )
+ }
+ />
+
+) {
+ const currentMinScale = useSharedValue(minScale);
+ const currentMaxScale = useSharedValue(maxScale);
+
const scale = useSharedValue(initialScale);
const lastScale = useSharedValue(initialScale);
@@ -37,41 +43,62 @@ export default function PanPinchView({
const isPinching = useSharedValue(false);
const isResetting = useSharedValue(false);
- const layout = useVector(contentDimensions.width, contentDimensions.height);
-
- const animatedStyle = useAnimatedStyle(() => {
- const translateX = offset.x.value + translation.x.value;
- const translateY = offset.y.value + translation.y.value;
- return {
- transform: [{ translateX }, { translateY }, { scale: scale.value }],
- };
- });
-
- const animateToInitialState = () => {
- 'worklet';
+ const contentSize = useVector(
+ contentDimensions.width,
+ contentDimensions.height
+ );
- isResetting.value = true;
+ const setContentSize = ({
+ width,
+ height,
+ }: {
+ width: number;
+ height: number;
+ }) => {
+ contentSize.x.value = width;
+ contentSize.y.value = height;
+ };
- scale.value = withTiming(initialScale);
- lastScale.value = withTiming(initialScale);
+ const scaleTo = (value: number, animated: boolean) => {
+ scale.value = animated ? withTiming(value) : value;
+ lastScale.value = value;
+ };
- translation.x.value = withTiming(0);
- translation.y.value = withTiming(0);
+ const translateTo = (x: number, y: number, animated: boolean) => {
+ translation.x.value = 0;
+ translation.y.value = 0;
+ offset.x.value = animated ? withTiming(x) : x;
+ offset.y.value = animated ? withTiming(y) : y;
+ };
- offset.x.value = withTiming(0);
- offset.y.value = withTiming(0);
+ const setMinScale = (value: number) => {
+ currentMinScale.value = value;
+ };
- adjustedFocal.x.value = withTiming(0);
- adjustedFocal.y.value = withTiming(0);
+ const setMaxScale = (value: number) => {
+ currentMaxScale.value = value;
+ };
- origin.x.value = withTiming(0);
- origin.y.value = withTiming(0);
+ const getScale = (): number => {
+ return scale.value;
+ };
- layout.x.value = contentDimensions.width;
- layout.y.value = contentDimensions.height;
+ useImperativeHandle(ref, () => ({
+ scaleTo,
+ setContentSize,
+ translateTo,
+ setMinScale,
+ setMaxScale,
+ getScale,
+ }));
- isPinching.value = false;
- };
+ const animatedStyle = useAnimatedStyle(() => {
+ const translateX = offset.x.value + translation.x.value;
+ const translateY = offset.y.value + translation.y.value;
+ return {
+ transform: [{ translateX }, { translateY }, { scale: scale.value }],
+ };
+ });
const setAdjustedFocal = ({
focalX,
@@ -82,28 +109,37 @@ export default function PanPinchView({
}) => {
'worklet';
- adjustedFocal.x.value = focalX - (layout.x.value / 2 + offset.x.value);
- adjustedFocal.y.value = focalY - (layout.y.value / 2 + offset.y.value);
+ adjustedFocal.x.value = focalX - (contentSize.x.value / 2 + offset.x.value);
+ adjustedFocal.y.value = focalY - (contentSize.y.value / 2 + offset.y.value);
};
const getEdges = () => {
'worklet';
const edges = { x: { min: 0, max: 0 }, y: { min: 0, max: 0 } };
- const newWidth = layout.x.value * scale.value;
- let scaleOffsetX = (newWidth - layout.x.value) / 2;
-
- edges.x.min = Math.round(
- (newWidth - containerDimensions.width) * -1 + scaleOffsetX
- );
- edges.x.max = scaleOffsetX;
+ const newWidth = contentSize.x.value * scale.value;
+ let scaleOffsetX = (newWidth - contentSize.x.value) / 2;
+ if (newWidth > containerDimensions.width) {
+ edges.x.min = Math.round(
+ (newWidth - containerDimensions.width) * -1 + scaleOffsetX
+ );
+ edges.x.max = scaleOffsetX;
+ } else {
+ edges.x.min = scaleOffsetX;
+ edges.x.max = containerDimensions.width - newWidth + scaleOffsetX;
+ }
- const newHeight = layout.y.value * scale.value;
- let scaleOffsetY = (newHeight - layout.y.value) / 2;
- edges.y.min = Math.round(
- (newHeight - containerDimensions.height) * -1 + scaleOffsetY
- );
- edges.y.max = scaleOffsetY;
+ const newHeight = contentSize.y.value * scale.value;
+ let scaleOffsetY = (newHeight - contentSize.y.value) / 2;
+ if (newHeight > containerDimensions.height) {
+ edges.y.min = Math.round(
+ (newHeight - containerDimensions.height) * -1 + scaleOffsetY
+ );
+ edges.y.max = scaleOffsetY;
+ } else {
+ edges.y.min = scaleOffsetY;
+ edges.y.max = containerDimensions.height - newHeight + scaleOffsetY;
+ }
return edges;
};
@@ -218,16 +254,6 @@ export default function PanPinchView({
const gestures = Gesture.Race(panGesture, pinchGesture);
- useEffect(() => {
- animateToInitialState();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [
- containerDimensions.width,
- containerDimensions.height,
- contentDimensions.width,
- contentDimensions.height,
- ]);
-
return (
@@ -244,8 +270,8 @@ export default function PanPinchView({
style={[
styles.content,
{
- width: contentDimensions.width,
- height: contentDimensions.height,
+ width: contentSize.x.value,
+ height: contentSize.y.value,
},
animatedStyle,
]}
@@ -256,7 +282,7 @@ export default function PanPinchView({
);
-}
+});
const styles = StyleSheet.create({
container: {
diff --git a/src/types.d.ts b/src/types.d.ts
index 3c4d42e..d3d9e98 100644
--- a/src/types.d.ts
+++ b/src/types.d.ts
@@ -4,6 +4,7 @@ type Dimensions = {
width: number;
height: number;
};
+
export type PanPinchViewProps = {
/**
* Dimensions of the container holding the zoomable View.
@@ -37,3 +38,12 @@ export type PanPinchViewProps = {
*/
initialScale?: number;
};
+
+export type PanPinchViewRef = {
+ scaleTo: Function;
+ setContentSize: Function;
+ translateTo: Function;
+ setMinScale: Function;
+ setMaxScale: Function;
+ getScale: Function;
+};