-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: ✨ implement vant-weapp circle component (#2)
* feat: ✨ implement circle component * style: 🎨 drop unnecessary blank line * fix: 🐛 format value as integer number, avoid infinite paint
- Loading branch information
1 parent
eb62ac2
commit 863f5f7
Showing
7 changed files
with
352 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -66,7 +66,7 @@ $ yarn build | |
|
||
### 展示组件 | ||
|
||
- [ ] Circle | ||
- [x] Circle | ||
- [x] Collapse | ||
- [x] Divider | ||
- [x] NoticeBar | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
.van-circle { | ||
position: relative; | ||
display: inline-block; | ||
text-align: center; | ||
} | ||
.van-circle__text { | ||
position: absolute; | ||
top: 50%; | ||
left: 0; | ||
width: 100%; | ||
transform: translateY(-50%); | ||
color: #323233; | ||
color: var(--circle-text-color, #323233); | ||
font-size: 16px; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
// packages | ||
import React, { | ||
FunctionComponent, | ||
CSSProperties, | ||
useMemo, | ||
useRef, | ||
useEffect, | ||
} from 'react'; | ||
import clsx from 'clsx'; | ||
import { useNativeEffect } from 'remax'; | ||
import { View, Canvas, CoverView } from 'remax/wechat'; | ||
// internal | ||
import uuid from '../tools/uuid'; | ||
import Deferred from '../tools/Deferred'; | ||
import { Switch, Case } from '../tools/Switch'; | ||
import withDefaultProps from '../tools/with-default-props-advance'; | ||
import { calcStrokeStyle, format, calcAngleRange } from './actions'; | ||
import './Circle.css'; | ||
|
||
// 默认值填充属性 | ||
interface NeutralCircleProps { | ||
size: number; | ||
color: string | Record<string, string>; | ||
layerColor: string; | ||
speed: number; | ||
strokeWidth: number; | ||
clockwise: boolean; | ||
} | ||
|
||
interface ExogenousCircleProps { | ||
// 容器类名,用以覆盖内部 | ||
className?: string; | ||
// 必须属性 | ||
value: number; | ||
// 填充色 | ||
fill?: string; | ||
// 文本 | ||
text: string; | ||
} | ||
|
||
type CircleProps = NeutralCircleProps & ExogenousCircleProps; | ||
|
||
// scope | ||
const DefaultCircleProps: NeutralCircleProps = { | ||
size: 100, | ||
color: '#1989fa', | ||
layerColor: '#fff', | ||
speed: 50, | ||
strokeWidth: 4, | ||
clockwise: true, | ||
}; | ||
|
||
const Circle: FunctionComponent<CircleProps> = (props) => { | ||
const { | ||
className, | ||
size, | ||
color, | ||
text, | ||
children, | ||
clockwise, | ||
layerColor, | ||
speed, | ||
fill, | ||
strokeWidth, | ||
value: _value, // 限制范围 | ||
} = props; | ||
const id = useMemo(() => uuid(), []); | ||
// limit range 0 ~ 100 | ||
const value = format(_value); | ||
const classnames = { | ||
container: clsx(className, 'van-circle'), | ||
}; | ||
const stylesheets: Record<string, CSSProperties> = { | ||
canvas: { | ||
width: `${size}px`, | ||
height: `${size}px`, | ||
}, | ||
}; | ||
|
||
const $context$ = useRef(Deferred<CanvasRenderingContext2D>()); | ||
const $point$ = useRef(0); | ||
const $pristine$ = useRef(true); | ||
const draw = ( | ||
context: CanvasRenderingContext2D, | ||
strokeStyle: string | CanvasGradient, | ||
beginAngle: number, | ||
endAngle: number | ||
): void => { | ||
const position = size / 2; | ||
const radius = position - strokeWidth / 2; | ||
|
||
// context.setStrokeStyle(strokeStyle); | ||
context.strokeStyle = strokeStyle; | ||
// context.setLineWidth(strokeWidth); | ||
context.lineWidth = strokeWidth; | ||
// context.setLineCap(lineCap); | ||
context.lineCap = 'round'; | ||
|
||
context.beginPath(); | ||
context.arc(position, position, radius, beginAngle, endAngle, !clockwise); | ||
context.stroke(); | ||
|
||
if (fill) { | ||
context.fillStyle = fill; | ||
context.fill(); | ||
} | ||
}; | ||
|
||
const paint = (point: number) => { | ||
$context$.current.promise.then((context) => { | ||
// 清理现场 | ||
context.clearRect(0, 0, size, size); | ||
|
||
// render layer circle | ||
draw(context, layerColor, 0, 2 * Math.PI); | ||
|
||
// render holver circle | ||
const [beginAngle, endAngle] = calcAngleRange(point, clockwise); | ||
const strokeStyle = calcStrokeStyle(context, color, size); | ||
|
||
draw(context, strokeStyle, beginAngle, endAngle); | ||
}); | ||
}; | ||
|
||
// resolve canvas context only once | ||
useNativeEffect(() => { | ||
const { pixelRatio: dpr } = wx.getSystemInfoSync(); | ||
|
||
wx.createSelectorQuery() | ||
.select(`#${id}`) | ||
.node() | ||
.exec((res) => { | ||
const canvas = res[0].node; | ||
const context = canvas.getContext('2d') as CanvasRenderingContext2D; | ||
|
||
// pre-setup | ||
canvas.width = size * dpr; | ||
canvas.height = size * dpr; | ||
|
||
context.scale(dpr, dpr); | ||
|
||
// callback ctx | ||
$context$.current.resolve(context); | ||
}); | ||
}, []); | ||
|
||
/* eslint-disable react-hooks/exhaustive-deps */ | ||
// 初次绘制无动效 | ||
useEffect(() => { | ||
paint(value); | ||
|
||
// 状态标记 | ||
$pristine$.current = false; | ||
// 内敛值更新 | ||
$point$.current = value; | ||
}, []); | ||
|
||
// 后续绘制处理动效 | ||
// 仅支持 value 响应,其他属性变更不支持 | ||
useEffect(() => { | ||
const interval = setInterval(() => { | ||
if (value !== $point$.current) { | ||
const { current } = $point$; | ||
const next = current < value ? current + 1 : current - 1; | ||
// 更新内部 step | ||
$point$.current = next; | ||
|
||
paint(next); | ||
} else { | ||
clearInterval(interval); | ||
} | ||
}, 1000 / speed); | ||
|
||
return () => { | ||
clearInterval(interval); | ||
}; | ||
}, [value]); | ||
|
||
return ( | ||
<View className={classnames.container}> | ||
<Canvas type="2d" id={id} style={stylesheets.canvas} /> | ||
<Switch> | ||
<Case in={!text}> | ||
<View className="van-circle__text">{children}</View> | ||
</Case> | ||
<Case default> | ||
<CoverView className="van-circle__text">{text}</CoverView> | ||
</Case> | ||
</Switch> | ||
</View> | ||
); | ||
}; | ||
|
||
export default withDefaultProps<ExogenousCircleProps, NeutralCircleProps>( | ||
DefaultCircleProps | ||
)(Circle); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
/** | ||
* @description - split canvas render logical | ||
* @author - huang.jian <[email protected]> | ||
*/ | ||
|
||
export type CircleColor = string | Record<string, string>; | ||
|
||
export const format = (rate: number): number => | ||
Math.round(Math.min(Math.max(rate, 0), 100)); | ||
|
||
export const calcStrokeStyle = ( | ||
context: CanvasRenderingContext2D, | ||
color: CircleColor, | ||
size: number | ||
): string | CanvasGradient => { | ||
if (typeof color === 'string') { | ||
return color; | ||
} | ||
|
||
const LinearColor = context.createLinearGradient(size, 0, 0, 0); | ||
|
||
Object.keys(color) | ||
.sort((a, b) => parseFloat(a) - parseFloat(b)) | ||
.map((key) => LinearColor.addColorStop(parseFloat(key) / 100, color[key])); | ||
|
||
return LinearColor; | ||
}; | ||
|
||
export const calcAngleRange = ( | ||
point: number, | ||
clockwise: boolean | ||
): [number, number] => { | ||
const round = 2 * Math.PI; | ||
const beginAngle = -Math.PI / 2; | ||
const progress = round * (point / 100); | ||
const endAngle = clockwise | ||
? beginAngle + progress | ||
: 3 * Math.PI - (beginAngle + progress); | ||
|
||
return [beginAngle, endAngle]; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/** | ||
* @description - nothing but export component | ||
*/ | ||
export { default } from './Circle'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
/** | ||
* @description - deferred promise | ||
* @author - huang.jian <[email protected]> | ||
*/ | ||
|
||
interface DeferreMedium<T> { | ||
promise: Promise<T>; | ||
resolve: (value?: T | PromiseLike<T>) => void; | ||
reject: (reason?: unknown) => void; | ||
} | ||
|
||
function Deferred<T>(): DeferreMedium<T> { | ||
const medium: Partial<DeferreMedium<T>> = {}; | ||
|
||
medium.promise = new Promise((resolve, reject) => { | ||
medium.resolve = resolve; | ||
medium.reject = reject; | ||
}); | ||
|
||
return medium as DeferreMedium<T>; | ||
} | ||
|
||
export default Deferred; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
// packages | ||
import React, { useState } from 'react'; | ||
import { View, Text } from 'remax/wechat'; | ||
|
||
// internal | ||
import Circle from '../../../packages/Circle'; | ||
import Button from '../../../packages/Button'; | ||
|
||
export default () => { | ||
const [value, setValue] = useState(40); | ||
const onClickMinus = () => { | ||
setValue((prev) => Math.max(0, prev - 5)); | ||
}; | ||
const onClickPlus = () => { | ||
setValue((prev) => Math.min(100, prev + 5)); | ||
}; | ||
const text = `${value}%`; | ||
|
||
return ( | ||
<View className="demo-block"> | ||
<Text className="demo-block__title">基础用法</Text> | ||
<View className="demo-block__content"> | ||
<Circle value={value} text={text} /> | ||
</View> | ||
<Text className="demo-block__title">宽度定制</Text> | ||
<View className="demo-block__content"> | ||
<Circle value={value} text={text} strokeWidth={6} /> | ||
</View> | ||
<Text className="demo-block__title">颜色定制</Text> | ||
<View className="demo-block__content"> | ||
<Circle value={value} text={text} layerColor="#eee" color="#ee0a24" /> | ||
</View> | ||
<Text className="demo-block__title">渐变色</Text> | ||
<View className="demo-block__content"> | ||
<Circle | ||
value={value} | ||
text={text} | ||
color={{ | ||
'0%': '#ffd01e', | ||
'100%': '#ee0a24', | ||
}} | ||
/> | ||
</View> | ||
<Text className="demo-block__title">逆时针</Text> | ||
<View className="demo-block__content"> | ||
<Circle value={value} text={text} color="#07c160" clockwise={false} /> | ||
</View> | ||
<Text className="demo-block__title">大小定制</Text> | ||
<View className="demo-block__content"> | ||
<Circle value={value} text={text} size={120} /> | ||
</View> | ||
<View className="demo-block__content"> | ||
<Button | ||
type="info" | ||
block | ||
onClick={onClickPlus} | ||
style={{ marginTop: '20px' }} | ||
> | ||
增加 | ||
</Button> | ||
<Button | ||
type="primary" | ||
block | ||
onClick={onClickMinus} | ||
style={{ marginTop: '10px' }} | ||
> | ||
减少 | ||
</Button> | ||
</View> | ||
</View> | ||
); | ||
}; |