@ray-js/ray-ipc-half-horizontal-drag 0.0.7-beta.0 → 0.0.7-beta.1

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/README.md CHANGED
@@ -16,6 +16,16 @@ $ yarn add @ray-js/ray-ipc-half-horizontal-drag
16
16
 
17
17
  ## Develop
18
18
 
19
+ ### Lynx Android background images
20
+
21
+ With `renderMode="lynx"`, Android URL backgrounds use a real `Image` layer (`aspectFill`) to match the CSS cover layout. iOS and the default WebView implementation keep their existing CSS path. Card/tag/close behavior and the public props are unchanged. The local background fix was verified on Android on 2026-11-02 and is pending publication.
22
+
23
+ ### Lynx Android edge feedback (local integration)
24
+
25
+ The local Lynx Android branch adds bounded edge drag feedback around the existing native ScrollView. Normal scrolling stays native; out-of-range horizontal drags offset the track with resistance and return it on release/cancel. Vertical gestures remain native, dragged cards cannot fire click/close handlers, and page hide clears the offset. Bounds are measured in logical px for phone/tablet layouts. iOS and WebView keep the existing path and public props.
26
+
27
+ This is a local source integration, not a published beta update. It does not implement a replacement momentum scroller or promise identical iOS fling-at-edge physics. The Android local integration was accepted on 2026-11-02: the user confirmed edge return and vertical scrolling are normal.
28
+
19
29
  ```sh
20
30
  # install deps
21
31
  yarn
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+ import { ScrollView } from '@ray-js/ray';
3
+ type ScrollProps = React.ComponentProps<typeof ScrollView> & {
4
+ hideScrollbar?: boolean;
5
+ };
6
+ type Touch = {
7
+ identifier?: number | string;
8
+ clientX?: number;
9
+ clientY?: number;
10
+ pageX?: number;
11
+ pageY?: number;
12
+ };
13
+ type TouchEvent = {
14
+ touches?: Touch[];
15
+ origin?: {
16
+ touches?: Touch[];
17
+ };
18
+ };
19
+ export type ClickGuard = (callback: ((event?: any) => void) | undefined, event: any) => void;
20
+ type Props = Omit<ScrollProps, 'children'> & {
21
+ children: (track: {
22
+ id: string;
23
+ style: React.CSSProperties;
24
+ }, guard: ClickGuard) => React.ReactElement;
25
+ };
26
+ export declare function rubberBand(distance: number, width: number): number;
27
+ /** Android-only visual compensation; native scrolling and vertical gesture arbitration stay native. */
28
+ export declare class AndroidBounceScrollImpl extends React.Component<Props, {
29
+ offset: number;
30
+ returning: boolean;
31
+ }> {
32
+ constructor(props: Props);
33
+ componentDidMount(): void;
34
+ componentDidUpdate(previous: Props): void;
35
+ componentWillUnmount(): void;
36
+ onHide: () => void;
37
+ private id;
38
+ private alive;
39
+ private measureId;
40
+ private width;
41
+ private range;
42
+ private scrollLeft;
43
+ private moved;
44
+ private press?;
45
+ private measure;
46
+ start: (event: TouchEvent) => void;
47
+ move: (event: TouchEvent) => void;
48
+ end: () => void;
49
+ cancel: () => void;
50
+ private guard;
51
+ private scroll;
52
+ render(): React.JSX.Element;
53
+ }
54
+ export declare const AndroidBounceScroll: React.ComponentType<Props>;
55
+ export {};
@@ -0,0 +1,171 @@
1
+ import _extends from "@babel/runtime/helpers/esm/extends";
2
+ import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
3
+ import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
4
+ const _excluded = ["children"];
5
+ import React from 'react';
6
+ import { createSelectorQuery, ScrollView, withPageLifecycle } from '@ray-js/ray';
7
+ let sequence = 0;
8
+
9
+ // Limit the displacement in logical px, independent of rpx conversion and screen size.
10
+ export function rubberBand(distance, width) {
11
+ if (!(width > 0) || !Number.isFinite(distance)) return 0;
12
+ const limit = Math.min(56, width * 0.16);
13
+ return Math.sign(distance) * Math.min(limit, Math.abs(distance) * 0.36 / (1 + Math.abs(distance) / width));
14
+ }
15
+ function point(event) {
16
+ var _event$origin, _event$touches;
17
+ const touches = (event === null || event === void 0 || (_event$origin = event.origin) === null || _event$origin === void 0 ? void 0 : _event$origin.touches) || (event === null || event === void 0 ? void 0 : event.touches);
18
+ if ((touches === null || touches === void 0 ? void 0 : touches.length) !== 1) return undefined;
19
+ return _objectSpread(_objectSpread({}, event === null || event === void 0 || (_event$touches = event.touches) === null || _event$touches === void 0 ? void 0 : _event$touches[0]), touches[0]);
20
+ }
21
+
22
+ /** Android-only visual compensation; native scrolling and vertical gesture arbitration stay native. */
23
+ export class AndroidBounceScrollImpl extends React.Component {
24
+ constructor(props) {
25
+ super(props);
26
+ this.state = {
27
+ offset: 0,
28
+ returning: false
29
+ };
30
+ }
31
+ componentDidMount() {
32
+ this.alive = true;
33
+ this.measure();
34
+ }
35
+ componentDidUpdate(previous) {
36
+ if (previous.children !== this.props.children) {
37
+ this.cancel();
38
+ this.measure();
39
+ }
40
+ }
41
+ componentWillUnmount() {
42
+ this.alive = false;
43
+ this.measureId++;
44
+ this.press = undefined;
45
+ }
46
+ onHide = () => this.cancel();
47
+ id = (() => `half-drag-bounce-${++sequence}`)();
48
+ alive = false;
49
+ measureId = 0;
50
+ width = 0;
51
+ range = 0;
52
+ scrollLeft = 0;
53
+ moved = false;
54
+ measure = () => {
55
+ const request = ++this.measureId;
56
+ const query = createSelectorQuery();
57
+ let viewport = 0;
58
+ let content = 0;
59
+ query.select(`#${this.id}`).boundingClientRect(rect => {
60
+ viewport = (rect === null || rect === void 0 ? void 0 : rect.width) || 0;
61
+ });
62
+ query.select(`#${this.id}-track`).boundingClientRect(rect => {
63
+ content = (rect === null || rect === void 0 ? void 0 : rect.width) || 0;
64
+ });
65
+ query.exec(() => {
66
+ if (!this.alive || request !== this.measureId || viewport <= 0 || content <= 0) return;
67
+ this.width = viewport;
68
+ this.range = Math.max(0, content - viewport);
69
+ });
70
+ };
71
+ start = event => {
72
+ var _touch$clientX, _touch$clientY, _touch$identifier;
73
+ const touch = point(event);
74
+ if (!touch) {
75
+ this.cancel();
76
+ return;
77
+ }
78
+ const x = (_touch$clientX = touch.clientX) !== null && _touch$clientX !== void 0 ? _touch$clientX : touch.pageX;
79
+ const y = (_touch$clientY = touch.clientY) !== null && _touch$clientY !== void 0 ? _touch$clientY : touch.pageY;
80
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return;
81
+ this.moved = false;
82
+ this.press = {
83
+ identifier: (_touch$identifier = touch.identifier) !== null && _touch$identifier !== void 0 ? _touch$identifier : 0,
84
+ x,
85
+ y,
86
+ scrollLeft: this.scrollLeft
87
+ };
88
+ this.setState({
89
+ offset: 0,
90
+ returning: false
91
+ });
92
+ this.measure();
93
+ };
94
+ move = event => {
95
+ var _touch$identifier2, _touch$clientX2, _touch$clientY2;
96
+ const touch = point(event);
97
+ const {
98
+ press
99
+ } = this;
100
+ if (!touch || !press || ((_touch$identifier2 = touch.identifier) !== null && _touch$identifier2 !== void 0 ? _touch$identifier2 : 0) !== press.identifier) {
101
+ this.cancel();
102
+ return;
103
+ }
104
+ const x = (_touch$clientX2 = touch.clientX) !== null && _touch$clientX2 !== void 0 ? _touch$clientX2 : touch.pageX;
105
+ const y = (_touch$clientY2 = touch.clientY) !== null && _touch$clientY2 !== void 0 ? _touch$clientY2 : touch.pageY;
106
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return;
107
+ const dx = x - press.x;
108
+ const dy = y - press.y;
109
+ if (!press.axis) {
110
+ if (Math.max(Math.abs(dx), Math.abs(dy)) < 6) return;
111
+ this.moved = true;
112
+ press.axis = Math.abs(dx) > Math.abs(dy) * 1.2 ? 'x' : 'y';
113
+ }
114
+ if (press.axis !== 'x' || this.range <= 0) return;
115
+ const requested = press.scrollLeft - dx;
116
+ const overflow = requested < 0 ? -requested : requested > this.range ? this.range - requested : 0;
117
+ const offset = rubberBand(overflow, this.width);
118
+ if (offset !== this.state.offset || this.state.returning) {
119
+ this.setState({
120
+ offset,
121
+ returning: false
122
+ });
123
+ }
124
+ };
125
+ end = () => {
126
+ this.press = undefined;
127
+ if (this.alive) this.setState({
128
+ offset: 0,
129
+ returning: true
130
+ });
131
+ };
132
+ cancel = () => {
133
+ this.moved = true;
134
+ this.end();
135
+ };
136
+ guard = (callback, event) => {
137
+ // Reset on the next touchstart, rather than a timer that might reject a later intentional tap.
138
+ if (this.alive && !this.moved) callback === null || callback === void 0 || callback(event);
139
+ };
140
+ scroll = event => {
141
+ var _this$props$onScroll, _this$props;
142
+ this.scrollLeft = Math.max(0, event.detail.scrollLeft || 0);
143
+ (_this$props$onScroll = (_this$props = this.props).onScroll) === null || _this$props$onScroll === void 0 || _this$props$onScroll.call(_this$props, event);
144
+ };
145
+ render() {
146
+ const _this$props2 = this.props,
147
+ {
148
+ children
149
+ } = _this$props2,
150
+ props = _objectWithoutProperties(_this$props2, _excluded);
151
+ return /*#__PURE__*/React.createElement(ScrollView, _extends({}, props, {
152
+ id: this.id,
153
+ onScroll: this.scroll,
154
+ onTouchStart: this.start,
155
+ onTouchMove: this.move,
156
+ onTouchEnd: this.end,
157
+ onTouchCancel: this.cancel
158
+ }), children({
159
+ id: `${this.id}-track`,
160
+ style: {
161
+ transform: `translateX(${this.state.offset}px)`,
162
+ transitionProperty: 'transform',
163
+ transitionDuration: this.state.returning ? '260ms' : '0ms',
164
+ transitionTimingFunction: 'cubic-bezier(0.2, 0.75, 0.3, 1)'
165
+ }
166
+ }, this.guard));
167
+ }
168
+ }
169
+
170
+ // Ray's older HOC declaration drops the wrapped component's props; runtime forwards them unchanged.
171
+ export const AndroidBounceScroll = withPageLifecycle(AndroidBounceScrollImpl);
package/lib/lynx/index.js CHANGED
@@ -1,11 +1,13 @@
1
+ import _extends from "@babel/runtime/helpers/esm/extends";
1
2
  import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
