@hzblj/kara-router 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/zoom-transition/index.js +8 -0
- package/lib/module/zoom-transition/index.js.map +1 -0
- package/lib/module/zoom-transition/use-zoom-transition-source-id.js +11 -0
- package/lib/module/zoom-transition/use-zoom-transition-source-id.js.map +1 -0
- package/lib/module/zoom-transition/with-zoom-transition.js +25 -0
- package/lib/module/zoom-transition/with-zoom-transition.js.map +1 -0
- package/lib/module/zoom-transition/zoom-transition-param.js +16 -0
- package/lib/module/zoom-transition/zoom-transition-param.js.map +1 -0
- package/lib/module/zoom-transition/zoom-transition.android.js +75 -0
- package/lib/module/zoom-transition/zoom-transition.android.js.map +1 -0
- package/lib/module/zoom-transition/zoom-transition.ios.js +47 -0
- package/lib/module/zoom-transition/zoom-transition.ios.js.map +1 -0
- package/lib/module/zoom-transition/zoom-transition.types.js +4 -0
- package/lib/module/zoom-transition/zoom-transition.types.js.map +1 -0
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/zoom-transition/index.d.ts +5 -0
- package/lib/typescript/zoom-transition/use-zoom-transition-source-id.d.ts +6 -0
- package/lib/typescript/zoom-transition/with-zoom-transition.d.ts +17 -0
- package/lib/typescript/zoom-transition/zoom-transition-param.d.ts +7 -0
- package/lib/typescript/zoom-transition/zoom-transition.d.ts +1 -0
- package/lib/typescript/zoom-transition/zoom-transition.ios.d.ts +2 -0
- package/lib/typescript/zoom-transition/zoom-transition.types.d.ts +93 -0
- package/package.json +4 -4
- package/src/index.ts +1 -0
- package/src/zoom-transition/index.ts +5 -0
- package/src/zoom-transition/use-zoom-transition-source-id.ts +8 -0
- package/src/zoom-transition/with-zoom-transition.ts +28 -0
- package/src/zoom-transition/zoom-transition-param.ts +14 -0
- package/src/zoom-transition/zoom-transition.android.tsx +81 -0
- package/src/zoom-transition/zoom-transition.ios.tsx +47 -0
- package/src/zoom-transition/zoom-transition.types.ts +108 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @hzblj/kara-router
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [[`62b7131`](https://github.com/hzblj/kara-ui/commit/62b713143bd03872ecdd89ca8de42d98fe589617)]:
|
|
8
|
+
- @hzblj/kara-native@0.6.0
|
|
9
|
+
- @hzblj/kara-components@0.6.0
|
|
10
|
+
- @hzblj/kara-theme@0.6.0
|
|
11
|
+
|
|
12
|
+
## 0.5.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- [#8](https://github.com/hzblj/kara-ui/pull/8) [`3b0a6fb`](https://github.com/hzblj/kara-ui/commit/3b0a6fbc49559720023fece2b5c6189ad4d88bc8) Thanks [@hzblj](https://github.com/hzblj)! - `ZoomTransition`: a push that grows out of the photo it was opened from and shrinks back into it.
|
|
17
|
+
|
|
18
|
+
- iOS 18: UIKit's zoom transition, with the interactive dismissal (swipe down, edge swipe, pinch) gated on the scroll views under the finger. Below iOS 18 the push stays a slide. Android: Material's container transform, both ways.
|
|
19
|
+
- Three pieces from `@hzblj/kara-ui/router`, tied by one identifier: `ZoomTransition.Source` on the list, `ZoomTransition.Enabler` and `ZoomTransition.Target` on the pushed screen; `useZoomTransitionSourceId`, `ZOOM_TRANSITION_PARAM`, `useZoomTransitionParam` and `withZoomTransition` (an Android fade under the transform) around them.
|
|
20
|
+
- Settings both platforms honour sit at the top (`scrim`); the rest is typed apart under `ios` (`dismissGesture`, `blur`, the target's `fit`) and `android` (`duration`, `easing`, `fade`, `path`, corner radii, the target's `fit` and `flex`).
|
|
21
|
+
- `ContextMenu`: with a `ZoomTransition.Source` in the trigger, a tap on the iOS preview zooms the preview's photo straight into the pushed screen. `previewTransition` (`zoom`, `pop`, `dismiss`) picks how the preview leaves.
|
|
22
|
+
- `DurationSize`, the keys of `duration`.
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [[`3b0a6fb`](https://github.com/hzblj/kara-ui/commit/3b0a6fbc49559720023fece2b5c6189ad4d88bc8)]:
|
|
27
|
+
- @hzblj/kara-native@0.5.0
|
|
28
|
+
- @hzblj/kara-components@0.5.0
|
|
29
|
+
- @hzblj/kara-theme@0.5.0
|
|
30
|
+
|
|
3
31
|
## 0.4.1
|
|
4
32
|
|
|
5
33
|
### Patch Changes
|
package/lib/module/index.js
CHANGED
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":[],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;AAAA,cAAc,uBAAoB;AAClC,cAAc,iBAAQ","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;AAAA,cAAc,uBAAoB;AAClC,cAAc,iBAAQ;AACtB,cAAc,4BAAmB","ignoreList":[]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
export * from "./use-zoom-transition-source-id.js";
|
|
4
|
+
export * from "./with-zoom-transition.js";
|
|
5
|
+
export * from './zoom-transition';
|
|
6
|
+
export * from "./zoom-transition.types.js";
|
|
7
|
+
export * from "./zoom-transition-param.js";
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["zoom-transition/index.ts"],"mappings":";;AAAA,cAAc,oCAAiC;AAC/C,cAAc,2BAAwB;AACtC,cAAc,mBAAmB;AACjC,cAAc,4BAAyB;AACvC,cAAc,4BAAyB","ignoreList":[]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useId } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An identifier for a `ZoomTransition.Source`, unique to this component instance and to `key` — the item it shows.
|
|
7
|
+
* Keyed by the item, a recycled list cell registers under a new identifier when it shows another item, so a detail
|
|
8
|
+
* opened from its previous item zooms back out plainly instead of into the wrong cell.
|
|
9
|
+
*/
|
|
10
|
+
export const useZoomTransitionSourceId = key => `${useId()}${key}`;
|
|
11
|
+
//# sourceMappingURL=use-zoom-transition-source-id.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["useId","useZoomTransitionSourceId","key"],"sourceRoot":"../../../src","sources":["zoom-transition/use-zoom-transition-source-id.ts"],"mappings":";;AAAA,SAAQA,KAAK,QAAO,OAAO;;AAE3B;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,yBAAyB,GAAIC,GAAW,IAAK,GAAGF,KAAK,CAAC,CAAC,GAAGE,GAAG,EAAE","ignoreList":[]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { duration } from '@hzblj/kara-theme';
|
|
4
|
+
import { Platform } from 'react-native';
|
|
5
|
+
import { ZOOM_TRANSITION_PARAM } from "./zoom-transition-param.js";
|
|
6
|
+
const hasZoomSource = params => !!params && !!params[ZOOM_TRANSITION_PARAM];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `Stack.Screen` options for a screen the zoom pushes. On Android a push whose params carry `ZOOM_TRANSITION_PARAM`
|
|
10
|
+
* becomes a quick fade, so Material's container transform reads as the transition instead of racing the stack's own
|
|
11
|
+
* slide; any other push keeps the stack's animation. Your options win: set `animation` yourself to keep another one.
|
|
12
|
+
* iOS needs nothing — UIKit's zoom replaces the push animation.
|
|
13
|
+
*/
|
|
14
|
+
export const withZoomTransition = options => props => {
|
|
15
|
+
const resolved = (typeof options === 'function' ? options(props) : options) ?? {};
|
|
16
|
+
if (Platform.OS !== 'android' || !hasZoomSource(props.route.params)) {
|
|
17
|
+
return resolved;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
animation: 'fade',
|
|
21
|
+
animationDuration: duration.d300,
|
|
22
|
+
...resolved
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=with-zoom-transition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["duration","Platform","ZOOM_TRANSITION_PARAM","hasZoomSource","params","withZoomTransition","options","props","resolved","OS","route","animation","animationDuration","d300"],"sourceRoot":"../../../src","sources":["zoom-transition/with-zoom-transition.ts"],"mappings":";;AAAA,SAAQA,QAAQ,QAAO,mBAAmB;AAC1C,SAAQC,QAAQ,QAAO,cAAc;AACrC,SAAQC,qBAAqB,QAAO,4BAAyB;AAM7D,MAAMC,aAAa,GAAIC,MAA0B,IAC/C,CAAC,CAACA,MAAM,IAAI,CAAC,CAAEA,MAAM,CAA6BF,qBAAqB,CAAC;;AAE1E;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMG,kBAAkB,GACHC,OAAsD,IAC/EC,KAAiB,IAAwC;EACxD,MAAMC,QAA2B,GAAG,CAAC,OAAOF,OAAO,KAAK,UAAU,GAAGA,OAAO,CAACC,KAAK,CAAC,GAAGD,OAAO,KAAK,CAAC,CAAC;EAEpG,IAAIL,QAAQ,CAACQ,EAAE,KAAK,SAAS,IAAI,CAACN,aAAa,CAACI,KAAK,CAACG,KAAK,CAACN,MAAM,CAAC,EAAE;IACnE,OAAOI,QAAQ;EACjB;EAEA,OAAO;IAACG,SAAS,EAAE,MAAM;IAAEC,iBAAiB,EAAEZ,QAAQ,CAACa,IAAI;IAAE,GAAGL;EAAQ,CAAC;AAC3E,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useLocalSearchParams } from 'expo-router';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The route param that carries a source's identifier to the screen it pushes — what `withZoomTransition` looks for.
|
|
7
|
+
* Push with `params: {[ZOOM_TRANSITION_PARAM]: sourceId}` and read it back with `useZoomTransitionParam`.
|
|
8
|
+
*/
|
|
9
|
+
export const ZOOM_TRANSITION_PARAM = 'zoomTransitionSourceId';
|
|
10
|
+
|
|
11
|
+
/** The source identifier this screen was pushed with — for its `ZoomTransition.Enabler` and `Target`. */
|
|
12
|
+
export const useZoomTransitionParam = () => {
|
|
13
|
+
const value = useLocalSearchParams()[ZOOM_TRANSITION_PARAM];
|
|
14
|
+
return (Array.isArray(value) ? value[0] : value) || undefined;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=zoom-transition-param.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["useLocalSearchParams","ZOOM_TRANSITION_PARAM","useZoomTransitionParam","value","Array","isArray","undefined"],"sourceRoot":"../../../src","sources":["zoom-transition/zoom-transition-param.ts"],"mappings":";;AAAA,SAAQA,oBAAoB,QAAO,aAAa;;AAEhD;AACA;AACA;AACA;AACA,OAAO,MAAMC,qBAAqB,GAAG,wBAAwB;;AAE7D;AACA,OAAO,MAAMC,sBAAsB,GAAGA,CAAA,KAA0B;EAC9D,MAAMC,KAAK,GAAGH,oBAAoB,CAAC,CAAC,CAACC,qBAAqB,CAAC;EAE3D,OAAO,CAACG,KAAK,CAACC,OAAO,CAACF,KAAK,CAAC,GAAGA,KAAK,CAAC,CAAC,CAAC,GAAGA,KAAK,KAAKG,SAAS;AAC/D,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { ZoomTransitionEnabler as NativeZoomTransitionEnabler, ZoomTransitionSource as NativeZoomTransitionSource, ZoomTransitionTarget as NativeZoomTransitionTarget, reverseZoomTransition } from '@hzblj/kara-native/jetpack-compose';
|
|
4
|
+
import { duration, radii, useAppTheme } from '@hzblj/kara-theme';
|
|
5
|
+
import { useNavigation } from 'expo-router';
|
|
6
|
+
import { useEffect, useMemo } from 'react';
|
|
7
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
8
|
+
const Source = ({
|
|
9
|
+
android,
|
|
10
|
+
children,
|
|
11
|
+
identifier
|
|
12
|
+
}) => identifier ? /*#__PURE__*/_jsx(NativeZoomTransitionSource, {
|
|
13
|
+
cornerRadius: radii[android?.borderRadius ?? 'none'],
|
|
14
|
+
identifier: identifier,
|
|
15
|
+
children: children
|
|
16
|
+
}) : children;
|
|
17
|
+
const Target = ({
|
|
18
|
+
android,
|
|
19
|
+
children,
|
|
20
|
+
identifier
|
|
21
|
+
}) => {
|
|
22
|
+
const flex = android?.flex;
|
|
23
|
+
const style = useMemo(() => flex === undefined ? undefined : {
|
|
24
|
+
flex
|
|
25
|
+
}, [flex]);
|
|
26
|
+
if (!identifier) {
|
|
27
|
+
return children;
|
|
28
|
+
}
|
|
29
|
+
return /*#__PURE__*/_jsx(NativeZoomTransitionTarget, {
|
|
30
|
+
cornerRadius: radii[android?.borderRadius ?? 'none'],
|
|
31
|
+
identifier: identifier,
|
|
32
|
+
isFittedToSourceAspectRatio: android?.fit === 'photo',
|
|
33
|
+
style: style,
|
|
34
|
+
children: children
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The target starts the way in natively once it is laid out. The way back cannot wait for the target to go: the pop is
|
|
40
|
+
* native, and by the time the screen unmounts it is already sliding away. So the transform is played back from
|
|
41
|
+
* `beforeRemove`, while the target is still on screen.
|
|
42
|
+
*/
|
|
43
|
+
const Enabler = ({
|
|
44
|
+
android,
|
|
45
|
+
scrim,
|
|
46
|
+
sourceIdentifier
|
|
47
|
+
}) => {
|
|
48
|
+
const navigation = useNavigation();
|
|
49
|
+
const theme = useAppTheme();
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!sourceIdentifier) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
return navigation.addListener('beforeRemove', () => {
|
|
55
|
+
void reverseZoomTransition(sourceIdentifier);
|
|
56
|
+
});
|
|
57
|
+
}, [navigation, sourceIdentifier]);
|
|
58
|
+
if (!sourceIdentifier) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return /*#__PURE__*/_jsx(NativeZoomTransitionEnabler, {
|
|
62
|
+
easing: android?.easing,
|
|
63
|
+
fadeMode: android?.fade,
|
|
64
|
+
pathMotion: android?.path,
|
|
65
|
+
scrimColor: scrim ? theme.color(scrim) : undefined,
|
|
66
|
+
sourceIdentifier: sourceIdentifier,
|
|
67
|
+
transformDuration: android?.duration ? duration[android.duration] : undefined
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
export const ZoomTransition = {
|
|
71
|
+
Enabler,
|
|
72
|
+
Source,
|
|
73
|
+
Target
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=zoom-transition.android.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["ZoomTransitionEnabler","NativeZoomTransitionEnabler","ZoomTransitionSource","NativeZoomTransitionSource","ZoomTransitionTarget","NativeZoomTransitionTarget","reverseZoomTransition","duration","radii","useAppTheme","useNavigation","useEffect","useMemo","jsx","_jsx","Source","android","children","identifier","cornerRadius","borderRadius","Target","flex","style","undefined","isFittedToSourceAspectRatio","fit","Enabler","scrim","sourceIdentifier","navigation","theme","addListener","easing","fadeMode","fade","pathMotion","path","scrimColor","color","transformDuration","ZoomTransition"],"sourceRoot":"../../../src","sources":["zoom-transition/zoom-transition.android.tsx"],"mappings":";;AAAA,SACEA,qBAAqB,IAAIC,2BAA2B,EACpDC,oBAAoB,IAAIC,0BAA0B,EAClDC,oBAAoB,IAAIC,0BAA0B,EAClDC,qBAAqB,QAChB,oCAAoC;AAC3C,SAAQC,QAAQ,EAAEC,KAAK,EAAEC,WAAW,QAAO,mBAAmB;AAC9D,SAAQC,aAAa,QAAO,aAAa;AACzC,SAAQC,SAAS,EAAEC,OAAO,QAAO,OAAO;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAQxC,MAAMC,MAAM,GAAGA,CAAC;EAACC,OAAO;EAAEC,QAAQ;EAAEC;AAAqC,CAAC,KACxEA,UAAU,gBACRJ,IAAA,CAACX,0BAA0B;EAACgB,YAAY,EAAEX,KAAK,CAACQ,OAAO,EAAEI,YAAY,IAAI,MAAM,CAAE;EAACF,UAAU,EAAEA,UAAW;EAAAD,QAAA,EACtGA;AAAQ,CACiB,CAAC,GAE7BA,QACD;AAEH,MAAMI,MAAM,GAAGA,CAAC;EAACL,OAAO;EAAEC,QAAQ;EAAEC;AAAqC,CAAC,KAAK;EAC7E,MAAMI,IAAI,GAAGN,OAAO,EAAEM,IAAI;EAC1B,MAAMC,KAAK,GAAGX,OAAO,CAAC,MAAOU,IAAI,KAAKE,SAAS,GAAGA,SAAS,GAAG;IAACF;EAAI,CAAE,EAAE,CAACA,IAAI,CAAC,CAAC;EAE9E,IAAI,CAACJ,UAAU,EAAE;IACf,OAAOD,QAAQ;EACjB;EAEA,oBACEH,IAAA,CAACT,0BAA0B;IACzBc,YAAY,EAAEX,KAAK,CAACQ,OAAO,EAAEI,YAAY,IAAI,MAAM,CAAE;IACrDF,UAAU,EAAEA,UAAW;IACvBO,2BAA2B,EAAET,OAAO,EAAEU,GAAG,KAAK,OAAQ;IACtDH,KAAK,EAAEA,KAAM;IAAAN,QAAA,EAEZA;EAAQ,CACiB,CAAC;AAEjC,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA,MAAMU,OAAO,GAAGA,CAAC;EAACX,OAAO;EAAEY,KAAK;EAAEC;AAA4C,CAAC,KAAK;EAClF,MAAMC,UAAU,GAAGpB,aAAa,CAAC,CAAC;EAClC,MAAMqB,KAAK,GAAGtB,WAAW,CAAC,CAAC;EAE3BE,SAAS,CAAC,MAAM;IACd,IAAI,CAACkB,gBAAgB,EAAE;MACrB;IACF;IAEA,OAAOC,UAAU,CAACE,WAAW,CAAC,cAAc,EAAE,MAAM;MAClD,KAAK1B,qBAAqB,CAACuB,gBAAgB,CAAC;IAC9C,CAAC,CAAC;EACJ,CAAC,EAAE,CAACC,UAAU,EAAED,gBAAgB,CAAC,CAAC;EAElC,IAAI,CAACA,gBAAgB,EAAE;IACrB,OAAO,IAAI;EACb;EAEA,oBACEf,IAAA,CAACb,2BAA2B;IAC1BgC,MAAM,EAAEjB,OAAO,EAAEiB,MAAO;IACxBC,QAAQ,EAAElB,OAAO,EAAEmB,IAAK;IACxBC,UAAU,EAAEpB,OAAO,EAAEqB,IAAK;IAC1BC,UAAU,EAAEV,KAAK,GAAGG,KAAK,CAACQ,KAAK,CAACX,KAAK,CAAC,GAAGJ,SAAU;IACnDK,gBAAgB,EAAEA,gBAAiB;IACnCW,iBAAiB,EAAExB,OAAO,EAAET,QAAQ,GAAGA,QAAQ,CAACS,OAAO,CAACT,QAAQ,CAAC,GAAGiB;EAAU,CAC/E,CAAC;AAEN,CAAC;AAED,OAAO,MAAMiB,cAAuC,GAAG;EAACd,OAAO;EAAEZ,MAAM;EAAEM;AAAM,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { ZoomTransitionEnabler as NativeZoomTransitionEnabler, ZoomTransitionSource as NativeZoomTransitionSource, ZoomTransitionTarget as NativeZoomTransitionTarget } from '@hzblj/kara-native/swift-ui';
|
|
4
|
+
import { useAppTheme } from '@hzblj/kara-theme';
|
|
5
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
6
|
+
const Source = ({
|
|
7
|
+
children,
|
|
8
|
+
identifier
|
|
9
|
+
}) => identifier ? /*#__PURE__*/_jsx(NativeZoomTransitionSource, {
|
|
10
|
+
identifier: identifier,
|
|
11
|
+
children: children
|
|
12
|
+
}) : children;
|
|
13
|
+
const Target = ({
|
|
14
|
+
children,
|
|
15
|
+
identifier,
|
|
16
|
+
ios
|
|
17
|
+
}) => identifier ? /*#__PURE__*/_jsx(NativeZoomTransitionTarget, {
|
|
18
|
+
identifier: identifier,
|
|
19
|
+
isAspectRatioMatched: ios?.fit !== 'target',
|
|
20
|
+
children: children
|
|
21
|
+
}) : children;
|
|
22
|
+
|
|
23
|
+
/** UIKit's zoom on the screen's own view controller; the source and target are looked up natively by identifier. */
|
|
24
|
+
const Enabler = ({
|
|
25
|
+
ios,
|
|
26
|
+
scrim,
|
|
27
|
+
sourceIdentifier
|
|
28
|
+
}) => {
|
|
29
|
+
const theme = useAppTheme();
|
|
30
|
+
if (!sourceIdentifier) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const dismissGesture = ios?.dismissGesture ?? 'any';
|
|
34
|
+
return /*#__PURE__*/_jsx(NativeZoomTransitionEnabler, {
|
|
35
|
+
dimmingBlur: ios?.blur,
|
|
36
|
+
dimmingColor: scrim ? theme.color(scrim) : undefined,
|
|
37
|
+
isDismissalEnabled: dismissGesture !== 'none',
|
|
38
|
+
isHorizontalDismissalEnabled: dismissGesture === 'any',
|
|
39
|
+
sourceIdentifier: sourceIdentifier
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
export const ZoomTransition = {
|
|
43
|
+
Enabler,
|
|
44
|
+
Source,
|
|
45
|
+
Target
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=zoom-transition.ios.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["ZoomTransitionEnabler","NativeZoomTransitionEnabler","ZoomTransitionSource","NativeZoomTransitionSource","ZoomTransitionTarget","NativeZoomTransitionTarget","useAppTheme","jsx","_jsx","Source","children","identifier","Target","ios","isAspectRatioMatched","fit","Enabler","scrim","sourceIdentifier","theme","dismissGesture","dimmingBlur","blur","dimmingColor","color","undefined","isDismissalEnabled","isHorizontalDismissalEnabled","ZoomTransition"],"sourceRoot":"../../../src","sources":["zoom-transition/zoom-transition.ios.tsx"],"mappings":";;AAAA,SACEA,qBAAqB,IAAIC,2BAA2B,EACpDC,oBAAoB,IAAIC,0BAA0B,EAClDC,oBAAoB,IAAIC,0BAA0B,QAC7C,6BAA6B;AACpC,SAAQC,WAAW,QAAO,mBAAmB;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAQ7C,MAAMC,MAAM,GAAGA,CAAC;EAACC,QAAQ;EAAEC;AAAqC,CAAC,KAC/DA,UAAU,gBAAGH,IAAA,CAACL,0BAA0B;EAACQ,UAAU,EAAEA,UAAW;EAAAD,QAAA,EAAEA;AAAQ,CAA6B,CAAC,GAAGA,QAAQ;AAErH,MAAME,MAAM,GAAGA,CAAC;EAACF,QAAQ;EAAEC,UAAU;EAAEE;AAA8B,CAAC,KACpEF,UAAU,gBACRH,IAAA,CAACH,0BAA0B;EAACM,UAAU,EAAEA,UAAW;EAACG,oBAAoB,EAAED,GAAG,EAAEE,GAAG,KAAK,QAAS;EAAAL,QAAA,EAC7FA;AAAQ,CACiB,CAAC,GAE7BA,QACD;;AAEH;AACA,MAAMM,OAAO,GAAGA,CAAC;EAACH,GAAG;EAAEI,KAAK;EAAEC;AAA4C,CAAC,KAAK;EAC9E,MAAMC,KAAK,GAAGb,WAAW,CAAC,CAAC;EAE3B,IAAI,CAACY,gBAAgB,EAAE;IACrB,OAAO,IAAI;EACb;EAEA,MAAME,cAAc,GAAGP,GAAG,EAAEO,cAAc,IAAI,KAAK;EAEnD,oBACEZ,IAAA,CAACP,2BAA2B;IAC1BoB,WAAW,EAAER,GAAG,EAAES,IAAK;IACvBC,YAAY,EAAEN,KAAK,GAAGE,KAAK,CAACK,KAAK,CAACP,KAAK,CAAC,GAAGQ,SAAU;IACrDC,kBAAkB,EAAEN,cAAc,KAAK,MAAO;IAC9CO,4BAA4B,EAAEP,cAAc,KAAK,KAAM;IACvDF,gBAAgB,EAAEA;EAAiB,CACpC,CAAC;AAEN,CAAC;AAED,OAAO,MAAMU,cAAuC,GAAG;EAACZ,OAAO;EAAEP,MAAM;EAAEG;AAAM,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["zoom-transition/zoom-transition.types.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An identifier for a `ZoomTransition.Source`, unique to this component instance and to `key` — the item it shows.
|
|
3
|
+
* Keyed by the item, a recycled list cell registers under a new identifier when it shows another item, so a detail
|
|
4
|
+
* opened from its previous item zooms back out plainly instead of into the wrong cell.
|
|
5
|
+
*/
|
|
6
|
+
export declare const useZoomTransitionSourceId: (key: string) => string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type RouteProps = {
|
|
2
|
+
route: {
|
|
3
|
+
params?: object;
|
|
4
|
+
};
|
|
5
|
+
};
|
|
6
|
+
type ZoomAnimation = {
|
|
7
|
+
animation?: 'fade';
|
|
8
|
+
animationDuration?: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* `Stack.Screen` options for a screen the zoom pushes. On Android a push whose params carry `ZOOM_TRANSITION_PARAM`
|
|
12
|
+
* becomes a quick fade, so Material's container transform reads as the transition instead of racing the stack's own
|
|
13
|
+
* slide; any other push keeps the stack's animation. Your options win: set `animation` yourself to keep another one.
|
|
14
|
+
* iOS needs nothing — UIKit's zoom replaces the push animation.
|
|
15
|
+
*/
|
|
16
|
+
export declare const withZoomTransition: <TOptions extends object>(options?: TOptions | ((props: RouteProps) => TOptions)) => (props: RouteProps) => Partial<TOptions> & ZoomAnimation;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The route param that carries a source's identifier to the screen it pushes — what `withZoomTransition` looks for.
|
|
3
|
+
* Push with `params: {[ZOOM_TRANSITION_PARAM]: sourceId}` and read it back with `useZoomTransitionParam`.
|
|
4
|
+
*/
|
|
5
|
+
export declare const ZOOM_TRANSITION_PARAM = "zoomTransitionSourceId";
|
|
6
|
+
/** The source identifier this screen was pushed with — for its `ZoomTransition.Enabler` and `Target`. */
|
|
7
|
+
export declare const useZoomTransitionParam: () => string | undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './zoom-transition.ios';
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ColorToken, DurationSize, RadiiSize } from '@hzblj/kara-theme';
|
|
2
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
3
|
+
export type ZoomTransitionSourceAndroidOptions = {
|
|
4
|
+
/** The corners the transform starts with — the source's own rounding (iOS reads the view's own). Default `none`. */
|
|
5
|
+
borderRadius?: RadiiSize;
|
|
6
|
+
};
|
|
7
|
+
export type ZoomTransitionSourceProps = {
|
|
8
|
+
/** Android-only settings. */
|
|
9
|
+
android?: ZoomTransitionSourceAndroidOptions;
|
|
10
|
+
/** A single view — the photo or card the zoom grows from. */
|
|
11
|
+
children: ReactElement;
|
|
12
|
+
/** Unset renders `children` alone: no zoom from here. Use `useZoomTransitionSourceId` for one unique per item. */
|
|
13
|
+
identifier?: string;
|
|
14
|
+
};
|
|
15
|
+
export type ZoomTransitionTargetIosOptions = {
|
|
16
|
+
/**
|
|
17
|
+
* What the source lands on. `source` (default): a rect of the source's aspect ratio centred in the target, so
|
|
18
|
+
* nothing is stretched. `target`: the whole target.
|
|
19
|
+
*/
|
|
20
|
+
fit?: 'source' | 'target';
|
|
21
|
+
};
|
|
22
|
+
export type ZoomTransitionTargetAndroidOptions = {
|
|
23
|
+
/** The corners the transform ends with — the target's own rounding. Default `none`. */
|
|
24
|
+
borderRadius?: RadiiSize;
|
|
25
|
+
/**
|
|
26
|
+
* What the source lands on. `target` (default): the whole target. `photo`: the photo inside it — where a `contain`
|
|
27
|
+
* photo sits in a tall page — with the photo's cropped edges fading in on the way.
|
|
28
|
+
*/
|
|
29
|
+
fit?: 'photo' | 'target';
|
|
30
|
+
/** The Android target is a real layout box (iOS adds none): give it `flex` when its child fills the parent. */
|
|
31
|
+
flex?: number;
|
|
32
|
+
};
|
|
33
|
+
export type ZoomTransitionTargetProps = {
|
|
34
|
+
/** Android-only settings. */
|
|
35
|
+
android?: ZoomTransitionTargetAndroidOptions;
|
|
36
|
+
/** A single view — what the source lands on in the pushed screen. */
|
|
37
|
+
children: ReactElement;
|
|
38
|
+
/** The source's identifier, handed over in the route params. Unset renders `children` alone. */
|
|
39
|
+
identifier?: string;
|
|
40
|
+
/** iOS-only settings. */
|
|
41
|
+
ios?: ZoomTransitionTargetIosOptions;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Which of UIKit's gestures may dismiss the screen back into its source. `any`: swipe down, edge swipe and pinch — a
|
|
45
|
+
* swipe only once no scroll view under the finger can still scroll that way. `vertical`: a downward swipe only, for
|
|
46
|
+
* content that pages sideways under its own gestures (a gallery). `none`: the back button only.
|
|
47
|
+
*/
|
|
48
|
+
export type ZoomTransitionDismissGesture = 'any' | 'none' | 'vertical';
|
|
49
|
+
export type ZoomTransitionEnablerIosOptions = {
|
|
50
|
+
/** A system material blurring the screen behind the zoomed one. Default none. */
|
|
51
|
+
blur?: 'regular' | 'thick' | 'thin' | 'ultraThin';
|
|
52
|
+
/** Default `any`. */
|
|
53
|
+
dismissGesture?: ZoomTransitionDismissGesture;
|
|
54
|
+
};
|
|
55
|
+
export type ZoomTransitionEnablerAndroidOptions = {
|
|
56
|
+
/** Default `d450`. iOS's zoom is UIKit's own spring. */
|
|
57
|
+
duration?: DurationSize;
|
|
58
|
+
/** Default `emphasized` (Material 3, `0.2, 0, 0, 1`); `standard` is `0.4, 0, 0.2, 1`, `decelerate` `0, 0, 0, 1`. */
|
|
59
|
+
easing?: 'decelerate' | 'emphasized' | 'linear' | 'standard';
|
|
60
|
+
/** How the two screens' contents fade into each other inside the container. Default `cross`. */
|
|
61
|
+
fade?: 'cross' | 'in' | 'out' | 'through';
|
|
62
|
+
/** The path the container travels. Default `arc`. */
|
|
63
|
+
path?: 'arc' | 'linear';
|
|
64
|
+
};
|
|
65
|
+
export type ZoomTransitionEnablerProps = {
|
|
66
|
+
/** Android-only settings: Material's container transform. */
|
|
67
|
+
android?: ZoomTransitionEnablerAndroidOptions;
|
|
68
|
+
/** iOS-only settings: UIKit's zoom. */
|
|
69
|
+
ios?: ZoomTransitionEnablerIosOptions;
|
|
70
|
+
/** Laid over the screen behind the zoomed one. Unset: UIKit's own dimming on iOS, none on Android. */
|
|
71
|
+
scrim?: ColorToken;
|
|
72
|
+
/** The source's identifier, handed over in the route params. Unset renders nothing: the push stays the stack's own. */
|
|
73
|
+
sourceIdentifier?: string;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* A push that grows out of the view it was opened from — a photo into its detail — and shrinks back into it. iOS 18:
|
|
77
|
+
* UIKit's zoom transition (`preferredTransition = .zoom`), interactive dismissal included; below iOS 18 the push stays
|
|
78
|
+
* the regular slide. Android: Material's container transform from the source to the target, both ways.
|
|
79
|
+
*
|
|
80
|
+
* Three pieces, tied by one identifier: `Source` around the view on the screen you push from, `Enabler` and `Target`
|
|
81
|
+
* on the pushed screen. Works with any Expo Router stack screen.
|
|
82
|
+
*/
|
|
83
|
+
export type ZoomTransitionComponent = {
|
|
84
|
+
/**
|
|
85
|
+
* Turns the push of the screen it is rendered in into the zoom, and carries the animation settings. Render it
|
|
86
|
+
* unconditionally, before any loading state: on iOS it has to exist in the screen's first commit.
|
|
87
|
+
*/
|
|
88
|
+
Enabler: (props: ZoomTransitionEnablerProps) => ReactNode;
|
|
89
|
+
/** Marks the view the zoom grows from. Inside a `ContextMenu`, a tap on the preview zooms the preview's photo. */
|
|
90
|
+
Source: (props: ZoomTransitionSourceProps) => ReactNode;
|
|
91
|
+
/** Marks what the source lands on in the pushed screen. It may render later than the enabler, once data arrives. */
|
|
92
|
+
Target: (props: ZoomTransitionTargetProps) => ReactNode;
|
|
93
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hzblj/kara-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Expo Router pieces of @hzblj/kara-ui: native tabs and a navigation theme mapped from the active kara theme.",
|
|
5
5
|
"author": "Jan Blazej",
|
|
6
6
|
"license": "MIT",
|
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
]
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@hzblj/kara-components": "^0.
|
|
63
|
-
"@hzblj/kara-native": "^0.
|
|
64
|
-
"@hzblj/kara-theme": "^0.
|
|
62
|
+
"@hzblj/kara-components": "^0.6.0",
|
|
63
|
+
"@hzblj/kara-native": "^0.6.0",
|
|
64
|
+
"@hzblj/kara-theme": "^0.6.0"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"expo-router": "*",
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import {useId} from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An identifier for a `ZoomTransition.Source`, unique to this component instance and to `key` — the item it shows.
|
|
5
|
+
* Keyed by the item, a recycled list cell registers under a new identifier when it shows another item, so a detail
|
|
6
|
+
* opened from its previous item zooms back out plainly instead of into the wrong cell.
|
|
7
|
+
*/
|
|
8
|
+
export const useZoomTransitionSourceId = (key: string) => `${useId()}${key}`
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {duration} from '@hzblj/kara-theme'
|
|
2
|
+
import {Platform} from 'react-native'
|
|
3
|
+
import {ZOOM_TRANSITION_PARAM} from './zoom-transition-param'
|
|
4
|
+
|
|
5
|
+
type RouteProps = {route: {params?: object}}
|
|
6
|
+
|
|
7
|
+
type ZoomAnimation = {animation?: 'fade'; animationDuration?: number}
|
|
8
|
+
|
|
9
|
+
const hasZoomSource = (params: object | undefined) =>
|
|
10
|
+
!!params && !!(params as Record<string, unknown>)[ZOOM_TRANSITION_PARAM]
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `Stack.Screen` options for a screen the zoom pushes. On Android a push whose params carry `ZOOM_TRANSITION_PARAM`
|
|
14
|
+
* becomes a quick fade, so Material's container transform reads as the transition instead of racing the stack's own
|
|
15
|
+
* slide; any other push keeps the stack's animation. Your options win: set `animation` yourself to keep another one.
|
|
16
|
+
* iOS needs nothing — UIKit's zoom replaces the push animation.
|
|
17
|
+
*/
|
|
18
|
+
export const withZoomTransition =
|
|
19
|
+
<TOptions extends object>(options?: TOptions | ((props: RouteProps) => TOptions)) =>
|
|
20
|
+
(props: RouteProps): Partial<TOptions> & ZoomAnimation => {
|
|
21
|
+
const resolved: Partial<TOptions> = (typeof options === 'function' ? options(props) : options) ?? {}
|
|
22
|
+
|
|
23
|
+
if (Platform.OS !== 'android' || !hasZoomSource(props.route.params)) {
|
|
24
|
+
return resolved
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {animation: 'fade', animationDuration: duration.d300, ...resolved}
|
|
28
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {useLocalSearchParams} from 'expo-router'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The route param that carries a source's identifier to the screen it pushes — what `withZoomTransition` looks for.
|
|
5
|
+
* Push with `params: {[ZOOM_TRANSITION_PARAM]: sourceId}` and read it back with `useZoomTransitionParam`.
|
|
6
|
+
*/
|
|
7
|
+
export const ZOOM_TRANSITION_PARAM = 'zoomTransitionSourceId'
|
|
8
|
+
|
|
9
|
+
/** The source identifier this screen was pushed with — for its `ZoomTransition.Enabler` and `Target`. */
|
|
10
|
+
export const useZoomTransitionParam = (): string | undefined => {
|
|
11
|
+
const value = useLocalSearchParams()[ZOOM_TRANSITION_PARAM]
|
|
12
|
+
|
|
13
|
+
return (Array.isArray(value) ? value[0] : value) || undefined
|
|
14
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ZoomTransitionEnabler as NativeZoomTransitionEnabler,
|
|
3
|
+
ZoomTransitionSource as NativeZoomTransitionSource,
|
|
4
|
+
ZoomTransitionTarget as NativeZoomTransitionTarget,
|
|
5
|
+
reverseZoomTransition,
|
|
6
|
+
} from '@hzblj/kara-native/jetpack-compose'
|
|
7
|
+
import {duration, radii, useAppTheme} from '@hzblj/kara-theme'
|
|
8
|
+
import {useNavigation} from 'expo-router'
|
|
9
|
+
import {useEffect, useMemo} from 'react'
|
|
10
|
+
import type {
|
|
11
|
+
ZoomTransitionComponent,
|
|
12
|
+
ZoomTransitionEnablerProps,
|
|
13
|
+
ZoomTransitionSourceProps,
|
|
14
|
+
ZoomTransitionTargetProps,
|
|
15
|
+
} from './zoom-transition.types'
|
|
16
|
+
|
|
17
|
+
const Source = ({android, children, identifier}: ZoomTransitionSourceProps) =>
|
|
18
|
+
identifier ? (
|
|
19
|
+
<NativeZoomTransitionSource cornerRadius={radii[android?.borderRadius ?? 'none']} identifier={identifier}>
|
|
20
|
+
{children}
|
|
21
|
+
</NativeZoomTransitionSource>
|
|
22
|
+
) : (
|
|
23
|
+
children
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const Target = ({android, children, identifier}: ZoomTransitionTargetProps) => {
|
|
27
|
+
const flex = android?.flex
|
|
28
|
+
const style = useMemo(() => (flex === undefined ? undefined : {flex}), [flex])
|
|
29
|
+
|
|
30
|
+
if (!identifier) {
|
|
31
|
+
return children
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<NativeZoomTransitionTarget
|
|
36
|
+
cornerRadius={radii[android?.borderRadius ?? 'none']}
|
|
37
|
+
identifier={identifier}
|
|
38
|
+
isFittedToSourceAspectRatio={android?.fit === 'photo'}
|
|
39
|
+
style={style}
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
42
|
+
</NativeZoomTransitionTarget>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The target starts the way in natively once it is laid out. The way back cannot wait for the target to go: the pop is
|
|
48
|
+
* native, and by the time the screen unmounts it is already sliding away. So the transform is played back from
|
|
49
|
+
* `beforeRemove`, while the target is still on screen.
|
|
50
|
+
*/
|
|
51
|
+
const Enabler = ({android, scrim, sourceIdentifier}: ZoomTransitionEnablerProps) => {
|
|
52
|
+
const navigation = useNavigation()
|
|
53
|
+
const theme = useAppTheme()
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!sourceIdentifier) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return navigation.addListener('beforeRemove', () => {
|
|
61
|
+
void reverseZoomTransition(sourceIdentifier)
|
|
62
|
+
})
|
|
63
|
+
}, [navigation, sourceIdentifier])
|
|
64
|
+
|
|
65
|
+
if (!sourceIdentifier) {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<NativeZoomTransitionEnabler
|
|
71
|
+
easing={android?.easing}
|
|
72
|
+
fadeMode={android?.fade}
|
|
73
|
+
pathMotion={android?.path}
|
|
74
|
+
scrimColor={scrim ? theme.color(scrim) : undefined}
|
|
75
|
+
sourceIdentifier={sourceIdentifier}
|
|
76
|
+
transformDuration={android?.duration ? duration[android.duration] : undefined}
|
|
77
|
+
/>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const ZoomTransition: ZoomTransitionComponent = {Enabler, Source, Target}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ZoomTransitionEnabler as NativeZoomTransitionEnabler,
|
|
3
|
+
ZoomTransitionSource as NativeZoomTransitionSource,
|
|
4
|
+
ZoomTransitionTarget as NativeZoomTransitionTarget,
|
|
5
|
+
} from '@hzblj/kara-native/swift-ui'
|
|
6
|
+
import {useAppTheme} from '@hzblj/kara-theme'
|
|
7
|
+
import type {
|
|
8
|
+
ZoomTransitionComponent,
|
|
9
|
+
ZoomTransitionEnablerProps,
|
|
10
|
+
ZoomTransitionSourceProps,
|
|
11
|
+
ZoomTransitionTargetProps,
|
|
12
|
+
} from './zoom-transition.types'
|
|
13
|
+
|
|
14
|
+
const Source = ({children, identifier}: ZoomTransitionSourceProps) =>
|
|
15
|
+
identifier ? <NativeZoomTransitionSource identifier={identifier}>{children}</NativeZoomTransitionSource> : children
|
|
16
|
+
|
|
17
|
+
const Target = ({children, identifier, ios}: ZoomTransitionTargetProps) =>
|
|
18
|
+
identifier ? (
|
|
19
|
+
<NativeZoomTransitionTarget identifier={identifier} isAspectRatioMatched={ios?.fit !== 'target'}>
|
|
20
|
+
{children}
|
|
21
|
+
</NativeZoomTransitionTarget>
|
|
22
|
+
) : (
|
|
23
|
+
children
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
/** UIKit's zoom on the screen's own view controller; the source and target are looked up natively by identifier. */
|
|
27
|
+
const Enabler = ({ios, scrim, sourceIdentifier}: ZoomTransitionEnablerProps) => {
|
|
28
|
+
const theme = useAppTheme()
|
|
29
|
+
|
|
30
|
+
if (!sourceIdentifier) {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const dismissGesture = ios?.dismissGesture ?? 'any'
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<NativeZoomTransitionEnabler
|
|
38
|
+
dimmingBlur={ios?.blur}
|
|
39
|
+
dimmingColor={scrim ? theme.color(scrim) : undefined}
|
|
40
|
+
isDismissalEnabled={dismissGesture !== 'none'}
|
|
41
|
+
isHorizontalDismissalEnabled={dismissGesture === 'any'}
|
|
42
|
+
sourceIdentifier={sourceIdentifier}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const ZoomTransition: ZoomTransitionComponent = {Enabler, Source, Target}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type {ColorToken, DurationSize, RadiiSize} from '@hzblj/kara-theme'
|
|
2
|
+
import type {ReactElement, ReactNode} from 'react'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Every prop at the top level works on both platforms. What only one platform can do lives under `ios` or `android`,
|
|
6
|
+
* typed apart, and is ignored on the other — so a call site reads which of its settings each platform honours.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type ZoomTransitionSourceAndroidOptions = {
|
|
10
|
+
/** The corners the transform starts with — the source's own rounding (iOS reads the view's own). Default `none`. */
|
|
11
|
+
borderRadius?: RadiiSize
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ZoomTransitionSourceProps = {
|
|
15
|
+
/** Android-only settings. */
|
|
16
|
+
android?: ZoomTransitionSourceAndroidOptions
|
|
17
|
+
/** A single view — the photo or card the zoom grows from. */
|
|
18
|
+
children: ReactElement
|
|
19
|
+
/** Unset renders `children` alone: no zoom from here. Use `useZoomTransitionSourceId` for one unique per item. */
|
|
20
|
+
identifier?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ZoomTransitionTargetIosOptions = {
|
|
24
|
+
/**
|
|
25
|
+
* What the source lands on. `source` (default): a rect of the source's aspect ratio centred in the target, so
|
|
26
|
+
* nothing is stretched. `target`: the whole target.
|
|
27
|
+
*/
|
|
28
|
+
fit?: 'source' | 'target'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ZoomTransitionTargetAndroidOptions = {
|
|
32
|
+
/** The corners the transform ends with — the target's own rounding. Default `none`. */
|
|
33
|
+
borderRadius?: RadiiSize
|
|
34
|
+
/**
|
|
35
|
+
* What the source lands on. `target` (default): the whole target. `photo`: the photo inside it — where a `contain`
|
|
36
|
+
* photo sits in a tall page — with the photo's cropped edges fading in on the way.
|
|
37
|
+
*/
|
|
38
|
+
fit?: 'photo' | 'target'
|
|
39
|
+
/** The Android target is a real layout box (iOS adds none): give it `flex` when its child fills the parent. */
|
|
40
|
+
flex?: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type ZoomTransitionTargetProps = {
|
|
44
|
+
/** Android-only settings. */
|
|
45
|
+
android?: ZoomTransitionTargetAndroidOptions
|
|
46
|
+
/** A single view — what the source lands on in the pushed screen. */
|
|
47
|
+
children: ReactElement
|
|
48
|
+
/** The source's identifier, handed over in the route params. Unset renders `children` alone. */
|
|
49
|
+
identifier?: string
|
|
50
|
+
/** iOS-only settings. */
|
|
51
|
+
ios?: ZoomTransitionTargetIosOptions
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Which of UIKit's gestures may dismiss the screen back into its source. `any`: swipe down, edge swipe and pinch — a
|
|
56
|
+
* swipe only once no scroll view under the finger can still scroll that way. `vertical`: a downward swipe only, for
|
|
57
|
+
* content that pages sideways under its own gestures (a gallery). `none`: the back button only.
|
|
58
|
+
*/
|
|
59
|
+
export type ZoomTransitionDismissGesture = 'any' | 'none' | 'vertical'
|
|
60
|
+
|
|
61
|
+
export type ZoomTransitionEnablerIosOptions = {
|
|
62
|
+
/** A system material blurring the screen behind the zoomed one. Default none. */
|
|
63
|
+
blur?: 'regular' | 'thick' | 'thin' | 'ultraThin'
|
|
64
|
+
/** Default `any`. */
|
|
65
|
+
dismissGesture?: ZoomTransitionDismissGesture
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type ZoomTransitionEnablerAndroidOptions = {
|
|
69
|
+
/** Default `d450`. iOS's zoom is UIKit's own spring. */
|
|
70
|
+
duration?: DurationSize
|
|
71
|
+
/** Default `emphasized` (Material 3, `0.2, 0, 0, 1`); `standard` is `0.4, 0, 0.2, 1`, `decelerate` `0, 0, 0, 1`. */
|
|
72
|
+
easing?: 'decelerate' | 'emphasized' | 'linear' | 'standard'
|
|
73
|
+
/** How the two screens' contents fade into each other inside the container. Default `cross`. */
|
|
74
|
+
fade?: 'cross' | 'in' | 'out' | 'through'
|
|
75
|
+
/** The path the container travels. Default `arc`. */
|
|
76
|
+
path?: 'arc' | 'linear'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type ZoomTransitionEnablerProps = {
|
|
80
|
+
/** Android-only settings: Material's container transform. */
|
|
81
|
+
android?: ZoomTransitionEnablerAndroidOptions
|
|
82
|
+
/** iOS-only settings: UIKit's zoom. */
|
|
83
|
+
ios?: ZoomTransitionEnablerIosOptions
|
|
84
|
+
/** Laid over the screen behind the zoomed one. Unset: UIKit's own dimming on iOS, none on Android. */
|
|
85
|
+
scrim?: ColorToken
|
|
86
|
+
/** The source's identifier, handed over in the route params. Unset renders nothing: the push stays the stack's own. */
|
|
87
|
+
sourceIdentifier?: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A push that grows out of the view it was opened from — a photo into its detail — and shrinks back into it. iOS 18:
|
|
92
|
+
* UIKit's zoom transition (`preferredTransition = .zoom`), interactive dismissal included; below iOS 18 the push stays
|
|
93
|
+
* the regular slide. Android: Material's container transform from the source to the target, both ways.
|
|
94
|
+
*
|
|
95
|
+
* Three pieces, tied by one identifier: `Source` around the view on the screen you push from, `Enabler` and `Target`
|
|
96
|
+
* on the pushed screen. Works with any Expo Router stack screen.
|
|
97
|
+
*/
|
|
98
|
+
export type ZoomTransitionComponent = {
|
|
99
|
+
/**
|
|
100
|
+
* Turns the push of the screen it is rendered in into the zoom, and carries the animation settings. Render it
|
|
101
|
+
* unconditionally, before any loading state: on iOS it has to exist in the screen's first commit.
|
|
102
|
+
*/
|
|
103
|
+
Enabler: (props: ZoomTransitionEnablerProps) => ReactNode
|
|
104
|
+
/** Marks the view the zoom grows from. Inside a `ContextMenu`, a tap on the preview zooms the preview's photo. */
|
|
105
|
+
Source: (props: ZoomTransitionSourceProps) => ReactNode
|
|
106
|
+
/** Marks what the source lands on in the pushed screen. It may render later than the enabler, once data arrives. */
|
|
107
|
+
Target: (props: ZoomTransitionTargetProps) => ReactNode
|
|
108
|
+
}
|