Skip to content

Instantly share code, notes, and snippets.

@wcandillon
Created September 3, 2018 09:28
Show Gist options
  • 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
}
});
@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