DEV Community

Alexander Hodes for B42

Posted on • Edited on

Implement Spotify Wrapped slider in React Native

Many people know Spotify Wrapped which displays the listening activity for the past year. The last part of this story provides a summary about top songs, top artists, top genre and total minutes. Here the users can select between different styles of this summary that can be shared.

We want to show you how to implement this component without using any third-party libraries.

Spotify Wrapped Summary

General functionality

In general there's just a horizontal scrolling container displaying different styles of this summary. When scrolling it's snapped that the selected summary is always centered.

Implementing the summary component

At first we create a basic summary component that is inspired by the design of Spotify. Here, an image is displayed at the top and some data is displayed in a table format below. Furthermore, we need to think about the size of this component, because it should not take the complete screen and should be responsive to fit different screen sizes. That's why we took 60% of the screen height and about 67% of the screen width.

const { width, height } = Dimensions.get("screen");

// calculating size of summary component
export const SUMMARY_HEIGHT = height * 0.6;
export const SUMMARY_WIDTH = width * 0.665;
Enter fullscreen mode Exit fullscreen mode

Implementing the design is using some flex styling and implementing some rows and columns for displaying the data. In addition, props for updating the background and text color are added.

Below, you can find a snippet of this summary component. The complete implementation can be found here.

type Props = {
  backgroundColor: string;
  textColor: string;
};

export const Summary: FunctionComponent<Props> = ({
  backgroundColor,
  textColor,
}) => {
  return (
    <View
      style={[
        styles.container,
        {
          backgroundColor: backgroundColor,
          height: SUMMARY_HEIGHT,
          width: SUMMARY_WIDTH,
        },
      ]}
    >
      <View style={styles.header}>
        <View style={[styles.headerPlaceholder, { backgroundColor: backgroundColor }]}></View>
      </View>
      <View style={styles.body}>
        <View style={styles.row}>
          <View style={styles.column}>
            <Text style={[styles.title, { color: textColor }]}>
              {"Top songs"}
            </Text>
            <Text style={[styles.text, { color: textColor }]}>{"#1 Song"}</Text>
          </View>
          <View style={styles.column}>
            <Text style={[styles.title, { color: textColor }]}>
              {"Top categories"}
            </Text>
          </View>
        </View>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    borderRadius: 8,
    padding: 16,
  },
  header: {
    flex: 1,
    backgroundColor: "white",
    marginBottom: 16,
    justifyContent: "center",
    alignItems: "center",
  },
  /** Further styles **/
});
Enter fullscreen mode Exit fullscreen mode

Looks great so far.

Summary component

Displaying multiple summaries

In the next step, we want to display multiple summaries next to each other. For achieving this, we can use the ScrollView with a horizontal orientation by setting the horizontal property. In addition, we add a container around the summaries for adding some space between them.

export default function App() {
  return (
    <View style={styles.container}>
      <View style={[styles.summaryScrollContainer, { height: SUMMARY_HEIGHT }]}>
        <ScrollView
          horizontal
          showsHorizontalScrollIndicator={false}
        >
          <View style={styles.summaryContainer}>
            <Summary backgroundColor="green" textColor="white" />
          </View>
          <View style={styles.summaryContainer}>
            <Summary backgroundColor="red" textColor="white" />
          </View>
          <View style={styles.summaryContainer}>
            <Summary backgroundColor="blue" textColor="white" />
          </View>
        </ScrollView>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "white",
    alignItems: "center",
    justifyContent: "center",
  },
  summaryScrollContainer: {
    marginBottom: 12,
  },
  summaryContainer: {
    paddingHorizontal: 8,
  },
});
Enter fullscreen mode Exit fullscreen mode

Furthermore we need to center the summaries in the ScrollView using the contentContainerStyle property. For centering the first and last element, we need to add some padding horizontally.

This padding can be calculated easily with this formula

padding = (SCREEN_WIDTH - (SUMMARY_WIDTH + SUMMARY_PADDING)) / 2