2
3
  import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
3
4
  const _excluded = ["background"];
4
5
  import "core-js/modules/esnext.iterator.map.js";
5
6
  import React from 'react';
6
- import { ScrollView, Text, View } from '@ray-js/ray';
7
+ import { getSystemInfoSync, Image, ScrollView, Text, View } from '@ray-js/ray';
7
8
  import clsx from 'clsx';
8
9
  import styles from './index.module.less';
10
+ import { AndroidBounceScroll } from './android-bounce';
9
11
 
10
12
  // Ray's newer host supports hideScrollbar; older supported Ray declarations omit it.
11
13
  // Keep this native-only prop local rather than changing installed dependency types.
@@ -39,45 +41,62 @@ const LynxHalfHorizontalDrag = _ref => {
39
41
  onScroll
40
42
  } = _ref;
41
43
  if (!data || data.length === 0) return null;
44
+ // Keep iOS on its existing CSS path; Android hosts may not draw CSS URL backgrounds.
45
+ const useImageBackground = getSystemInfoSync().platform === 'android';
42
46
  const scrollable = data.length >= 3 || showCloseBtn;
43
- return /*#__PURE__*/React.createElement(View, {
44
- className: clsx(styles.root, className),
45
- style: style
46
- }, /*#__PURE__*/React.createElement(LynxScrollView, {
47
- className: styles.viewport,
48
- scrollX: scrollable,
49
- scrollY: false,
50
- hideScrollbar: true,
51
- onScroll: onScroll
52
- }, /*#__PURE__*/React.createElement(View, {
47
+ const renderTrack = (trackProps, guard) => /*#__PURE__*/React.createElement(View, _extends({}, trackProps, {
53
48
  className: clsx(styles.track, !scrollable && styles.staticTrack)
54
- }, data.map((item, index) => /*#__PURE__*/React.createElement(View, {
55
- key: index // eslint-disable-line react/no-array-index-key
56
- ,
57
- className: clsx(styles.item, scrollable ? styles.scrollingItem : styles.staticItem, data.length === 1 && scrollable && styles.singleScrollingItem, index === 0 && styles.firstItem, index === data.length - 1 && !showCloseBtn && styles.lastItem, item.className),
58
- style: getBackgroundStyle(_objectSpread(_objectSpread({}, item.style), {}, {
59
- backgroundImage: item.backgroundImage ? `url(${item.backgroundImage})` : undefined,
60
- background: item.background || undefined
61
- })),
62
- onClick: item.onClick
63
- }, /*#__PURE__*/React.createElement(View, {
64
- className: styles.icon
65
- }, item.icon), /*#__PURE__*/React.createElement(View, {
66
- className: styles.title
67
- }, isText(item.title) ? /*#__PURE__*/React.createElement(Text, {
68
- className: styles.titleText,
69
- "text-maxline": 2
70
- }, item.title) : item.title), !!item.tag && /*#__PURE__*/React.createElement(View, {
71
- className: clsx(styles.tag, item.tagClassName),
72
- style: getBackgroundStyle(item.tagStyle)
73
- }, isText(item.tag) ? /*#__PURE__*/React.createElement(Text, {
74
- className: styles.tagText,
75
- "text-maxline": 1
76
- }, item.tag) : item.tag))), showCloseBtn && /*#__PURE__*/React.createElement(View, {
49
+ }), data.map((item, index) => {
50
+ var _item$style;
51
+ const hasImageBackground = useImageBackground && !!item.backgroundImage;
52
+ return /*#__PURE__*/React.createElement(View, {
53
+ key: index // eslint-disable-line react/no-array-index-key
54
+ ,
55
+ className: clsx(styles.item, scrollable ? styles.scrollingItem : styles.staticItem, data.length === 1 && scrollable && styles.singleScrollingItem, index === 0 && styles.firstItem, index === data.length - 1 && !showCloseBtn && styles.lastItem, item.className),
56
+ style: getBackgroundStyle(_objectSpread(_objectSpread({}, item.style), {}, {
57
+ backgroundImage: item.backgroundImage && !hasImageBackground ? `url(${item.backgroundImage})` : undefined,
58
+ background: item.background || undefined
59
+ }, hasImageBackground ? {
60
+ zIndex: 0
61
+ } : {})),
62
+ onClick: guard ? event => guard(item.onClick, event) : item.onClick
63
+ }, hasImageBackground && /*#__PURE__*/React.createElement(Image, {
64
+ src: item.backgroundImage,
65
+ mode: "aspectFill",
66
+ className: styles.backgroundImage,
67
+ style: ((_item$style = item.style) === null || _item$style === void 0 ? void 0 : _item$style.borderRadius) !== undefined ? {
68
+ borderRadius: item.style.borderRadius
69
+ } : undefined
70
+ }), /*#__PURE__*/React.createElement(View, {
71
+ className: styles.icon
72
+ }, item.icon), /*#__PURE__*/React.createElement(View, {
73
+ className: styles.title
74
+ }, isText(item.title) ? /*#__PURE__*/React.createElement(Text, {
75
+ className: styles.titleText,
76
+ "text-maxline": 2
77
+ }, item.title) : item.title), !!item.tag && /*#__PURE__*/React.createElement(View, {
78
+ className: clsx(styles.tag, item.tagClassName),
79
+ style: getBackgroundStyle(item.tagStyle)
80
+ }, isText(item.tag) ? /*#__PURE__*/React.createElement(Text, {
81
+ className: styles.tagText,
82
+ "text-maxline": 1
83
+ }, item.tag) : item.tag));
84
+ }), showCloseBtn && /*#__PURE__*/React.createElement(View, {
77
85
  className: styles.closeBtn,
78
- onClick: onCloseClick
86
+ onClick: guard ? event => guard(onCloseClick, event) : onCloseClick
79
87
  }, closeBtnIcon, isText(closeBtnTitle) ? /*#__PURE__*/React.createElement(Text, {
80
88
  className: styles.closeText
81
- }, closeBtnTitle) : closeBtnTitle))));
89
+ }, closeBtnTitle) : closeBtnTitle));
90
+ const scrollProps = {
91
+ className: styles.viewport,
92
+ scrollX: scrollable,
93
+ scrollY: false,
94
+ hideScrollbar: true,
95
+ onScroll
96
+ };
97
+ return /*#__PURE__*/React.createElement(View, {
98
+ className: clsx(styles.root, className),
99
+ style: style
100
+ }, useImageBackground && scrollable ? /*#__PURE__*/React.createElement(AndroidBounceScroll, scrollProps, renderTrack) : /*#__PURE__*/React.createElement(LynxScrollView, scrollProps, renderTrack()));
82
101
  };
83
102
  export default LynxHalfHorizontalDrag;
@@ -61,6 +61,16 @@
61
61
  width: var(--half-drag-item-width);
62
62
  margin-left: var(--half-drag-gap);
63
63
  }
64
+ .backgroundImage {
65
+ position: absolute;
66
+ top: 0;
67
+ left: 0;
68
+ width: 100%;
69
+ height: 100%;
70
+ border-radius: var(--half-drag-radius);
71
+ z-index: -1;
72
+ pointer-events: none;
73
+ }
64
74
  .singleScrollingItem {
65
75
  width: var(--half-drag-single-width);
66
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ray-js/ray-ipc-half-horizontal-drag",
3
- "version": "0.0.7-beta.0",
3
+ "version": "0.0.7-beta.1",
4
4
  "description": "水平方向可拖动,显示出下一个组件的小部分内容",
5
5
  "main": "lib/index",
6
6
  "files": [