mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-10 00:27:41 +08:00
@ -1,11 +1,12 @@
|
||||
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, State, Watch, h, readTask, writeTask } from '@stencil/core';
|
||||
|
||||
import { getTimeGivenProgression } from '../../';
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import { Gesture, GestureDetail, RefresherEventDetail } from '../../interface';
|
||||
import { Animation, Gesture, GestureDetail, RefresherEventDetail } from '../../interface';
|
||||
import { clamp } from '../../utils/helpers';
|
||||
import { hapticImpact } from '../../utils/native/haptic';
|
||||
|
||||
import { handleScrollWhilePulling, handleScrollWhileRefreshing, setSpinnerOpacity, shouldUseNativeRefresher, translateElement } from './refresher.utils';
|
||||
import { createPullingAnimation, createSnapBackAnimation, getRefresherAnimationType, handleScrollWhilePulling, handleScrollWhileRefreshing, setSpinnerOpacity, shouldUseNativeRefresher, transitionEndAsync, translateElement } from './refresher.utils';
|
||||
|
||||
@Component({
|
||||
tag: 'ion-refresher',
|
||||
@ -28,6 +29,7 @@ export class Refresher implements ComponentInterface {
|
||||
private didRefresh = false;
|
||||
private lastVelocityY = 0;
|
||||
private elementToTransform?: HTMLElement;
|
||||
private animations: Animation[] = [];
|
||||
|
||||
@State() private nativeRefresher = false;
|
||||
|
||||
@ -142,26 +144,24 @@ export class Refresher implements ComponentInterface {
|
||||
private async resetNativeRefresher(el: HTMLElement | undefined, state: RefresherState) {
|
||||
this.state = state;
|
||||
|
||||
if (el !== undefined) {
|
||||
if (getIonMode(this) === 'ios') {
|
||||
await translateElement(el, undefined);
|
||||
} else {
|
||||
await transitionEndAsync(this.el.querySelector('.refresher-refreshing-icon'));
|
||||
}
|
||||
|
||||
this.didRefresh = false;
|
||||
this.needsCompletion = false;
|
||||
this.pointerDown = false;
|
||||
this.animations.forEach(ani => ani.destroy());
|
||||
this.animations = [];
|
||||
this.progress = 0;
|
||||
|
||||
this.state = RefresherState.Inactive;
|
||||
}
|
||||
|
||||
private async setupNativeRefresher(contentEl: HTMLIonContentElement | null) {
|
||||
if (this.scrollListenerCallback || !contentEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pullingSpinner = this.el.querySelector('ion-refresher-content .refresher-pulling ion-spinner') as HTMLElement;
|
||||
const refreshingSpinner = this.el.querySelector('ion-refresher-content .refresher-refreshing ion-spinner') as HTMLElement;
|
||||
|
||||
private async setupiOSNativeRefresher(pullingSpinner: HTMLIonSpinnerElement, refreshingSpinner: HTMLIonSpinnerElement) {
|
||||
this.elementToTransform = this.scrollEl!.querySelector(`#scroll-content`) as HTMLElement | undefined;
|
||||
this.nativeRefresher = true;
|
||||
const ticks = pullingSpinner.shadowRoot!.querySelectorAll('svg');
|
||||
const MAX_PULL = this.scrollEl!.clientHeight * 0.16;
|
||||
const NUM_TICKS = ticks.length;
|
||||
@ -198,7 +198,10 @@ export class Refresher implements ComponentInterface {
|
||||
this.ionStart.emit();
|
||||
}
|
||||
|
||||
this.ionPull.emit();
|
||||
// emit "pulling" on every move
|
||||
if (this.pointerDown) {
|
||||
this.ionPull.emit();
|
||||
}
|
||||
}
|
||||
|
||||
// delay showing the next tick marks until user has pulled 30px
|
||||
@ -235,39 +238,144 @@ export class Refresher implements ComponentInterface {
|
||||
this.scrollEl!.addEventListener('scroll', this.scrollListenerCallback);
|
||||
|
||||
this.gesture = (await import('../../utils/gesture')).createGesture({
|
||||
el: this.scrollEl!,
|
||||
gestureName: 'refresher',
|
||||
gesturePriority: 10,
|
||||
direction: 'y',
|
||||
threshold: 0,
|
||||
onStart: () => {
|
||||
this.pointerDown = true;
|
||||
el: this.scrollEl!,
|
||||
gestureName: 'refresher',
|
||||
gesturePriority: 10,
|
||||
direction: 'y',
|
||||
threshold: 0,
|
||||
onStart: () => {
|
||||
this.pointerDown = true;
|
||||
|
||||
if (!this.didRefresh) {
|
||||
translateElement(this.elementToTransform, '0px');
|
||||
}
|
||||
},
|
||||
onMove: ev => {
|
||||
this.lastVelocityY = ev.velocityY;
|
||||
},
|
||||
onEnd: () => {
|
||||
this.pointerDown = false;
|
||||
this.didStart = false;
|
||||
|
||||
if (this.needsCompletion) {
|
||||
this.resetNativeRefresher(this.elementToTransform, RefresherState.Completing);
|
||||
this.needsCompletion = false;
|
||||
} else if (this.didRefresh) {
|
||||
readTask(() => translateElement(this.elementToTransform, `${this.el.clientHeight}px`));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!this.didRefresh) {
|
||||
translateElement(this.elementToTransform, '0px');
|
||||
}
|
||||
},
|
||||
onMove: ev => {
|
||||
this.lastVelocityY = ev.velocityY;
|
||||
},
|
||||
onEnd: () => {
|
||||
this.pointerDown = false;
|
||||
this.didStart = false;
|
||||
|
||||
if (this.needsCompletion) {
|
||||
this.resetNativeRefresher(this.elementToTransform, RefresherState.Completing);
|
||||
this.needsCompletion = false;
|
||||
} else if (this.didRefresh) {
|
||||
readTask(() => {
|
||||
translateElement(this.elementToTransform, `${this.el.clientHeight}px`);
|
||||
});
|
||||
}
|
||||
},
|
||||
this.disabledChanged();
|
||||
}
|
||||
|
||||
private async setupMDNativeRefresher(contentEl: HTMLIonContentElement, pullingSpinner: HTMLIonSpinnerElement, refreshingSpinner: HTMLIonSpinnerElement) {
|
||||
const circle = pullingSpinner.shadowRoot!.querySelector('circle');
|
||||
const pullingRefresherIcon = this.el.querySelector('ion-refresher-content .refresher-pulling-icon') as HTMLElement;
|
||||
const refreshingCircle = refreshingSpinner.shadowRoot!.querySelector('circle');
|
||||
|
||||
if (circle !== null && refreshingCircle !== null) {
|
||||
writeTask(() => {
|
||||
circle.style.setProperty('animation', 'none');
|
||||
|
||||
// This lines up the animation on the refreshing spinner with the pulling spinner
|
||||
refreshingSpinner.style.setProperty('animation-delay', '-655ms');
|
||||
refreshingCircle.style.setProperty('animation-delay', '-655ms');
|
||||
});
|
||||
}
|
||||
|
||||
this.gesture = (await import('../../utils/gesture')).createGesture({
|
||||
el: this.scrollEl!,
|
||||
gestureName: 'refresher',
|
||||
gesturePriority: 10,
|
||||
direction: 'y',
|
||||
threshold: 0,
|
||||
canStart: () => this.state !== RefresherState.Refreshing && this.state !== RefresherState.Completing && this.scrollEl!.scrollTop === 0,
|
||||
onStart: (ev: GestureDetail) => {
|
||||
ev.data = { animation: undefined, didStart: false, cancelled: false };
|
||||
},
|
||||
onMove: (ev: GestureDetail) => {
|
||||
if ((ev.velocityY < 0 && this.progress === 0 && !ev.data.didStart) || ev.data.cancelled) {
|
||||
ev.data.cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ev.data.didStart) {
|
||||
ev.data.didStart = true;
|
||||
|
||||
this.state = RefresherState.Pulling;
|
||||
|
||||
writeTask(() => {
|
||||
const animationType = getRefresherAnimationType(contentEl);
|
||||
const animation = createPullingAnimation(animationType, pullingRefresherIcon);
|
||||
ev.data.animation = animation;
|
||||
|
||||
this.scrollEl!.style.setProperty('--overflow', 'hidden');
|
||||
|
||||
animation.progressStart(false, 0);
|
||||
this.ionStart.emit();
|
||||
this.animations.push(animation);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Since we are using an easing curve, slow the gesture tracking down a bit
|
||||
this.progress = clamp(0, (ev.deltaY / 180) * 0.5, 1);
|
||||
ev.data.animation.progressStep(this.progress);
|
||||
this.ionPull.emit();
|
||||
},
|
||||
onEnd: (ev: GestureDetail) => {
|
||||
if (!ev.data.didStart) { return; }
|
||||
|
||||
writeTask(() => this.scrollEl!.style.removeProperty('--overflow'));
|
||||
if (this.progress <= 0.4) {
|
||||
this.gesture!.enable(false);
|
||||
|
||||
ev.data.animation
|
||||
.progressEnd(0, this.progress, 500)
|
||||
.onFinish(() => {
|
||||
this.animations.forEach(ani => ani.destroy());
|
||||
this.animations = [];
|
||||
this.gesture!.enable(true);
|
||||
this.state = RefresherState.Inactive;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const progress = getTimeGivenProgression([0, 0], [0, 0], [1, 1], [1, 1], this.progress)[0];
|
||||
const snapBackAnimation = createSnapBackAnimation(pullingRefresherIcon);
|
||||
this.animations.push(snapBackAnimation);
|
||||
writeTask(async () => {
|
||||
pullingRefresherIcon.style.setProperty('--ion-pulling-refresher-translate', `${(progress * 100)}px`);
|
||||
ev.data.animation.progressEnd();
|
||||
await snapBackAnimation.play();
|
||||
this.beginRefresh();
|
||||
ev.data.animation.destroy();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.disabledChanged();
|
||||
}
|
||||
|
||||
private async setupNativeRefresher(contentEl: HTMLIonContentElement | null) {
|
||||
if (this.scrollListenerCallback || !contentEl || this.nativeRefresher) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.nativeRefresher = true;
|
||||
|
||||
const pullingSpinner = this.el.querySelector('ion-refresher-content .refresher-pulling ion-spinner') as HTMLIonSpinnerElement;
|
||||
const refreshingSpinner = this.el.querySelector('ion-refresher-content .refresher-refreshing ion-spinner') as HTMLIonSpinnerElement;
|
||||
|
||||
if (getIonMode(this) === 'ios') {
|
||||
this.setupiOSNativeRefresher(pullingSpinner, refreshingSpinner);
|
||||
} else {
|
||||
this.setupMDNativeRefresher(contentEl, pullingSpinner, refreshingSpinner);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.checkNativeRefresher();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user