refactor(activator): ripple activator

This commit is contained in:
Adam Bradley
2015-09-21 13:48:58 -05:00
parent e184b1f9f2
commit 88c547c535
8 changed files with 222 additions and 173 deletions

View File

@@ -7,7 +7,7 @@ import {ClickBlock} from '../../util/click-block';
import * as util from '../../util/util';
// injectables
import {Activator} from './activator';
import {TapClick} from '../tap-click/tap-click';
import {ActionSheet} from '../action-sheet/action-sheet';
import {Modal} from '../modal/modal';
import {Popup} from '../popup/popup';
@@ -279,7 +279,7 @@ export function ionicBootstrap(rootComponentType, config) {
let app = initApp(window, document, config, platform);
// TODO: probs need a better way to inject global injectables
let activator = new Activator(app, config, window, document);
let tapClick = new TapClick(app, config, window, document);
let actionSheet = new ActionSheet(app, config);
let modal = new Modal(app, config);
let popup = new Popup(app, config);
@@ -289,7 +289,7 @@ export function ionicBootstrap(rootComponentType, config) {
bind(IonicApp).toValue(app),
bind(IonicConfig).toValue(config),
bind(IonicPlatform).toValue(platform),
bind(Activator).toValue(activator),
bind(TapClick).toValue(tapClick),
bind(ActionSheet).toValue(actionSheet),
bind(Modal).toValue(modal),
bind(Popup).toValue(popup),

View File

@@ -9,6 +9,7 @@ $button-md-box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px
$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-border-radius: 2px !default;
$button-md-animation-curve: cubic-bezier(0.4, 0, 0.2, 1) !default;
$button-md-transition-duration: 300ms !default;
$button-md-clear-hover-bg-color: rgba(158, 158, 158, 0.1);
$button-md-clear-active-bg-color: rgba(158, 158, 158, 0.2);
@@ -27,9 +28,9 @@ button,
font-size: $button-md-font-size;
box-shadow: $button-md-box-shadow;
transition: box-shadow 0.2s $button-md-animation-curve,
background-color 0.2s $button-md-animation-curve,
color 0.2s $button-md-animation-curve;
transition: box-shadow $button-md-transition-duration $button-md-animation-curve,
background-color $button-md-transition-duration $button-md-animation-curve,
color $button-md-transition-duration $button-md-animation-curve;
&.activated {
box-shadow: $button-md-box-shadow-active;
@@ -122,8 +123,8 @@ button,
// Generate Material Design Button Auxiliary Colors
// --------------------------------------------------
@each $color-name, $value in auxiliary-colors() {
@each $color-name, $color-value in auxiliary-colors() {
@include button-theme-md($color-name, $value);
@include button-theme-md($color-name, $color-value);
}

View File

@@ -1,67 +0,0 @@
import * as dom from '../../util/dom';
export function start(key, ele, pointerX, pointerY) {
// ensure this key is cleaned up
removeRipple(key);
// throttle how many ripples can happen quickly
if (lastRipple + 150 > Date.now()) return;
lastRipple = Date.now();
let r = ele.getBoundingClientRect();
let rippleSize = Math.sqrt(r.width * r.width + r.height * r.height) * 2 + 2;
let rippleEle = activeElements[key] = 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';
let centerX = (r.width / 2);
let hitX = (pointerX - r.left);
let distanceFromCenter = Math.abs(centerX - hitX);
let percent = 1 - (distanceFromCenter / centerX);
let duration = 300 + (300 * percent);
rippleEle.style.transitionDuration = rippleEle.style.webkitTransitionDuration = (duration + 'ms');
ele.classList.add('ripple-wave');
ele.appendChild(rippleEle);
// kick off animaiton
setTimeout(function() {
rippleEle.classList.add('ripple-animate');
}, 32);
}
export function end(key) {
let rippleEle = activeElements[key];
if (rippleEle) {
rippleEle.classList.add('ripple-animate-out');
rippleEle.classList.remove('ripple-animate');
removeElements[key] = rippleEle;
setTimeout(function() {
removeRipple(key);
}, 500);
}
delete activeElements[key];
}
function removeRipple(key) {
let rippleEle = removeElements[key];
if (rippleEle) {
rippleEle.parentNode.removeChild(rippleEle);
}
delete removeElements[key];
}
let lastRipple = 0;
let activeElements = {};
let removeElements = {};

View File

@@ -0,0 +1,80 @@
import {raf} from '../../util/dom';
export class Activator {
constructor(app, config) {
this.app = app;
this.id = 0;
this.queue = {};
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 = {};
});
}
upAction(ev) {
// the user was pressing down, then just let up
setTimeout(() => {
this.clearState();
}, this.clearStateTimeout);
}
clearState(ev) {
// all states should return to normal
if (!this.app.isEnabled() && this.clearAttempt < 30) {
// the app is actively disabled, so don't bother deactivating anything.
// this makes it easier on the GPU so it doesn't have to redraw any
// buttons during a transition. This will retry in XX milliseconds.
++this.clearAttempt;
this.upAction();
} 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.clearAttempt = 0;
}
}
}

View File

@@ -18,15 +18,15 @@ md-ripple {
overflow: hidden;
pointer-events: none;
transform: scale(0.01) translateZ(0);
transition: transform 300ms $ripple-animation-curve, opacity 400ms $ripple-animation-curve;
transform: scale(0.001) translateZ(0);
transition: transform 280ms $ripple-animation-curve, opacity 400ms ease;
}
md-ripple.ripple-animate {
md-ripple.ripple-expand {
transform: scale(1) translateZ(0);
}
md-ripple.ripple-animate-out {
md-ripple.ripple-fade-out {
transform: scale(1) translateZ(0);
opacity: 0;
}

View File

@@ -0,0 +1,104 @@
import {Activator} from './activator';
import {raf, transitionEnd} from '../../util/dom';
export class RippleActivator extends Activator {
constructor(app, config) {
super(app, config);
}
downAction(targetEle, pointerX, pointerY) {
super.downAction(targetEle, pointerX, pointerY, (targetEle) => {
if (!targetEle || !targetEle.parentNode) return;
// clean out any existing ripple elements
removeAll(targetEle);
// 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');
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.clientWidth; // force render
transitionEnd(rippleEle).then(ev => {
// the ripple animation has ended
ev.target.dataset.expanded = true;
fadeOutRipple(ev.target);
});
// kick off animaiton
rippleEle.classList.add('ripple-expand');
});
}
upAction(ev) {
// the user was pressing down, then just let up
super.upAction(ev);
// immediately remove the activated css class and clear the queue
// this stops the background from changing colors, not stop the ripple
this.queue = {};
let targetEle = ev.target;
raf(() => {
if (targetEle && targetEle.parentNode) {
targetEle.classList.remove(this.activatedClass);
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);
}
});
// start fading out the ripple
rippleEle.classList.add('ripple-fade-out');
rippleEle.dataset.removing = 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');
}

View File

@@ -1,8 +1,10 @@
import {raf, pointerCoord, hasPointerMoved} from '../../util/dom';
import * as ripple from '../material/ripple';
import {pointerCoord, hasPointerMoved, transitionEnd} from '../../util/dom';
import * as ripple from './ripple';
import {Activator} from './activator';
import {RippleActivator} from './ripple';
export class Activator {
export class TapClick {
constructor(app: IonicApp, config: IonicConfig, window, document) {
const self = this;
@@ -11,19 +13,18 @@ export class Activator {
self.win = window;
self.doc = document;
self.id = 0;
self.queue = {};
self.active = {};
self.activatedClass = 'activated';
self.deactivateTimeout = 180;
self.deactivateAttempt = 0;
self.pointerTolerance = 4;
self.isTouch = false;
self.lastTouch = 0;
self.disableClick = 0;
self.disableClickLimit = 2500;
self.tapPolyfill = config.setting('tapPolyfill');
self.mdRipple = config.setting('mdRipple');
if (config.setting('mdRipple')) {
self.activator = new RippleActivator(app, config);
} else {
self.activator = new Activator(app, config);
}
function bindDom(type, listener, useCapture) {
@@ -35,17 +36,17 @@ export class Activator {
}, true);
bindDom('touchstart', function(ev) {
self.isTouch = true;
self.lastTouch = Date.now();
self.pointerStart(ev);
});
bindDom('touchend', function(ev) {
self.isTouch = true;
self.lastTouch = Date.now();
self.touchEnd(ev);
});
bindDom('touchcancel', function(ev) {
self.isTouch = true;
self.lastTouch = Date.now();
self.pointerCancel(ev);
});
@@ -62,7 +63,7 @@ export class Activator {
let moveCoord = pointerCoord(ev);
if ( hasPointerMoved(10, self.start, moveCoord) ) {
self.pointerCancel();
self.pointerCancel(ev);
}
};
@@ -115,7 +116,7 @@ export class Activator {
ev.preventDefault();
ev.stopPropagation();
} else if (!self.isTouch) {
} else if (self.lastTouch + 999 < Date.now()) {
this.pointerStart(ev);
}
}
@@ -131,7 +132,7 @@ export class Activator {
ev.stopPropagation();
}
if (!self.isTouch) {
if (self.lastTouch + 999 < Date.now()) {
this.pointerEnd(ev);
}
}
@@ -143,10 +144,9 @@ export class Activator {
pointerStart(ev) {
let targetEle = this.getActivatableTarget(ev.target);
if (targetEle && this.app.isEnabled()) {
if (targetEle) {
this.start = pointerCoord(ev);
this.queueActivate(targetEle);
this.activator.downAction(targetEle, this.start.x, this.start.y);
this.moveListeners(true);
} else {
@@ -158,16 +158,16 @@ export class Activator {
* TODO
*/
pointerEnd(ev) {
this.queueDeactivate();
this.activator.upAction(ev);
this.moveListeners(false);
}
/**
* TODO
*/
pointerCancel() {
pointerCancel(ev) {
console.debug('pointerCancel')
this.deactivate();
this.activator.clearState(ev);
this.moveListeners(false);
this.disableClick = Date.now();
}
@@ -203,7 +203,6 @@ export class Activator {
ev.preventDefault();
ev.stopPropagation();
}
this.isTouch = false;
}
getActivatableTarget(ele) {
@@ -231,72 +230,4 @@ export class Activator {
return false;
}
queueActivate(ele) {
const self = this;
self.queue[++self.id] = ele;
if (self.id > 19) self.id = 0;
raf(function(){
// activate all elements in the queue
for (var key in self.queue) {
self.activate(key, self.queue[key]);
}
self.queue = {};
});
}
activate(key, ele) {
if (ele) {
ele.classList.add(this.activatedClass);
if (this.mdRipple) {
ripple.start(key, ele, this.start.x, this.start.y);
}
this.active[key] = ele;
}
}
queueDeactivate() {
const self = this;
setTimeout(function() {
self.deactivate();
}, self.deactivateTimeout);
}
deactivate() {
const self = this;
if (!self.app.isEnabled() && self.deactivateAttempt < 30) {
// the app is actively disabled, so don't bother deactivating anything.
// this makes it easier on the GPU so it doesn't have to redraw any
// buttons during a transition. This will retry in XX milliseconds.
++self.deactivateAttempt;
self.queueDeactivate();
} else {
// not actively transitioning, good to deactivate any elements
// clear out any elements that are queued to be set to active
self.queue = {};
// in the next frame, remove the active class from all active elements
raf(function() {
for (var key in self.active) {
if (self.active[key]) {
self.active[key].classList.remove(self.activatedClass);
}
if (self.mdRipple) {
ripple.end(key);
}
delete self.active[key];
}
});
self.deactivateAttempt = 0;
}
}
}

View File

@@ -3,7 +3,6 @@
// Material Design Components
@import
"components/material/ripple",
"components/toolbar/modes/md",
"components/action-sheet/modes/md",
"components/button/modes/md",
@@ -16,4 +15,5 @@
"components/nav-bar/modes/md",
"components/radio/modes/md",
"components/switch/modes/md",
"components/tabs/modes/md";
"components/tabs/modes/md",
"components/tap-click/ripple";