Material Design Ripple Button
Posted wlbreath
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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: `$radiuspx`,
height: `$radiuspx`,
top: `$this.props.toppx`,
left: `$this.props.leftpx`,
transitionDuration: `$this.props.durations`,
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下载到
以上是关于Material Design Ripple Button的主要内容,如果未能解决你的问题,请参考以下文章
将 Material Design Touch Ripple 应用于 ImageButton?
Android Material Design : Ripple Effect水波波纹荡漾的视觉交互设计
Android 5.X 新特性详解——Material Design 动画效果
Android Material Design-Creating Apps with Material Design(用 Material Design设计App)-(零)