Skip to content

Commit 55af7d4

Browse files
committed
Make drag more reliable
1 parent f0e04ae commit 55af7d4

File tree

4 files changed

+201
-203
lines changed

4 files changed

+201
-203
lines changed

LICENSE.txt

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
MIT License
2-
3-
Copyright (c) Faisal Arshed
4-
Permission is hereby granted, free of charge, to any person obtaining a copy
5-
of this software and associated documentation files (the "Software"), to deal
6-
in the Software without restriction, including without limitation the rights
7-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8-
copies of the Software, and to permit persons to whom the Software is
9-
furnished to do so, subject to the following conditions:
10-
11-
The above copyright notice and this permission notice shall be included in all
12-
copies or substantial portions of the Software.
13-
14-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1+
MIT License
2+
3+
Copyright (c) Faisal Arshed
4+
Permission is hereby granted, free of charge, to any person obtaining a copy
5+
of this software and associated documentation files (the "Software"), to deal
6+
in the Software without restriction, including without limitation the rights
7+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
copies of the Software, and to permit persons to whom the Software is
9+
furnished to do so, subject to the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included in all
12+
copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2020
SOFTWARE.

index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { default } from './lib/QuickScrollList';
1+
export { default } from './lib/QuickScrollList';

lib/QuickScrollList.js

