Files
akmon/uni_modules/lime-loading/index_old.uts
2026-01-20 08:04:15 +08:00

595 lines
15 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 引入颜色处理库
import { tinyColor } from '@/uni_modules/lime-color';
// ===================== 类型定义 =====================
/**
* 加载动画类型
* circular: 环形加载动画
* spinner: 旋转器加载动画
* failed: 失败状态动画
*/
export type LoadingType = 'circular' | 'spinner' | 'failed';
/**
* 操作类型
* play: 开始动画
* failed: 显示失败状态
* clear: 清除动画
* destroy: 销毁实例
*/
export type TickType = 'play' | 'failed' | 'clear' | 'destroy' | 'pause'
/**
* 加载组件配置选项
* @property type - 初始动画类型
* @property strokeColor - 线条颜色
* @property ratio - 尺寸比例
* @property immediate - 是否立即启动
*/
export type UseLoadingOptions = {
type : LoadingType;
strokeColor : string;
ratio : number;
immediate ?: boolean;
};
/**
* 加载组件返回接口
*/
export type UseLoadingReturn = {
// state : Ref<boolean>;
// setOptions: (options: UseLoadingOptions) => void
ratio : 1;
type : LoadingType;
color : string;//Ref<string>;
play : () => void;
failed : () => void;
clear : () => void;
destroy : () => void;
pause : () => void;
}
/**
* 画布尺寸信息
*/
export type Dimensions = {
width : number;
height : number;
size : number
}
/**
* 线段坐标点
*/
type Point = {
x1 : number
y1 : number
x2 : number
y2 : number
}
/**
* 画布上下文信息
*/
type LoadingCanvasContext = {
ctx : Ref<DrawableContext | null>;
dimensions : Ref<Dimensions>;
updateDimensions : (el : UniElement) => void;
};
/**
* 动画参数配置
*/
type AnimationParams = {
width : number
height : number
center : number[] // 元组类型,明确表示两个数值的坐标
color : string // 使用Ref类型包裹字符串
size : number // 数值类型尺寸
}
// ===================== 动画管理器 =====================
type AnimationFrameHandler = () => boolean;
/**
* 动画管理类
* 封装动画的启动/停止逻辑
*/
export class AnimationManager {
time : number = 1000 / 60 // 默认帧率60fps
private timer : number = -1;// 定时器ID
private isDestroyed : boolean = false; // 销毁状态
private drawFrame : AnimationFrameHandler// 帧绘制函数
private lastTime : number = 0;
private isRunning : boolean = false;
type : LoadingType;
constructor(drawFrame : AnimationFrameHandler, type : LoadingType = 'circular') {
this.type = type
this.drawFrame = drawFrame
}
/** 启动动画循环 */
start() {
if (this.isRunning) return;
this.isRunning = true;
this.lastTime = Date.now();
let animate : ((task : number) => void) | null = null
animate = (task : number) => {
if (!this.isRunning) return;
if (this.isDestroyed) return;
const delta = Date.now() - this.lastTime;
if (delta >= this.time || delta < 10) {
const shouldContinue : boolean = this.drawFrame();
this.lastTime = Date.now()
if (!shouldContinue) {
this.stop();
return;
}
}
this.timer = requestAnimationFrame(animate!);
};
animate(Date.now());
}
/** 停止动画并清理资源 */
stop() {
cancelAnimationFrame(this.timer)
this.isDestroyed = true;
}
pause() {
this.isRunning = false;
cancelAnimationFrame(this.timer);
}
}
// ===================== 工具函数 =====================
/**
* 缓动函数 - 三次缓入缓出
* @param t 时间系数 (0-1)
* @returns 计算后的进度值
*/
function easeInOutCubic(t : number) : number {
return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
}
// ===================== 画布管理 =====================
/**
* 获取画布上下文信息
* @param element 画布元素引用
* @returns 包含画布上下文和尺寸信息的对象
*/
//_element : Ref<UniElement | null>
export function useCanvas() : LoadingCanvasContext {
const ctx = shallowRef<DrawableContext | null>(null);
const dimensions = ref<Dimensions>({
width: 0,
height: 0,
size: 0
});
const updateDimensions = (el : UniElement) => {
// const rect = el.getBoundingClientRect();
// // 鸿蒙尺寸为0时 再次尺寸变化会不渲染
// if(rect.width == 0) return
// if(ctx.value == null) {
// ctx.value = el.getDrawableContext() as DrawableContext;
// }
// dimensions.value.width = rect.width;
// dimensions.value.height = rect.height;
// dimensions.value.size = Math.min(rect.width, rect.height);
el.getBoundingClientRectAsync()?.then(rect => {
if (rect.width == 0 || rect.height == 0) return
ctx.value = el.getDrawableContext() as DrawableContext;
dimensions.value.width = rect.width;
dimensions.value.height = rect.height;
dimensions.value.size = Math.min(rect.width, rect.height);
})
};
return {
ctx,
dimensions,
updateDimensions
} as LoadingCanvasContext
}
// ===================== 动画创建函数 =====================
/**
* 创建环形加载动画
* @param ctx 画布上下文
* @param animationParams 动画参数
* @returns 动画管理器实例
*/
function createCircularAnimation(ctx : DrawableContext, animationParams : AnimationParams) : AnimationManager {
const { size, color, width, height } = animationParams
let startAngle = 0; // 起始角度
let endAngle = 0; // 结束角度
let rotate = 0; // 旋转角度
// 动画参数配置
const MIN_ANGLE = 5; // 最小保持角度
const ARC_LENGTH = 359.5 // 最大弧长(避免闭合)
const PI = Math.PI / 180 // 角度转弧度系数
const SPEED = 0.018 // 动画速度
const ROTATE_INTERVAL = 0.09 // 旋转增量
const lineWidth = size / 10; // 线宽计算
const x = width / 2 // 中心点X
const y = height / 2 // 中心点Y
const radius = size / 2 - lineWidth // 实际绘制半径
/** 帧绘制函数 */
const drawFrame = () : boolean => {
ctx.reset();
// 绘制圆弧
ctx.beginPath();
ctx.arc(
x,
y,
radius,
startAngle * PI + rotate,
endAngle * PI + rotate
);
ctx.lineWidth = lineWidth;
ctx.strokeStyle = color;
ctx.stroke();
// 角度更新逻辑
if (endAngle < ARC_LENGTH) {
endAngle = Math.min(ARC_LENGTH, endAngle + (ARC_LENGTH - MIN_ANGLE) * SPEED);
} else if (startAngle < ARC_LENGTH) {
startAngle = Math.min(ARC_LENGTH, startAngle + (ARC_LENGTH - MIN_ANGLE) * SPEED);
} else {
// 重置时保留最小可见角度
startAngle = 0;
endAngle = MIN_ANGLE;
}
rotate = (rotate + ROTATE_INTERVAL) % 360; // 持续旋转并限制范围
ctx.update()
return true
}
return new AnimationManager(drawFrame)
}
/**
* 创建旋转器动画
* @param ctx 画布上下文
* @param animationParams 动画参数
* @returns 动画管理器实例
*/
function createSpinnerAnimation(ctx : DrawableContext, animationParams : AnimationParams) : AnimationManager {
const { size, color, center } = animationParams
const steps = 12; // 旋转线条数量
let step = 0; // 当前步数
const lineWidth = size / 10; // 线宽
// #ifdef APP-HARMONY
const length = size / 3.4 - lineWidth; // 线长
// #endif
// #ifndef APP-HARMONY
const length = size / 3.6 - lineWidth; // 线长
// #endif
const offset = size / 4; // 距中心偏移
const [x, y] = center // 中心坐标
/** 生成颜色渐变数组 */
function generateColorGradient(hex : string, steps : number) : string[] {
const colors : string[] = []
const _color = tinyColor(hex)
for (let i = 1; i <= steps; i++) {
_color.setAlpha(i / steps);
colors.push(_color.toRgbString());
}
return colors
}
// 计算颜色渐变
let colors = computed(() : string[] => generateColorGradient(color, steps))
/** 帧绘制函数 */
const drawFrame = () : boolean => {
ctx.reset();
for (let i = 0; i < steps; i++) {
const stepAngle = 360 / steps; // 单步角度
const angle = stepAngle * i; // 当前角度
const index = (steps + i - step) % steps // 颜色索引
// const index = (i + step) % steps;
// console.log('index', index)
// 计算线段坐标
const radian = angle * Math.PI / 180;
const cos = Math.cos(radian);
const sin = Math.sin(radian);
// 绘制线段
ctx.beginPath();
ctx.moveTo(x + offset * cos, y + offset * sin);
ctx.lineTo(x + (offset + length) * cos, y + (offset + length) * sin);
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
ctx.strokeStyle = colors.value[index];
ctx.stroke();
}
// step += 1
step = (step + 1) % steps; // 限制step范围
ctx.update()
return true
}
return new AnimationManager(drawFrame)
}
/**
* 计算圆周上指定角度的点的坐标
* @param centerX 圆心的 X 坐标
* @param centerY 圆心的 Y 坐标
* @param radius 圆的半径
* @param angleDegrees 角度(以度为单位)
* @returns 包含 X 和 Y 坐标的对象
*/
function getPointOnCircle(
centerX : number,
centerY : number,
radius : number,
angleDegrees : number
) : number[] {
// 将角度转换为弧度
const angleRadians = (angleDegrees * Math.PI) / 180;
// 计算点的 X 和 Y 坐标
const x = centerX + radius * Math.cos(angleRadians);
const y = centerY + radius * Math.sin(angleRadians);
return [x, y]
}
/**
* 创建失败状态动画包含X图标和外围圆圈
* @param ctx 画布上下文
* @param animationParams 动画参数
* @returns 动画管理器实例
*/
function createFailedAnimation(ctx : DrawableContext, animationParams : AnimationParams) : AnimationManager {
const { width, height, size, color } = animationParams
const innerSize = size * 0.8 // 内圈尺寸
const lineWidth = innerSize / 10; // 线宽
const lineLength = (size - lineWidth) / 2 // X长度
const centerX = width / 2;
const centerY = height / 2;
const [startX1, startY] = getPointOnCircle(centerX, centerY, lineLength / 2, 180 + 45)
const [startX2] = getPointOnCircle(centerX, centerY, lineLength / 2, 180 + 90 + 45)
const angleRadians1 = 45 * Math.PI / 180
const angleRadians2 = (45 - 90) * Math.PI / 180
const radius = (size - lineWidth) / 2
const totalSteps = 36; // 总动画步数
function generateSteps(stepsCount : number) : Point[][] {
const halfStepsCount = stepsCount / 2;
const step = lineLength / halfStepsCount
const steps : Point[][] = []
for (let i = 0; i < stepsCount; i++) {
const sub : Point[] = []
const index = i % 18 + 1
if (i < halfStepsCount) {
const x2 = Math.sin(angleRadians1) * step * index + startX1
const y2 = Math.cos(angleRadians1) * step * index + startY
const start1 = {
x1: startX1,
y1: startY,
x2,
y2,
} as Point
sub.push(start1)
} else {
sub.push(steps[halfStepsCount - 1][0])
const x2 = Math.sin(angleRadians2) * step * index + startX2
const y2 = Math.cos(angleRadians2) * step * index + startY
const start2 = {
x1: startX2,
y1: startY,
x2,
y2,
} as Point
sub.push(start2)
}
steps.push(sub)
}
return steps
}
const steps = generateSteps(totalSteps);
const drawFrame = () : boolean => {
const drawStep = steps.shift()!
ctx.reset()
ctx.lineWidth = lineWidth;
ctx.strokeStyle = color;
// 绘制逐渐显示的圆
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, (2 * Math.PI) * (totalSteps - steps.length) / totalSteps);
ctx.lineWidth = lineWidth;
ctx.strokeStyle = color;
ctx.stroke();
// 绘制X
ctx.beginPath();
drawStep.forEach(item => {
ctx.beginPath();
ctx.moveTo(item.x1, item.y1)
ctx.lineTo(item.x2, item.y2)
ctx.stroke();
})
ctx.update()
return steps.length != 0
}
return new AnimationManager(drawFrame)
}
// ===================== 主Hook函数 =====================
/**
* 加载动画组合式函数
* @param element 画布元素引用
* @returns 加载控制器实例
*/
export function useLoading(
element : Ref<UniElement | null>,
// options : UseLoadingOptions
) : UseLoadingReturn {
const ticks = ref<TickType[]>([]);
const currentTick = ref<TickType>('clear');
const currentAnimation = shallowRef<AnimationManager | null>(null);
const state = reactive<UseLoadingReturn>({
color: '#000',
type: 'circular',
ratio: 1,
play: () => {
// if (currentTick.value == 'pause' && currentAnimation.value != null) {
// // 从暂停状态恢复时直接启动动画管理器
// currentAnimation.value?.start()
// } else {
// // 首次播放或类型变化时重新初始化
// ticks.value.length = 0
// ticks.value.push('play')
// }
ticks.value.length = 0
ticks.value.push('play')
},
failed: () => {
ticks.value.length = 0
ticks.value.push('failed')
},
clear: () => {
ticks.value.length = 0
ticks.value.push('clear')
},
destroy: () => {
ticks.value.length = 0
ticks.value.push('destroy')
},
pause: () => {
ticks.value.push('pause')
}
})
const { ctx, dimensions, updateDimensions } = useCanvas();
const resizeObserver : UniResizeObserver = new UniResizeObserver((_entries : UniResizeObserverEntry[]) => {
updateDimensions(element.value!)
});
// 计算动画参数
const animationParams = computed(() : AnimationParams => {
return {
width: dimensions.value.width,
height: dimensions.value.height,
center: [dimensions.value.width / 2, dimensions.value.height / 2],
color: state.color,
size: state.ratio > 1 ? state.ratio : dimensions.value.size * state.ratio
} as AnimationParams
})
const startAnimation = (type : LoadingType) => {
currentAnimation.value?.pause();
if (currentAnimation.value?.type == type) {
currentAnimation.value!.start()
return
}
if (type == 'circular') {
currentAnimation.value = createCircularAnimation(ctx.value!, animationParams.value)
currentAnimation.value!.time = 1000 / 30
currentAnimation.value!.start()
return
}
if (type == 'spinner') {
currentAnimation.value = createSpinnerAnimation(ctx.value!, animationParams.value)
currentAnimation.value!.time = 1000 / 10
currentAnimation.value!.start()
return
}
if (type == 'failed') {
currentAnimation.value = createFailedAnimation(ctx.value!, animationParams.value)
currentAnimation.value?.start()
return
}
}
const failed = () => {
startAnimation('failed')
}
const play = () => {
startAnimation(state.type)
}
const clear = () => {
currentAnimation.value?.stop();
ctx.value?.reset();
ctx.value?.update();
}
const destroy = () => {
clear();
resizeObserver.disconnect();
}
watch(animationParams, () => {
if (['clear', 'destroy', 'pause'].includes(currentTick.value)) return
startAnimation(state.type)
})
watchEffect(() => {
if (ctx.value == null) return
const tick = ticks.value.pop()
if (ticks.value.length > 0) { }
if (tick != null) {
currentTick.value = tick
}
if (tick == 'play') {
play()
return
}
if (tick == 'failed') {
failed()
return
}
if (tick == 'clear') {
clear()
return
}
if (tick == 'destroy') {
destroy()
return
}
if (tick == 'pause') {
if (currentAnimation.value == null) {
play()
}
currentAnimation.value?.pause();
}
})
watchEffect(() => {
if (element.value == null) return
resizeObserver.observe(element.value!);
// #ifdef APP-IOS || APP-HARMONY
// APP-HARMONY 在下拉组件中使用居中会影响绘制 故先获取一次
updateDimensions(element.value!)
// #endif
})
onUnmounted(destroy);
return state
}