基于uniapp实现微信小程序弧形滑动选择器
产品经理:“咱微信小程序上那个滑动选择能不能改成弧形的?可以任意角度那种”
摸鱼中的我:“什么弧形?弧形滑动选择器?那不是很简单吗?SVG一把梭就完事”
打开项目->开始愉快的CV之旅~
5分钟后。咦,怎么不生效的!编译器挂了吧!
一顿折腾。。。打开百度,Google,微信社区。。日内瓦!微信小程序不支持SVG!
我:“完了,这次想不跑路都不行了”
产品经理:“不是很简单的吗?就这啊”
我内心:“MMP,乐色微信小程序 tui~ 我去找现成的 此处不留爷 自有留爷处”
啪啪啪,打开Chrome开始曲线救国 github 掘金 Dcloud插件市场一顿搜索。。。
一顿操作,10分钟后。。完了 找不到现有的组件可以CV。。。
好好好 这么玩是吧 没有?那就造!小爱同学,打开勿扰模式!
开整
核心代码
/* _____________⬇️计算当前所在角度⬇️_____________ */
const calcAngle = (x: number, y: number) => {
const AB = Math.sqrt(Math.pow(A.x - O.x, 2) + Math.pow(A.y - O.y, 2)),
AC = Math.sqrt(Math.pow(A.x - x, 2) + Math.pow(A.y - y, 2)),
BC = Math.sqrt(Math.pow(O.x - x, 2) + Math.pow(O.y - y, 2)),
cosA = (Math.pow(AB, 2) + Math.pow(AC, 2) - Math.pow(BC, 2)) / (2 * AB * AC)
// 触摸的点到圆心的距离超过范围
if (AC > r * 1.2 || AC < r * .6) return
let angleA = Math.round(Math.acos(cosA) * 180 / (Math.PI))
// 判断夹角在顺时针还是逆时针
const dx = x - A.x;
const dy = y - A.y;
if ((O.x - A.x) * dy - (O.y - A.y) * dx < 0) {
angleA = 360 - angleA
}
const tempAngle = angleA - startAngle < 0 ? Math.round((360 - startAngle + angleA) / allAngle * 100) : Math.round((angleA - startAngle) / allAngle * 100)
if (tempAngle > 100) return
emit('update:modelValue', tempAngle)
}
/* _____________⬇️计算滑块所在坐标点⬇️_____________ */
const dotPosition = computed(() => calcDotPosition(props.modelValue))
const calcDotPosition = (num: number) => {
const cosA = Math.cos((startAngle + allAngle * num / 100) * 2 * Math.PI / 360),
sinA = Math.sin((startAngle + allAngle * num / 100) * 2 * Math.PI / 360),
x = (O.x - A.x) * cosA - (O.y - A.y) * sinA + A.x,
y = (O.y - A.y) * cosA + (O.x - A.x) * sinA + A.y;
return { x, y }
}
封装成组件
完整代码
<template>
<view class="arc-container" :style="{ opacity: dotPosition?.x ? 1 : 0 }" @touchstart.stop="onMouseMove"
@touchmove.stop="onMouseMove">
<view class="arc-circle arc-circle-bg" />
<view class="arc-circle" v-if="showActLine" />
<view class="arc-dot"
:style="{ top: `${dotPosition?.y - dotR}px`, left: `${dotPosition?.x - dotR}px`, opacity: showDot ? '1' : '0' }" />
<view class="custom-box">
<slot />
</view>
</view>
</template>
<style lang="scss" scoped>
.arc-container {
$size: v-bind(boxSize);
$lineMo: v-bind(lineDot);
width: $size;
height: $size;
margin: auto;
display: flex;
position: relative;
align-items: center;
justify-content: center;
.custom-box {
position: absolute;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
}
.arc-circle {
$mask: v-bind(mask);
$startPosX: v-bind("startDot.x");
$startPosY: v-bind("startDot.y");
width: $size;
height: $size;
mask: $mask;
position: absolute;
border-radius: 50%;
background: v-bind(actLine);
&::before,
&::after {
content: "";
inset: 0;
width: $lineMo;
height: $lineMo;
border-radius: 50%;
position: absolute;
background: v-bind(actColor);
top: calc($startPosY - $lineMo / 2);
left: calc($startPosX - $lineMo / 2);
}
&::after {
left: unset;
$actEndPosX: v-bind("actEndDot.x");
$actEndPosY: v-bind("actEndDot.y");
top: calc($actEndPosY - $lineMo / 2);
left: calc($actEndPosX - $lineMo / 2);
background: v-bind(actColor);
}
&-bg {
background: v-bind(bgLine);
&::before,
&::after {
inset: 0;
content: "";
width: $lineMo;
height: $lineMo;
position: absolute;
border-radius: 50%;
background: v-bind(bgColor);
top: calc($startPosY - $lineMo / 2);
left: calc($startPosX - $lineMo / 2);
}
&::after {
left: unset;
$endPosX: v-bind("endDot.x");
$endPosY: v-bind("endDot.y");
top: calc($endPosY - $lineMo / 2);
left: calc($endPosX - $lineMo / 2);
}
}
}
}
.arc-dot {
$dotColor: v-bind("props.dotColor");
$dotSize: v-bind(dotStyle);
position: absolute;
width: $dotSize;
height: $dotSize;
border-radius: 50%;
background: $dotColor ;
box-shadow: 0 0 10rpx rgba(105, 104, 104, 0.3);
}
</style>
<script lang="ts" setup>
import { ref, computed } from "vue";
/* _____________⬇️rpx转px⬇️_____________ */
const RpxToPx = (rpx: number) => {
const { screenWidth } = uni.getSystemInfoSync()
return screenWidth / 750 * rpx
}
/* _____________⬇️参数配置⬇️_____________ */
interface Config {
canMove?: boolean, // 是否可滑动
size: number, // 元素直径(rpx)
start: number, // 起始位置(0-100 0为12点钟方向)
arcLength: number, // 弧长(0-100 如:50为半圆)
modelValue: number, // 滑块当前位置(百分比)
bgColor: string, // 弧线底部线背景色
actColor: string, // 弧线活动色
dotColor: string, // 滑块背景色
dotSize: number, // 滑块大小(rpx)
lineWidth: number, // 弧线宽度(rpx)
showDot: boolean, // 是否显示滑块
showActLine: boolean, // 是否显示动态弧线
}
const props = withDefaults(defineProps<Config>(), {
canMove: true,
size: 600,
start: 75,
modelValue: 50,
arcLength: 50,
bgColor: '#F5F6F7',
// bgColor: '#BFBFBF',
actColor: '#813ACB',
dotColor: '#FFFF00',
dotSize: 100,
lineWidth: 60,
showDot: true,
showActLine: true
})
const { size, start, dotSize, arcLength, bgColor, actColor, lineWidth } = props
const startAngle = start / 100 * 360, // 起始角度转换圆心角
allAngle = arcLength / 100 * 360; // 弧长
const r = RpxToPx(size / 2), // 半径
dotR = RpxToPx(dotSize / 2), // 滑块半径
A = { x: r, y: r }, // 圆心坐标
O = { x: r, y: r * (lineWidth / size) }; // 0度坐标点
const emit = defineEmits(['update:modelValue'])
/* _____________⬇️css bind⬇️_____________ */
const dotStyle = ref(`${RpxToPx(dotSize)}px`)
const boxSize = computed(() => `${RpxToPx(size)}px`)
const lineDot = ref(`${RpxToPx(lineWidth)}px`) // 弧线首尾圆角大小css
const mask = computed(() => { // css mask
const lineW = `${RpxToPx((size - lineWidth * 2) / 2)}px`
return `radial-gradient(transparent, transparent ${lineW}, #000 ${lineW}, #000 100%)`
})
const bgLine = computed(() => { // 背景弧线css
let generate = `conic-gradient(transparent 0, transparent ${start - .1}%, ${bgColor} ${start}%, ${bgColor} ${start + arcLength}%, transparent ${start + arcLength}%)`
if (arcLength === 100) {
generate = `conic-gradient(${bgColor} 0, ${bgColor} 100%)`
} else if (start + arcLength >= 100) {
const abs = start + arcLength - 100
generate = `conic-gradient(${bgColor} 0, ${bgColor} ${abs - .1}%,transparent ${abs}%, transparent ${start - .1}%, ${bgColor} ${start}%,${bgColor} 100%)`
}
return generate
})
const actLine = computed(() => { // 滑动弧线css
const nowPos = start + props.modelValue / 100 * arcLength
let generate = `conic-gradient(transparent 0, transparent ${start - .1}%, ${actColor} ${start}%, ${actColor} ${nowPos}%, transparent ${nowPos}%)`
if (start + arcLength >= 100) {
if (nowPos >= 100) {
const abs = parseInt(`${props.modelValue / 100 * arcLength + start - 100}`)
generate = `conic-gradient(${actColor} 0, ${actColor} ${abs}%,transparent ${abs + .1}%,transparent ${start - .1}%, ${actColor} ${start}%,${actColor} 100%)`
}
}
return generate
})
const startDot = computed(() => { // 弧线起点圆角位置
return {
x: `${calcDotPosition(0).x}px`,
y: `${calcDotPosition(0).y}px`
}
})
const endDot = computed(() => { // 背景弧线结束圆角位置
return {
x: `${calcDotPosition(100).x}px`,
y: `${calcDotPosition(100).y}px`
}
})
const actEndDot = computed(() => { // 当前滑动弧线圆角位置
return {
x: `${calcDotPosition(props.modelValue).x}px`,
y: `${calcDotPosition(props.modelValue).y}px`
}
})
/* _____________⬇️触摸和滑动事件⬇️_____________ */
const onMouseMove = (e: any) => {
if (!props.canMove) return
const { changedTouches } = e
const x = changedTouches[0].pageX
const y = changedTouches[0].pageY
const { top, left } = arcOffset.value
calcAngle(x - left, y - top)
}
/* _____________⬇️计算当前所在角度⬇️_____________ */
const calcAngle = (x: number, y: number) => {
const AB = Math.sqrt(Math.pow(A.x - O.x, 2) + Math.pow(A.y - O.y, 2)),
AC = Math.sqrt(Math.pow(A.x - x, 2) + Math.pow(A.y - y, 2)),
BC = Math.sqrt(Math.pow(O.x - x, 2) + Math.pow(O.y - y, 2)),
cosA = (Math.pow(AB, 2) + Math.pow(AC, 2) - Math.pow(BC, 2)) / (2 * AB * AC)
// 触摸的点到圆心的距离超过范围
if (AC > r * 1.2 || AC < r * .6) return
let angleA = Math.round(Math.acos(cosA) * 180 / (Math.PI))
// 判断夹角在顺时针还是逆时针
const dx = x - A.x;
const dy = y - A.y;
if ((O.x - A.x) * dy - (O.y - A.y) * dx < 0) {
angleA = 360 - angleA
}
const tempAngle = angleA - startAngle < 0 ? Math.round((360 - startAngle + angleA) / allAngle * 100) : Math.round((angleA - startAngle) / allAngle * 100)
if (tempAngle > 100) return
emit('update:modelValue', tempAngle)
}
/* _____________⬇️计算滑块所在坐标点⬇️_____________ */
const dotPosition = computed(() => calcDotPosition(props.modelValue))
const calcDotPosition = (num: number) => {
const cosA = Math.cos((startAngle + allAngle * num / 100) * 2 * Math.PI / 360),
sinA = Math.sin((startAngle + allAngle * num / 100) * 2 * Math.PI / 360),
x = (O.x - A.x) * cosA - (O.y - A.y) * sinA + A.x,
y = (O.y - A.y) * cosA + (O.x - A.x) * sinA + A.y;
return { x, y }
}
/* _____________⬇️获取元素偏移量⬇️_____________ */
const arcOffset = ref({ top: 0, left: 0 }) // 容器偏移量
const query = uni.createSelectorQuery().in(getCurrentInstance())
const init = async () => {
await query.select('.arc-circle').boundingClientRect((res: any) => {
const { top, left, width } = res
arcOffset.value = { top, left }
})
await query.exec()
await nextTick()
await calcDotPosition(props.modelValue)
}
init()
</script>
好了,来看看效果
封装成了组件 可以更灵活配置
| 组件参数:config | 说明 |
|---|---|
| canMove | 是否可滑动 |
| size | 元素直径(rpx) |
| start | 起始位置(0-100 0为12点钟方向) |
| arcLength | 弧长(0-100 如:50为半圆) |
| modelValue | 滑块当前位置(百分比) |
| bgColor | 弧线底部线背景色 |
| actColor | 弧线活动色 |
| dotColor | 滑块背景色 |
| dotSize | 滑块大小(rpx) |
| lineWidth | 弧线宽度(rpx) |
| showDot | 是否显示滑块 |
| showActLine | 是否显示动态弧线 |
扫码体验效果
觉得好用的记得点点赞嘿嘿
不说了 产品经理说要请我下午茶 我得去斩他一波