parent
17a28c0f68
commit
a704da9240
@ -0,0 +1,171 @@ |
||||
import Taro from "@tarojs/taro"; |
||||
import { View, Image } from '@tarojs/components' |
||||
import { Component, ReactNode } from "react"; |
||||
import indicator from './loading.svg' |
||||
import './style.scss' |
||||
|
||||
// 动画状态
|
||||
type Status = |
||||
| 'dismissed' |
||||
| 'forward' |
||||
| 'reverse' |
||||
| 'completed' |
||||
|
||||
interface Props { |
||||
enable?: boolean |
||||
overlay?: boolean |
||||
} |
||||
|
||||
interface State { |
||||
status?: Status |
||||
background?: Record<string, unknown>, |
||||
rotation?: Record<string, unknown>, |
||||
} |
||||
|
||||
type StateSetter = (state: State) => void |
||||
|
||||
type Controller = { |
||||
setTick: (enabled: boolean | undefined) => void |
||||
clear: () => void |
||||
} |
||||
|
||||
function createController(setState: StateSetter): Controller { |
||||
const background = Taro.createAnimation({ duration: 600 }) |
||||
const rotation = Taro.createAnimation({ duration: 600 }) |
||||
let rotateTimer: ReturnType<typeof setTimeout> | undefined |
||||
let status: Status | undefined |
||||
|
||||
const notifyListener = () => setState({ |
||||
status, |
||||
background: background.export(), |
||||
rotation: rotation.export(), |
||||
}) |
||||
|
||||
const clearAnimation = (notify = true) => { |
||||
// 清空旋转动画定时器
|
||||
if (rotateTimer != null) { |
||||
clearTimeout(rotateTimer) |
||||
rotateTimer = undefined |
||||
} |
||||
|
||||
// 清空动画
|
||||
background.export() |
||||
rotation.export() |
||||
|
||||
// 通知 UI 刷新
|
||||
if (notify) { |
||||
notifyListener() |
||||
} |
||||
} |
||||
|
||||
const setAnimation = (opacity: number) => { |
||||
// 停止旋转动画
|
||||
if (rotateTimer != null) { |
||||
clearTimeout(rotateTimer) |
||||
} |
||||
|
||||
// 旋转动画定时器
|
||||
const rotate = () => { |
||||
rotation.opacity(opacity).rotate(360).step({ duration: 600 }) |
||||
notifyListener() |
||||
rotateTimer = setTimeout(rotate, 600) |
||||
} |
||||
|
||||
// 背景动画
|
||||
background.backgroundColor(`rgba(255,255,255,${opacity})`).step() |
||||
|
||||
// 启动旋转动画
|
||||
rotate() |
||||
} |
||||
|
||||
const onFinish = (opacity: number, nextStatus: Status) => { |
||||
const lockStatus = status |
||||
setTimeout(() => { |
||||
if (lockStatus === status) { |
||||
background.backgroundColor(`rgba(255,255,255,${opacity})`).step({ duration: 0 }) |
||||
if (nextStatus === 'dismissed') { |
||||
clearAnimation() |
||||
} |
||||
status = nextStatus |
||||
notifyListener() |
||||
} |
||||
}, 600) |
||||
} |
||||
|
||||
const setStatus = (newStatus: Status, opacity: number) => { |
||||
if (status !== newStatus) { |
||||
status = newStatus |
||||
setAnimation(opacity) |
||||
|
||||
if (status === 'reverse') { |
||||
onFinish(0, 'dismissed') |
||||
} else if (status === 'forward') { |
||||
onFinish(1, 'completed') |
||||
} |
||||
} |
||||
} |
||||
|
||||
const setTick = (enabled?: boolean) => { |
||||
if (enabled !== true) { |
||||
if (status === 'dismissed') { |
||||
clearAnimation() |
||||
} else if (status !== 'reverse') { |
||||
setStatus('reverse', 0) |
||||
} |
||||
} else if (status === 'completed') { |
||||
// ignore
|
||||
} else if (status !== 'forward') { |
||||
setStatus('forward', 1) |
||||
} |
||||
} |
||||
|
||||
return { |
||||
setTick, |
||||
clear() { |
||||
clearAnimation(false) |
||||
}, |
||||
} |
||||
} |
||||
|
||||
export default class Spin extends Component<Props, State> { |
||||
constructor(props) { |
||||
super(props) |
||||
this.controller = createController(this.onAnimation) |
||||
this.state = {} |
||||
} |
||||
|
||||
controller: Controller |
||||
|
||||
onAnimation = (state: State) => { |
||||
this.setState((s) => ({...s, ...state})) |
||||
} |
||||
|
||||
componentDidMount(): void { |
||||
console.log(this.props.enable) |
||||
this.controller.setTick(this.props.enable) |
||||
} |
||||
|
||||
componentDidUpdate(): void { |
||||
this.controller.setTick(this.props.enable) |
||||
} |
||||
|
||||
componentWillUnmount(): void { |
||||
this.controller.clear() |
||||
} |
||||
|
||||
shouldComponentUpdate(nextProps: Readonly<Props>, nextState: Readonly<State>): boolean { |
||||
return nextProps.enable !== this.props.enable |
||||
|| nextProps.overlay !== this.props.overlay |
||||
|| nextState.status != this.state.status |
||||
} |
||||
|
||||
render(): ReactNode { |
||||
return ( |
||||
<View className={`spinner-wrapper ${this.state.status} ${this.props.overlay ? 'is-fixed' : ''}`}> |
||||
<View className={`spinner ${this.state.status}`}> |
||||
<Image className="spinner-icon" src={indicator} /> |
||||
</View> |
||||
</View> |
||||
) |
||||
} |
||||
} |
After Width: | Height: | Size: 813 B |
@ -0,0 +1,66 @@ |
||||
.spinner-wrapper { |
||||
background-color: rgba( #fff, 1.0); |
||||
transition: background-color 1200ms ease-out; |
||||
|
||||
&.is-fixed { |
||||
z-index: 99999; |
||||
position: fixed; |
||||
width: 100%; |
||||
height: 100%; |
||||
top: 0; |
||||
left: 0; |
||||
right: 0; |
||||
bottom: 0; |
||||
} |
||||
|
||||
&.reverse, |
||||
&.dismissed { |
||||
background-color: rgba( #fff, 0.0); |
||||
} |
||||
|
||||
&.dismissed { |
||||
position: fixed; |
||||
width: 0; |
||||
height: 0; |
||||
left: -10000px; |
||||
top: -10000px; |
||||
right: auto; |
||||
bottom: auto; |
||||
overflow: hidden; |
||||
} |
||||
} |
||||
|
||||
.spinner-wrapper, |
||||
.spinner { |
||||
display: inline-flex; |
||||
flex-direction: column; |
||||
justify-content: center; |
||||
align-items: center; |
||||
} |
||||
|
||||
.spinner { |
||||
padding: 10rpx; |
||||
opacity: 1; |
||||
transition: opacity 1200ms ease-out; |
||||
|
||||
&.reverse, |
||||
&.dismissed { |
||||
opacity: 0; |
||||
} |
||||
} |
||||
|
||||
.spinner-icon { |
||||
width: 38px; |
||||
height: 38px; |
||||
animation: spinner-rotation 600ms linear infinite; |
||||
} |
||||
|
||||
@keyframes spinner-rotation { |
||||
from { |
||||
transform: rotate(0); |
||||
} |
||||
|
||||
to { |
||||
transform: rotate(360deg); |
||||
} |
||||
} |
@ -0,0 +1,37 @@ |
||||
.single-cover { |
||||
position: absolute; |
||||
bottom: 0; |
||||
top: 0; |
||||
margin: auto; |
||||
height: 220rpx; |
||||
right: 20rpx; |
||||
max-width: 50%; |
||||
min-width: 300rpx; |
||||
padding: 25rpx; |
||||
background: rgba(#000, .7); |
||||
border-radius: 20rpx; |
||||
line-height: 40rpx; |
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: space-between; |
||||
|
||||
view:first-child { |
||||
font-weight: bold; |
||||
color: #fff; |
||||
} |
||||
|
||||
view:not(:first-child) { |
||||
background: #fff; |
||||
border-radius: 10rpx; |
||||
padding: 15rpx; |
||||
} |
||||
} |
||||
|
||||
|
||||
.correct{ |
||||
|
||||
} |
||||
|
||||
.mistake{ |
||||
|
||||
} |
After Width: | Height: | Size: 3.4 KiB |
Loading…
Reference in new issue