Skip to content

Instantly share code, notes, and snippets.

@wcandillon
Created September 3, 2018 09:28
Show Gist options
  • Star 21 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save wcandillon/288e994cda3216d1dbef9482a9322804 to your computer and use it in GitHub Desktop.
Save wcandillon/288e994cda3216d1dbef9482a9322804 to your computer and use it in GitHub Desktop.
React Native Apple Wallet Animation
import React from "react";
import {
StyleSheet,
Text,
View,
ScrollView,
Animated,
SafeAreaView,
Dimensions
} from "react-native";
const cardHeight = 250;
const cardTitle = 45;
const cardPadding = 10;
const { height } = Dimensions.get("window");
const cards = [
{
name: "Shot",
color: "#a9d0b6",
price: "30 CHF"
},
{
name: "Juice",
color: "#e9bbd1",
price: "64 CHF"
},
{
name: "Mighty Juice",
color: "#eba65c",
price: "80 CHF"
},
{
name: "Sandwich",
color: "#95c3e4",
price: "85 CHF"
},
{
name: "Combi",
color: "#1c1c1c",
price: "145 CHF"
},
{
name: "Signature",
color: "#a390bc",
price: "92 CHF"
},
{
name: "Coffee",
color: "#fef2a0",
price: "47 CHF"
}
];
export default class App extends React.Component {
state = {
y: new Animated.Value(0)
};
render() {
const { y } = this.state;
return (
<SafeAreaView style={styles.root}>
<View style={styles.container}>
<View style={StyleSheet.absoluteFill}>
{cards.map((card, i) => {
const inputRange = [-cardHeight, 0];
const outputRange = [
cardHeight * i,
(cardHeight - cardTitle) * -i
];
if (i > 0) {
inputRange.push(cardPadding * i);
outputRange.push((cardHeight - cardPadding) * -i);
}
const translateY = y.interpolate({
inputRange,
outputRange,
extrapolateRight: "clamp"
});
return (
<Animated.View
key={card.name}
style={{ transform: [{ translateY }] }}
>
<View
style={[styles.card, { backgroundColor: card.color }]}
/>
</Animated.View>
);
})}
</View>
<Animated.ScrollView
scrollEventThrottle={16}
contentContainerStyle={styles.content}
showsVerticalScrollIndicator={false}
onScroll={Animated.event(
[
{
nativeEvent: {
contentOffset: { y }
}
}
],
{ useNativeDriver: true }
)}
/>
</View>
</SafeAreaView>
);
}
}
const styles = StyleSheet.create({
root: {
flex: 1,
margin: 16
},
container: {
flex: 1
},
content: {
height: height * 2
},
card: {
height: cardHeight,
borderRadius: 10
}
});
@brunolemos
Copy link

@BishwajitNepali
Copy link

Cool This is awesome.

@oneEyedSunday
Copy link

Hey William, why does the scrollview not get the scroll event without the height and the safeareview. I've tried using pointerEvents="none" on the View with the absolute fill to no avail.

@1ik
Copy link

1ik commented Feb 15, 2019

how can i make the items inside the card clickable, the TouchableOpacity is not working inside the card

@pukhalski
Copy link

Pst, pst. This was an initial draft of Joe & The Juice Wallet animation (it is a React Native app actually): https://snack.expo.io/@pukhalski/dW5uYW

@pukhalski
Copy link

@1ik, To make items clickable you need to know the position of each card and emulate a click event by handling a click on the overlay.

@DipanshKhandelwal
Copy link

The scroll is fixed at below and I'm not able to view the whole cards. ( I tested in Android device as well as an Android Emulator )
Can someone help me with this, please?

@gidox
Copy link

gidox commented Oct 11, 2019

Can anyone help me please, i trying add onpress function in each card

@kaushiknamtoar321
Copy link

is there any sample code for implementing the on click event on the cards using touchable opacity

@farzd
Copy link

