diff --git a/ionic/animations/animation.ts b/ionic/animations/animation.ts index 6525b7083b..4a0bd81689 100644 --- a/ionic/animations/animation.ts +++ b/ionic/animations/animation.ts @@ -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 = {}; diff --git a/ionic/components/button/modes/md.scss b/ionic/components/button/modes/md.scss index bd1020f785..a8b79bf7da 100644 --- a/ionic/components/button/modes/md.scss +++ b/ionic/components/button/modes/md.scss @@ -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; diff --git a/ionic/components/tap-click/activator.ts b/ionic/components/tap-click/activator.ts index f7358c1e05..562317dbe7 100644 --- a/ionic/components/tap-click/activator.ts +++ b/ionic/components/tap-click/activator.ts @@ -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 = []; + } + } diff --git a/ionic/components/tap-click/ripple.scss b/ionic/components/tap-click/ripple.scss index e435a97680..2f212411ec 100644 --- a/ionic/components/tap-click/ripple.scss +++ b/ionic/components/tap-click/ripple.scss @@ -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; } diff --git a/ionic/components/tap-click/ripple.ts b/ionic/components/tap-click/ripple.ts index 02ff010435..1c9fc1b267 100644 --- a/ionic/components/tap-click/ripple.ts +++ b/ionic/components/tap-click/ripple.ts @@ -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/; diff --git a/ionic/components/tap-click/tap-click.ts b/ionic/components/tap-click/tap-click.ts index 00c282c44a..4bdd58bfbd 100644 --- a/ionic/components/tap-click/tap-click.ts +++ b/ionic/components/tap-click/tap-click.ts @@ -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(); } diff --git a/ionic/util/dom.ts b/ionic/util/dom.ts index e3b8b9de2a..d9e8939eb3 100644 --- a/ionic/util/dom.ts +++ b/ionic/util/dom.ts @@ -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 diff --git a/ionic/util/mixins.scss b/ionic/util/mixins.scss index 8c2aba2019..a48ca45970 100644 --- a/ionic/util/mixins.scss +++ b/ionic/util/mixins.scss @@ -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