mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
refactor(activator): ripple activator
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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 = {};
|
||||
80
ionic/components/tap-click/activator.ts
Normal file
80
ionic/components/tap-click/activator.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
104
ionic/components/tap-click/ripple.ts
Normal file
104
ionic/components/tap-click/ripple.ts
Normal 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');
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user