farzd commented Sep 8, 2021

This is kinda useless because theres noway to click the cards, if you do an overlay touchable opacity and map the coordinates, it works only when they are stacked in the original state, if theres too many cards and you scroll up to minimise the stack then your coordinates change. Just seems very hacky to make clickable items work this way

anyway
with this hack, i have it working, see video and rough code below
https://user-images.githubusercontent.com/1423413/132573698-20f7f5c5-65bf-44d4-b24e-dd263fcc7fab.mov

import React, { useState, useEffect } from 'react'
import {
  StyleSheet,
  Dimensions,
  TouchableOpacity,
  View,
  StatusBar,
  Animated,
  SafeAreaView,
  Text,
} from 'react-native'

const cardHeight = 250
const cardTitle = 45
const cardPadding = 10

const cards = [
  {
    name: 'Shot',
    color: '#EFF6FF',
    price: '30 CHF',
  },
  {
    name: 'Juice',
    color: '#DBEAFE',
    price: '64 CHF',
  },
  {
    name: 'Mighty Juice',
    color: '#BFDBFE',
    price: '80 CHF',
  },
  {
    name: 'Sandwich',
    color: '#93C5FD',
    price: '85 CHF',
  },
  {
    name: 'Combi',
    color: '#60A5FA',
    price: '145 CHF',
  },
  {
    name: 'Signature',
    color: '#3B82F6',
    price: '92 CHF',
  },
  {
    name: 'Coffee',
    color: '#2563EB',
    price: '47 CHF',
  },
]

const { height } = Dimensions.get('window')

const Page = ({ navigation }) => {
  const [selected, setSelected] = useState(null)
  const [space, setSpace] = useState([])
  const [coor, setCoor] = useState([])
  const [y] = useState(new Animated.Value(0))

  useEffect(() => {
    const cardVals = cards.map((card, i, arr) => {
      const start = cardPadding * i
      const end = (cardHeight - cardPadding) * -i
      return {
        start,
        end,
      }
    })

    const coord = cardVals.map((item, i, arr) => {
      const start = cardTitle * i
      let end = start + cardTitle

      if (i === arr.length - 1) {
        end += cardHeight - cardTitle
      }
      return {
        y1: start,
        y2: end,
        anim: new Animated.Value((cardHeight - cardTitle) * -i),
      }
    })

    setCoor(coord)
    setSpace(cardVals)
  }, [])

  return (
    <View style={styles.wrapper}>
      <SafeAreaView style={styles.container}>
        <View style={styles.container}>
          <TouchableOpacity
            onPress={() => {
              coor.forEach(
                (item, i) => {
                  Animated.spring(coor[i].anim, {
                    bounciness: 4,
                    toValue: (cardHeight - cardTitle) * -i,
                    duration: 500,
                    useNativeDriver: true,
                  }).start()
                },
                () => {
                  setSelected(null)
                }
              )
            }}
          >
            {selected && <Text>Done</Text>}
          </TouchableOpacity>
          <View style={styles.container}>
            <View style={StyleSheet.absoluteFill}>
              {!!space.length &&
                cards.map((card, i) => {
                  const inputRange = [-cardHeight, 0]
                  const outputRange = [
                    cardHeight * i,
                    (cardHeight - cardTitle) * -i,
                  ]
                  if (i > 0) {
                    inputRange.push(space[i].start)
                    outputRange.push(space[i].end)
                  }
                  const translateY = y.interpolate({
                    inputRange,
                    outputRange,
                    extrapolateRight: 'clamp',
                  })
                  if (selected) {
                    if (selected === i) {
                      return (
                        <Animated.View
                          key={card.name}
                          style={{
                            ...styles.card,
                            backgroundColor: card.color,
                            transform: [
                              {
                                translateY: coor[i].anim,
                              },
                            ],
                          }}
                        >
                          <Text>card {i}</Text>
                        </Animated.View>
                      )
                    } else {
                      return (
                        <Animated.View
                          key={card.name}
                          style={{
                            ...styles.card,
                            backgroundColor: card.color,
                            transform: [
                              {
                                translateY: coor[i].anim,
                              },
                            ],
                          }}
                        >
                          <Text>card {i}</Text>
                        </Animated.View>
                      )
                    }
                  }
                  return (
                    <Animated.View
                      key={card.name}
                      style={{
                        ...styles.card,
                        opacity: 1,
                        backgroundColor: card.color,
                        transform: [{ translateY }],
                      }}
                    >
                      <Text>card {i}</Text>
                    </Animated.View>
                  )
                })}
            </View>
            <Animated.ScrollView
              scrollEventThrottle={16}
              contentContainerStyle={styles.content}
              showsVerticalScrollIndicator={false}
              onScroll={Animated.event(
                [
                  {
                    nativeEvent: {
                      contentOffset: { y },
                    },
                  },
                ],
                { useNativeDriver: true }
              )}
            >
              <TouchableOpacity
                onPress={(evt) => {
                  const posY = evt.nativeEvent.locationY
                  coor.forEach(({ y1, y2 }, i) => {
                    if (posY >= y1 && posY <= y2) {
                      setSelected(i)

                      Animated.spring(coor[i].anim, {
                        toValue: (cardHeight - cardTitle) * -i - i * cardTitle,
                        duration: 300,
                        useNativeDriver: true,
                      }).start()
                    } else {
                      Animated.spring(coor[i].anim, {
                        toValue: 800,
                        duration: 500,
                        useNativeDriver: true,
                      }).start()
                    }
                  })
                }}
                style={[
                  {
                    height: cards.length * 75,
                  },
                ]}
              />
            </Animated.ScrollView>
          </View>
        </View>
      </SafeAreaView>
    </View>
  )
}

