@ledvance/base 1.3.71 → 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,589 @@
1
+ import React from 'react';
2
+ import {
3
+ View,
4
+ StyleSheet,
5
+ PanResponder,
6
+ PanResponderInstance,
7
+ LayoutChangeEvent,
8
+ Animated,
9
+ GestureResponderEvent,
10
+ PanResponderGestureState,
11
+ ViewStyle,
12
+ Easing,
13
+ StyleProp,
14
+ TextStyle,
15
+ } from 'react-native';
16
+ import { IconFont, TYText } from 'tuya-panel-kit';
17
+ import icons from './brightness-icons';
18
+
19
+ interface IPercentProps {
20
+ percent: number;
21
+ colorOver?: string;
22
+ colorInner?: string;
23
+ width?: number | string;
24
+ height?: number | string;
25
+ brightWidth: number;
26
+ /**
27
+ * 图标尺寸
28
+ */
29
+ iconSize?: number;
30
+ /**
31
+ * 百分比文字样式
32
+ */
33
+ percentStyle?: StyleProp<TextStyle>;
34
+ }
35
+
36
+ export class Percent extends React.Component<IPercentProps, IPercentProps> {
37
+ constructor(props: IPercentProps) {
38
+ super(props);
39
+ this.state = { ...this.props };
40
+ }
41
+
42
+ componentDidUpdate(prevProps: IPercentProps) {
43
+ if (prevProps.percent !== this.props.percent ||
44
+ prevProps.brightWidth !== this.props.brightWidth ||
45
+ prevProps.colorOver !== this.props.colorOver ||
46
+ prevProps.colorInner !== this.props.colorInner) {
47
+ this.setState({ ...this.props });
48
+ }
49
+ }
50
+
51
+ shouldComponentUpdate(nextProps: IPercentProps, nextState: IPercentProps) {
52
+ // 优化渲染性能,只在关键属性变化时重渲染
53
+ return (
54
+ nextState.percent !== this.state.percent ||
55
+ nextState.brightWidth !== this.state.brightWidth ||
56
+ nextState.colorOver !== this.state.colorOver ||
57
+ nextState.colorInner !== this.state.colorInner ||
58
+ nextProps.iconSize !== this.props.iconSize
59
+ );
60
+ }
61
+
62
+ setNativeProps(nextProps: IPercentProps) {
63
+ // 对于React Native 0.59.10,直接更新state是最可靠的方式
64
+ // 因为TYText组件可能不支持直接的原生属性更新
65
+ const { percent, brightWidth } = nextProps;
66
+
67
+ if (percent !== undefined || brightWidth !== undefined) {
68
+ // 直接更新state,但使用批量更新避免多次渲染
69
+ this.setState(prevState => ({
70
+ ...prevState,
71
+ ...(percent !== undefined && { percent }),
72
+ ...(brightWidth !== undefined && { brightWidth })
73
+ }));
74
+ }
75
+ }
76
+
77
+ render() {
78
+ const {
79
+ percent,
80
+ height,
81
+ width,
82
+ brightWidth,
83
+ colorOver,
84
+ colorInner,
85
+ iconSize,
86
+ percentStyle,
87
+ } = this.state;
88
+ let icon = icons.brightLevel1;
89
+ if (percent > 20 && percent <= 60) {
90
+ icon = icons.brightLevel2;
91
+ } else if (percent > 60) {
92
+ icon = icons.brightLevel3;
93
+ }
94
+ const percentText = `${percent}%`;
95
+ return (
96
+ <View style={[styles.percent, { height, width }]}>
97
+ <View style={{ flex: 1, alignItems: 'center', flexDirection: 'row' }}>
98
+ <IconFont d={icon} size={iconSize || 32} color={colorOver} style={styles.percentIcon} />
99
+ <TYText style={[styles.percentText, percentStyle, { color: colorOver }]}>
100
+ {percentText}
101
+ </TYText>
102
+ </View>
103
+ <View
104
+ style={{
105
+ position: 'absolute',
106
+ overflow: 'hidden',
107
+ opacity: 1,
108
+ height: '100%',
109
+ width: brightWidth,
110
+ alignItems: 'center',
111
+ flexDirection: 'row',
112
+ backgroundColor: colorOver,
113
+ }}
114
+ >
115
+ <IconFont style={styles.percentIcon} d={icon} size={iconSize || 32} color={colorInner} />
116
+ <TYText style={[styles.percentText, percentStyle, { color: colorInner }]}>
117
+ {percentText}
118
+ </TYText>
119
+ </View>
120
+ </View>
121
+ );
122
+ }
123
+ }
124
+ const defaultProps = {
125
+ value: 0,
126
+ min: 10,
127
+ max: 1000,
128
+ minPercent: 1,
129
+ disabled: false,
130
+ fontColor: '#000',
131
+ trackColor: '#666666',
132
+ activeColor: '#fff',
133
+ // formatPercent: null,
134
+ invalidSwipeDistance: 7,
135
+ clickEnabled: false, // 是否可以点击选择
136
+ onGrant(_v: number) {},
137
+ onMove(_v: number) {},
138
+ onRelease(_v: number) {},
139
+ onPress(_v: number) {},
140
+ showAnimation: true, // 使用动画显示
141
+ /**
142
+ * 背景透明度动画值
143
+ */
144
+ opacityAnimationValue: 1,
145
+ /**
146
+ * 背景透明度动画时间
147
+ */
148
+ opacityAnimationDuration: 150,
149
+ };
150
+
151
+ export interface IBrightOption {
152
+ /**
153
+ * 最小值
154
+ */
155
+ min?: number;
156
+ /**
157
+ * 最大值
158
+ */
159
+ max?: number;
160
+ /**
161
+ * 最小百分比
162
+ */
163
+ minPercent?: number;
164
+ /**
165
+ * 字体颜色
166
+ */
167
+ fontColor?: string;
168
+ /**
169
+ * 轨道背景色
170
+ */
171
+ trackColor?: string;
172
+ /**
173
+ * 激活区颜色
174
+ */
175
+ activeColor?: string;
176
+ /**
177
+ * 滑动生效的开始距离
178
+ * 默认为 7 个像素距离
179
+ */
180
+ invalidSwipeDistance?: number;
181
+ formatPercent?: (value: number) => number;
182
+ style?: StyleProp<ViewStyle>;
183
+ /**
184
+ * 图标大小
185
+ */
186
+ iconSize?: number;
187
+ /**
188
+ * 百分比样式
189
+ */
190
+ percentStyle?: StyleProp<TextStyle>;
191
+ }
192
+
193
+ type DefaultProps = Readonly<typeof defaultProps>;
194
+
195
+ type IProps = {
196
+ style?: ViewStyle | ViewStyle[];
197
+ formatPercent?: (value: number) => number;
198
+ iconSize?: number;
199
+ percentStyle?: StyleProp<TextStyle>;
200
+ } & DefaultProps;
201
+
202
+ interface IState {
203
+ value: number;
204
+ }
205
+
206
+ export default class Slider extends React.Component<IProps, IState> {
207
+ static defaultProps: DefaultProps = defaultProps;
208
+
209
+ constructor(props: IProps) {
210
+ super(props);
211
+ this.bgOpacityAnim = new Animated.Value(this.props.opacityAnimationValue);
212
+ this._panResponder = PanResponder.create({
213
+ // 要求成为响应者:
214
+ onStartShouldSetPanResponder: this.handleStartPanResponder,
215
+ onMoveShouldSetPanResponder: this.handleSetPanResponder,
216
+ onPanResponderTerminationRequest: () => !this.moving,
217
+ onPanResponderMove: this.onMove,
218
+ onPanResponderRelease: this.onRelease,
219
+ // 当前有其他的东西成为响应器并且没有释放它。
220
+ onPanResponderReject: this.handleTerminate,
221
+ onPanResponderTerminate: this.handleTerminate,
222
+ });
223
+
224
+ this.state = { value: this.props.value };
225
+ }
226
+
227
+ componentDidUpdate(prevProps: IProps) {
228
+ if (!this.locked) {
229
+ if (prevProps.value !== this.props.value) {
230
+ const newBrightWidth = this.valueToWidth(this.props.value);
231
+ const newPercent = this.formatPercent(this.props.value);
232
+
233
+ // 外部更新值时的同步处理
234
+
235
+ this.brightWidth = newBrightWidth;
236
+
237
+ // 关键修复:同时更新动画值和实例变量
238
+ this.setAnimationValue(newBrightWidth, false); // 使用动画更新
239
+ this.setState({ value: this.props.value });
240
+
241
+ // 同步更新Percent组件的显示
242
+ if (this.percentRef) {
243
+ this.percentRef.setNativeProps({
244
+ percent: newPercent,
245
+ brightWidth: newBrightWidth
246
+ });
247
+ }
248
+ }
249
+ }
250
+ if (prevProps.opacityAnimationValue !== this.props.opacityAnimationValue) {
251
+ if (this.bgOpacityAnimationRef) {
252
+ this.bgOpacityAnimationRef.stop();
253
+ }
254
+ this.bgOpacityAnimationRef = Animated.timing(this.bgOpacityAnim, {
255
+ toValue: this.props.opacityAnimationValue,
256
+ duration: this.props.opacityAnimationDuration,
257
+ useNativeDriver: true,
258
+ });
259
+ this.bgOpacityAnimationRef.start();
260
+ }
261
+ }
262
+
263
+ componentWillUnmount() {
264
+ // 清理动画
265
+ if (this.brightAnimate) {
266
+ this.brightAnimate.removeAllListeners();
267
+ this.brightAnimate.stopAnimation();
268
+ }
269
+
270
+ if (this.bgOpacityAnim) {
271
+ this.bgOpacityAnim.removeAllListeners();
272
+ this.bgOpacityAnim.stopAnimation();
273
+ }
274
+
275
+ // 停止正在进行的动画
276
+ if (this.bgOpacityAnimationRef) {
277
+ this.bgOpacityAnimationRef.stop();
278
+ this.bgOpacityAnimationRef = null;
279
+ }
280
+
281
+ // 清理组件状态
282
+ this.locked = false;
283
+ this.moving = false;
284
+ }
285
+
286
+ shouldComponentUpdate(nextProps: IProps, nextState: IState) {
287
+ if (this.locked) return false;
288
+
289
+ // 优化性能:只比较关键属性
290
+ const propsChanged = (
291
+ nextProps.value !== this.props.value ||
292
+ nextProps.disabled !== this.props.disabled ||
293
+ nextProps.trackColor !== this.props.trackColor ||
294
+ nextProps.activeColor !== this.props.activeColor ||
295
+ nextProps.fontColor !== this.props.fontColor ||
296
+ nextProps.clickEnabled !== this.props.clickEnabled ||
297
+ nextProps.opacityAnimationValue !== this.props.opacityAnimationValue
298
+ );
299
+
300
+ const stateChanged = nextState.value !== this.state.value;
301
+
302
+ return propsChanged || stateChanged;
303
+ }
304
+
305
+ onMove = (_e: GestureResponderEvent, gesture: PanResponderGestureState) => {
306
+ if (!this.moving) {
307
+ // 滑动一定象素后,将不允许其他手势抢权
308
+ if (Math.abs(gesture.dx) < this.props.invalidSwipeDistance) {
309
+ // 小于一定象素不做滑动
310
+ return;
311
+ }
312
+ // 开始手势
313
+ this.props.onGrant(this.state.value);
314
+ this.moving = true;
315
+ this.locked = true;
316
+ }
317
+ this.handleMove(gesture, this.props.onMove, false);
318
+ };
319
+
320
+ onRelease = (e: GestureResponderEvent, gesture: PanResponderGestureState) => {
321
+ if (this.moving) {
322
+ this.moving = false;
323
+ this.locked = false;
324
+ this.handleMove(gesture, this.props.onRelease, true);
325
+ } else if (this.props.clickEnabled) {
326
+ const now = +new Date();
327
+ if (Math.abs(gesture.dx) < 4 && Math.abs(gesture.dy) < 4 && now - this.grantTime < 300) {
328
+ const { locationX } = e.nativeEvent;
329
+ this.handleMove(
330
+ { dx: locationX - this.brightWidth } as PanResponderGestureState,
331
+ this.props.onPress,
332
+ true
333
+ );
334
+ }
335
+ }
336
+ };
337
+
338
+ setAnimationValue(value: number, isSliding = false) {
339
+ if (!isSliding && this.props.showAnimation) {
340
+ // 获取当前动画值进行比较,而不是brightWidth
341
+ const currentAnimValue = (this.brightAnimate as any)._value;
342
+ if (Math.abs(value - currentAnimValue) > 0.1) { // 添加小的容差
343
+ this.brightAnimate.stopAnimation();
344
+ const duration = isSliding
345
+ ? 32
346
+ : Math.round((300 * Math.abs(value - currentAnimValue)) / this.sliderWidth);
347
+ Animated.timing(this.brightAnimate, {
348
+ toValue: value,
349
+ duration: Math.max(duration, 100), // 确保最小动画时间
350
+ easing: Easing.linear,
351
+ useNativeDriver: false
352
+ }).start();
353
+ }
354
+ } else {
355
+ this.brightAnimate.setValue(value);
356
+ }
357
+ }
358
+
359
+ private _panResponder: PanResponderInstance;
360
+ private percentRef: Percent;
361
+ private locked = false;
362
+ private sliderWidth = 0;
363
+ private brightWidth = 0;
364
+ private brightAnimate: Animated.Value = new Animated.Value(0);
365
+ private showPercent = false;
366
+ private moving = false;
367
+ private grantTime = 0;
368
+ private bgOpacityAnim: Animated.Value = new Animated.Value(1);
369
+ private bgOpacityAnimationRef: Animated.CompositeAnimation | null = null;
370
+
371
+ handleStartPanResponder = () => {
372
+ if (this.props.disabled) {
373
+ return false;
374
+ }
375
+ this.grantTime = +new Date();
376
+ return this.props.clickEnabled;
377
+ };
378
+
379
+ handleSetPanResponder = (_e: GestureResponderEvent, gesture: PanResponderGestureState) => {
380
+ if (this.props.disabled) {
381
+ return false;
382
+ }
383
+ // 滑动一定象素后,将不允许其他手势抢权
384
+ if (Math.abs(gesture.dx) >= this.props.invalidSwipeDistance) {
385
+ // 小于一定象素不做滑动
386
+ if (!this.moving) {
387
+ this.props.onGrant(this.state.value);
388
+ this.moving = true;
389
+ this.locked = true;
390
+ }
391
+ return true;
392
+ }
393
+ if (this.moving) {
394
+ return true;
395
+ }
396
+ return false;
397
+ };
398
+
399
+ handleMove(gesture: PanResponderGestureState, callback: (value: number) => void, isEnd = false) {
400
+ const { dx } = gesture;
401
+ let width: number = this.brightWidth + dx;
402
+ // 边界处理
403
+ if (width < 0) {
404
+ width = 0;
405
+ } else if (width > this.sliderWidth) {
406
+ width = this.sliderWidth;
407
+ }
408
+
409
+ const value = this.coorToValue(width);
410
+ width = this.valueToWidth(value);
411
+
412
+ // 统一更新动画和百分比显示,确保同步
413
+ this.setAnimationValue(width, !isEnd);
414
+
415
+ // 实时更新Percent组件,确保同步
416
+ if (this.percentRef) {
417
+ const newPercent = this.formatPercent(value);
418
+ // 移除节流限制,确保实时同步
419
+ this.percentRef.setNativeProps({ percent: newPercent, brightWidth: width });
420
+ }
421
+
422
+ if (isEnd) {
423
+ this.brightWidth = width;
424
+ this.setState({ value });
425
+ }
426
+ callback(value);
427
+ }
428
+
429
+ handleTerminate = () => {
430
+ // 响应器已经从该视图抽离
431
+ this.moving = false;
432
+ this.locked = false;
433
+ };
434
+
435
+ handleLayout = (e: LayoutChangeEvent) => {
436
+ const { width } = e.nativeEvent.layout;
437
+ this.sliderWidth = width;
438
+ this.brightWidth = this.valueToWidth(this.state.value);
439
+
440
+ // 确保动画值和实例变量同步
441
+ this.brightAnimate.setValue(this.brightWidth);
442
+ this.showPercent = true;
443
+
444
+ // 初始化时同步Percent组件
445
+ if (this.percentRef) {
446
+ const newPercent = this.formatPercent(this.state.value);
447
+ this.percentRef.setNativeProps({
448
+ percent: newPercent,
449
+ brightWidth: this.brightWidth
450
+ });
451
+ }
452
+
453
+ this.forceUpdate();
454
+ };
455
+
456
+ formatPercent(value: number) {
457
+ const { min, max, formatPercent, minPercent } = this.props;
458
+
459
+ // 添加输入验证
460
+ if (typeof value !== 'number' || isNaN(value)) {
461
+ console.warn('Slider: Invalid value provided to formatPercent');
462
+ return minPercent || 0;
463
+ }
464
+
465
+ if (formatPercent) {
466
+ return formatPercent(value);
467
+ }
468
+ return Math.round(((value - min) * (100 - minPercent)) / (max - min) + minPercent);
469
+ }
470
+
471
+ valueToWidth(value: number) {
472
+ const { min, max } = this.props;
473
+
474
+ // 添加输入验证和边界处理
475
+ if (typeof value !== 'number' || isNaN(value)) {
476
+ console.warn('Slider: Invalid value provided to valueToWidth');
477
+ return 0;
478
+ }
479
+
480
+ const clampedValue = Math.max(min, Math.min(max, value));
481
+ const percent = (clampedValue - min) / (max - min);
482
+ return percent * this.sliderWidth;
483
+ }
484
+
485
+ coorToValue(x: number) {
486
+ const { min, max } = this.props;
487
+
488
+ // 添加输入验证和边界处理
489
+ if (typeof x !== 'number' || isNaN(x) || this.sliderWidth === 0) {
490
+ console.warn('Slider: Invalid coordinate provided to coorToValue');
491
+ return min;
492
+ }
493
+
494
+ const clampedX = Math.max(0, Math.min(this.sliderWidth, x));
495
+ return Math.round((clampedX / this.sliderWidth) * (max - min) + min);
496
+ }
497
+
498
+ render() {
499
+ const { trackColor, activeColor, fontColor, style, iconSize, percentStyle } = this.props;
500
+ const containerStyle = [styles.container, style];
501
+ const { height = 45 } = StyleSheet.flatten(containerStyle);
502
+ return (
503
+ <View
504
+ style={containerStyle}
505
+ accessibilityLabel="ReactColorPicker_Slider"
506
+ onLayout={this.handleLayout}
507
+ pointerEvents="box-only"
508
+ {...this._panResponder.panHandlers}
509
+ >
510
+ <Animated.View
511
+ style={{
512
+ height: '100%',
513
+ width: '100%',
514
+ opacity: this.bgOpacityAnim,
515
+ }}
516
+ >
517
+ <View
518
+ style={[
519
+ styles.track,
520
+ {
521
+ backgroundColor: trackColor,
522
+ },
523
+ ]}
524
+ />
525
+ <Animated.View
526
+ style={[
527
+ styles.mark,
528
+ {
529
+ backgroundColor: activeColor,
530
+ width: this.brightAnimate,
531
+ },
532
+ ]}
533
+ />
534
+ {this.showPercent && (
535
+ <Percent
536
+ ref={(ref: Percent) => {
537
+ this.percentRef = ref;
538
+ }}
539
+ percent={this.formatPercent(this.state.value)}
540
+ colorOver={activeColor}
541
+ colorInner={fontColor}
542
+ width={this.sliderWidth}
543
+ height={height}
544
+ brightWidth={this.brightWidth}
545
+ iconSize={iconSize}
546
+ percentStyle={percentStyle}
547
+ />
548
+ )}
549
+ </Animated.View>
550
+ </View>
551
+ );
552
+ }
553
+ }
554
+
555
+ const styles = StyleSheet.create({
556
+ container: {
557
+ alignItems: 'center',
558
+ flexDirection: 'row',
559
+ height: 45,
560
+ justifyContent: 'center',
561
+ width: '100%',
562
+ },
563
+ mark: {
564
+ backgroundColor: '#fff',
565
+ height: '100%',
566
+ left: 0,
567
+ position: 'absolute',
568
+ },
569
+ percent: {
570
+ alignItems: 'center',
571
+ flexDirection: 'row',
572
+ height: '100%',
573
+ justifyContent: 'flex-start',
574
+ left: 0,
575
+ position: 'absolute',
576
+ },
577
+ percentIcon: {
578
+ marginLeft: 16,
579
+ },
580
+ percentText: {
581
+ marginLeft: 9,
582
+ width: 50,
583
+ },
584
+ track: {
585
+ backgroundColor: '#666666',
586
+ height: '100%',
587
+ width: '100%',
588
+ },
589
+ });
@@ -0,0 +1,94 @@
1
+ import React, { Component } from 'react';
2
+ import { View, Image, StyleSheet, Animated, NativeComponent } from 'react-native';
3
+
4
+ interface IProps {
5
+ img?: any;
6
+ color: string;
7
+ size?: number;
8
+ x: number;
9
+ y: number;
10
+ disabled?: boolean;
11
+ }
12
+
13
+ export default class Thumb extends Component<IProps> {
14
+ componentDidUpdate(prevProps: IProps) {
15
+ if (prevProps.x !== this.props.x) {
16
+ this.animateX.setValue(this.props.x);
17
+ }
18
+ if (prevProps.y !== this.props.y) {
19
+ this.animateY.setValue(this.props.y);
20
+ }
21
+ }
22
+
23
+ componentWillUnmount() {
24
+ // 清理动画
25
+ if (this.animateX) {
26
+ this.animateX.removeAllListeners();
27
+ this.animateX.stopAnimation();
28
+ }
29
+ if (this.animateY) {
30
+ this.animateY.removeAllListeners();
31
+ this.animateY.stopAnimation();
32
+ }
33
+ }
34
+
35
+ setNativeProps(props: IProps) {
36
+ const { x, y, color } = props;
37
+ if (typeof x === 'number') {
38
+ this.animateX.setValue(x);
39
+ }
40
+ if (typeof y === 'number') {
41
+ this.animateY.setValue(y);
42
+ }
43
+ if (typeof color === 'string') {
44
+ this.previewRef.setNativeProps({
45
+ style: {
46
+ backgroundColor: color,
47
+ },
48
+ });
49
+ }
50
+ }
51
+
52
+ private animateX = new Animated.Value(this.props.x);
53
+ private animateY = new Animated.Value(this.props.y);
54
+ private previewRef: NativeComponent;
55
+
56
+ render() {
57
+ const { color, size, img } = this.props;
58
+ const maskWidth = (size / 38) * 62;
59
+ const halfWidth = maskWidth / 2;
60
+ return (
61
+ <Animated.View
62
+ style={[
63
+ styles.thumb,
64
+ {
65
+ top: -halfWidth,
66
+ left: -halfWidth,
67
+ width: maskWidth,
68
+ height: maskWidth,
69
+ transform: [{ translateX: this.animateX }, { translateY: this.animateY }],
70
+ },
71
+ ]}
72
+ >
73
+ <View
74
+ ref={(ref: NativeComponent) => {
75
+ this.previewRef = ref;
76
+ }}
77
+ style={{ borderRadius: size / 2, width: size, height: size, backgroundColor: color }}
78
+ />
79
+ <Image
80
+ source={img}
81
+ style={[StyleSheet.absoluteFill, { width: maskWidth, height: maskWidth }]}
82
+ />
83
+ </Animated.View>
84
+ );
85
+ }
86
+ }
87
+
88
+ const styles = StyleSheet.create({
89
+ thumb: {
90
+ alignItems: 'center',
91
+ justifyContent: 'center',
92
+ position: 'absolute',
93
+ },
94
+ });