mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
refactor(ripple): web animations api
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
import {CSS} from '../util/dom';
|
||||
import {extend} from '../util/util';
|
||||
|
||||
const RENDER_DELAY = 36;
|
||||
let AnimationRegistry = {};
|
||||
|
||||
/**
|
||||
Animation Steps/Process
|
||||
@@ -26,10 +24,13 @@ let AnimationRegistry = {};
|
||||
|
||||
export class Animation {
|
||||
|
||||
constructor(ele) {
|
||||
constructor(ele, opts={}) {
|
||||
this._el = [];
|
||||
this._chld = [];
|
||||
this._ani = [];
|
||||
this._opts = extend({
|
||||
renderDelay: 36
|
||||
}, opts);
|
||||
|
||||
this._bfAdd = [];
|
||||
this._bfSty = {};
|
||||
@@ -238,10 +239,10 @@ export class Animation {
|
||||
});
|
||||
}
|
||||
|
||||
if (this._duration > RENDER_DELAY) {
|
||||
if (self._duration > this._opts.renderDelay) {
|
||||
// begin each animation when everything is rendered in their starting point
|
||||
// give the browser some time to render everything in place before starting
|
||||
setTimeout(kickoff, RENDER_DELAY);
|
||||
setTimeout(kickoff, this._opts.renderDelay);
|
||||
|
||||
} else {
|
||||
// no need to render everything in there place before animating in
|
||||
@@ -294,7 +295,8 @@ export class Animation {
|
||||
this._to,
|
||||
this.duration(),
|
||||
this.easing(),
|
||||
this.playbackRate() );
|
||||
this.playbackRate(),
|
||||
this._opts.renderDelay );
|
||||
|
||||
if (animation.shouldAnimate) {
|
||||
this._ani.push(animation);
|
||||
@@ -510,7 +512,7 @@ export class Animation {
|
||||
|
||||
class Animate {
|
||||
|
||||
constructor(ele, fromEffect, toEffect, duration, easingConfig, playbackRate) {
|
||||
constructor(ele, fromEffect, toEffect, duration, easingConfig, playbackRate, renderDelay) {
|
||||
// https://w3c.github.io/web-animations/
|
||||
// not using the direct API methods because they're still in flux
|
||||
// however, element.animate() seems locked in and uses the latest
|
||||
@@ -522,7 +524,7 @@ class Animate {
|
||||
|
||||
this.toEffect = parseEffect(toEffect);
|
||||
|
||||
this.shouldAnimate = (duration > RENDER_DELAY);
|
||||
this.shouldAnimate = (duration > renderDelay);
|
||||
|
||||
if (!this.shouldAnimate) {
|
||||
return inlineStyle(ele, this.toEffect);
|
||||
@@ -864,3 +866,5 @@ const EASING_FN = {
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
let AnimationRegistry = {};
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
$button-md-font-size: 1.4rem !default;
|
||||
$button-md-min-height: 3.6rem !default;
|
||||
$button-md-padding: 0 1.1em !default;
|
||||
$button-md-box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12) !default;
|
||||
$button-md-box-shadow-active: 0 4px 5px 0 rgba(0, 0, 0, 0.14),0 1px 10px 0 rgba(0, 0, 0, 0.12),0 2px 4px -1px rgba(0, 0, 0, 0.2); //0 1px 3px 0 rgba(0, 0, 0, 0.3); //0 2px 5px 0 rgba(0, 0, 0, 0.26) !default;
|
||||
$button-md-box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12) !default;
|
||||
$button-md-box-shadow-active: 0 3px 5px rgba(0, 0, 0, 0.14), 0 3px 5px rgba(0, 0, 0, 0.21) !default;
|
||||
|
||||
$button-md-border-radius: 2px !default;
|
||||
$button-md-animation-curve: cubic-bezier(0.4, 0, 0.2, 1) !default;
|
||||
$button-md-transition-duration: 300ms !default;
|
||||
|
||||
@@ -5,53 +5,34 @@ export class Activator {
|
||||
|
||||
constructor(app, config) {
|
||||
this.app = app;
|
||||
this.id = 0;
|
||||
this.queue = {};
|
||||
this.active = {};
|
||||
this.active = [];
|
||||
this.clearStateTimeout = 180;
|
||||
this.clearAttempt = 0;
|
||||
this.activatedClass = config.setting('activatedClass') || 'activated';
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.lastActivate = 0;
|
||||
}
|
||||
|
||||
downAction(targetEle, pointerX, pointerY, callback) {
|
||||
// the user just pressed down
|
||||
|
||||
// throttle how many activates fire off in XXms
|
||||
let now = Date.now();
|
||||
if (this.lastActivate + 50 > now) return;
|
||||
this.lastActivate = now;
|
||||
|
||||
// remember where they pressed
|
||||
this.x = pointerX;
|
||||
this.y = pointerY;
|
||||
|
||||
// remember this is the active element
|
||||
let id = ++this.id;
|
||||
this.queue[id] = targetEle;
|
||||
if (this.id > 9) this.id = 0;
|
||||
|
||||
// activate targetEle
|
||||
raf(() => {
|
||||
for (let eleId in this.queue) {
|
||||
this.queue[eleId].classList.add(this.activatedClass);
|
||||
this.active[eleId] = this.queue[eleId];
|
||||
}
|
||||
callback && callback(targetEle);
|
||||
this.queue = {};
|
||||
});
|
||||
targetEle.classList.add(this.activatedClass);
|
||||
this.active.push(targetEle);
|
||||
}
|
||||
|
||||
upAction(ev) {
|
||||
upAction() {
|
||||
// the user was pressing down, then just let up
|
||||
setTimeout(() => {
|
||||
this.clearState();
|
||||
}, this.clearStateTimeout);
|
||||
}
|
||||
|
||||
clearState(ev) {
|
||||
clearState() {
|
||||
// all states should return to normal
|
||||
|
||||
if (!this.app.isEnabled() && this.clearAttempt < 30) {
|
||||
@@ -63,18 +44,17 @@ export class Activator {
|
||||
|
||||
} else {
|
||||
// not actively transitioning, good to deactivate any elements
|
||||
this.queue = {};
|
||||
|
||||
// remove the active class from all active elements
|
||||
for (var key in this.active) {
|
||||
if (this.active[key]) {
|
||||
this.active[key].classList.remove(this.activatedClass);
|
||||
delete this.active[key];
|
||||
}
|
||||
}
|
||||
|
||||
this.deactivate();
|
||||
this.clearAttempt = 0;
|
||||
}
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
// remove the active class from all active elements
|
||||
for (let i = 0; i < this.active.length; i++) {
|
||||
this.active[i].classList.remove(this.activatedClass);
|
||||
}
|
||||
this.active = [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// Material Design Ripple Effect
|
||||
// --------------------------------------------------
|
||||
|
||||
$ripple-background-color: #fff !default;
|
||||
$ripple-animation-curve: cubic-bezier(0, 0, 0.2, 1) !default;
|
||||
$ripple-background-color: rgba(0, 0, 0, 0.1) !default;
|
||||
|
||||
|
||||
md-ripple {
|
||||
@@ -13,20 +12,9 @@ md-ripple {
|
||||
border-radius: 50%;
|
||||
|
||||
background: $ripple-background-color;
|
||||
opacity: 0.3;
|
||||
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
|
||||
transform: scale(0.001) translateZ(0);
|
||||
transition: transform 280ms $ripple-animation-curve, opacity 400ms ease;
|
||||
}
|
||||
|
||||
md-ripple.ripple-expand {
|
||||
transform: scale(1) translateZ(0);
|
||||
}
|
||||
|
||||
md-ripple.ripple-fade-out {
|
||||
transform: scale(1) translateZ(0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -1,108 +1,117 @@
|
||||
import {Activator} from './activator';
|
||||
import {raf, transitionEnd} from '../../util/dom';
|
||||
import {raf, removeElement} from '../../util/dom';
|
||||
import {Animation} from '../../animations/animation';
|
||||
|
||||
|
||||
export class RippleActivator extends Activator {
|
||||
|
||||
constructor(app, config) {
|
||||
super(app, config);
|
||||
this.ripples = {};
|
||||
}
|
||||
|
||||
downAction(targetEle, pointerX, pointerY) {
|
||||
super.downAction(targetEle, pointerX, pointerY, (targetEle) => {
|
||||
super.downAction(targetEle, pointerX, pointerY);
|
||||
|
||||
if (!targetEle || !targetEle.parentNode || NO_RIPPLE_TAGNAMES.test(targetEle.tagName)) return;
|
||||
if (!isRippleElement(targetEle)) return;
|
||||
|
||||
// clean out any existing ripple elements
|
||||
removeAll(targetEle);
|
||||
// create a new ripple element
|
||||
let r = targetEle.getBoundingClientRect();
|
||||
let x = Math.max(Math.abs(r.width - pointerX), pointerX) * 2;
|
||||
let y = Math.max(Math.abs(r.height - pointerY), pointerY) * 2;
|
||||
let size = (Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))) - 10;
|
||||
|
||||
// create a new ripple element
|
||||
let r = targetEle.getBoundingClientRect();
|
||||
let rippleSize = Math.sqrt(r.width * r.width + r.height * r.height) * 2 + 2;
|
||||
let rippleEle = document.createElement('md-ripple');
|
||||
let eleStyle = rippleEle.style;
|
||||
eleStyle.width = size + 'px';
|
||||
eleStyle.height = size + 'px';
|
||||
eleStyle.marginTop = -(size / 2) + 'px';
|
||||
eleStyle.marginLeft = -(size / 2) + 'px';
|
||||
eleStyle.left = (pointerX - r.left) + 'px';
|
||||
eleStyle.top = (pointerY - r.top) + 'px';
|
||||
|
||||
let rippleEle = document.createElement('md-ripple');
|
||||
rippleEle.style.width = rippleSize + 'px';
|
||||
rippleEle.style.height = rippleSize + 'px';
|
||||
rippleEle.style.marginTop = -(rippleSize / 2) + 'px';
|
||||
rippleEle.style.marginLeft = -(rippleSize / 2) + 'px';
|
||||
rippleEle.style.left = (pointerX - r.left) + 'px';
|
||||
rippleEle.style.top = (pointerY - r.top) + 'px';
|
||||
targetEle.appendChild(rippleEle);
|
||||
|
||||
targetEle.appendChild(rippleEle);
|
||||
let ripple = this.ripples[Date.now()] = { ele: rippleEle };
|
||||
|
||||
transitionEnd(rippleEle).then(ev => {
|
||||
// the ripple animation has ended
|
||||
ev.target.dataset.expanded = true;
|
||||
fadeOutRipple(ev.target);
|
||||
});
|
||||
// expand the circle from the users starting point
|
||||
// start slow, and when they let up, then speed up the animation
|
||||
ripple.expand = new Animation(rippleEle, {renderDelay: 0});
|
||||
ripple.expand
|
||||
.fromTo('scale', '0.001', '1')
|
||||
.duration(300)
|
||||
.playbackRate(0.35)
|
||||
.onFinish(()=> {
|
||||
// finished expanding
|
||||
ripple.expand && ripple.expand.dispose();
|
||||
ripple.expand = null;
|
||||
ripple.expanded = true;
|
||||
this.next();
|
||||
})
|
||||
.play();
|
||||
|
||||
// kick off animaiton
|
||||
raf(() => {
|
||||
rippleEle.classList.add('ripple-expand');
|
||||
});
|
||||
});
|
||||
this.next();
|
||||
}
|
||||
|
||||
upAction(ev) {
|
||||
// the user was pressing down, then just let up
|
||||
super.upAction(ev);
|
||||
upAction() {
|
||||
this.deactivate();
|
||||
|
||||
// immediately remove the activated css class and clear the queue
|
||||
// this stops the background from changing colors, not stop the ripple
|
||||
this.queue = {};
|
||||
let ripple;
|
||||
for (let rippleId in this.ripples) {
|
||||
ripple = this.ripples[rippleId];
|
||||
|
||||
if (ev && ev.target) {
|
||||
raf(() => {
|
||||
let targetEle = ev.target;
|
||||
if (targetEle && targetEle.parentNode) {
|
||||
targetEle.classList.remove(this.activatedClass);
|
||||
if (!ripple.fade) {
|
||||
// ripple has not been let up yet
|
||||
// spped up the rate if the animation is still going
|
||||
setTimeout(() => {
|
||||
ripple.expand && ripple.expand.playbackRate(1);
|
||||
ripple.fade = new Animation(ripple.ele);
|
||||
ripple.fade
|
||||
.fadeOut()
|
||||
.duration(750)
|
||||
.onFinish(() => {
|
||||
ripple.fade && ripple.fade.dispose();
|
||||
ripple.fade = null;
|
||||
ripple.faded = true;
|
||||
this.next();
|
||||
})
|
||||
.play();
|
||||
|
||||
let rippleElements = getRippleElements(targetEle);
|
||||
for (let i = 0, l = rippleElements.length; i < l; i++) {
|
||||
rippleElements[i].dataset.pointerUp = true;
|
||||
fadeOutRipple(rippleElements[i]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clearState(ev) {
|
||||
super.clearState(ev);
|
||||
if (ev && ev.target) {
|
||||
removeAll(ev.target);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function fadeOutRipple(rippleEle) {
|
||||
if (rippleEle && rippleEle.dataset.pointerUp && rippleEle.dataset.expanded && !rippleEle.dataset.removing) {
|
||||
|
||||
transitionEnd(rippleEle).then(ev => {
|
||||
// the ripple has faded out completely
|
||||
let rippleEle = ev.target;
|
||||
if (rippleEle.parentNode) {
|
||||
rippleEle.parentNode.removeChild(rippleEle);
|
||||
}, 16);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// start fading out the ripple
|
||||
rippleEle.classList.add('ripple-fade-out');
|
||||
rippleEle.dataset.removing = true;
|
||||
this.next();
|
||||
}
|
||||
|
||||
next(forceComplete) {
|
||||
let ripple, rippleEle;
|
||||
for (let rippleId in this.ripples) {
|
||||
ripple = this.ripples[rippleId];
|
||||
|
||||
if ((ripple.expanded && ripple.faded && ripple.ele) ||
|
||||
forceComplete ||
|
||||
parseInt(rippleId) + 5000 < Date.now()) {
|
||||
// finished expanding and the user has lifted the pointer
|
||||
ripple.expand && ripple.expand.dispose();
|
||||
ripple.fade && ripple.fade.dispose();
|
||||
removeElement(ripple.ele);
|
||||
ripple.ele = ripple.expand = ripple.fade = null;
|
||||
delete this.ripples[rippleId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearState() {
|
||||
this.deactivate();
|
||||
this.next(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function removeAll(targetEle) {
|
||||
let rippleElements = getRippleElements(targetEle);
|
||||
for (let i = 0; i < rippleElements.length; i++) {
|
||||
rippleElements[i].dataset.pointerUp = rippleElements[i].dataset.expanded = true;
|
||||
fadeOutRipple(rippleElements[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function getRippleElements(containerEle) {
|
||||
return containerEle && containerEle.querySelectorAll('md-ripple');
|
||||
function isRippleElement(targetEle) {
|
||||
return (targetEle && targetEle.parentNode && !(NO_RIPPLE_TAGNAMES.test(targetEle.tagName)));
|
||||
}
|
||||
|
||||
const NO_RIPPLE_TAGNAMES = /BACKDROP/;
|
||||
|
||||
@@ -157,7 +157,7 @@ export class TapClick {
|
||||
* TODO
|
||||
*/
|
||||
pointerEnd(ev) {
|
||||
this.activator.upAction(ev);
|
||||
this.activator.upAction();
|
||||
this.moveListeners(false);
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ export class TapClick {
|
||||
*/
|
||||
pointerCancel(ev) {
|
||||
console.debug('pointerCancel')
|
||||
this.activator.clearState(ev);
|
||||
this.activator.clearState();
|
||||
this.moveListeners(false);
|
||||
this.disableClick = Date.now();
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ export function hasFocusedTextInput() {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function closest(el, selector) {
|
||||
export function closest(ele, selector) {
|
||||
var matchesFn;
|
||||
|
||||
// find vendor prefix
|
||||
@@ -208,17 +208,21 @@ export function closest(el, selector) {
|
||||
})
|
||||
|
||||
// traverse parents
|
||||
while (el!==null) {
|
||||
parent = el.parentElement;
|
||||
while (ele !== null) {
|
||||
parent = ele.parentElement;
|
||||
if (parent!==null && parent[matchesFn](selector)) {
|
||||
return parent;
|
||||
}
|
||||
el = parent;
|
||||
ele = parent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function removeElement(ele) {
|
||||
ele && ele.parentNode && ele.parentNode.removeChild(ele);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the element offsetWidth and offsetHeight. Values are cached
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
@if ($lightness-percent < 35) {
|
||||
// dark foreground color, so light it up
|
||||
@return lighten($color-value, $amount);
|
||||
@return lighten($color-value, $amount * 2);
|
||||
}
|
||||
|
||||
// default to darken
|
||||
|
||||
Reference in New Issue
Block a user