const styles = StyleSheet.create({
  wrapper: {
    flex: 1,
  },
  container: {
    flex: 1,
    paddingTop: StatusBar.currentHeight,
    marginHorizontal: 16,
  },
  Text: {
    color: '#fff',
    fontSize: 30,
    textAlign: 'center',
    letterSpacing: 2,
    textShadowColor: 'rgba(0, 0, 0, 1)',
    textShadowOffset: { width: 1, height: 1 },
    textShadowRadius: 2,
    marginBottom: 16,
  },
  content: {
    height,
  },
  card: {
    height: cardHeight,
    borderRadius: 10,
    padding: 8,
  },
})

export default Page

@bkalmuk
Copy link

bkalmuk commented Nov 25, 2021

how can i make the items inside the card clickable, the TouchableOpacity is not working inside the card

I did like this and it worked

<SafeAreaView style={styles.root}>
        <View style={styles.container}>
          <Animated.ScrollView
            scrollEventThrottle={16}
            contentContainerStyle={styles.content}
            showsVerticalScrollIndicator={false}
            onScroll={Animated.event(
              [
                {
                  nativeEvent: {
                    contentOffset: { y }
                  }
                }
              ],
              { useNativeDriver: true }
            )}
          >
<View style={StyleSheet.absoluteFill}>
            {cards.map((card, i) => {
              const inputRange = [-cardHeight, 0];
              const outputRange = [
                cardHeight * i,
                (cardHeight - cardTitle) * -i
              ];
              if (i > 0) {
                inputRange.push(cardPadding * i);
                outputRange.push((cardHeight - cardPadding) * -i);
              }
              const translateY = y.interpolate({
                inputRange,
                outputRange,
                extrapolateRight: "clamp"
              });
              return (
                <Animated.View
                  key={card.name}
                  style={{ transform: [{ translateY }] }}
                >
                  <View
                    style={[styles.card, { backgroundColor: card.color }]}
                  />
                </Animated.View>
              );
            })}
          </View>
           </ Animated.ScrollView>
        </View>
      </SafeAreaView>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment