@ledvance/base 1.3.68 → 1.3.72

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.
@@ -0,0 +1,448 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ /* eslint-disable react/no-array-index-key */
3
+ import React, { Component } from 'react';
4
+ import {
5
+ View,
6
+ PanResponder,
7
+ GestureResponderEvent,
8
+ LayoutChangeEvent,
9
+ PanResponderGestureState,
10
+ PanResponderInstance,
11
+ Animated,
12
+ StyleProp,
13
+ ViewStyle,
14
+ } from 'react-native';
15
+ import _ from 'lodash';
16
+ import { Svg, Rect, Defs, LinearGradient, Stop } from 'react-native-svg';
17
+ import Thumb from './Thumb';
18
+ import Res from './res';
19
+
20
+ export interface Point {
21
+ x: number;
22
+ y: number;
23
+ }
24
+
25
+ /**
26
+ * thumb 的有效区域
27
+ */
28
+ export interface ValidBound {
29
+ width: number;
30
+ height: number;
31
+ x: number;
32
+ y: number;
33
+ }
34
+
35
+ export interface ILinearColors {
36
+ offset: string;
37
+ stopColor: string;
38
+ stopOpacity: number;
39
+ }
40
+
41
+ export interface ILinear {
42
+ x1?: string | number;
43
+ x2?: string | number;
44
+ y1?: string | number;
45
+ y2?: string | number;
46
+ colors: ILinearColors[];
47
+ }
48
+
49
+ export const defaultProps = {
50
+ bgs: [] as ILinear[],
51
+ thumbComponent: Thumb,
52
+ disabled: false,
53
+ thumbSize: 38,
54
+ touchThumbSize: 60,
55
+ showThumbWhenDisabled: true,
56
+ clickEnabled: true, // 是否可以点击选择
57
+ lossShow: false, // 数据未在设备生效处理,若为true,则loassColor 启效,并thumb及亮度调节会变成指定颜色
58
+ lossColor: 'rgba(0,0,0,0.2)',
59
+ // eslint-disable-next-line import/no-unresolved
60
+ thumbImg: Res.thumbMask,
61
+ onGrant(_v: any) {},
62
+ onMove(_v: any) {},
63
+ onRelease(_v: any) {},
64
+ onPress(_v: any) {},
65
+ /**
66
+ * 背景透明度动画值
67
+ */
68
+ opacityAnimationValue: 1,
69
+ /**
70
+ * 背景透明度动画时间
71
+ */
72
+ opacityAnimationDuration: 150,
73
+ };
74
+
75
+ type DefaultProps = Readonly<typeof defaultProps>;
76
+
77
+ interface IProps extends DefaultProps {
78
+ value: any;
79
+ style?: StyleProp<ViewStyle>;
80
+ pickerStyle?: StyleProp<ViewStyle>;
81
+ coorToValue: (coor: Point, validBound: ValidBound) => any;
82
+ valueToCoor: (value: any, originCoor?: Point, validBound?: ValidBound) => Point;
83
+ valueToColor: (value: any) => string;
84
+ initData: (validBound?: ValidBound) => void;
85
+ }
86
+
87
+ interface IState {
88
+ value: any;
89
+ }
90
+
91
+ let idIndex = 0;
92
+
93
+ export default class RectPicker extends Component<IProps, IState> {
94
+ static defaultProps = defaultProps;
95
+
96
+ constructor(props: IProps) {
97
+ super(props);
98
+ this.state = { value: this.props.value };
99
+ this.bgOpacityAnim = new Animated.Value(this.props.opacityAnimationValue);
100
+ // rn的坑,需要在此赋值才有效果
101
+ this._panResponder = PanResponder.create({
102
+ onStartShouldSetPanResponder: this.handleSetResponder,
103
+ onPanResponderTerminationRequest: () => !this.locked,
104
+ onPanResponderGrant: this.handleGrant,
105
+ onPanResponderMove: this.handleMove,
106
+ onPanResponderRelease: this.handleRelease,
107
+ });
108
+ }
109
+
110
+ componentDidUpdate(prevProps: IProps) {
111
+ if (!this.locked) {
112
+ if (!_.isEqual(prevProps.value, this.props.value)) {
113
+ this.setState({ value: this.props.value });
114
+ const position = this.valueToCoor(this.props.value);
115
+ const color = this.valueToColor(this.props.value);
116
+ this.updateThumbPosition(position, color, true);
117
+ }
118
+ if (prevProps.lossShow !== this.props.lossShow) {
119
+ const color = this.valueToColor(this.props.value);
120
+ this.updateThumbPosition(
121
+ this.thumbPosition,
122
+ this.props.lossShow ? this.props.lossColor : color,
123
+ true
124
+ );
125
+ }
126
+ if (prevProps.thumbSize !== this.props.thumbSize) {
127
+ this.handleViewBoxChange(this.props.thumbSize);
128
+ }
129
+ if (prevProps.opacityAnimationValue !== this.props.opacityAnimationValue) {
130
+ if (this.bgOpacityAnimationRef) {
131
+ this.bgOpacityAnimationRef.stop();
132
+ }
133
+ this.bgOpacityAnimationRef = Animated.timing(this.bgOpacityAnim, {
134
+ toValue: this.props.opacityAnimationValue,
135
+ duration: this.props.opacityAnimationDuration,
136
+ useNativeDriver: true,
137
+ });
138
+ this.bgOpacityAnimationRef.start();
139
+ }
140
+ }
141
+ }
142
+
143
+ shouldComponentUpdate(nextProps: IProps, nextState: IState) {
144
+ if (this.locked) return false;
145
+
146
+ // 优化性能:只比较关键属性,避免深度比较
147
+ const propsChanged = (
148
+ !_.isEqual(nextProps.value, this.props.value) ||
149
+ nextProps.lossShow !== this.props.lossShow ||
150
+ nextProps.lossColor !== this.props.lossColor ||
151
+ nextProps.thumbSize !== this.props.thumbSize ||
152
+ nextProps.disabled !== this.props.disabled ||
153
+ nextProps.opacityAnimationValue !== this.props.opacityAnimationValue ||
154
+ nextProps.clickEnabled !== this.props.clickEnabled
155
+ );
156
+
157
+ const stateChanged = !_.isEqual(nextState.value, this.state.value);
158
+
159
+ return propsChanged || stateChanged;
160
+ }
161
+
162
+ private _panResponder: PanResponderInstance;
163
+ private thumbPosition: Point = { x: 0, y: 0 };
164
+ private color = 'red';
165
+ private showPicker = false;
166
+ private pickerWidth = 200;
167
+ private pickerHeight = 200;
168
+ private validBound: ValidBound = { x: 0, y: 0, width: 0, height: 0 };
169
+ private locked = false; // 是否锁定组件,锁定后,组件接受react正常更新
170
+ private thumbRef: Thumb;
171
+ private isThumbFocus = false;
172
+ private grantTime = 0;
173
+ private linearGradientId = `rectPicker_${idIndex++}`;
174
+ private bgOpacityAnim: Animated.Value = new Animated.Value(1);
175
+ private bgOpacityAnimationRef: Animated.CompositeAnimation | null = null;
176
+
177
+ componentWillUnmount() {
178
+ // 清理动画
179
+ if (this.bgOpacityAnim) {
180
+ this.bgOpacityAnim.removeAllListeners();
181
+ this.bgOpacityAnim.stopAnimation();
182
+ }
183
+
184
+ // 停止正在进行的动画
185
+ if (this.bgOpacityAnimationRef) {
186
+ this.bgOpacityAnimationRef.stop();
187
+ this.bgOpacityAnimationRef = null;
188
+ }
189
+
190
+ // 清理组件状态
191
+ this.locked = false;
192
+ this.isThumbFocus = false;
193
+ }
194
+
195
+ coorToValue(point: Point) {
196
+ const { coorToValue } = this.props;
197
+ if (typeof coorToValue === 'function') {
198
+ return coorToValue(point, this.validBound);
199
+ }
200
+ return point;
201
+ }
202
+
203
+ valueToCoor(value: any, originCoor?: Point): Point {
204
+ const { valueToCoor } = this.props;
205
+
206
+ // 是否有显示区, 无显示区直接返回原点坐标
207
+ const { width, height } = this.validBound;
208
+ if (width === 0 || height === 0) {
209
+ return { x: 0, y: 0 };
210
+ }
211
+ if (typeof valueToCoor === 'function') {
212
+ return valueToCoor(value, originCoor, this.validBound);
213
+ }
214
+ return originCoor || { x: 0, y: 0 };
215
+ }
216
+
217
+ valueToColor(value: any): string {
218
+ const { valueToColor } = this.props;
219
+ if (typeof valueToColor === 'function') {
220
+ return valueToColor(value);
221
+ }
222
+ return 'transparent';
223
+ }
224
+
225
+ firPropsEvent(cb: (params?: any) => void, ...args: any[]) {
226
+ cb && cb(...args);
227
+ }
228
+
229
+ handleSetResponder = (e: GestureResponderEvent) => {
230
+ if (this.props.disabled) {
231
+ return false;
232
+ }
233
+ // 是否点中标记
234
+ const { locationX, locationY } = e.nativeEvent;
235
+ const { thumbSize, touchThumbSize, clickEnabled } = this.props;
236
+ let validRadius = thumbSize / 2;
237
+ if (touchThumbSize) {
238
+ validRadius = touchThumbSize / 2;
239
+ }
240
+ const { x, y } = this.thumbPosition;
241
+ const length = Math.sqrt((locationX - x) ** 2 + (locationY - y) ** 2);
242
+ if (length <= validRadius) {
243
+ this.isThumbFocus = true;
244
+ return true;
245
+ }
246
+ this.isThumbFocus = false;
247
+ this.grantTime = +new Date();
248
+ return clickEnabled;
249
+ };
250
+
251
+ handleGrant = () => {
252
+ if (this.isThumbFocus) {
253
+ this.locked = true;
254
+ this.firPropsEvent(this.props.onGrant, this.state.value);
255
+ }
256
+ };
257
+
258
+ handleMove = (_e: GestureResponderEvent, gesture: PanResponderGestureState) => {
259
+ if (this.isThumbFocus) {
260
+ const value = this.handleGestureMove(gesture);
261
+ this.firPropsEvent(this.props.onMove, value);
262
+ }
263
+ };
264
+
265
+ handleRelease = (e: GestureResponderEvent, gesture: PanResponderGestureState) => {
266
+ if (this.isThumbFocus) {
267
+ this.locked = false;
268
+ const value = this.handleGestureMove(gesture, true);
269
+ this.setState({ value });
270
+ this.firPropsEvent(this.props.onRelease, value);
271
+ } else if (this.props.clickEnabled) {
272
+ // 点击选择颜色
273
+ const now = +new Date();
274
+ if (Math.abs(gesture.dx) < 4 && Math.abs(gesture.dy) < 4 && now - this.grantTime < 300) {
275
+ // 点击位置
276
+ const { locationX, locationY } = e.nativeEvent;
277
+ const { x: newX, y: newY } = this.formatCoor(locationX, locationY);
278
+ const value = this.coorToValue({ x: newX, y: newY });
279
+ const coor = this.valueToCoor(value, { x: newX, y: newY });
280
+ const color = this.valueToColor(value);
281
+ this.updateThumbPosition(coor, color, true);
282
+
283
+ this.firPropsEvent(this.props.onPress, value);
284
+ }
285
+ }
286
+ };
287
+
288
+ handleGestureMove(e: PanResponderGestureState, isEnd = false) {
289
+ const { dx, dy } = e;
290
+ const { x, y } = this.thumbPosition;
291
+ // 边界处理
292
+ const { x: newX, y: newY } = this.formatCoor(x + dx, y + dy);
293
+
294
+ // 转为实际值,再转回成坐标
295
+ const value = this.coorToValue({ x: newX, y: newY });
296
+ const coor = this.valueToCoor(value, { x: newX, y: newY });
297
+ const color = this.valueToColor(value);
298
+ this.updateThumbPosition(coor, color, isEnd);
299
+ return value;
300
+ }
301
+
302
+ formatCoor(x: number, y: number) {
303
+ const {
304
+ width: validWidth,
305
+ height: validHeight,
306
+ x: validStartX,
307
+ y: validStartY,
308
+ } = this.validBound;
309
+ let newX = x;
310
+ let newY = y;
311
+ // 边界处理
312
+ if (newX < validStartX) {
313
+ newX = validStartX;
314
+ } else if (newX > validWidth + validStartX) {
315
+ newX = validWidth + validStartX;
316
+ }
317
+ if (newY < validStartY) {
318
+ newY = validStartY;
319
+ } else if (newY > validHeight + validStartY) {
320
+ newY = validHeight + validStartY;
321
+ }
322
+
323
+ return { x: newX, y: newY };
324
+ }
325
+
326
+ updateThumbPosition(coor: Point, color: string, isEnd: boolean) {
327
+ // 添加输入验证
328
+ if (!coor || typeof coor.x !== 'number' || typeof coor.y !== 'number') {
329
+ console.warn('RectPicker: Invalid coordinates provided to updateThumbPosition');
330
+ return;
331
+ }
332
+
333
+ if (typeof color !== 'string') {
334
+ console.warn('RectPicker: Invalid color provided to updateThumbPosition');
335
+ return;
336
+ }
337
+
338
+ this.thumbRef && this.thumbRef.setNativeProps({ color, ...coor });
339
+ if (isEnd) {
340
+ this.color = color;
341
+ this.thumbPosition = coor;
342
+ }
343
+ }
344
+
345
+ handleViewBoxChange = async (thumbSize: number) => {
346
+ this.validBound = {
347
+ width: this.pickerWidth - thumbSize,
348
+ height: this.pickerHeight - thumbSize,
349
+ x: thumbSize / 2,
350
+ y: thumbSize / 2,
351
+ };
352
+ await this.props.initData(this.validBound);
353
+ this.thumbPosition = this.valueToCoor(this.props.value);
354
+ this.forceUpdate();
355
+ };
356
+
357
+ handlePickerLayout = async (e: LayoutChangeEvent) => {
358
+ const { thumbSize, lossColor, lossShow } = this.props;
359
+ const { width, height } = e.nativeEvent.layout;
360
+ if (width !== this.pickerWidth || height !== this.pickerHeight) {
361
+ this.pickerWidth = width || 10; // svg 尺寸不能为0,此处确保值大于0
362
+ this.pickerHeight = height || 10; // svg 尺寸不能为0,此处确保值大于0
363
+ if (!this.showPicker) {
364
+ this.showPicker = true;
365
+ this.color = lossShow ? lossColor : this.valueToColor(this.props.value);
366
+ }
367
+ await this.handleViewBoxChange(thumbSize);
368
+ }
369
+ };
370
+
371
+ render() {
372
+ const {
373
+ style,
374
+ pickerStyle,
375
+ bgs,
376
+ thumbComponent: ThumbView,
377
+ disabled,
378
+ thumbSize,
379
+ thumbImg,
380
+ } = this.props;
381
+ const { showPicker, pickerHeight, pickerWidth, thumbPosition } = this;
382
+ return (
383
+ <View
384
+ style={[{ flex: 1 }, style]}
385
+ {...this._panResponder.panHandlers}
386
+ pointerEvents="box-only"
387
+ onLayout={this.handlePickerLayout}
388
+ >
389
+ {showPicker && (
390
+ <Animated.View
391
+ style={[
392
+ {
393
+ opacity: this.bgOpacityAnim,
394
+ },
395
+ pickerStyle,
396
+ ]}
397
+ >
398
+ <Svg
399
+ height={pickerHeight}
400
+ width={pickerWidth}
401
+ viewBox={`0 0 ${pickerWidth} ${pickerHeight}`}
402
+ >
403
+ <Defs>
404
+ {bgs.map(({ x1 = '0%', x2 = '100%', y1 = '0%', y2 = '0%', colors }, index) => (
405
+ <LinearGradient
406
+ key={index}
407
+ id={`${this.linearGradientId}_${index}`}
408
+ x1={x1}
409
+ x2={x2}
410
+ y1={y1}
411
+ y2={y2}
412
+ >
413
+ {colors.map((color, i) => (
414
+ <Stop key={i} {...color} />
415
+ ))}
416
+ </LinearGradient>
417
+ ))}
418
+ </Defs>
419
+ {bgs.map((_bg, index) => (
420
+ <Rect
421
+ key={index}
422
+ fill={`url(#${this.linearGradientId}_${index})`}
423
+ x="0"
424
+ y="0"
425
+ width={pickerWidth}
426
+ height={pickerHeight}
427
+ />
428
+ ))}
429
+ </Svg>
430
+ </Animated.View>
431
+ )}
432
+ {/* render thumb */}
433
+ {showPicker && (
434
+ <ThumbView
435
+ ref={(ref: Thumb) => {
436
+ this.thumbRef = ref;
437
+ }}
438
+ {...thumbPosition}
439
+ img={thumbImg}
440
+ size={thumbSize}
441
+ color={this.color}
442
+ disabled={disabled}
443
+ />
444
+ )}
445
+ </View>
446
+ );
447
+ }
448
+ }