Translated into code:

  const screenWidth = Dimensions.get("screen").width;

  // 16 is padding that is added horizontally around summary
  const summaryWidth = SUMMARY_WIDTH + 16
  // padding that is located on the left and right for centering the summary item
  const horizontalPadding = (screenWidth - summaryWidth) / 2;
Enter fullscreen mode Exit fullscreen mode

Now we got the summary items displayed in a centered and horizontal ScrollView.

Summary items in ScrollView

Improving the scrolling

When inspecting the scrolling behavior in Spotify, some snapping can be found. We can implement this behavior in the ScrollView using snapToInterval. This property helps to stop at a specific position. We can use our summaryWidth variable (containing SUMMARY_WIDTH and padding) for this.

Another useful property is decelerationRate, which is used for defining speed for users fingers lift when scrolling.

return (
  <ScrollView
    ref={scrollViewRef}
    horizontal
    showsHorizontalScrollIndicator={false}
    contentContainerStyle={{ paddingHorizontal: horizontalPadding }}
    decelerationRate="fast"
    // snap interval for width of summary items
    snapToInterval={summaryWidth}
  >
  {/* summary components  */}
  </ScrollView>
)
Enter fullscreen mode Exit fullscreen mode

Now it's already working fine.

Implement pagination dots

Next step is to implement the dots for displaying the current summary item. At first, we need to store the index of the current summary item in the state. And update this when a dot is pressed.

// further code
const [selected, setSelected] = useState(0);

const changePage = (index: number) => {
  setSelected(index)
}
// further code
Enter fullscreen mode Exit fullscreen mode

We place the dots below the scroll view and in a row. The dot itself is just a circle with a background color which is changed when it's selected. Furthermore it accepts a callback for changing the current summary when it's pressed.

<View style={styles.dotsContainer}>
  <Dot selected={selected === 0} onPress={() => changePage(0)} />
  <Dot selected={selected === 1} onPress={() => changePage(1)} />
  <Dot selected={selected === 2} onPress={() => changePage(2)} />
</View>
Enter fullscreen mode Exit fullscreen mode

Pressing the dot is working, but it's not connected to the ScrollView yet. For achieving this we need to do two things. First, creating a reference of the ScrollView that we can animate the scrolling when a dot is pressed. And second, we need to update the state and scrolling finished.

Animate scrolling when dot is pressed

When pressing a dot the position of the x axis needs to be calculated. This can be done easily by multiplying selected index with the summary width.

const scrollViewRef = useRef<ScrollView>(null)

const changePage = (index: number) => {
  if (scrollViewRef.current) {
    setSelected(index)
    // calculate scroll position by multiplying selected index with with of summary item
    scrollViewRef.current.scrollTo({ x: index * summaryWidth, animated: true })
  }
}

return (
  <ScrollView
    ref={scrollViewRef}
  >
  {/* summary components  */}
  </ScrollView>
)
Enter fullscreen mode Exit fullscreen mode

Updating state when scrolling finished

Updating the state can be done listening to onMomentumScrollEnd property of ScrollView. It provides some information about the current scroll position when scrolling ended. We can calculate the current index by dividing the contentOffset for x axis with the summary width.

const onScrollEnd = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
  const item = Math.round(event.nativeEvent.contentOffset.x / summaryWidth)
  setSelected(item)
}

return (
  <ScrollView
    // updating the index when scroll is ended
    onMomentumScrollEnd={onScrollEnd}
  >
   {/* summary components  */}
  </ScrollView>
)
Enter fullscreen mode Exit fullscreen mode

Result

The result looks pretty nice. We can just add the overScrollMode property and set it to never. in the ScrollView. It removes some dragging visuals on Android.

Result

Further thoughts

The same thing can be achieved as well with a FlatList instead of a ScrollView. Here you just need to create an array of items for rendering the different summary styles.

The only feature missing is the sharing functionality where you can share the selected summary style on social media.

Resources

Top comments (0)