问题
Google’s paper / material design http://www.google.com/design/spec/material-design/introduction.html is a really clean look that I think is going to see a lot of use. Polymer has a bunch of “paper-elements” ready to go and the web community is already playing with different ways to implement it. For this question I’m specifically looking at the button click effect.
It has a ripple of activation color that radiates from your click. Here is polymer’s example: http://www.polymer-project.org/components/paper-elements/demo.html#paper-button , here is a css jquery example: http://thecodeplayer.com/walkthrough/ripple-click-effect-google-material-design
My question is how to go about implementing it?
Taking a look at the polymer example When you mousedown it radiates a background color shift maybe instead of the colored opacity ripple in the other example. It holds when it reaches it’s limit and then on mouseup it quickly fades out.
Since I could easily see the code behind the second example I tried implementing it in a similar fashion as it had but with the exception of using touch events instead of click since I wanted it to hold the effect if all i did was touch but not release.
I tried scaling, transitioning the position setting the opacity but getting the placement and the effect of radiating outwards from the point of touch was beyond me or at least from the time I’ve invested so far. In truth I’m just under experienced in the animation department in general.
Any thoughts on how to implement it?
回答1:
I also wanted this effect, but I've not seen any implementations either. I decided to go with a CSS radial gradient in the button's background-image. I'm centering the ripple (the gradient's circle) at the touch/mouse point. I extended the Surface module in order to hook into the render cycle.
There are two Transitionables, one for the diameter of the gradient and one for gradient opacity. Both of these are reset after the interaction. When the user clicks on a button, the Surface stores the X and Y offset and then transitions the gradient diameter to its max value. When the user releases the button, it transitions the gradient opacity to 0.
The render cycle is constantly setting the background-image to a radial gradient with the circle at the X and Y offset, and getting the opacity and gradient diameter from the two Transitionables.
I can't tell you whether I've implemented the ripple button effect using best practices, but I like the result.
var Surface = require('famous/core/Surface');
var Timer = require('famous/utilities/Timer');
var Transitionable = require('famous/transitions/Transitionable');
// Extend the button surface to tap into .render()
// Probably should include touch events
function ButtonSurface() {
Surface.apply(this, arguments);
this.gradientOpacity = new Transitionable(0.1);
this.gradientSize = new Transitionable(0);
this.offsetX = 0;
this.offsetY = 0;
this.on('mousedown', function (data) {
this.offsetX = (data.offsetX || data.layerX) + 'px';
this.offsetY = (data.offsetY || data.layerY) + 'px';
this.gradientOpacity.set(0.1);
this.gradientSize.set(0);
this.gradientSize.set(100, {
duration: 300,
curve: 'easeOut'
});
}.bind(this));
this.on('mouseup', function () {
this.gradientOpacity.set(0, {
duration: 300,
curve: 'easeOut'
});
});
this.on('mouseleave', function () {
this.gradientOpacity.set(0, {
duration: 300,
curve: 'easeOut'
});
});
}
ButtonSurface.prototype = Object.create(Surface.prototype);
ButtonSurface.prototype.constructor = ButtonSurface;
ButtonSurface.prototype.render = function () {
var gradientOpacity = this.gradientOpacity.get();
var gradientSize = this.gradientSize.get();
var fadeSize = gradientSize * 0.75;
this.setProperties({
backgroundImage: 'radial-gradient(circle at ' + this.offsetX + ' ' + this.offsetY + ', rgba(0,0,0,' + gradientOpacity + '), rgba(0,0,0,' + gradientOpacity + ') ' + gradientSize + 'px, rgba(255,255,255,' + gradientOpacity + ') ' + gradientSize + 'px)'
});
// return what Surface expects
return this.id;
};
You can check out my fiddle here.
回答2:
Clay Awesome work love your version I'll probably tweak it a little and use it instead of my own.
define(function(require, exports, module) {
var Engine = require('famous/core/Engine');
var Surface = require('famous/core/Surface');
var Modifier = require('famous/core/Modifier');
var StateModifier = require('famous/modifiers/StateModifier');
var Transform = require('famous/core/Transform');
var View = require('famous/core/View');
var Transitionable = require('famous/transitions/Transitionable');
var ImageSurface = require("famous/surfaces/ImageSurface");
var OptionsManager = require('famous/core/OptionsManager');
var ContainerSurface = require("famous/surfaces/ContainerSurface");
var EventHandler = require('famous/core/EventHandler');
var RenderNode = require('famous/core/RenderNode');
var Draggable = require('famous/modifiers/Draggable');
var Easing = require('famous/transitions/Easing');
function PaperButton(options) {
View.apply(this, arguments);
this.options = Object.create(PaperButton.DEFAULT_OPTIONS);
this.optionsManager = new OptionsManager(this.options);
if (options) this.optionsManager.patch(options);
this.rootModifier = new StateModifier({
size:this.options.size
});
this.mainNode = this.add(this.rootModifier);
this._eventOutput = new EventHandler();
EventHandler.setOutputHandler(this, this._eventOutput);
_createControls.call(this);
this.refresh();
};
PaperButton.prototype = Object.create(View.prototype);
PaperButton.prototype.constructor = PaperButton;
PaperButton.prototype.refresh = function() {
var _inactiveBackground = 'grey';
var _activeBackground = this.options.backgroundColor + '0.8)';
this.surfaceSync.setProperties({boxShadow:_makeBoxShadow(this.options.enabled ? _droppedShadow : _noShadow)});
this.surfaceSync.setProperties({background:_setBackground(this.options.enabled ? _activeBackground: _inactiveBackground)});
};
PaperButton.prototype.getEnabled = function() {
return this.options.enabled;
};
PaperButton.prototype.setEnabled = function(enabled) {
if(enabled == this.options.enabled) { return; }
this.options.enabled = enabled;
this.refresh();
};
PaperButton.DEFAULT_OPTIONS = {
size:[269,50],//size of the button
content:'Button',//button text
backgroundColor:'rgba(68, 135, 250,',//rgba values only, cliped after the third values comma
color:'white',//text color
fontSize:'21px',
enabled: true,
};
var _width = window.innerWidth;
var _height = window.innerHeight;
var _noShadow = [0,0,0,0,0];
var _droppedShadow = [0,2,8,0,0.8];
var _liftedShadow = [0,5,15,0,0.8];
var _compareShadows = function(left, right) {
var i = left.length;
while(i>0) {
if(left[i]!=right[i--]){
return false;
}
}
return true;
};
var _boxShadow = ['', 'px ', '', 'px ', '', 'px ', '', 'px rgba(0,0,0,', '', ')'];
var _makeBoxShadow = function(data) {
_boxShadow[0] = data[0];
_boxShadow[2] = data[1];
_boxShadow[4] = data[2];
_boxShadow[6] = data[3];
_boxShadow[8] = data[4];
return _boxShadow.join('');
};
var _setBackground = function(data) {
return data;
};
var _animateShadow = function(initial, target, transition, comparer, callback) {
var _initial = initial;
var _target = target;
var _transition = transition;
var _current = initial;
var _transitionable = new Transitionable(_current);
var _handler;
var _prerender = function(goal) {
return function() {
_current = _transitionable.get();
callback(_current);
if (comparer(_current, goal)) {
//if (_current == _target || _current == _initial) {
Engine.removeListener('prerender', _handler);
}
};
};
return {
play: function() {
//
//if(!this.options.enabled) { return; }
_transitionable.halt();
_transitionable.set(_target, _transition);
_handler = _prerender(_target);
Engine.on('prerender', _handler);
},
rewind: function() {
//
//if(!this.options.enabled) { return; }
_transitionable.halt();
_transitionable.set(_initial, _transition);
_handler = _prerender(_initial);
Engine.on('prerender', _handler);
},
}
}
function _createControls() {
var self = this;
var _container = new ContainerSurface({
size:self.options.size,
properties:{
overflow:'hidden'
}
});
this.mainNode.add(_container);
var clicked = new Surface({
size:[200,200],
properties:{
background:'blue',
borderRadius:'200px',
display:'none'
}
});
clicked.mod = new StateModifier({
origin:[0.5,0.5]
});
_container.add(clicked.mod).add(clicked);
this.surfaceSync = new Surface({
size:self.options.size,
content:self.options.content,
properties:{
lineHeight:self.options.size[1] + 'px',
textAlign:'center',
fontWeight:'600',
background:self.options.backgroundColor + '0.8)',
color:self.options.color,
fontSize:self.options.fontSize,
}
});
this.mainNode.add(this.surfaceSync);
this.surfaceSync.on('touchstart', touchEffect);
this.surfaceSync.on('touchend', endTouchEffect);
clicked.mod.setTransform(
Transform.scale(-1, -1, -1),
{ duration : 0, curve: Easing.outBack }
);
var animator = _animateShadow(_droppedShadow, _liftedShadow, { duration : 500, curve: Easing.outBack }, _compareShadows, function(data) {
if(!this.options.enabled) { return; }
this.surfaceSync.setProperties({boxShadow:_makeBoxShadow(data)});
}.bind(this));
function touchEffect(e){
var temp = e.target.getBoundingClientRect();
var size = this.getSize();
var offsetY = e.changedTouches[0].pageY - (temp.bottom - (size[1] / 2));
var offsetX = e.changedTouches[0].pageX - (temp.right - (size[0] / 2));
clicked.setProperties({left:offsetX+'px',top: offsetY+'px',display:'block'});
var shadowTransitionable = new Transitionable([0,2,8,-1,0.65]);
clicked.mod.setTransform(
Transform.scale(2, 2, 2),
{ duration : 350, curve: Easing.outBack }
);
animator.play();
};
function endTouchEffect(){
clicked.mod.setTransform(
Transform.scale(-1, -1, -1),
{ duration : 300, curve: Easing.outBack }
);
clicked.setProperties({display:'none'});
animator.rewind();
};
};
module.exports = PaperButton;
});
回答3:
Update Clay Smith's answer to satisfy mobile environment.
Actually I use this ButtonSuface on Phonegap/Cordova. Works great.
define(function(require, exports, module) {
var Surface = require('famous/core/Surface');
var Timer = require('famous/utilities/Timer');
var Transitionable = require('famous/transitions/Transitionable');
// Extend the button surface to tap into .render()
// Probably should include touch events
function ButtonSurface() {
Surface.apply(this, arguments);
this.gradientOpacity = new Transitionable(0);
this.gradientSize = new Transitionable(0);
this.offsetX = 0;
this.offsetY = 0;
this.on('touchstart', function (data) {
this.offsetX = (data.targetTouches[0].clientX - this._element.getBoundingClientRect().left) + 'px';
this.offsetY = (data.targetTouches[0].clientY - this._element.getBoundingClientRect().top) + 'px';
this.gradientOpacity.set(0.2);
this.gradientSize.set(0);
this.gradientSize.set(100, {
duration: 250,
curve: 'easeOut'
});
});
this.on('touchend', function (data) {
this.gradientOpacity.set(0, {
duration: 250,
curve: 'easeOut'
});
});
}
ButtonSurface.prototype = Object.create(Surface.prototype);
ButtonSurface.prototype.constructor = ButtonSurface;
ButtonSurface.prototype.render = function () {
var gradientOpacity = this.gradientOpacity.get();
var gradientSize = this.gradientSize.get();
var fadeSize = gradientSize * 0.75;
this.setProperties({
backgroundImage: 'radial-gradient(circle at ' + this.offsetX + ' ' + this.offsetY + ', rgba(0,0,0,' + gradientOpacity + '), rgba(0,0,0,' + gradientOpacity + ') ' + gradientSize + 'px, rgba(255,255,255,' + gradientOpacity + ') ' + gradientSize + 'px)'
});
// return what Surface expects
return this.id;
};
module.exports= ButtonSurface;
});
来源:https://stackoverflow.com/questions/24946191/how-to-implement-google-paper-button-effects