├── .expo-shared
└── assets.json
├── .gitignore
├── App.js
├── README.md
├── app.json
├── assets
├── icomoon.ttf
├── icon.png
└── splash.png
├── babel.config.js
├── button.js
├── icon.js
├── package.json
├── yarn-error.log
└── yarn.lock
/.expo-shared/assets.json:
--------------------------------------------------------------------------------
1 | {
2 | "f9155ac790fd02fadcdeca367b02581c04a353aa6d5aa84409a59f6804c87acd": true,
3 | "89ed26367cdb9b771858e026f2eb95bfdb90e5ae943e716575327ec325f39c44": true
4 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | .expo/*
3 | npm-debug.*
4 | *.jks
5 | *.p8
6 | *.p12
7 | *.key
8 | *.mobileprovision
9 | *.orig.*
10 | web-build/
11 | web-report/
12 |
13 | # macOS
14 | .DS_Store
15 |
--------------------------------------------------------------------------------
/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { StyleSheet, View } from "react-native";
3 | import * as Font from "expo-font";
4 | import Button from "./button";
5 |
6 | export default function App() {
7 | const [fontsLoaded, setFontsLoaded] = useState(false);
8 |
9 | const loadFonts = async () => {
10 | await Font.loadAsync({
11 | icon: require("./assets/icomoon.ttf"),
12 | });
13 | setFontsLoaded(true);
14 | };
15 |
16 | useEffect(() => {
17 | loadFonts();
18 | }, []);
19 |
20 | return {fontsLoaded && };
21 | }
22 |
23 | const styles = StyleSheet.create({
24 | container: {
25 | flex: 1,
26 | backgroundColor: "white",
27 | alignItems: "center",
28 | justifyContent: "center",
29 | },
30 | });
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Hi! This is The DeleteButton Animation created with React Native/Expo. This repo contains pretty interesting way how you could animate the views and Texts while preserving 60 FPS.
2 |
3 |
4 | ## Delete Button
5 |
6 | https://github.com/alexandrius/react-native-delete-button/assets/5978212/1c2bfe4a-832f-4079-823b-a03fa75c788b
7 |
8 |
9 |
10 | * Animate Button
11 | * Custom Delete Button
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "Blank Template",
4 | "slug": "DeleteButton",
5 | "privacy": "public",
6 | "platforms": [
7 | "ios",
8 | "android",
9 | "web"
10 | ],
11 | "version": "1.0.0",
12 | "orientation": "portrait",
13 | "icon": "./assets/icon.png",
14 | "splash": {
15 | "image": "./assets/splash.png",
16 | "resizeMode": "contain",
17 | "backgroundColor": "#ffffff"
18 | },
19 | "updates": {
20 | "fallbackToCacheTimeout": 0
21 | },
22 | "assetBundlePatterns": [
23 | "**/*"
24 | ],
25 | "ios": {
26 | "supportsTablet": true
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/assets/icomoon.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexandrius/react-native-delete-button/f723e9a7efb39fe310d4ed344cd8cc064715860f/assets/icomoon.ttf
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexandrius/react-native-delete-button/f723e9a7efb39fe310d4ed344cd8cc064715860f/assets/icon.png
--------------------------------------------------------------------------------
/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexandrius/react-native-delete-button/f723e9a7efb39fe310d4ed344cd8cc064715860f/assets/splash.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/button.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef } from "react";
2 | import {
3 | TouchableOpacity,
4 | View,
5 | Animated,
6 | StyleSheet,
7 | Easing,
8 | Platform,
9 | } from "react-native";
10 | import Icon from "./icon";
11 |
12 | const ICON_SIZE = 24;
13 | const MAX_SCALE = 4;
14 | const MAX_POSITION = 5;
15 | const RESET_POSITION = 6;
16 | const BUTTON_HEIGHT = 45;
17 | const DELETE_TEXT_ARR = "Delete".split("");
18 |
19 | const easing = Easing.bezier(0.11, 0, 0.5, 0);
20 | const useNativeDriver = false;
21 |
22 | const styles = StyleSheet.create({
23 | button: {
24 | height: BUTTON_HEIGHT,
25 | paddingHorizontal: 15,
26 | backgroundColor: "red",
27 | borderRadius: 11,
28 | overflow: "hidden",
29 | justifyContent: "center",
30 | },
31 | contentContainer: {
32 | flexDirection: "row",
33 | alignItems: "center",
34 | height: 100,
35 | width: 100,
36 | },
37 | labelContainer: {
38 | position: "absolute",
39 | height: BUTTON_HEIGHT,
40 | width: "100%",
41 | alignItems: "center",
42 | justifyContent: "center",
43 | left: 25,
44 | },
45 | letterContainer: {
46 | flexDirection: "row",
47 | flexWrap: "wrap",
48 | },
49 | letter: {
50 | color: "white",
51 | fontWeight: "bold",
52 | marginRight: Platform.select({ ios: 0, android: 1 }),
53 | },
54 | top: {
55 | alignItems: "center",
56 | justifyContent: "center",
57 | },
58 | bottom: {
59 | alignItems: "center",
60 | justifyContent: "center",
61 | position: "absolute",
62 | },
63 | });
64 |
65 | export default function DeleteButton() {
66 | const [scale] = useState(new Animated.Value(1));
67 | const [position] = useState(new Animated.Value(1));
68 | const [letterAnimations] = useState(
69 | DELETE_TEXT_ARR.map((_) => new Animated.Value(0))
70 | );
71 | const [letterOpacities] = useState(
72 | DELETE_TEXT_ARR.map((_) => new Animated.Value(1))
73 | );
74 | const [width, setWidth] = useState();
75 | const animationFinishedRef = useRef(true);
76 |
77 | const ANDROID_POSITION_VALUE = ICON_SIZE / 2;
78 |
79 | const ANDROID_TRANSFORMS = Platform.select({
80 | android: [
81 | {
82 | translateX: scale.interpolate({
83 | inputRange: [1, MAX_SCALE],
84 | outputRange: [0, ANDROID_POSITION_VALUE * 2],
85 | }),
86 | },
87 | { scale },
88 | {
89 | translateX: 0,
90 | },
91 | ],
92 | ios: [],
93 | });
94 |
95 | const resetLetters = () => {
96 | letterOpacities.forEach((opacity, index) => {
97 | Animated.timing(opacity, {
98 | toValue: 1,
99 | delay: index * 50,
100 | duration: 200,
101 | easing,
102 | useNativeDriver,
103 | }).start();
104 | });
105 | animationFinishedRef.current = true;
106 | };
107 |
108 | const startReset = () => {
109 | letterOpacities.forEach((o) => o.setValue(0));
110 | letterAnimations.forEach((anim) => anim.setValue(0));
111 |
112 | Animated.timing(position, {
113 | toValue: MAX_POSITION,
114 | duration: 400,
115 | useNativeDriver,
116 | }).start();
117 |
118 | Animated.timing(scale, {
119 | toValue: 1,
120 | duration: 400,
121 | useNativeDriver,
122 | }).start(() => {
123 | setTimeout(() => {
124 | Animated.timing(position, {
125 | toValue: RESET_POSITION,
126 | duration: 400,
127 | easing: Easing.bezier(0.64, 0, 0.78, 0),
128 | useNativeDriver,
129 | }).start(() => {
130 | position.setValue(1);
131 | resetLetters();
132 | });
133 | }, 200);
134 | });
135 | };
136 |
137 | const startLetterAnimations = () => {
138 | letterAnimations.forEach((anim, index) => {
139 | Animated.timing(anim, {
140 | toValue: 1,
141 | delay: index * 60,
142 | duration: 300,
143 | easing,
144 | useNativeDriver,
145 | }).start(index === letterAnimations.length - 1 ? startReset : null);
146 | });
147 | };
148 |
149 | const startAnimation = () => {
150 | const config = {
151 | toValue: MAX_SCALE,
152 | duration: 600,
153 | easing: Easing.bezier(0.25, 1, 0.5, 1),
154 | useNativeDriver,
155 | };
156 | Animated.timing(position, config).start();
157 | Animated.timing(scale, config).start(startLetterAnimations);
158 | };
159 |
160 | const topTransforms = [
161 | {
162 | translateY: position.interpolate({
163 | inputRange: [1, MAX_SCALE, MAX_POSITION, RESET_POSITION],
164 | outputRange: [0, Platform.select({ ios: 7, android: 17 }), 0, 0],
165 | }),
166 | },
167 | {
168 | translateX: position.interpolate({
169 | inputRange: [1, MAX_SCALE, MAX_POSITION, RESET_POSITION],
170 | outputRange: [0, Platform.select({ ios: 3, android: 20 }), 40, 0],
171 | }),
172 | },
173 | {
174 | rotateZ: position.interpolate({
175 | inputRange: [1, MAX_SCALE, MAX_POSITION, RESET_POSITION],
176 | outputRange: ["0deg", "-15deg", "0deg", "0deg"],
177 | }),
178 | },
179 | ...ANDROID_TRANSFORMS,
180 | ];
181 |
182 | const bottomTransforms = [
183 | {
184 | translateY: position.interpolate({
185 | inputRange: [1, MAX_SCALE, MAX_POSITION, RESET_POSITION],
186 | outputRange: [0, Platform.select({ ios: 20, android: 23 }), 0, 0],
187 | }),
188 | },
189 | {
190 | translateX: position.interpolate({
191 | inputRange: [1, MAX_SCALE, MAX_POSITION, RESET_POSITION],
192 | outputRange: [0, Platform.select({ ios: -2, android: 14 }), 40, 0],
193 | }),
194 | },
195 | {
196 | rotateZ: position.interpolate({
197 | inputRange: [1, MAX_SCALE, MAX_POSITION, RESET_POSITION],
198 | outputRange: ["0deg", "1deg", "0deg", "0deg"],
199 | }),
200 | },
201 | ...ANDROID_TRANSFORMS,
202 | ];
203 |
204 | const iconSize = Platform.select({
205 | ios: Animated.multiply(ICON_SIZE, scale),
206 | android: ICON_SIZE,
207 | });
208 |
209 | const letterContainerTransforms = [
210 | {
211 | rotateZ: scale.interpolate({
212 | inputRange: [1, MAX_SCALE],
213 | outputRange: ["0deg", "-15deg"],
214 | }),
215 | },
216 | {
217 | translateY: scale.interpolate({
218 | inputRange: [1, MAX_SCALE],
219 | outputRange: [0, -3],
220 | }),
221 | },
222 | ];
223 |
224 | const getLetterTransforms = (i) => [
225 | {
226 | translateX: letterAnimations[i].interpolate({
227 | inputRange: [0, 1],
228 | outputRange: [0, -8 * i],
229 | }),
230 | },
231 | {
232 | translateY: letterAnimations[i].interpolate({
233 | inputRange: [0, 1],
234 | outputRange: [0, 20],
235 | }),
236 | },
237 | {
238 | rotateZ: letterAnimations[i].interpolate({
239 | inputRange: [0, 1],
240 | outputRange: ["0deg", "-5deg"],
241 | }),
242 | },
243 | ];
244 |
245 | return (
246 | {
248 | const { layout } = nativeEvent;
249 | if (!width && layout.width) {
250 | //Don't change boundaries on iOS since we are animat
251 | setWidth(layout.width);
252 | }
253 | }}
254 | activeOpacity={0.8}
255 | style={[styles.button, { width }]}
256 | onPress={() => {
257 | if (animationFinishedRef.current) {
258 | animationFinishedRef.current = false;
259 | startAnimation();
260 | }
261 | }}
262 | >
263 |
264 |
265 | {/* TOP */}
266 |
272 |
273 |
274 | {/*BOTTOM */}
275 |
281 |
282 |
283 |
284 |
285 |
286 |
292 | {DELETE_TEXT_ARR.map((l, i) => {
293 | const opacity = letterOpacities[i];
294 |
295 | return (
296 |
304 | {l}
305 |
306 | );
307 | })}
308 |
309 |
310 |
311 | );
312 | }
313 |
--------------------------------------------------------------------------------
/icon.js:
--------------------------------------------------------------------------------
1 | import { createIconSet } from "@expo/vector-icons";
2 | import React from "react";
3 | import { Animated } from "react-native";
4 |
5 | const DeleteIconFont = createIconSet(
6 | {
7 | Bottom: 59648,
8 | Top: 59649,
9 | },
10 | "icon"
11 | );
12 |
13 | class Icon extends React.Component {
14 | render() {
15 | return ;
16 | }
17 | }
18 |
19 | export default Animated.createAnimatedComponent(Icon);
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "deletebutton",
3 | "scripts": {
4 | "start": "expo start",
5 | "android": "expo start --android",
6 | "ios": "expo start --ios",
7 | "web": "expo start --web",
8 | "eject": "expo eject"
9 | },
10 | "dependencies": {
11 | "expo": "~49.0.19",
12 | "expo-font": "~11.4.0",
13 | "react": "18.2.0",
14 | "react-dom": "18.2.0",
15 | "react-native": "0.72.6",
16 | "react-native-svg": "13.9.0",
17 | "react-native-web": "~0.19.6"
18 | },
19 | "devDependencies": {
20 | "@babel/core": "^7.20.0",
21 | "babel-preset-expo": "^9.5.0"
22 | },
23 | "private": true
24 | }
25 |
--------------------------------------------------------------------------------