• 基于Taro + React 实现微信小程序半圆滑块组件、半圆进度条、弧形进度条、半圆滑行轨道(附源码)


    效果:

    功能点:

    1、四个档位

    2、可点击加减切换档位

    3、可以点击区域切换档位

    4、可以滑动切换档位

    目的:

    给大家提供一些实现思路,找了一圈,一些文章基本不能直接用,错漏百出,代码还藏着掖着,希望可以帮到大家

    代码

    ts的写法风格

    index.tsx     

    1. import { View, ITouchEvent, BaseTouchEvent } from '@tarojs/components'
    2. import Taro from '@tarojs/taro'
    3. import { useState } from 'react'
    4. import styles from './index.module.less'
    5. import classNames from 'classnames'
    6. import { debounce } from '~/utils/util'
    7. enum ANGLES {
    8. ANGLES_135 = -135,
    9. ANGLES_90 = -90,
    10. ANGLES_45 = -45,
    11. ANGLES_0 = 0
    12. }
    13. enum MODE_VALUE {
    14. MODE_1 = 1,
    15. MODE_2 = 2,
    16. MODE_3 = 3,
    17. MODE_4 = 4
    18. }
    19. const HalfCircle = () => {
    20. const [state, setState] = useState({
    21. originAngle: ANGLES.ANGLES_135,
    22. isTouch: false,
    23. val: MODE_VALUE.MODE_1,
    24. originX: 0,
    25. originY: 0
    26. })
    27. /** 半圆的半径 */
    28. const RADIUS = 150
    29. /** 半径的一半 */
    30. const RADIUS_HALF = RADIUS / 2
    31. /** 4/3 圆的直径 */
    32. const RADIUS_THIRD = RADIUS_HALF * 3
    33. /** 直径 */
    34. const RADIUS_DOUBLE = RADIUS * 2
    35. /** 误差 */
    36. const DEVIATION = 25
    37. /** 是否开启点击振动 */
    38. const isVibrateShort = true
    39. const getAngle = () => {
    40. return {
    41. transform: `rotate(${state.originAngle}deg)`,
    42. transition: `all ${state.isTouch ? ' 0.2s' : ' 0.55s'}`
    43. }
    44. }
    45. /**
    46. * 根据坐标判断是否在半圆轨道上,半圆为RADIUS,误差为DEVIATION
    47. * @param pageX
    48. * @param pageY
    49. */
    50. const isInHalfCircleLine = (pageX: number, pageY: number, deviation?: number) => {
    51. const DEVIATION_VALUE = deviation || DEVIATION
    52. const squareSum = (pageX - RADIUS) * (pageX - RADIUS) + (pageY - RADIUS) * (pageY - RADIUS)
    53. const min = (RADIUS - DEVIATION_VALUE) * (RADIUS - DEVIATION_VALUE)
    54. const max = (RADIUS + DEVIATION_VALUE) * (RADIUS + DEVIATION_VALUE)
    55. return squareSum >= min && squareSum <= max
    56. }
    57. /** 根据做标点,获取档位 0 -> 4, -45 -> 3, -90 -> 2, -135 -> 1,从而获取旋转的角度 */
    58. const setGear = (pageX: number, pageY: number) => {
    59. let val = state.val
    60. let originAngle = state.originAngle
    61. if (isInHalfCircleLine(pageX, pageY)) {
    62. if (pageX > 0 && pageX <= RADIUS_HALF) {
    63. val = MODE_VALUE.MODE_1
    64. originAngle = ANGLES.ANGLES_135
    65. } else if (pageX > RADIUS_HALF && pageX <= RADIUS) {
    66. val = MODE_VALUE.MODE_2
    67. originAngle = ANGLES.ANGLES_90
    68. } else if (pageX > RADIUS && pageX <= RADIUS_THIRD) {
    69. val = MODE_VALUE.MODE_3
    70. originAngle = ANGLES.ANGLES_45
    71. } else {
    72. val = MODE_VALUE.MODE_4
    73. originAngle = ANGLES.ANGLES_0
    74. }
    75. }
    76. if (state.val === val) return
    77. setState((old) => {
    78. return {
    79. ...old,
    80. originAngle,
    81. val
    82. }
    83. })
    84. if (isVibrateShort) {
    85. setTimeout(() => {
    86. Taro.vibrateShort()
    87. }, 200)
    88. }
    89. }
    90. /**
    91. * 滑动比较细腻,根据x轴坐标,calcX判断是否前进还是后退
    92. * @param pageX
    93. * @param pageY
    94. */
    95. const setGearSibler = (pageX: number, pageY: number) => {
    96. let val = state.val
    97. let originAngle = state.originAngle
    98. const calcX = pageX - state.originX
    99. /** 把误差值增加,方便滑动 */
    100. if (isInHalfCircleLine(pageX, pageY, 50)) {
    101. if (pageX > 0 && pageX <= RADIUS_HALF) {
    102. if (calcX > 0) {
    103. /** 向前滑动,就前进一个档位 */
    104. val = MODE_VALUE.MODE_2
    105. originAngle = ANGLES.ANGLES_90
    106. } else {
    107. /** 向后滑动,就后退一个档位 */
    108. val = MODE_VALUE.MODE_1
    109. originAngle = ANGLES.ANGLES_135
    110. }
    111. } else if (pageX > RADIUS_HALF && pageX <= RADIUS) {
    112. if (calcX > 0) {
    113. val = MODE_VALUE.MODE_2
    114. originAngle = ANGLES.ANGLES_90
    115. } else {
    116. val = MODE_VALUE.MODE_1
    117. originAngle = ANGLES.ANGLES_135
    118. }
    119. } else if (pageX > RADIUS && pageX <= RADIUS_THIRD) {
    120. if (calcX > 0) {
    121. val = MODE_VALUE.MODE_3
    122. originAngle = ANGLES.ANGLES_45
    123. } else {
    124. val = MODE_VALUE.MODE_2
    125. originAngle = ANGLES.ANGLES_90
    126. }
    127. } else {
    128. if (calcX > 0) {
    129. val = MODE_VALUE.MODE_4
    130. originAngle = ANGLES.ANGLES_0
    131. } else {
    132. val = MODE_VALUE.MODE_3
    133. originAngle = ANGLES.ANGLES_45
    134. }
    135. }
    136. }
    137. setState((old) => {
    138. return {
    139. ...old,
    140. originAngle,
    141. val
    142. }
    143. })
    144. }
    145. /**
    146. * 获取正确的坐标点
    147. * @param pageX
    148. * @param pageY
    149. * @returns
    150. */
    151. const getRealXY = (
    152. pageX: number,
    153. pageY: number
    154. ): Promise<{
    155. realX: number
    156. realY: number
    157. }> => {
    158. return new Promise((resolve) => {
    159. Taro.createSelectorQuery()
    160. .select('#sliderBgcId')
    161. .boundingClientRect((rect) => {
    162. const { left, top } = rect
    163. /** 获取真实的做标点 */
    164. const realX = pageX - left
    165. const realY = pageY - top
    166. resolve({
    167. realX,
    168. realY
    169. })
    170. })
    171. .exec()
    172. })
    173. }
    174. const onTouchEnd = (event: BaseTouchEvent<any>) => {
    175. setState((old) => {
    176. return {
    177. ...old,
    178. isTouch: false
    179. }
    180. })
    181. }
    182. const onTouchMove = debounce(async (event: BaseTouchEvent<any>) => {
    183. const { pageX, pageY } = event.changedTouches[0]
    184. const { realX, realY } = await getRealXY(pageX, pageY)
    185. if (isInHalfCircleLine(realX, realY)) {
    186. setGearSibler(realX, realY)
    187. }
    188. }, 100)
    189. const onTouchStart = async (event: BaseTouchEvent<any>) => {
    190. const { pageX, pageY } = event.changedTouches[0]
    191. const { realX, realY } = await getRealXY(pageX, pageY)
    192. setState((old) => {
    193. return {
    194. ...old,
    195. originX: realX,
    196. originY: realY,
    197. isTouch: true
    198. }
    199. })
    200. }
    201. /** 点击设置档位 */
    202. const onHandleFirstTouch = async (event: BaseTouchEvent<any>) => {
    203. const { pageX, pageY } = event.changedTouches[0]
    204. const { realX, realY } = await getRealXY(pageX, pageY)
    205. if (isInHalfCircleLine(realX, realY)) {
    206. setGear(realX, realY)
    207. }
    208. }
    209. const lose = () => {
    210. if (state.isTouch) return
    211. if (state.val === 1) return Taro.showToast({
    212. title: '最低只能1挡',
    213. icon: 'error',
    214. duration: 2000
    215. })
    216. setState((old) => {
    217. return {
    218. ...old,
    219. originAngle: state.originAngle - 45,
    220. val: state.val - 1
    221. }
    222. })
    223. if (isVibrateShort) {
    224. Taro.vibrateShort()
    225. }
    226. }
    227. const add = () => {
    228. if (state.isTouch) return
    229. if (state.val === 4) return Taro.showToast({
    230. title: '最高只能4挡',
    231. icon: 'error',
    232. duration: 2000
    233. })
    234. setState((old) => {
    235. return {
    236. ...old,
    237. originAngle: state.originAngle + 45,
    238. val: state.val + 1
    239. }
    240. })
    241. if (isVibrateShort) {
    242. Taro.vibrateShort()
    243. }
    244. }
    245. return (
    246. <View
    247. className={styles.slider}
    248. // onTouchEnd={(event) => onTouchEnd(event)}
    249. // onTouchMove={(event) => onTouchMove(event)}
    250. // onTouchStart={(event) => onTouchStart(event)}
    251. onClick={onHandleFirstTouch}
    252. >
    253. <View className={styles.activeSliderSet}>
    254. <View className={styles.activeSlider} style={getAngle()} />
    255. </View>
    256. <View className={styles.origin} id="origin">
    257. <View className={styles.long} style={getAngle()}>
    258. <View
    259. className={styles.circle}
    260. onTouchMove={(event) => onTouchMove(event as BaseTouchEvent<any>)}
    261. onTouchStart={(event) => onTouchStart(event as BaseTouchEvent<any>)}
    262. onTouchEnd={(event) => onTouchEnd(event as BaseTouchEvent<any>)}
    263. />
    264. </View>
    265. </View>
    266. {/* 背景 */}
    267. <View className={styles.sliderBgc} id="sliderBgcId" />
    268. {/* 刻度 */}
    269. {/* <View className={styles.scaleBgc} /> */}
    270. <View className={styles.centerContent}>
    271. <View className={styles.centerText}>能量档位</View>
    272. <View className={styles.btn_air_bar}>
    273. <View className={classNames(styles.btn_air, styles.btn_air_left)} onClick={lose}>
    274. -
    275. </View>
    276. <View className={styles.val}>
    277. <View className="val_text">{state.val}</View>
    278. </View>
    279. <View className={classNames(styles.btn_air, styles.btn_air_right)} onClick={add}>
    280. +
    281. </View>
    282. </View>
    283. </View>
    284. </View>
    285. )
    286. }
    287. export default HalfCircle

    index.module.less

    1. @color-brand: #EBC795 ;
    2. @borderColor:#706D6D;
    3. @sliderWidth:10px;
    4. @radius:150px;
    5. @long: 150px;
    6. @border-radius: @long;
    7. .slider {
    8. position: relative;
    9. padding-bottom: @sliderWidth / 2;
    10. background-color: #000;
    11. width: 100vw;
    12. display: flex;
    13. justify-content: center;
    14. align-items: center;
    15. // 背景色
    16. .sliderBgc {
    17. width: @long*2;
    18. height: @long;
    19. border: @sliderWidth solid;
    20. border-radius: @border-radius @border-radius 0 0;
    21. border-color: @borderColor;
    22. border-bottom: none;
    23. }
    24. .scaleBgc {
    25. width: @long*2 + @sliderWidth *2;
    26. height: @long + @sliderWidth;
    27. position: absolute;
    28. // bottom: 0;
    29. // left: 0;
    30. border: @sliderWidth solid;
    31. border-radius: @border-radius + @sliderWidth @border-radius + @sliderWidth 0 0;
    32. border-color: transparent;
    33. border-bottom: none;
    34. top: -10px;
    35. background-clip: padding-box, border-box;
    36. background-origin: padding-box, border-box;
    37. background-image: linear-gradient(to right, #000, #000), linear-gradient(90deg, #FFD1B2, #E49E6B);
    38. }
    39. // 激活色
    40. .activeSliderSet {
    41. position: absolute;
    42. width: (@long) *2;
    43. height: @long;
    44. // left: 0;
    45. // bottom: 0;
    46. z-index: 2;
    47. overflow: hidden;
    48. .activeSlider {
    49. bottom: 0;
    50. left: 0;
    51. width: @long*2;
    52. height: @long;
    53. border: @sliderWidth solid;
    54. border-color: @color-brand;
    55. // border-color: transparent !important;
    56. border-radius: @border-radius @border-radius 0 0;
    57. border-bottom: none;
    58. transform: rotate(-100deg);
    59. transform-origin: @long @long;
    60. // background-clip: padding-box, border-box;
    61. // background-origin: padding-box, border-box;
    62. // background-image: linear-gradient(to right, #000, #000), linear-gradient(90deg, #FFD1B2, #E49E6B);
    63. }
    64. }
    65. .origin {
    66. width: 0;
    67. height: 0;
    68. position: absolute;
    69. background-color: rgba(0, 0, 0, 0.1);
    70. bottom: 0;
    71. left: 50%;
    72. z-index: 11;
    73. transform: translateX(50%);
    74. .long {
    75. width: @long - (@sliderWidth / 2);
    76. height: 0;
    77. z-index: 9999;
    78. position: absolute;
    79. top: 0;
    80. left: 0;
    81. transform-origin: 0 0;
    82. .circle {
    83. width: 16px;
    84. height: 16px;
    85. border-radius: 50%;
    86. position: absolute;
    87. top: 50%;
    88. right: 0;
    89. transform: translate(50%, -50%);
    90. background-color: #000;
    91. border: #fff 4px solid;
    92. z-index: 999;
    93. padding: 5px;
    94. }
    95. }
    96. }
    97. }
    98. .centerContent {
    99. position: absolute;
    100. bottom: 0;
    101. left: 50%;
    102. transform: translateX(-50%);
    103. z-index: 99;
    104. margin-bottom: 20px;
    105. .centerText {
    106. text-align: center;
    107. color: var(--q-light-color-text-secondary, var(--text-secondary, #8C8C8C));
    108. font-size: 10px;
    109. margin-bottom: 25px;
    110. }
    111. .btn_air_bar {
    112. display: flex;
    113. align-items: center;
    114. .btn_air {
    115. width: 30px;
    116. height: 30px;
    117. border-radius: 50%;
    118. background-color: wheat;
    119. font-size: 16px;
    120. font-weight: 500;
    121. display: flex;
    122. align-items: center;
    123. justify-content: center;
    124. }
    125. .btn_air_left {
    126. background-color: #706D6D;
    127. color: white;
    128. }
    129. .btn_air_right {
    130. background-color: white;
    131. color: #706D6D;
    132. }
    133. .val {
    134. height: 26px;
    135. display: flex;
    136. align-items: center;
    137. margin: 0 30px;
    138. font-size: 26px;
    139. font-weight: 700;
    140. }
    141. }
    142. }

    防抖的工具函数debounce 的详细代码:

    import { debounce } from '~/utils/util'

    1. function debounceextends Function>(func: T, delay: number): T {
    2. let timeout
    3. return function (this: any, ...args: any[]): void {
    4. const context = this;
    5. clearTimeout(timeout);
    6. timeout = setTimeout(() => {
    7. func.apply(context, args);
    8. }, delay);
    9. } as any;
    10. }

  • 相关阅读:
    了解下SparkSQL中的笛卡尔积
    常见分布式事务解决方案
    如何把.mhd和.raw文件转换为DICOM文件
    蓝桥杯(杂题1)
    socket:内核初始化及创建流(文件)详细过程
    Redis知识-实战篇(1)
    Kali Linux基础篇(一) 信息收集
    呼吁改正《上海市卫生健康信息技术应用创新白皮书》 C# 被认定为A 组件 的 错误认知
    【图像边缘检测】基于matlab灰度图像的积累加权边缘检测【含Matlab源码 2010期】
    三、性能分析工具的使用
  • 原文地址:https://blog.csdn.net/weixin_42442713/article/details/134157768