Material Design Ripple Button

今天来实现一下android material design 中ripple波纹效果,最终效果如下:

这里写图片描述

因为我关注的是实现的原理,所以这里没有考虑兼容性,所以对于感兴趣的同学,如果下了源代码,请在chrome下运行。

另外对于代码比较刚兴趣的同学,可以在Material Design Ripple Button下载到

实现原理

原理很简单,就是在点击的位置添加一个圆形的span。开始的时候这个span的半径为0,透明度为1。然后慢慢的我们扩大圆形span的半径,减小透明度。最后等到圆形span的透明度为0的时候,我们再将添加的span移除就可以了。

代码实现

因为代码是用react 和es6实现的,对于不了解的同学可以参考下面的资料:
React
es6
babel
browserify

index.html

<html>
    <head>
        <meta charset="utf-8">

        <style type="text/css">
            * {
                margin: 0;
                padding: 0;
                box-sizing: border-box;
            }

            #app {
                margin: 100px;
            }
        </style>
    </head>

    <body>
        <div id="app"></div>

        <script type="text/javascript" src="./bundle.js"></script>
    </body>
</html>

第19行id为app的div是整个demo的入口元素,我们演示的那个按钮就是放在那个div中的,

第21行中我们引入了browserify 打包后的js文件(bundle.js),其中打包之前的文件包括index.js、button.js和circle-ripple.js,下面我们将会详细的介绍这几个文件

index.js

import React from "react";
import ReactDOM from "react-dom";

import Button from "./lib/button";

ReactDOM.render(
    <Button label="BUTTON"/>,
    document.getElementById("app")
);

demo的入口js文件,这里我们引入实现好的Button,然后将其放在了index.html中id为app的div中

button.js

import React from 'react';
import ReactDOM from 'react-dom';
import ReactTransitionGroup from 'react-addons-transition-group';

import CircleRipple from './circle-ripple';

const DefaultButtonStyle = {
    position: 'relative',
    display: 'inline-block',
    padding: '0 16px',
    height: '36px',
    lineHeight: '36px',
    overflow: 'hidden',
    fontSize: '14px',
    outline: 'none',
    border: 0,
    cursor: 'pointer',
    textAlign: 'center',
    background: '#ffffff',
    boxShadow: '0 0 6px 0 #C3B7B7',
};

const DefaultTransitionGroupStyle= {
    display: 'block',
    position: 'absolute',
    top: 0,
    left: 0,
    width: '100%',  
    height: '100%',
};

const DefaultLabelStyle = {
    position: 'relative',
    background: 'transparent',
};

let uuid = 0;

class Button extends React.Component {
    constructor(props){
        super(props);

        this.state = {
            ripples: []
        };

        [
            '_handleClick',
            '_removeRipple',
            '_getCircleRipplePosition',
            '_getBtnStyle',
            '_getTransitonGroupStyle',
            '_getLabelStyle',

        ].forEach(func => {
            this[func] = this[func].bind(this);
        });
    }

    _handleClick(e){
        this.props.onClick && this.props.onClick(e);
        let ripples = this.state.ripples;

        let position = this._getCircleRipplePosition(e);

        let newRipple = (
            <CircleRipple 
               key={uuid++} 
               {...position} 
               onAnimationEnd={ () =>{ this._removeRipple(newRipple); } } />
        );

        ripples.push(newRipple);
        this.setState({
            ripples: ripples
        });
    }

    _removeRipple(ripple){
        let ripples = this.state.ripples;

        for(let i=0, len=ripples.length; i < len; ++i){
            if(ripple === ripples[i]){
                ripples.splice(i, 1);
                break;
            }
        }

        this.setState({
            ripples: ripples
        });
    }

    _getCircleRipplePosition(e){
        let el = ReactDOM.findDOMNode(this);
        let rect = el.getBoundingClientRect();

        return {
            top: e.pageY - (rect.top + window.scrollY),
            left: e.pageX - (rect.left + window.scrollX)
        };
    }

    _getBtnStyle(){
        return Object.assign({}, DefaultButtonStyle, this.props.style);
    }

    _getTransitonGroupStyle(){
        return Object.assign({}, DefaultTransitionGroupStyle);
    }   

    _getLabelStyle(){
        return Object.assign({}, DefaultLabelStyle, this.props.labelStyle);
    }

    render(){
        let {
            className,

        } = this.props;

        return (
            <button
               className={className}
               style={this._getBtnStyle()}
               onClick={ this._handleClick }>

               <ReactTransitionGroup 
                  style={this._getTransitonGroupStyle()}>
                  {this.state.ripples}
               </ReactTransitionGroup>

               <span
                  style={this._getLabelStyle()}>
                  {this.props.label || ''}
               </span>

            </button>
        );
    }
}

Button.defaultProps = {
    label: "",
    style: {},
    labelStyle: {},
};

Button.propTypes = {
    label: React.PropTypes.string,
    onClick: React.PropTypes.func,
    style: React.PropTypes.object,
    labelStyle: React.PropTypes.object,
};

export default Button;

首先我们来看看render函数的实现,其实很简单就是一个button中包括了一些简单的内容而已

