UNPKG

12.1 kBJavaScriptView Raw
1import React, { Component } from "react";
2import times from "lodash/times";
3import { View, Text, Animated, PanResponder, Image, StyleSheet, Platform, Dimensions } from "react-native";
4// RATING IMAGES WITH STATIC BACKGROUND COLOR (white)
5const STAR_IMAGE = require("./images/star.png");
6const HEART_IMAGE = require("./images/heart.png");
7const ROCKET_IMAGE = require("./images/rocket.png");
8const BELL_IMAGE = require("./images/bell.png");
9const TYPES = {
10 star: {
11 source: STAR_IMAGE,
12 color: "#f1c40f",
13 backgroundColor: "white"
14 },
15 heart: {
16 source: HEART_IMAGE,
17 color: "#e74c3c",
18 backgroundColor: "white"
19 },
20 rocket: {
21 source: ROCKET_IMAGE,
22 color: "#2ecc71",
23 backgroundColor: "white"
24 },
25 bell: {
26 source: BELL_IMAGE,
27 color: "#f39c12",
28 backgroundColor: "white"
29 },
30 custom: {}
31};
32const fractionsType = (props, propName, componentName) => {
33 if (props[propName]) {
34 const value = props[propName];
35 if (typeof value === "number") {
36 return value >= 0 && value <= 20 ?
37 null :
38 new Error(`\`${propName}\` in \`${componentName}\` must be between 0 and 20`);
39 }
40 return new Error(`\`${propName}\` in \`${componentName}\` must be a number`);
41 }
42};
43export default class SwipeRating extends Component {
44 constructor(props) {
45 super(props);
46 const { onStartRating, onSwipeRating, onFinishRating, fractions } = this.props;
47 const position = new Animated.ValueXY();
48 const panResponder = PanResponder.create({
49 onStartShouldSetPanResponder: () => true,
50 onPanResponderGrant: (event, gesture) => {
51 const newPosition = new Animated.ValueXY();
52 const tapPositionX = gesture.x0 - this.state.centerX + gesture.dx;
53 newPosition.setValue({ x: tapPositionX, y: 0 });
54 if (this.state.isComponentMounted) {
55 this.setState({ position: newPosition, value: tapPositionX });
56 const rating = this.getCurrentRating(tapPositionX);
57 if (typeof onStartRating === "function") {
58 onStartRating(rating);
59 }
60 }
61 },
62 onPanResponderMove: (event, gesture) => {
63 const newPosition = new Animated.ValueXY();
64 const tapPositionX = gesture.x0 - this.state.centerX + gesture.dx;
65 newPosition.setValue({ x: tapPositionX, y: 0 });
66 if (this.state.isComponentMounted) {
67 this.setState({ position: newPosition, value: tapPositionX });
68 const rating = this.getCurrentRating(tapPositionX);
69 if (typeof onSwipeRating === "function") {
70 onSwipeRating(rating);
71 }
72 }
73 },
74 onPanResponderRelease: () => {
75 const rating = this.getCurrentRating(this.state.value);
76 if (rating >= this.props.minValue) {
77 if (!fractions) {
78 // 'round up' to the nearest rating image
79 this.setCurrentRating(rating);
80 }
81 if (typeof onFinishRating === "function") {
82 onFinishRating(rating);
83 }
84 }
85 }
86 });
87 this.state = {
88 panResponder,
89 position,
90 display: false,
91 isComponentMounted: false
92 };
93 }
94 componentDidMount() {
95 try {
96 this.setState({ display: true, isComponentMounted: true }, () => this.setCurrentRating(this.props.startingValue));
97 }
98 catch (err) {
99 // eslint-disable-next-line no-console
100 console.log(err);
101 }
102 }
103 componentDidUpdate(prevProps) {
104 if (this.props.startingValue !== prevProps.startingValue) {
105 this.setCurrentRating(this.props.startingValue);
106 }
107 }
108 handleLayoutChange() {
109 // eslint-disable-next-line max-params
110 this.ratingRef.measure((fx, fy, width, height, px) => {
111 const halfWidth = width / 2;
112 const pageXWithinWindow = px % Dimensions.get("window").width;
113 this.setState({
114 centerX: pageXWithinWindow + halfWidth
115 });
116 });
117 }
118 getPrimaryViewStyle() {
119 const { position } = this.state;
120 const { imageSize, ratingCount, type } = this.props;
121 const { color } = TYPES[type];
122 const width = position.x.interpolate({
123 inputRange: [
124 -ratingCount * (imageSize / 2),
125 0,
126 ratingCount * (imageSize / 2)
127 ],
128 outputRange: [
129 0,
130 ratingCount * imageSize / 2,
131 ratingCount * imageSize
132 ],
133 extrapolate: "clamp"
134 }, {
135 useNativeDriver: true
136 });
137 return {
138 backgroundColor: color,
139 width,
140 height: width ? imageSize : 0
141 };
142 }
143 getSecondaryViewStyle() {
144 const { position } = this.state;
145 const { imageSize, ratingCount, type } = this.props;
146 const { backgroundColor } = TYPES[type];
147 const width = position.x.interpolate({
148 inputRange: [
149 -ratingCount * (imageSize / 2),
150 0,
151 ratingCount * (imageSize / 2)
152 ],
153 outputRange: [
154 ratingCount * imageSize,
155 ratingCount * imageSize / 2,
156 0
157 ],
158 extrapolate: "clamp"
159 }, {
160 useNativeDriver: true
161 });
162 return {
163 backgroundColor,
164 width,
165 height: width ? imageSize : 0
166 };
167 }
168 renderRatings() {
169 const { imageSize, ratingCount, type, tintColor } = this.props;
170 const { source } = TYPES[type];
171 return times(ratingCount, index => <View key={index} style={styles.starContainer}>
172 <Image source={source} style={{ width: imageSize, height: imageSize, tintColor }}/>
173 </View>);
174 }
175 // eslint-disable-next-line max-statements
176 getCurrentRating(value) {
177 const { fractions, imageSize, ratingCount } = this.props;
178 const startingValue = ratingCount / 2;
179 let currentRating = this.props.minValue ? this.props.minValue : 0;
180 if (value > ratingCount * imageSize / 2) {
181 currentRating = ratingCount;
182 }
183 else if (value < -ratingCount * imageSize / 2) {
184 currentRating = this.props.minValue ? this.props.minValue : 0;
185 }
186 else if (value <= imageSize || value > imageSize) {
187 const diff = value / imageSize;
188 currentRating = startingValue + diff;
189 currentRating = fractions ?
190 Number(currentRating.toFixed(fractions)) :
191 Math.ceil(currentRating);
192 }
193 else {
194 currentRating = fractions ?
195 Number(startingValue.toFixed(fractions)) :
196 Math.ceil(startingValue);
197 }
198 if (this.props.jumpValue > 0 &&
199 this.props.jumpValue < this.props.ratingCount) {
200 return (Math.ceil(currentRating * (1 / this.props.jumpValue)) /
201 (1 / this.props.jumpValue));
202 }
203 else {
204 return currentRating;
205 }
206 }
207 // eslint-disable-next-line max-statements
208 setCurrentRating(rating) {
209 const { imageSize, ratingCount } = this.props;
210 // `initialRating` corresponds to `startingValue` in the getter. Naming it
211 // Differently here avoids confusion with `value` below.
212 const initialRating = ratingCount / 2;
213 let value = null;
214 if (rating > ratingCount) {
215 value = ratingCount * imageSize / 2;
216 }
217 else if (rating < 0) {
218 value = -ratingCount * imageSize / 2;
219 }
220 else if (rating < ratingCount / 2 || rating > ratingCount / 2) {
221 value = (rating - initialRating) * imageSize;
222 }
223 else {
224 value = 0;
225 }
226 const newPosition = new Animated.ValueXY();
227 newPosition.setValue({ x: value, y: 0 });
228 if (this.state.isComponentMounted) {
229 this.setState({ position: newPosition, value });
230 }
231 }
232 displayCurrentRating() {
233 const { ratingCount, type, readonly, showReadOnlyText, ratingTextColor } = this.props;
234 const color = ratingTextColor || TYPES[type].color;
235 return (<View style={styles.showRatingView}>
236 <View style={styles.ratingView}>
237 <Text style={[styles.ratingText, { color }]}>Rating: </Text>
238 <Text style={[styles.currentRatingText, { color }]}>
239 {this.getCurrentRating(this.state.value)}
240 </Text>
241 <Text style={[styles.maxRatingText, { color }]}>/{ratingCount}</Text>
242 </View>
243 <View>
244 {readonly && showReadOnlyText &&
245 <Text style={[styles.readonlyLabel, { color }]}>(readonly)</Text>}
246 </View>
247 </View>);
248 }
249 render() {
250 const { readonly, type, ratingImage, ratingColor, ratingBackgroundColor, style, showRating } = this.props;
251 if (type === "custom") {
252 const custom = {
253 source: ratingImage,
254 color: ratingColor,
255 backgroundColor: ratingBackgroundColor
256 };
257 TYPES.custom = custom;
258 }
259 return this.state.display ?
260 <View pointerEvents={readonly ? "none" : "auto"} style={style}>
261 {showRating && this.displayCurrentRating()}
262 <View style={styles.starsWrapper} {...this.state.panResponder.panHandlers}>
263 <View style={styles.starsInsideWrapper} onLayout={() => {
264 this.handleLayoutChange();
265 }} ref={view => {
266 this.ratingRef = view;
267 }}>
268 <Animated.View style={this.getPrimaryViewStyle()}/>
269 <Animated.View style={this.getSecondaryViewStyle()}/>
270 </View>
271 {this.renderRatings()}
272 </View>
273 </View> :
274 null;
275 }
276 componentWillUnmount() {
277 this.setState({ isComponentMounted: false });
278 }
279}
280SwipeRating.defaultProps = {
281 type: "star",
282 ratingImage: STAR_IMAGE,
283 ratingColor: "#f1c40f",
284 ratingBackgroundColor: "white",
285 ratingCount: 5,
286 showReadOnlyText: true,
287 imageSize: 40,
288 minValue: 0,
289 jumpValue: 0
290};
291const styles = StyleSheet.create({
292 starsWrapper: {
293 flexDirection: "row",
294 justifyContent: "center",
295 alignItems: "center"
296 },
297 starsInsideWrapper: {
298 position: "absolute",
299 top: 0,
300 left: 0,
301 right: 0,
302 bottom: 0,
303 flexDirection: "row",
304 justifyContent: "center",
305 alignItems: "center"
306 },
307 showRatingView: {
308 flexDirection: "column",
309 justifyContent: "center",
310 alignItems: "center",
311 paddingBottom: 5
312 },
313 ratingView: {
314 flexDirection: "row",
315 justifyContent: "center",
316 alignItems: "center",
317 paddingBottom: 5
318 },
319 ratingText: {
320 fontSize: 15,
321 textAlign: "center",
322 fontFamily: Platform.OS === "ios" ? "Trebuchet MS" : null,
323 color: "#34495e"
324 },
325 readonlyLabel: {
326 justifyContent: "center",
327 alignItems: "center",
328 fontSize: 12,
329 textAlign: "center",
330 fontFamily: Platform.OS === "ios" ? "Trebuchet MS" : null,
331 color: "#34495a"
332 },
333 currentRatingText: {
334 fontSize: 30,
335 textAlign: "center",
336 fontFamily: Platform.OS === "ios" ? "Trebuchet MS" : null
337 },
338 maxRatingText: {
339 fontSize: 18,
340 textAlign: "center",
341 fontFamily: Platform.OS === "ios" ? "Trebuchet MS" : null,
342 color: "#34495e"
343 }
344});