likes
comments
collection
share

如何自己用React写一个带有涟漪效果的Avatar组件

作者站长头像
站长
· 阅读数 53

作者:Stony Chen

先看看最终的效果,如下图:如何自己用React写一个带有涟漪效果的Avatar组件如何自己用React写一个带有涟漪效果的Avatar组件

组件效果以及样式我们参考了https://v4.mui.com/components...,但组件属性和Material-UI有些不一样。另外我们还多做了一个Avatar组件环绕的涟漪效果。

需要完成的组件以及属性列表:如何自己用React写一个带有涟漪效果的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
                    }
                }
            }
        }
    ]
}

然后重新启动项目,可以看到项目运行如下图如何自己用React写一个带有涟漪效果的Avatar组件

Step 3 文件目录准备

准备示例头像文件,并放置在public目录下如何自己用React写一个带有涟漪效果的Avatar组件

准备如下代码文件结构,此处我们没有使用less的module结构如何自己用React写一个带有涟漪效果的Avatar组件

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有点匆忙,还有一些不尽意的地方,还待改善,以及做的过程中可能遇到的一些问题:

  1. 这里只是作为demo,并未使用module化的less样式。
  2. 我在想如果AvatarGroup包含一个带Badge的Avatar的话,样式可能会乱掉。不过我觉得不应该这么使用吧,AvatarGroup的子组件必须是Avatar组件,如果不是则可以加个报错提示。
  3. 一开始发现自己不会查看Material UI上的涟漪@keyframes动画效果,后来我用safari浏览可以查到@keyframes。

参考:https://v4.mui.com/components...

转载自:https://segmentfault.com/a/1190000041687315
评论
请登录