<button
   className={className}
   style={this._getBtnStyle()}
   onClick={ this._handleClick }>

   <ReactTransitionGroup 
      style={this._getTransitonGroupStyle()}>
      {this.state.ripples}
   </ReactTransitionGroup>

   <span
      style={this._getLabelStyle()}>
      {this.props.label || ''}
   </span>

</button>

我们发现button中有两个子元素,一个是ReactTransitionGroup,这个就是用来包括ripple波纹的,我们的波纹元素是保存在this.state.ripples中的,在构造函数中我们将它初始化为一个空的数组。第二个元素就是span,这个元素是用来显示label的(也就是我们想要显示在按钮中的内容)

然后我们可以看到我们给button元素添加了一个onClick事件,用_handleClick函数来响应点击事件,让我们看一下这个函数里面都做了什么。

_handleClick(e){
   this.props.onClick && this.props.onClick(e);
   let ripples = this.state.ripples;

   let position = this._getCircleRipplePosition(e);
   let newRipple = (
      <CircleRipple 
         key={uuid++} 
         {...position} 
         onAnimationEnd={ () =>{ this._removeRipple(newRipple); } } />
      );

   ripples.push(newRipple);
   this.setState({
      ripples: ripples
   });
}

也很简单,如果用户绑定了对应的点击事件,首先调用用户绑定的函数,然后我们创建一个CircleRipple,然后将其添加到this.state.ripples中。在创建CircleRipple的时候,我们给它添加了一个onAnimationEnd事件的回调函数,这个回调函数会在波纹动画效果执行完之后运行,我们这里是在波纹动画执行完以后将CircleRipple从button中删除。

circle-ripple.js

import React from "react";
import ReactDOM from "react-dom";

const DefaultStyle = {
    position: "absolute",
    width: "100%",
    height: "100%",
    top: 0,
    left: 0,
    opacity: 1,
    borderRadius: "50%",
    background: "#ABA6A6",
    transform: "translate(-50%, -50%) scale(0)",
    transitionTimingFunction: 'ease-out',
    transitionDuration: '0.5s',
    transitionProperty: 'transform, opacity',
};

class CircleRipple extends React.Component {
    constructor(props){
        super(props);

        this.state = {
            style: DefaultStyle,
        };

        this._timeoutId;

        [
            '_startAnimation',

        ].forEach(func=>{
            this[func] = this[func].bind(this);
        });
    }

    componentWillAppear(callback){
        setTimeout(callback, 0);
    }

    componentWillEnter(callback) {
        setTimeout(callback, 0);
    }

    componentDidAppear(){
        this._startAnimation();
    }

    componentDidEnter(){
        this._startAnimation();
    }

    componentDidMount(){
        if(this.props.onAnimationEnd){
            this._timeoutId = setTimeout(this.props.onAnimationEnd, this.props.duration * 1000);
        }
    }

    componentWillMount(){
        if(this._timeoutId !== null && this._timeoutId !== void 0){
            clearTimeout(this._timeoutId);
            delete this._timeoutId;
        }
    }

    _startAnimation(){
        const thisEl   = ReactDOM.findDOMNode(this);
        const parentEl = thisEl.parentElement || thisEl.parentNode;

        let parentStyle = window.getComputedStyle(parentEl, null);

        let radius = Math.max(parseInt(parentStyle.width), parseInt(parentStyle.height)) * 2;

        let style = Object.assign({}, DefaultStyle, {
            opacity: 0,
            width: `${radius}px`,
            height: `${radius}px`,
            top: `${this.props.top}px`,
            left: `${this.props.left}px`,
            transitionDuration: `${this.props.duration}s`,
            transform: "translate(-50%, -50%) scale(1)",
        });

        this.setState({style: style});
    }

    render(){       
        return (
            <span style ={this.state.style}></span>
        );
    }
}

CircleRipple.defaultProps = {
    top: 0,
    left: 0,
    duration: 0.5,
    style: DefaultStyle,
};

CircleRipple.propTypes = {
    top: React.PropTypes.number,
    left: React.PropTypes.number,
    style: React.PropTypes.object,

    duration: React.PropTypes.number,
    onAnimationEnd: React.PropTypes.func
};

export default CircleRipple;

这里实现比较简单就是在circle-ripple在被添加到ReactTransitionGroup的时候,播放scale和opacity动画,scale动画时候scale动画是通过transform来实现的,主要为transform: scale(0) —> transform: scale(1)。对于opacity动画就更简单了就是从1到0的过程而已。

不过这里有几点是值得注意的地方:

第一点:circle-ripple的position是absolute,top和left都为用户点击的位置。由于top和left都是指本身元素的左上角相对于父元素的位移,可是 ,可是这个不是我们想要的,我们想要的是,我们的元素的中心点应该在用户点击的位置,这里我们是通过tansform:translate(-50%, -50%)来实现的。

第二点:因为这里我们的动画是通过css中transition来实现的,因为我们这里有两个动画同时执行的,动画结束事件的触发如果通过transitionend事件来实现的话,这里将会触发两次transitionend事件,这样处理比较麻烦,所以这里我们是通过setTimeout实现的。

以上就是整个代码的实现过程,对于写的不足的地方,请尽量提出,我也好学习。对于代码比较刚兴趣的同学,可以在Material Design Ripple Button下载到

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值