乐趣区

关于javascript:如何自己用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 有点匆忙,还有一些不尽意的中央,还待改善,以及做的过程中可能遇到的一些问题:

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

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

退出移动版