實作過場動畫 - React Transition Group + animate.css
React 過場動畫實作。CSS animation, React.v17
引言
製作過場動畫提昇UX。
React 動畫方案很多。複雜又炫麗的動畫相對的碼量也很驚人。然而我們要的不多只要簡簡單單的過場即可。經研究數天,發現結合 React Transition Group 與 animate.css 就能滿足我們的需求。
關鍵源碼紀錄
案例:單一物件過場動畫
使用 CSSTransition
元件搭配 css animation class。
顯示(mount)與移除(unmount)加入過場動畫。
import { Container, } from '@mui/material'
import { Paper } from '@mui/material'
import { H3, H4, AButton } from 'components/highorder'
import { useState, useReducer } from 'react'
// CSS
import { CSSTransition } from "react-transition-group";
import 'animate.css';
export default (props) => {
const [show, toggleShow] = useReducer(f => !f, true)
return (
<Container>
<H3>CSSTransition sample</H3>
<AButton mutant='primary' label={`show:${show}`} onClick={toggleShow} />
<CSSTransition
in={show}
timeout={300}
classNames={{
enter: "animate__animated",
enterActive: "animate__fadeInUp",
exit: "animate__animated",
exitActive: "animate__fadeOutDown"
}}
unmountOnExit
>
<Paper sx={{ p: 4 }}>
<H3>
一個動畫元素<br />
An animated element
</H3>
</Paper>
</CSSTransition>
<H4>EOF</H4>
</Container >
)
}
案例:多物件過場動畫,如:Todo List
使用TransitionGroup
、 CSSTransition
元件搭配 css animation class。
新增與移除加入過場動畫。
import { Container, Box, Divider, Paper, IconButton } from '@mui/material'
import { List, ListItem, ListItemText, ListItemIcon } from '@mui/material'
import { FormControl, OutlinedInput } from '@mui/material'
import { H3 } from 'components/highorder'
// hooks
import { useState } from 'react'
import { useAppSelector, useAppDispatch } from 'hooks/hooks'
import { addItem, rmvItem, toggleItem } from 'views/demo2/dm2030/todoListSlice'
// icons
import DoneIcon from '@mui/icons-material/CheckCircle';
import UndoIcon from '@mui/icons-material/RadioButtonUnchecked';
import ClearIcon from '@mui/icons-material/Clear'
// CSS
import clsx from 'clsx'
import { TransitionGroup, CSSTransition } from 'react-transition-group'
import 'animate.css'
export default (props) => {
const [newText, setNewText] = useState('');
const todoList = useAppSelector(store => store.todoList)
const dispatch = useAppDispatch()
return (
<Container>
<H3>Todos</H3>
<Paper sx={{ p: 2 }}>
<OutlinedInput
placeholder="What needs to bo done?"
fullWidth
value={newText}
onChange={e => setNewText(e.target.value)}
onKeyUp={e => {
if (e.key === 'Enter') {
dispatch(addItem(newText))
setNewText('')
}
}}
/>
<List>
<TransitionGroup>
{todoList.map((todo) => (
<CSSTransition
key={todo.id}
timeout={400}
classNames={{
enter: "animate__animated",
enterActive: "animate__fadeInUp",
exit: "animate__animated",
exitActive: "animate__backOutDown"
}}
>
<ListItem sx={{ p: 2, mb: 1 }}
secondaryAction={
<IconButton edge="end" onClick={() => dispatch(rmvItem(todo.id))}>
<ClearIcon />
</IconButton>
}
>
<ListItemIcon onClick={() => dispatch(toggleItem(todo.id))} >
{todo.completed ? <DoneIcon color="success" /> : <UndoIcon />}
</ListItemIcon>
<ListItemText>
{todo.text}
</ListItemText>
</ListItem>
</CSSTransition>
))}
</TransitionGroup>
</List>
</Paper>
</Container >
)
}
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
import type { AppState, AppThunk } from 'store/store'
export interface ToDoItem {
id: number, // a unique number
text: string, // the text the user typed in
completed: boolean, // a boolean flag
color: string, // An optional color category
}
const initialState: ToDoItem[] = [
{ id: 1, text: 'Learn React', completed: true, color: null },
{ id: 2, text: 'Learn Redux', completed: false, color: 'purple' },
{ id: 3, text: 'Build something fun!', completed: false, color: 'blue' },
]
export const todoListSlice = createSlice({
name: 'todoList',
initialState,
// The `reducers` field lets us define reducers and generate associated actions
reducers: {
addItem: (todoList /* state */, action: PayloadAction<string>) => {
const newId = todoList.reduce((max,cur)=> (max <= cur.id ? cur.id + 1 : max), 1);
const newItem:ToDoItem ={
id: newId,
text: action.payload,
completed: false,
color: null
};
todoList.push(newItem)
},
rmvItem: (todoList /* state */, action: PayloadAction<number>) => {
const itemId = action.payload
const itemIdx = todoList.findIndex(c => c.id === itemId)
todoList.splice(itemIdx, 1)
},
toggleItem: (todoList /* state */, action: PayloadAction<number>) => {
const itemId = action.payload
todoList.map(cur => {
if(cur.id === itemId) cur.completed = !cur.completed;
return cur
})
}
},
})
export const {
addItem,
rmvItem,
toggleItem,
} = todoListSlice.actions
export default todoListSlice.reducer;
EOF
Last updated