├── assets ├── icon.png ├── favicon.png ├── splash.png └── adaptive-icon.png ├── tsconfig.json ├── babel.config.js ├── .gitignore ├── App.tsx ├── package.json ├── app.json ├── data.ts └── HomeScreen.tsx /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iam-rohid/rn-scroll-animation/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iam-rohid/rn-scroll-animation/HEAD/assets/favicon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iam-rohid/rn-scroll-animation/HEAD/assets/splash.png -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iam-rohid/rn-scroll-animation/HEAD/assets/adaptive-icon.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ["babel-preset-expo"], 5 | plugins: ["react-native-reanimated/plugin"], 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import "react-native-gesture-handler"; 2 | import { StatusBar } from "expo-status-bar"; 3 | import { StyleSheet } from "react-native"; 4 | import { GestureHandlerRootView } from "react-native-gesture-handler"; 5 | import { SafeAreaProvider } from "react-native-safe-area-context"; 6 | import HomeScreen from "./HomeScreen"; 7 | 8 | export default function App() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | const styles = StyleSheet.create({ 20 | container: { 21 | flex: 1, 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scroll-animation", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "expo": "~49.0.15", 13 | "expo-status-bar": "~1.6.0", 14 | "react": "18.2.0", 15 | "react-native": "0.72.6", 16 | "react-native-reanimated": "~3.3.0", 17 | "react-native-gesture-handler": "~2.12.0", 18 | "react-native-safe-area-context": "4.6.3", 19 | "react-native-svg": "13.9.0" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.20.0", 23 | "@types/react": "~18.2.14", 24 | "typescript": "^5.1.3" 25 | }, 26 | "private": true 27 | } 28 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "scroll-animation", 4 | "slug": "scroll-animation", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "assetBundlePatterns": [ 15 | "**/*" 16 | ], 17 | "ios": { 18 | "supportsTablet": true, 19 | "infoPlist": { 20 | "CADisableMinimumFrameDurationOnPhone": true, 21 | }, 22 | }, 23 | "android": { 24 | "adaptiveIcon": { 25 | "foregroundImage": "./assets/adaptive-icon.png", 26 | "backgroundColor": "#ffffff" 27 | } 28 | }, 29 | "web": { 30 | "favicon": "./assets/favicon.png" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /data.ts: -------------------------------------------------------------------------------- 1 | export type Card = { 2 | id: number; 3 | balance: number; 4 | color: string; 5 | fgColor: string; 6 | }; 7 | 8 | export const allCards: Card[] = [ 9 | { 10 | id: 1, 11 | balance: 21424, 12 | color: "#4f46e5", 13 | fgColor: "#ffffff", 14 | }, 15 | { 16 | id: 2, 17 | balance: 46700, 18 | color: "#2563eb", 19 | fgColor: "#ffffff", 20 | }, 21 | { 22 | id: 3, 23 | balance: 30000, 24 | color: "#db2777", 25 | fgColor: "#ffffff", 26 | }, 27 | { 28 | id: 4, 29 | balance: 430802, 30 | color: "#e11d48", 31 | fgColor: "#ffffff", 32 | }, 33 | ]; 34 | 35 | export type Expense = { 36 | id: number; 37 | title: string; 38 | type: string; 39 | price: string; 40 | }; 41 | 42 | export const allExpenses: Expense[] = [ 43 | { 44 | id: 1, 45 | title: "Amazon Echo Dot", 46 | type: "Amazon", 47 | price: "$29.99", 48 | }, 49 | { 50 | id: 2, 51 | title: "Amazon Prime Membership", 52 | type: "Amazon", 53 | price: "$119", 54 | }, 55 | { 56 | id: 3, 57 | title: "Netflix Subscription", 58 | type: "Mobile Subscription", 59 | price: "$12.99", 60 | }, 61 | { 62 | id: 4, 63 | title: "iPhone 12 Pro", 64 | type: "Mobile", 65 | price: "$999", 66 | }, 67 | { 68 | id: 5, 69 | title: "Kindle Paperwhite", 70 | type: "Amazon", 71 | price: "$149.99", 72 | }, 73 | { 74 | id: 6, 75 | title: "Google Play Store Apps", 76 | type: "Mobile Subscription", 77 | price: "$5.99", 78 | }, 79 | { 80 | id: 7, 81 | title: "Laptop Charger", 82 | type: "Electronics", 83 | price: "$39.99", 84 | }, 85 | { 86 | id: 8, 87 | title: "Spotify Premium", 88 | type: "Mobile Subscription", 89 | price: "$9.99", 90 | }, 91 | { 92 | id: 9, 93 | title: "Toilet Paper", 94 | type: "Household", 95 | price: "$4.99", 96 | }, 97 | { 98 | id: 10, 99 | title: "Nike Running Shoes", 100 | type: "Sports", 101 | price: "$79.99", 102 | }, 103 | { 104 | id: 11, 105 | title: "Hulu Subscription", 106 | type: "Mobile Subscription", 107 | price: "$7.99", 108 | }, 109 | { 110 | id: 12, 111 | title: "Groceries", 112 | type: "Food", 113 | price: "$50", 114 | }, 115 | { 116 | id: 13, 117 | title: "Smart Home Security Camera", 118 | type: "Electronics", 119 | price: "$149", 120 | }, 121 | { 122 | id: 14, 123 | title: "Gym Membership", 124 | type: "Fitness", 125 | price: "$30", 126 | }, 127 | { 128 | id: 15, 129 | title: "PlayStation 5", 130 | type: "Gaming", 131 | price: "$499.99", 132 | }, 133 | { 134 | id: 16, 135 | title: "Ebook - The Great Gatsby", 136 | type: "Books", 137 | price: "$4.99", 138 | }, 139 | { 140 | id: 17, 141 | title: "Home Decor Items", 142 | type: "Home", 143 | price: "$25", 144 | }, 145 | { 146 | id: 18, 147 | title: "Car Insurance", 148 | type: "Insurance", 149 | price: "$100", 150 | }, 151 | { 152 | id: 19, 153 | title: "Electricity Bill", 154 | type: "Utilities", 155 | price: "$75", 156 | }, 157 | { 158 | id: 20, 159 | title: "Movie Tickets", 160 | type: "Entertainment", 161 | price: "$12.50", 162 | }, 163 | ]; 164 | -------------------------------------------------------------------------------- /HomeScreen.tsx: -------------------------------------------------------------------------------- 1 | import { setStatusBarStyle } from "expo-status-bar"; 2 | import { 3 | StyleSheet, 4 | Text, 5 | TouchableOpacity, 6 | View, 7 | useWindowDimensions, 8 | } from "react-native"; 9 | import { 10 | SafeAreaView, 11 | useSafeAreaInsets, 12 | } from "react-native-safe-area-context"; 13 | import Animated, { 14 | Extrapolate, 15 | interpolate, 16 | runOnJS, 17 | useAnimatedScrollHandler, 18 | useAnimatedStyle, 19 | useSharedValue, 20 | } from "react-native-reanimated"; 21 | import FeatherIcons from "@expo/vector-icons/Feather"; 22 | import { allExpenses, allCards, Expense } from "./data"; 23 | 24 | const CARD_HEIGHT = 120; 25 | const TITLE_HEIGHT = 80; 26 | 27 | const HomeScreen = () => { 28 | const dimentions = useWindowDimensions(); 29 | const insets = useSafeAreaInsets(); 30 | const scrollY = useSharedValue(0); 31 | const expandMode = useSharedValue(false); 32 | 33 | const scrollHandler = useAnimatedScrollHandler({ 34 | onScroll: (event) => { 35 | scrollY.value = event.contentOffset.y; 36 | if (event.contentOffset.y >= 80 && !expandMode.value) { 37 | expandMode.value = true; 38 | runOnJS(setStatusBarStyle)("light"); 39 | } 40 | if (event.contentOffset.y < 80 && expandMode.value) { 41 | expandMode.value = false; 42 | runOnJS(setStatusBarStyle)("auto"); 43 | } 44 | }, 45 | }); 46 | 47 | const cardScrollListStyle = useAnimatedStyle(() => ({ 48 | top: Math.max(0, -(scrollY.value - 80)), 49 | })); 50 | 51 | const cardWrapperStyle = useAnimatedStyle(() => ({ 52 | paddingHorizontal: interpolate( 53 | scrollY.value, 54 | [0, 80], 55 | [16, 0], 56 | Extrapolate.CLAMP 57 | ), 58 | })); 59 | 60 | const cardStyle = useAnimatedStyle(() => { 61 | const borderRadius = interpolate( 62 | scrollY.value, 63 | [60, 80], 64 | [24, 0], 65 | Extrapolate.CLAMP 66 | ); 67 | return { 68 | borderTopLeftRadius: borderRadius, 69 | borderTopRightRadius: borderRadius, 70 | height: interpolate( 71 | scrollY.value, 72 | [0, 80], 73 | [CARD_HEIGHT, CARD_HEIGHT + insets.top], 74 | Extrapolate.CLAMP 75 | ), 76 | }; 77 | }); 78 | 79 | return ( 80 | <> 81 | `expense-${item.id}`} 88 | renderItem={({ item }) => } 89 | ListHeaderComponent={() => ( 90 | 91 | 92 | Hello Rohid 93 | 94 | 95 | 96 | 97 | Filter By 98 | 103 | 104 | 105 | 106 | )} 107 | /> 108 | `card-${item.id}`} 116 | renderItem={({ item }) => ( 117 | 124 | 127 | 130 | 131 | Current Balance 132 | 133 | 134 | ${item.balance.toLocaleString()} 135 | 136 | 137 | 138 | )} 139 | /> 140 | 141 | ); 142 | }; 143 | 144 | export default HomeScreen; 145 | 146 | function ExpanseItem({ expense }: { expense: Expense }) { 147 | return ( 148 | 149 | 150 | {expense.title} 151 | {expense.type} 152 | 153 | {expense.price} 154 | 155 | ); 156 | } 157 | 158 | const styles = StyleSheet.create({ 159 | container: { flex: 1 }, 160 | listHeaderTitleContainer: { 161 | paddingHorizontal: 16, 162 | paddingBottom: 16, 163 | height: TITLE_HEIGHT, 164 | justifyContent: "flex-end", 165 | }, 166 | filterContainer: { 167 | flexDirection: "row", 168 | alignItems: "center", 169 | justifyContent: "flex-end", 170 | padding: 16, 171 | }, 172 | filterButton: { flexDirection: "row", alignItems: "center", gap: 2 }, 173 | filterButtonLabel: { opacity: 0.7, fontSize: 14 }, 174 | filterButtonIcon: { opacity: 0.7 }, 175 | listHeaderTitle: { fontSize: 36, fontWeight: "800" }, 176 | listItem: { 177 | flexDirection: "row", 178 | alignItems: "center", 179 | paddingHorizontal: 16, 180 | paddingVertical: 12, 181 | }, 182 | listItemContent: { 183 | flex: 1, 184 | }, 185 | listItemTitle: { fontSize: 16, fontWeight: "500" }, 186 | listItemSubtitle: { fontSize: 14, marginTop: 2, opacity: 0.5 }, 187 | listItemValue: { fontSize: 16, fontWeight: "600", textAlign: "right" }, 188 | cardsList: { 189 | position: "absolute", 190 | left: 0, 191 | right: 0, 192 | }, 193 | cardWrapper: { 194 | justifyContent: "flex-end", 195 | }, 196 | card: { 197 | justifyContent: "flex-end", 198 | padding: 16, 199 | position: "relative", 200 | overflow: "hidden", 201 | borderRadius: 24, 202 | }, 203 | cardTitle: { 204 | fontSize: 16, 205 | marginBottom: 4, 206 | }, 207 | cardCircle: { 208 | position: "absolute", 209 | width: 300, 210 | height: 300, 211 | borderRadius: 300, 212 | backgroundColor: "#ffffff", 213 | opacity: 0.1, 214 | top: -60, 215 | left: -100, 216 | }, 217 | cardPriceText: { 218 | fontSize: 32, 219 | fontWeight: "700", 220 | }, 221 | }); 222 | --------------------------------------------------------------------------------