Lines changed: 161 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -1,163 +1,161 @@
1-
import React from 'react';
2-
import { View, FlatList, Animated, Dimensions } from 'react-native';
3-
import { PanGestureHandler } from 'react-native-gesture-handler';
4-
5-
const ScreenWidth = Dimensions.get('window').width;
6-
7-
class QuickScrollList extends React.Component {
8-
constructor(props) {
9-
super(props);
10-
this.position = new Animated.Value(0);
11-
this.scrollBar = new Animated.Value(ScreenWidth);
12-
this.flatlistRef = React.createRef();
13-
this.disableOnScrollEvent = false;
14-
}
15-
16-
static defaultProps = {
17-
flashDuration: 40,
18-
flashOutDuration: 2000,
19-
rightOffset: 10,
20-
thumbHeight: 60,
21-
hiddenPosition: ScreenWidth + 10,
22-
touchAreaWidth: 25,
23-
thumbStyle: {},
24-
scrollbarStyle: {},
25-
containerStyle: {}
26-
};
27-
28-
createRef = ref => {
29-
this.flatlistRef = ref;
30-
this.props.ref && this.props.ref(ref);
31-
};
32-
33-
onThumbDrag = event => {
34-
const { data, itemHeight, thumbHeight, viewportHeight } = this.props;
35-
const availableHeight = viewportHeight - thumbHeight;
36-
const positionY = this.position.__getValue();
37-
const gestureY = event.nativeEvent.absoluteY;
38-
if (gestureY >= 0 && gestureY <= availableHeight) {
39-
this.disableOnScrollEvent = true;
40-
const thumbPos = (positionY / (viewportHeight - thumbHeight)).toFixed(3);
41-
let lastIndex = data.length - Math.floor(viewportHeight / itemHeight) + 1;
42-
let index = Math.floor(lastIndex * thumbPos);
43-
if (index > lastIndex) index = lastIndex;
44-
if (index < 0) index = 0;
45-
Animated.event([{ nativeEvent: { absoluteY: this.position } }])(event);
46-
this.flatlistRef.scrollToIndex({
47-
index,
48-
viewPosition: 0,
49-
animated: true
50-
});
51-
}
52-
};
53-
54-
moveThumbOnScroll = e => {
55-
if (this.disableOnScrollEvent) {
56-
this.disableOnScrollEvent = false;
57-
return;
58-
}
59-
const { itemHeight, data, thumbHeight, viewportHeight } = this.props;
60-
const listHeight = data.length * itemHeight;
61-
const endPosition = listHeight - viewportHeight;
62-
const offsetY = e.nativeEvent.contentOffset.y;
63-
const diff = (viewportHeight - thumbHeight) / endPosition;
64-
this.position.setValue(offsetY * diff);
65-
};
66-
67-
flashScrollBar = () => {
68-
const { flashDuration, rightOffset } = this.props;
69-
Animated.timing(this.scrollBar, {
70-
toValue: ScreenWidth - rightOffset,
71-
duration: flashDuration,
72-
useNativeDriver: true
73-
}).start();
74-
};
75-
76-
onScroll = (event, gesture) => {
77-
this.flashScrollBar();
78-
this.moveThumbOnScroll(event);
79-
this.props.onScroll && this.props.onScroll(event, gesture);
80-
};
81-
82-
onScrollEnd = (event, gesture) => {
83-
const { flashDuration, flashOutDuration } = this.props;
84-
const flashOut = Animated.timing(this.scrollBar, {
85-
toValue: this.props.hiddenPosition,
86-
duration: flashDuration,
87-
useNativeDriver: true
88-
});
89-
setTimeout(() => flashOut.start(), flashOutDuration);
90-
this.props.onScrollEndDrag && this.props.onScrollEndDrag(event, gesture);
91-
};
92-
93-
convertStyle(prop) {
94-
if (Array.isArray(prop)) {
95-
let propObj = {};
96-
prop.forEach(val => {
97-
propObj = { ...propObj, ...val };
98-
});
99-
return propObj;
100-
}
101-
return prop;
102-
}
103-
104-
render() {
105-
//prettier-ignore
106-
const { thumbHeight, thumbStyle, scrollbarStyle, containerStyle, viewportHeight, touchAreaWidth } = this.props;
107-
const rightOffset = { transform: [{ translateX: this.scrollBar }] };
108-
const thumbTransform = { transform: [{ translateY: this.position }] };
109-
return (
110-
<View style={[styles.mainWrapper, this.convertStyle(containerStyle)]}>
111-
<FlatList
112-
{...this.props}
113-
ref={this.createRef}
114-
onScroll={this.onScroll}
115-
onScrollEndDrag={this.onScrollEnd}
116-
showsVerticalScrollIndicator={false}
117-
onScrollToIndexFailed={() => {}}
118-
/>
119-
<Animated.View
120-
style={[
121-
styles.scrollBar,
122-
rightOffset,
123-
{ height: viewportHeight },
124-
this.convertStyle(scrollbarStyle)
125-
]}>
126-
<PanGestureHandler
127-
onGestureEvent={this.onThumbDrag}
128-
hitSlop={{ left: touchAreaWidth }}
129-
maxPointers={1}>
130-
<Animated.View
131-
style={[
132-
styles.thumb,
133-
thumbTransform,
134-
{ height: thumbHeight },
135-
this.convertStyle(thumbStyle)
136-
]}
137-
/>
138-
</PanGestureHandler>
139-
</Animated.View>
140-
</View>
141-
);
142-
}
143-
}
144-
145-
export default QuickScrollList;
146-
147-
const styles = {
148-
mainWrapper: {
149-
flex: 1
150-
},
151-
scrollBar: {
152-
position: 'absolute',
153-
width: 10,
154-
backgroundColor: 'transparent',
155-
alignItems: 'center'
156-
},
157-
thumb: {
158-
width: 4,
159-
borderRadius: 4,
160-
backgroundColor: '#4C4C4C',
161-
elevation: 2
162-
}
163-
};
1+
import React from 'react';
2+
import { View, FlatList, Animated, Dimensions } from 'react-native';
3+
import { PanGestureHandler } from 'react-native-gesture-handler';
4+
5+
const ScreenWidth = Dimensions.get('window').width;
6+
7+
class QuickScrollList extends React.Component {
8+
static defaultProps = {
9+
flashDuration: 40,
10+
flashOutDuration: 2000,
11+
rightOffset: 10,
12+
thumbHeight: 60,
13+
hiddenPosition: ScreenWidth + 10,
14+
touchAreaWidth: 25,
15+
thumbStyle: {},
16+
scrollbarStyle: {},
17+
containerStyle: {}
18+
};
19+
20+
position = new Animated.Value(0);
21+
scrollBar = new Animated.Value(ScreenWidth);
22+
flatlistRef = React.createRef();
23+
disableOnScrollEvent = false;
24+
25+
createRef = (ref) => {
26+
this.flatlistRef = ref;
27+
this.props.ref && this.props.ref(ref);
28+
};
29+
30+
onThumbDrag = (event) => {
31+
const { data, itemHeight, thumbHeight, viewportHeight } = this.props;
32+
const availableHeight = viewportHeight - thumbHeight;
33+
const positionY = this.position.__getValue();
34+
const gestureY = event.nativeEvent.absoluteY;
35+
if (gestureY >= 0 && gestureY <= availableHeight) {
36+
this.disableOnScrollEvent = true;
37+
const thumbPos = (positionY / (viewportHeight - thumbHeight)).toFixed(3);
38+
let lastIndex = data.length - Math.floor(viewportHeight / itemHeight) + 1;
39+
let index = Math.floor(lastIndex * thumbPos);
40+
if (index > lastIndex) index = lastIndex;
41+
if (index < 0) index = 0;
42+
Animated.event([{ nativeEvent: { absoluteY: this.position } }])(event);
43+
this.flatlistRef.scrollToIndex({
44+
index,
45+
viewPosition: 0,
46+
animated: true
47+
});
48+
}
49+
};
50+
51+
moveThumbOnScroll = (e) => {
52+
if (this.disableOnScrollEvent) {
53+
this.disableOnScrollEvent = false;
54+
return;
55+
}
56+
const { itemHeight, data, thumbHeight, viewportHeight } = this.props;
57+
const listHeight = data.length * itemHeight;
58+
const endPosition = listHeight - viewportHeight;
59+
const offsetY = e.nativeEvent.contentOffset.y;
60+
const diff = (viewportHeight - thumbHeight) / endPosition;
61+
this.position.setValue(offsetY * diff);
62+
};
63+
64+
flashScrollBar = () => {
65+
const { flashDuration, rightOffset } = this.props;
66+
Animated.timing(this.scrollBar, {
67+
toValue: ScreenWidth - rightOffset,
68+
duration: flashDuration,
69+
useNativeDriver: true
70+
}).start();
71+
};
72+
73+
onScroll = (event, gesture) => {
74+
this.flashScrollBar();
75+
this.moveThumbOnScroll(event);
76+
this.props.onScroll && this.props.onScroll(event, gesture);
77+
};
78+
79+
onScrollGlideEnd = (event, gesture) => {
80+
const { flashDuration, flashOutDuration } = this.props;
81+
const flashOut = Animated.timing(this.scrollBar, {
82+
toValue: this.props.hiddenPosition,
83+
duration: flashDuration,
84+
useNativeDriver: true
85+
});
86+
setTimeout(() => flashOut.start(), flashOutDuration);
87+
this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(event, gesture);
88+
};
89+
90+
convertStyle(prop) {
91+
if (Array.isArray(prop)) {
92+
let propObj = {};
93+
prop.forEach((val) => {
94+
propObj = { ...propObj, ...val };
95+
});
96+
return propObj;
97+
}
98+
return prop;
99+
}
100+
101+
render() {
102+
//prettier-ignore
103+
const { thumbHeight, thumbStyle, scrollbarStyle, containerStyle, viewportHeight, touchAreaWidth } = this.props;
104+
const rightOffset = { transform: [{ translateX: this.scrollBar }] };
105+
const thumbTransform = { transform: [{ translateY: this.position }] };
106+
return (
107+
<View style={[styles.mainWrapper, this.convertStyle(containerStyle)]}>
108+
<FlatList
109+
{...this.props}
110+
ref={this.createRef}
111+
onScroll={this.onScroll}
112+
onScrollEndDrag={this.onScrollEnd}
113+
onMomentumScrollEnd={this.onScrollGlideEnd}
114+
showsVerticalScrollIndicator={false}
115+
onScrollToIndexFailed={() => {}}
116+
/>
117+
<Animated.View
118+
style={[
119+
styles.scrollBar,
120+
rightOffset,
121+
{ height: viewportHeight },
122+
this.convertStyle(scrollbarStyle)
123+
]}>
124+
<PanGestureHandler
125+
onGestureEvent={this.onThumbDrag}
126+
hitSlop={{ left: touchAreaWidth }}
127+
maxPointers={1}>
128+
<Animated.View
129+
style={[
130+
styles.thumb,
131+
thumbTransform,
132+
{ height: thumbHeight },
133+
this.convertStyle(thumbStyle)
134+
]}
135+
/>
136+
</PanGestureHandler>
137+
</Animated.View>
138+
</View>
139+
);
140+
}
141+
}
142+
143+
export default QuickScrollList;
144+
145+
const styles = {
146+
mainWrapper: {
147+
flex: 1
148+
},
149+
scrollBar: {
150+
position: 'absolute',
151+
width: 10,
152+
backgroundColor: 'transparent',
153+
alignItems: 'center'
154+
},
155+
thumb: {
156+
width: 4,
157+
borderRadius: 4,
158+
backgroundColor: '#4C4C4C',
159+
elevation: 2
160+
}
161+
};

0 commit comments

Comments
 (0)