forked from craftzdog/react-native-animated-todo
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(checkbox): add animated checkbox
- Loading branch information
Showing
15 changed files
with
384 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,11 @@ | ||
import React from 'react' | ||
import AppContainer from './src/components/app-container' | ||
import Main from './src/screens/main' | ||
import Navigator from './src/' | ||
|
||
export default function App() { | ||
return ( | ||
<AppContainer> | ||
<Main /> | ||
<Navigator /> | ||
</AppContainer> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
module.exports = function(api) { | ||
api.cache(true); | ||
module.exports = function (api) { | ||
api.cache(true) | ||
return { | ||
presets: ['babel-preset-expo'], | ||
}; | ||
}; | ||
plugins: ['react-native-reanimated/plugin'] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import React, { useEffect, memo } from 'react' | ||
import Animated, { | ||
Easing, | ||
useSharedValue, | ||
useAnimatedProps, | ||
withTiming, | ||
interpolateColor | ||
} from 'react-native-reanimated' | ||
import Svg, { Path, Defs, ClipPath, G } from 'react-native-svg' | ||
import AnimatedStroke from './animated-stroke' | ||
|
||
const MARGIN = 10 | ||
const vWidth = 64 + MARGIN | ||
const vHeight = 64 + MARGIN | ||
const checkMarkPath = | ||
'M15 31.1977C23.1081 36.4884 29.5946 43 29.5946 43C29.5946 43 37.5 25.5 69 1.5' | ||
const outlineBoxPath = | ||
'M24 0.5H40C48.5809 0.5 54.4147 2.18067 58.117 5.88299C61.8193 9.58532 63.5 15.4191 63.5 24V40C63.5 48.5809 61.8193 54.4147 58.117 58.117C54.4147 61.8193 48.5809 63.5 40 63.5H24C15.4191 63.5 9.58532 61.8193 5.88299 58.117C2.18067 54.4147 0.5 48.5809 0.5 40V24C0.5 15.4191 2.18067 9.58532 5.88299 5.88299C9.58532 2.18067 15.4191 0.5 24 0.5Z' | ||
|
||
const AnimatedPath = Animated.createAnimatedComponent(Path) | ||
|
||
interface Props { | ||
checked?: boolean | ||
highlightColor: string | ||
checkmarkColor: string | ||
boxOutlineColor: string | ||
} | ||
|
||
const AnimatedCheckbox = (props: Props) => { | ||
const { checked, checkmarkColor, highlightColor, boxOutlineColor } = props | ||
|
||
const progress = useSharedValue(0) | ||
|
||
useEffect(() => { | ||
progress.value = withTiming(checked ? 1 : 0, { | ||
duration: checked ? 300 : 100, | ||
easing: Easing.linear | ||
}) | ||
}, [checked]) | ||
|
||
const animatedBoxProps = useAnimatedProps( | ||
() => ({ | ||
stroke: interpolateColor( | ||
Easing.bezier(0.16, 1, 0.3, 1)(progress.value), | ||
[0, 1], | ||
[boxOutlineColor, highlightColor], | ||
'RGB' | ||
), | ||
fill: interpolateColor( | ||
Easing.bezier(0.16, 1, 0.3, 1)(progress.value), | ||
[0, 1], | ||
['#00000000', highlightColor], | ||
'RGB' | ||
) | ||
}), | ||
[highlightColor, boxOutlineColor] | ||
) | ||
|
||
return ( | ||
<Svg | ||
viewBox={[-MARGIN, -MARGIN, vWidth + MARGIN, vHeight + MARGIN].join(' ')} | ||
> | ||
<Defs> | ||
<ClipPath id="clipPath"> | ||
<Path | ||
fill="white" | ||
stroke="gray" | ||
strokeLinejoin="round" | ||
strokeLinecap="round" | ||
d={outlineBoxPath} | ||
/> | ||
</ClipPath> | ||
</Defs> | ||
<AnimatedStroke | ||
progress={progress} | ||
d={checkMarkPath} | ||
stroke={highlightColor} | ||
strokeWidth={10} | ||
strokeLinejoin="round" | ||
strokeLinecap="round" | ||
strokeOpacity={checked || false ? 1 : 0} | ||
/> | ||
<AnimatedPath | ||
d={outlineBoxPath} | ||
strokeWidth={7} | ||
strokeLinejoin="round" | ||
strokeLinecap="round" | ||
animatedProps={animatedBoxProps} | ||
/> | ||
<G clipPath="url(#clipPath)"> | ||
<AnimatedStroke | ||
progress={progress} | ||
d={checkMarkPath} | ||
stroke={checkmarkColor} | ||
strokeWidth={10} | ||
strokeLinejoin="round" | ||
strokeLinecap="round" | ||
strokeOpacity={checked || false ? 1 : 0} | ||
/> | ||
</G> | ||
</Svg> | ||
) | ||
} | ||
|
||
export default AnimatedCheckbox |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import React, { useRef, useState } from 'react' | ||
import Animated, { Easing, useAnimatedProps } from 'react-native-reanimated' | ||
import { Path, PathProps } from 'react-native-svg' | ||
|
||
interface AnimatedStrokeProps extends PathProps { | ||
progress: Animated.SharedValue<number> | ||
} | ||
|
||
const AnimatedPath = Animated.createAnimatedComponent(Path) | ||
|
||
const AnimatedStroke = ({ progress, ...pathProps }: AnimatedStrokeProps) => { | ||
const [length, setLength] = useState(0) | ||
const ref = useRef<typeof AnimatedPath>(null) | ||
const animatedProps = useAnimatedProps(() => ({ | ||
strokeDashoffset: Math.max( | ||
0, | ||
length - length * Easing.bezier(0.37, 0, 0.63, 1)(progress.value) - 0.1 | ||
) | ||
})) | ||
|
||
return ( | ||
<AnimatedPath | ||
animatedProps={animatedProps} | ||
// @ts-ignore | ||
onLayout={() => setLength(ref.current!.getTotalLength())} | ||
// @ts-ignore | ||
ref={ref} | ||
strokeDasharray={length} | ||
{...pathProps} | ||
/> | ||
) | ||
} | ||
|
||
export default AnimatedStroke |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import React, { useEffect, memo } from 'react' | ||
import { Pressable } from 'react-native' | ||
import { Text, HStack, Box } from 'native-base' | ||
import Animated, { | ||
Easing, | ||
useSharedValue, | ||
useAnimatedStyle, | ||
withTiming, | ||
withSequence, | ||
withDelay, | ||
interpolateColor | ||
} from 'react-native-reanimated' | ||
|
||
interface Props { | ||
strikethrough: boolean | ||
textColor: string | ||
inactiveTextColor: string | ||
onPress?: () => void | ||
children?: React.ReactNode | ||
} | ||
|
||
const AnimatedBox = Animated.createAnimatedComponent(Box) | ||
const AnimatedHStack = Animated.createAnimatedComponent(HStack) | ||
const AnimatedText = Animated.createAnimatedComponent(Text) | ||
|
||
const AnimatedTaskLabel = memo((props: Props) => { | ||
const { strikethrough, textColor, inactiveTextColor, onPress, children } = | ||
props | ||
|
||
const hstackOffset = useSharedValue(0) | ||
const hstackAnimatedStyles = useAnimatedStyle( | ||
() => ({ | ||
transform: [{ translateX: hstackOffset.value }] | ||
}), | ||
[strikethrough] | ||
) | ||
const textColorProgress = useSharedValue(0) | ||
const textColorAnimatedStyles = useAnimatedStyle( | ||
() => ({ | ||
color: interpolateColor( | ||
textColorProgress.value, | ||
[0, 1], | ||
[textColor, inactiveTextColor] | ||
) | ||
}), | ||
[strikethrough, textColor, inactiveTextColor] | ||
) | ||
const strikethroughWidth = useSharedValue(0) | ||
const strikethroughAnimatedStyles = useAnimatedStyle( | ||
() => ({ | ||
width: `${strikethroughWidth.value * 100}%`, | ||
borderBottomColor: interpolateColor( | ||
textColorProgress.value, | ||
[0, 1], | ||
[textColor, inactiveTextColor] | ||
) | ||
}), | ||
[strikethrough, textColor, inactiveTextColor] | ||
) | ||
|
||
useEffect(() => { | ||
const easing = Easing.out(Easing.quad) | ||
if (strikethrough) { | ||
hstackOffset.value = withSequence( | ||
withTiming(4, { duration: 200, easing }), | ||
withTiming(0, { duration: 200, easing }) | ||
) | ||
strikethroughWidth.value = withTiming(1, { duration: 400, easing }) | ||
textColorProgress.value = withDelay( | ||
1000, | ||
withTiming(1, { duration: 400, easing }) | ||
) | ||
} else { | ||
strikethroughWidth.value = withTiming(0, { duration: 400, easing }) | ||
textColorProgress.value = withTiming(0, { duration: 400, easing }) | ||
} | ||
}) | ||
|
||
return ( | ||
<Pressable onPress={onPress}> | ||
<AnimatedHStack alignItems="center" style={[hstackAnimatedStyles]}> | ||
<AnimatedText | ||
fontSize={19} | ||
noOfLines={1} | ||
isTruncated | ||
px={1} | ||
style={[textColorAnimatedStyles]} | ||
> | ||
{children} | ||
</AnimatedText> | ||
<AnimatedBox | ||
position="absolute" | ||
h={1} | ||
borderBottomWidth={1} | ||
style={[strikethroughAnimatedStyles]} | ||
/> | ||
</AnimatedHStack> | ||
</Pressable> | ||
) | ||
}) | ||
|
||
export default AnimatedTaskLabel |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import React, { useCallback } from 'react' | ||
import { Pressable } from 'react-native' | ||
import { | ||
Box, | ||
HStack, | ||
Text, | ||
useTheme, | ||
themeTools, | ||
useColorModeValue | ||
} from 'native-base' | ||
import AnimatedCheckbox from './animated-checkbox' | ||
import AnimatedTaskLabel from './animated-task-label' | ||
|
||
interface Props { | ||
isDone: boolean | ||
onToggleCheckbox?: () => void | ||
} | ||
|
||
const TaskItem = (props: Props) => { | ||
const { isDone, onToggleCheckbox } = props | ||
const theme = useTheme() | ||
const highlightColor = themeTools.getColor( | ||
theme, | ||
useColorModeValue('blue.500', 'blue.400') | ||
) | ||
const boxStroke = themeTools.getColor( | ||
theme, | ||
useColorModeValue('muted.300', 'muted.500') | ||
) | ||
const checkmarkColor = themeTools.getColor( | ||
theme, | ||
useColorModeValue('white', 'white') | ||
) | ||
const activeTextColor = themeTools.getColor( | ||
theme, | ||
useColorModeValue('darkText', 'lightText') | ||
) | ||
const doneTextColor = themeTools.getColor( | ||
theme, | ||
useColorModeValue('muted.400', 'muted.600') | ||
) | ||
|
||
return ( | ||
<HStack | ||
alignItems="center" | ||
w="full" | ||
px={4} | ||
py={2} | ||
bg={useColorModeValue('warmGray.50', 'primary.900')} | ||
> | ||
<Box width={30} height={30} mr={2}> | ||
<Pressable onPress={onToggleCheckbox}> | ||
<AnimatedCheckbox | ||
highlightColor={highlightColor} | ||
checkmarkColor={checkmarkColor} | ||
boxOutlineColor={boxStroke} | ||
checked={isDone} | ||
/> | ||
</Pressable> | ||
</Box> | ||
<AnimatedTaskLabel | ||
textColor={activeTextColor} | ||
inactiveTextColor={doneTextColor} | ||
strikethrough={isDone} | ||
> | ||
Task Item | ||
</AnimatedTaskLabel> | ||
</HStack> | ||
) | ||
} | ||
|
||
export default TaskItem |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import React from 'react' | ||
import { createDrawerNavigator } from '@react-navigation/drawer' | ||
import MainScreen from './screens/main-screen' | ||
import AboutScreen from './screens/about-screen' | ||
|
||
const Drawer = createDrawerNavigator() | ||
|
||
const App = () => { | ||
return ( | ||
<Drawer.Navigator initialRouteName="Main"> | ||
<Drawer.Screen name="Main" component={MainScreen} /> | ||
<Drawer.Screen name="About" component={AboutScreen} /> | ||
</Drawer.Navigator> | ||
) | ||
} | ||
|
||
export default App |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import React from 'react' | ||
import { Box, Text, VStack } from 'native-base' | ||
|
||
const AboutScreen = () => { | ||
return ( | ||
<VStack> | ||
<Box> | ||
<Text>About</Text> | ||
</Box> | ||
</VStack> | ||
) | ||
} | ||
|
||
export default AboutScreen |
Oops, something went wrong.