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