如何自己用React写一个带有涟漪效果的Avatar组件
作者:Stony Chen
先看看最终的效果,如下图:
组件效果以及样式我们参考了https://v4.mui.com/components...,但组件属性和Material-UI有些不一样。另外我们还多做了一个Avatar组件环绕的涟漪效果。
需要完成的组件以及属性列表:
Step 1 我们用下面的命令初始化一个React项目
使用 yarn 创建React项目
$ yarn create react-app avatar-demo --template typescript
然后我们进入项目并启动
$ cd avatar-demo
$ yarn start
Step 2 高级配置
安装 craco , craco-less , react-icons 和 classnames
$ yarn add @craco/craco
$ yarn add craco-less
$ yarn add react-icons
$ yarn add classnames
修改 package.json 里的 scripts 属性
/* package.json */
"scripts": {
- "start": "react-scripts start",
- "build": "react-scripts build",
- "test": "react-scripts test",
- "eject": "react-scripts eject"
+ "start": "craco start",
+ "build": "craco build",
+ "test": "craco test",
+ "eject": "craco eject"
}
根目录下新增 craco.config.js
const CracoLessPlugin = require("craco-less")
module.exports = {
plugins: [
{
plugin:CracoLessPlugin,
options:{
lessLoaderOptions:{
lessOptions:{
modifyVars :{
// "@prmary-color":"#0073d5"
},
javascriptEnabled:true
}
}
}
}
]
}
然后重新启动项目,可以看到项目运行如下图
Step 3 文件目录准备
准备示例头像文件,并放置在public目录下
准备如下代码文件结构,此处我们没有使用less的module结构
Step 4 创建Avatar组件
简单粗暴一点,说什么都不如直接上代码,此处我们使用ripple属性作为开关来控制外围涟漪效果,以及可以配置涟漪效果颜色。另外size使用number,可以随意修改,不再使用large, small之类,更加灵活
\src\components\avatar\index.tsx
import classNames from "classnames"
import "./index.less"
export type AvatarProps = {
alt?: string
src?: string
size?: number
icon?: any
ripple?: boolean
rippleColor?: string
[key:string]: any
}
const Avatar = (props:AvatarProps) => {
const { children, alt, src, size, icon, ripple, rippleColor, className, style, ...others } = props
const classes = classNames({
avatar: true,
[className!]: className
})
const finalSize = size || 40
const finalStyle = {
...style,
width: `${finalSize}px`,
height: `${finalSize}px`
}
const rippleStyle = {
border: `2px solid ${rippleColor}`,
width: `${finalSize}px`,
height: `${finalSize}px`
}
if(ripple){
return (
<div className="ripple-container" style={{width:`${finalSize}px`,height:`${finalSize}px`}}>
<div className={classes} style={finalStyle} {...others}>
{src ? <img alt={alt} src={src}></img>:children}
{icon}
</div>
<span className="ripple" style={rippleStyle}></span>
</div>
)
} else {
return (
<div className={classes} style={finalStyle} {...others}>
{src ? <img alt={alt} src={src}></img>:children}
{icon}
</div>
)
}
}
export default Avatar
以及样式, 此处我们使用了动画效果来做涟漪效果,并参考了Material UI的效果
\src\components\avatar\index.less
.avatar {
height:40px;
width: 40px;
border-radius: 50%;
font-size: 1.25rem;
line-height: 1;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
color: rgb(18, 18, 18);
background: rgb(117, 117, 117);
z-index: 1;
img {
width: 100%;
height: 100%;
text-align: center;
object-fit: cover;
color: transparent;
text-indent: 10000px;
}
}
@-webkit-keyframes rippleFrames {
0% {
-webkit-transform: scale(.8);
-moz-transform: scale(.8);
-ms-transform: scale(.8);
transform: scale(.8);
opacity: 1;
}
100% {
-webkit-transform: scale(1.4);
-moz-transform: scale(1.4);
-ms-transform: scale(1.4);
transform: scale(1.4);
opacity: 0;
}
}
@keyframes rippleFrames {
0% {
-webkit-transform: scale(.8);
-moz-transform: scale(.8);
-ms-transform: scale(.8);
transform: scale(.8);
opacity: 1;
}
100% {
-webkit-transform: scale(1.4);
-moz-transform: scale(1.4);
-ms-transform: scale(1.4);
transform: scale(1.4);
opacity: 0;
}
}
.ripple-container {
position: relative;
border-radius: 50%;
width: 40px;
height: 40px;
box-sizing: border-box;
.avatar {
position: absolute;
}
.ripple {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
animation: 1.2s ease-in-out 0s infinite normal none running rippleFrames;
border: 2px solid red;
z-index: 0;
box-sizing: border-box;
}
}
Step 5 创建AvatarGroup组件
代码如下,此处须注意如果total大于当前的子组件,我们需要再多创建一个Avatar组件显示 +N 的效果, 并且大小需要和前面的Avatar大小一致
\src\components\avatar-group\index.tsx
import classNames from "classnames"
import Avatar from "../avatar"
import "./index.less"
export type AvatarGroupProps = {
total?: number
[key:string]: any
}
const AvatarGroup = (props:AvatarGroupProps) => {
const { children, total, className, ...others } = props
const classes = classNames({
"avatar-group": true,
[className!]: className
})
let finalSize = 40
if(children && children.type && children.type.name === 'Avatar') {
finalSize = children.props.size || finalSize
} else if (children && children.length > 0){
if(children[0].type && children[0].type.name === 'Avatar'){
finalSize = children[0].props.size || finalSize
}
}
return (
<div className={classes} {...others}>
{children}
{total && total - children.length ? <Avatar size={finalSize} style={{fontSize:"0.85rem"}}>{`+${total - children.length}`}</Avatar> : null}
</div>
)
}
export default AvatarGroup
以及样式,AvatarGroup组件样式相对就比较简单一点
\src\components\avatar-group\index.less
.avatar-group {
display: flex;
>* {
margin-left: -8px;
border: 1px solid #fff;
box-sizing: border-box;
}
> :first-child {
margin-left: 0;
}
}
Step 6 创建Badge组件
代码如下,同样我们还是用ripple属性来控制Badge组件的涟漪效果。另外这里注意的是位置,我们暴露了left,top, 但是好像在不同的电脑和浏览器,位置似乎有点点偏差
\src\components\badge\index.tsx
import classNames from "classnames"
import React from "react"
import "./index.less"
export type BadgeProps = {
ripple?: boolean
size?: number
color?: string
left?: string
top?: string
content?: React.ReactDOM | string | number
[key:string]: any
}
const Badge = (props:BadgeProps) => {
const { children, size, color, ripple, top, left, content, className, ...others } = props
const classes = classNames({
"badge-container": true,
ripple: ripple,
[className!]: className
})
let finalSize = size || 8
let location = { left, top}
if(!top) {
location.top ="30px"
}
if(!left) {
location.left ="30px"
}
const style = {
...location,
background: color,
color: ripple ? color : "#fff",
width: content ? "auto" : finalSize,
height: finalSize,
padding: content ? "0 3px" : 0,
borderRadius: Math.floor(finalSize! / 2),
fontSize: finalSize! - 2 > 12 ? 12 : finalSize! - 2,
}
return (
<div className={classes} {...others}>
{children}
<span className="badge" style={style}>{content}</span>
</div>
)
}
export default Badge
以及样式, 用了和前面类似的涟漪效果
\src\components\badge\index.less
@-webkit-keyframes badgeRippleFrames {
0% {
-webkit-transform: scale(.8);
-moz-transform: scale(.8);
-ms-transform: scale(.8);
transform: scale(.8);
opacity: 1;
}
100% {
-webkit-transform: scale(2.4);
-moz-transform: scale(2.4);
-ms-transform: scale(2.4);
transform: scale(2.4);
opacity: 0;
}
}
@keyframes badgeRippleFrames {
0% {
-webkit-transform: scale(.8);
-moz-transform: scale(.8);
-ms-transform: scale(.8);
transform: scale(.8);
opacity: 1;
}
100% {
-webkit-transform: scale(2.4);
-moz-transform: scale(2.4);
-ms-transform: scale(2.4);
transform: scale(2.4);
opacity: 0;
}
}
.badge-container {
position: relative;
.badge {
position: absolute;
width: 8px;
height: 8px;
background: rgb(68, 183, 0);
color: rgb(68, 183, 0);
box-shadow: #fff 0 0 0 2px;
z-index: 1;
box-sizing: border-box;
border-radius: 50%;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
&.ripple {
.badge {
&::after {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
border: 1px solid currentColor;
animation: 1.2s ease-in-out 0s infinite normal none running badgeRippleFrames;
content: '';
box-sizing: border-box;
}
}
}
}
Step 7 最后一步,修改App.tsx和App.less文件,展示demo
代码如下,我们一排一排地展示不同类型的效果
\src\App.tsx
import logo from './logo.svg';
import Avatar from './components/avatar';
import Badge from './components/badge';
import AvatarGroup from './components/avatar-group';
import { MdAirplay, MdAlarm, MdShare } from 'react-icons/md'
import './App.less';
function App() {
return (
<div className="App">
<div className='row'>
<Avatar alt="Peter Pan" ripple rippleColor='#44b700' src="samples/1.jpg"></Avatar>
<Avatar alt="Peter Pan" ripple src="/samples/2.jpg"></Avatar>
<Avatar alt="Peter Pan" ripple rippleColor='green' src="/samples/3.jpg"></Avatar>
<Avatar alt="Peter Pan" src="/samples/4.jpg"></Avatar>
<Avatar alt="Peter Pan" src={logo}></Avatar>
</div>
<div className='row'>
<Badge>
<Avatar alt="Peter Pan" src="/samples/4.jpg"></Avatar>
</Badge>
<Badge color="yellow">
<Avatar alt="Peter Pan" src="/samples/4.jpg"></Avatar>
</Badge>
<Badge color="grey">
<Avatar alt="Peter Pan" src="/samples/4.jpg"></Avatar>
</Badge>
<Badge color="red" size={10} ripple>
<Avatar alt="Peter Pan" src="samples/1.jpg"></Avatar>
</Badge>
<Badge color="green" size={10} ripple>
<Avatar alt="Peter Pan" src="/samples/2.jpg"></Avatar>
</Badge>
<Badge color="blue" size={10} ripple top="2px">
<Avatar alt="Peter Pan" src="/samples/3.jpg"></Avatar>
</Badge>
<Badge ripple>
<Avatar alt="Peter Pan" src="/samples/4.jpg"></Avatar>
</Badge>
<Badge content={20} size={12} color="red">
<Avatar alt="Peter Pan" src={logo}></Avatar>
</Badge>
</div>
<div className='row'>
<Avatar alt="Peter Pan" size={24} src="/samples/1.jpg"></Avatar>
<Avatar alt="Peter Pan" src="/samples/2.jpg"></Avatar>
<Avatar alt="Peter Pan" size={56} src="/samples/3.jpg"></Avatar>
<Avatar alt="Peter Pan" size={80} src="/samples/4.jpg"></Avatar>
</div>
<div className='row'>
<Avatar>K</Avatar>
<Avatar style={{background:"yellow"}}>S</Avatar>
<Avatar style={{background:"blue"}}>J</Avatar>
<Avatar style={{background:"red"}}>N</Avatar>
<Avatar icon={<MdAlarm/>}></Avatar>
<Avatar icon={<MdAirplay/>}></Avatar>
<Avatar icon={<MdShare/>}></Avatar>
</div>
<div className='row'>
<AvatarGroup total={20}>
<Avatar alt="Peter Pan" src="/samples/1.jpg"></Avatar>
<Avatar alt="Peter Pan" src="/samples/2.jpg"></Avatar>
<Avatar alt="Peter Pan" src="/samples/3.jpg"></Avatar>
<Avatar alt="Peter Pan" src="/samples/4.jpg"></Avatar>
</AvatarGroup>
</div>
<div className='row'>
<AvatarGroup total={20}>
<Avatar alt="Peter Pan" ripple rippleColor='red' src="/samples/1.jpg"></Avatar>
<Avatar alt="Peter Pan" src="/samples/2.jpg"></Avatar>
<Avatar alt="Peter Pan" src="/samples/3.jpg"></Avatar>
<Avatar alt="Peter Pan" src="/samples/4.jpg"></Avatar>
</AvatarGroup>
</div>
</div>
);
}
export default App;
以及样式
\src\App.less
.App {
text-align: center;
padding: 30px;
}
.row {
margin-bottom: 50px;
display: flex;
flex-wrap: wrap;
>* {
margin: 0 20px 20px 0;
}
}
总结
通过以上的辛苦努力,我们终于能够得到前面效果图所示的完美成果。做这个demo有点匆忙,还有一些不尽意的地方,还待改善,以及做的过程中可能遇到的一些问题:
- 这里只是作为demo,并未使用module化的less样式。
- 我在想如果AvatarGroup包含一个带Badge的Avatar的话,样式可能会乱掉。不过我觉得不应该这么使用吧,AvatarGroup的子组件必须是Avatar组件,如果不是则可以加个报错提示。
- 一开始发现自己不会查看Material UI上的涟漪@keyframes动画效果,后来我用safari浏览可以查到@keyframes。
转载自:https://segmentfault.com/a/1190000041687315