Skip to content

Commit

Permalink
Merge 7dded0d into fced535
Browse files Browse the repository at this point in the history
  • Loading branch information
artyorsh committed Jul 26, 2019
2 parents fced535 + 7dded0d commit 151d63e
Show file tree
Hide file tree
Showing 10 changed files with 629 additions and 0 deletions.
63 changes: 63 additions & 0 deletions src/framework/ui/animation/animation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Animated } from 'react-native';

export const DEFAULT_CONFIG: AnimationConfig = {
cycles: 1,
useNativeDriver: true,
};

/**
* @property cycles - number of animation cycles. -1 for infinite
*/
export interface AnimationConfig extends Animated.AnimationConfig {
cycles?: number;
}

export abstract class Animation<C extends AnimationConfig, R> {

protected abstract animation: Animated.CompositeAnimation;
protected counter: number = 0;
protected endCallback: Animated.EndCallback;
protected running: boolean = false;
protected config: C;

public abstract toProps(): R;

constructor(config?: C) {
this.config = {
...DEFAULT_CONFIG,
...config,
};
}

public start(callback?: Animated.EndCallback) {
this.endCallback = callback;
this.running = true;

this.animation.start(this.onAnimationEnd);
}

public stop() {
this.running = false;

this.animation.stop();
}

public release() {
this.stop();
}

protected onAnimationEnd = (result: Animated.EndResult) => {
this.counter += 1;
if (this.counter === this.config.cycles) {
this.stop();
}
if (this.running) {
this.start(this.endCallback);
}
if (!this.running) {
this.counter = 0;
this.endCallback && this.endCallback(result);
this.endCallback = null;
}
};
}
5 changes: 5 additions & 0 deletions src/framework/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ export {
RadioGroupProps,
RadioGroupElement,
} from './radioGroup/radioGroup.component';
export {
Spinner,
SpinnerProps,
SpinnerElement,
} from './spinner/spinner.component';
export {
TabView,
TabViewProps,
Expand Down
165 changes: 165 additions & 0 deletions src/framework/ui/spinner/animation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import {
Animated,
Easing,
EasingFunction,
ViewStyle,
} from 'react-native';
import {
Animation,
AnimationConfig,
} from '../animation/animation';

const PI: number = 180;
const PI2: number = 360;
const OFFSET_MIN: number = PI / 12;
const OFFSET_MAX: number = PI / 6;

const BaseBezierEasing: EasingFunction = Easing.bezier(0.4, 0.0, 0.7, 1.0);

const StartArcEasing: EasingFunction = (progress: number): number => {
return -PI + OFFSET_MIN + (PI - OFFSET_MAX) * BaseBezierEasing(progress);
};

const EndArcEasing: EasingFunction = (progress: number): number => {
return PI2 - OFFSET_MIN + (-PI + OFFSET_MAX) * BaseBezierEasing(progress);
};

const DEFAULT_CONFIG: SpinnerAnimationConfig = {
duration: 2400,
easing: Easing.linear,
cycles: -1,
};

type TimingAnimationConfig = Omit<Animated.TimingAnimationConfig, 'toValue'>;

export interface SpinnerAnimationStyle {
container: ViewStyle;
start: ViewStyle;
end: ViewStyle;
}

export type SpinnerAnimationConfig = AnimationConfig & TimingAnimationConfig;

/**
* Animates a Spinner in a Material Design way.
*
* Thanks these guys for open sourcing the algorithm: https://github.com/n4kz/react-native-indicators
*/
export class SpinnerAnimation extends Animation<SpinnerAnimationConfig, SpinnerAnimationStyle> {

private animationValue: Animated.Value;
private animationFrames: number[];
private arcSize: number;

protected get animation(): Animated.CompositeAnimation {
return Animated.timing(this.animationValue, { toValue: 1.0, ...this.config });
}

constructor(arcSize: number, config?: SpinnerAnimationConfig) {
super({ ...DEFAULT_CONFIG, ...config });
this.arcSize = arcSize;
this.animationValue = new Animated.Value(0);
this.animationFrames = this.createFrameRange(this.config.duration);
}

public start(callback?: Animated.EndCallback) {
// reset animation value before the next animation cycle
this.animationValue.setValue(0);
super.start(callback);
}

public stop() {
super.stop();
this.animationValue.setValue(0);
}

/**
* @returns {SpinnerAnimationStyle} - an object that contains container, start and end arcs transform styles.
*/
public toProps(): SpinnerAnimationStyle {
const containerInterpolation: Animated.AnimatedInterpolation = this.createContainerInterpolation();
const startArcInterpolation: Animated.AnimatedInterpolation = this.createArcInterpolation(StartArcEasing);
const endArcInterpolation: Animated.AnimatedInterpolation = this.createArcInterpolation(EndArcEasing);

return {
container: this.toStyleTransformProp(containerInterpolation),
start: this.toStyleTransformProp(startArcInterpolation),
end: this.toStyleTransformProp(endArcInterpolation, {
transform: [{ translateY: -this.arcSize / 2 }],
}),
};
}

/**
* @param {number} duration - animation duration.
* @returns an array of frames fitted into animation.
*/
private createFrameRange = (duration: number): number[] => {
const numberOfFrames: number = 60 * duration / 1000;

return new Array(numberOfFrames).fill(0);
};

private createContainerInterpolation = (): Animated.AnimatedInterpolation => {
return this.animationValue.interpolate({
inputRange: [0, 1],
outputRange: [
this.toDegValue(OFFSET_MAX + OFFSET_MIN),
this.toDegValue((2 * PI2 + OFFSET_MAX + OFFSET_MIN)),
],
});
};

private createArcInterpolation = (easing: EasingFunction): Animated.AnimatedInterpolation => {
return this.animationValue.interpolate({
inputRange: this.createArcInterpolationInputRange(),
outputRange: this.createArcInterpolationOutputRange(easing),
});
};

/**
* Maps the animation frames into initial animation values specific for each frame.
*
* @returns a container interpolation input range in a numeric format.
*/
private createArcInterpolationInputRange = (): number[] => {
return this.animationFrames.map((item: number, frame: number): number => {
return frame / (this.animationFrames.length - 1);
});
};

/**
* Maps the animation frames into a final animation values specific for each frame.
*
* @param {(progress: number) => number} easing - Easing function specific for the arc.
* @returns an arc interpolation end values eased with an `easing` function in a StyleSheet degree format.
*/
private createArcInterpolationOutputRange = (easing: EasingFunction): string[] => {
return this.animationFrames.map((item: number, frame: number): string => {
const progress: number = 2 * frame / (this.animationFrames.length - 1);
const boundedProgress: number = Math.min(2.0 - progress, progress);

return this.toDegValue(easing(boundedProgress));
});
};

/**
* @param {Animated.AnimatedInterpolation} rotate - animated rotation animationValue.
* @param {ViewStyle} source - initial StyleSheet object.
* @returns a final StyleSheet object with a `rotate` animation value.
*/
private toStyleTransformProp = (rotate: Animated.AnimatedInterpolation, source: ViewStyle = {}): ViewStyle => {
const transform = [...(source.transform || []), { rotate }];

// @ts-ignore: AnimatedInterpolation does not fit RotateTransform type declaration
return { ...source, transform };
};

/**
* @param {number} source - degrees in a numeric format.
* @returns degrees in a StyleSheet format.
*/
private toDegValue = (source: number): string => {
return `${source}deg`;
};
}

0 comments on commit 151d63e

Please sign in to comment.