BigWheelView.ets 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. import { IBestToast } from '@ibestservices/ibest-ui'
  2. import { CellStorage } from '.'
  3. import { YTAvoid } from '../../utils/YTAvoid'
  4. import { yTRouter } from '../../utils/YTRouter'
  5. import { yTToast } from '../../utils/YTToast'
  6. import { ReNameInput } from './ReNameInput'
  7. import { Cell, Sector } from './Sector'
  8. @Component
  9. export struct BigWheelView{
  10. @StorageProp(YTAvoid.SAFE_TOP_KEY) safeBottom: number = 0
  11. //持久化的数组,方便下次打开应用直接承接上一次编辑的数据
  12. @StorageLink('bigwheel')
  13. DBCells:CellStorage[]=[]
  14. //持久化没选中数组id,方便退出应用再次开发还能接着转
  15. @StorageLink('unselectbigwheel')
  16. DBUnselectCell:number[]=[]
  17. //持久化选中数组id,方便退出应用再次开发还能接着转
  18. @StorageLink('selectbigwheel')
  19. DBSelectCell:number[]=[]
  20. //转盘操作数组
  21. @State cells: Cell[] = []; // 存储单元格的数组
  22. //随着每次切换操作,需要重置触发画布重新画
  23. @Watch('drawCircleWithCustomRadii')
  24. @State changeCanvas:number=0
  25. @State wheelWidth: number = 250; // 转盘的宽度
  26. @State currentAngle: number = 0; // 当前转盘的角度
  27. @State selectedName: string = ""; // 选中的名称
  28. //持久化存储是否重复
  29. @StorageLink('isrepeat')
  30. isRepeat:boolean=false
  31. //需要操作的数组,选中没选中
  32. @State selected:Cell[]=[]
  33. @State UnSelected:Cell[]=[]
  34. @State randomAngle:number=0
  35. isAnimating: boolean = false; // 动画状态
  36. colorIndex: number = 0; // 颜色索引
  37. @StorageLink('duration')
  38. spinDuration:number=5000
  39. @State spinDurationTime:number=this.spinDuration/1000
  40. //是否显示设置界面
  41. @State isShow:boolean=false
  42. private settings: RenderingContextSettings = new RenderingContextSettings(true)
  43. private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
  44. //转盘选中判断
  45. @State currSelectNumber:number=1
  46. @State number:number=4 //转盘数量
  47. //打开半模态命名模态
  48. @State isshowRename:boolean=false
  49. /**
  50. * 后面迭代保障可以更改转盘的初始的颜色
  51. */
  52. colorPalette: string[] = [ // 颜色调色板
  53. "#fff",
  54. "#fff",
  55. "#fff",
  56. "#fff",
  57. "#fff",
  58. "#fff",
  59. ];
  60. filter(){
  61. //每次选中的数组有变化,那就要重新过滤,就要移除没选中的数组里面的数据
  62. //先获取选中id数组
  63. this.DBSelectCell=this.selected.map(item=>item.id) as number[]
  64. //再根据选中数组id过滤没选中数组
  65. this.UnSelected = this.cells.filter(item => !this.DBSelectCell.includes(item.id));
  66. //再根据没选中数组,获取没选中数组id
  67. this.DBUnselectCell=this.UnSelected.map(item=>item.id)
  68. }
  69. // 组件即将出现时调用
  70. aboutToAppear() {
  71. if(this.DBCells.length==0) {
  72. this.cells=[]
  73. this.cells.push(new Cell(1, 1, "转盘1",'#fff' ));
  74. this.cells.push(new Cell(2, 1, "转盘2", '#fff' ));
  75. this.cells.push(new Cell(3, 1, "转盘3",'#fff' ));
  76. this.cells.push(new Cell(4, 1, "转盘4", '#fff' ));
  77. this.calculateAngles(); // 计算角度
  78. //如果没有之前的记录,存一下没选中的数组和数组id
  79. this.UnSelected=this.cells
  80. this.DBUnselectCell=this.UnSelected.map(item=>item.id)
  81. }else{
  82. //如果有上次记录,需要先获取上一次得轮盘
  83. this.DBCells.forEach((item)=>{
  84. this.cells.push(new Cell(item.id,item.proportion,item.title,item.color))
  85. })
  86. this.calculateAngles(); // 计算角度
  87. //轮盘数
  88. this.number=this.cells.length
  89. //再获取没选中的和选中的
  90. this.selected=[]
  91. this.UnSelected=[]
  92. for (let index = 0; index < this.cells.length; index++) {
  93. //遍历扇形,通过选中的数组id,找到选中的数组
  94. this.DBSelectCell.forEach((I)=>{
  95. if(this.cells[index].id==I){
  96. this.selected.push(this.cells[index])
  97. }
  98. })
  99. }
  100. //再找没选中数组
  101. this.UnSelected=this.cells.filter((item)=>{
  102. return !this.DBSelectCell.includes(item.id)
  103. })
  104. this.currSelectNumber=this.number-3
  105. }
  106. }
  107. //动画
  108. private startAnimation(){
  109. if (this.isAnimating) { // 如果正在动画中,返回
  110. return;
  111. }
  112. if(this.selected.length==this.cells.length){
  113. yTToast.doubleConfirm({
  114. message: '当前已经转完所有,是否重置', click: () => {
  115. //背景色变成白色
  116. this.cells.forEach((item)=>{
  117. item.color='#fff'
  118. })
  119. //重置,所有的转盘重新开始
  120. this.selected=[]
  121. this.UnSelected=[]
  122. this.UnSelected=this.cells
  123. this.DBUnselectCell=this.UnSelected.map(item=>item.id)
  124. this.DBSelectCell=[]
  125. this.DBCells=this.cells.map((item)=>{
  126. return {
  127. id:item.id,
  128. title:item.title,
  129. proportion:item.proportion,
  130. color:item.color
  131. } as CellStorage
  132. })
  133. yTToast.hide()
  134. }
  135. })
  136. return
  137. }
  138. //如果都已经被选了,则不能再转了
  139. this.calculateNoSelectedAngles()
  140. this.selectedName = ""; // 清空选中的名称
  141. this.isAnimating = true; // 设置动画状态为正在动画
  142. animateTo({ // 开始动画
  143. duration: this.spinDuration, // 动画持续时间为5000毫秒
  144. curve: Curve.EaseInOut, // 动画曲线为缓入缓出
  145. onFinish: () => { // 动画完成后的回调
  146. this.currentAngle %= 360; // 保持当前角度在0到360之间
  147. for (const cell of this.cells) {
  148. // 遍历每个单元格
  149. // 检查当前角度是否在单元格的角度范围内
  150. if (360 - this.currentAngle >= cell.angleStart && 360 - this.currentAngle <= cell.angleEnd) {
  151. if(!this.isRepeat) {
  152. this.selected.push(cell)
  153. // this.DBSelectCell.push(cell)
  154. //这里需要下次点击的时候
  155. cell.color = '#e5e7ea'
  156. this.filter()
  157. }else{
  158. // this.selected=[]
  159. }
  160. this.selectedName = cell.title; // 设置选中的名称为当前单元格的标题
  161. //每次转弯要保存
  162. break; // 找到后退出循环
  163. }
  164. }
  165. this.DBCells=this.cells.map((item)=>{
  166. return {
  167. id:item.id,
  168. title:item.title,
  169. proportion:item.proportion,
  170. color:item.color
  171. } as CellStorage
  172. })
  173. // promptAction.showToast({
  174. // message:'选中的数组'+JSON.stringify(this.selected)+'没选中的数组'+JSON.stringify(this.UnSelected)+'选中的数组id'+JSON.stringify(this.DBSelectCell)+'没选中的数组id'+JSON.stringify(this.DBUnselectCell)
  175. // })
  176. this.isAnimating = false; // 设置动画状态为未动画
  177. },
  178. }, () => { // 动画进行中的回调
  179. //在这里判断
  180. // this.randomAngle=Math.floor(Math.random()*360)
  181. // this.currentAngle += (360 * this.spinDuration/1000 + Math.floor(Math.random() * 360)); // 更新当前角度,增加随机旋转
  182. //在这里算已经选过的,不能在指了
  183. // this.currentAngle += (360 * this.spinDuration/1000 + Math.floor(Math.random() * 360)); // 更新当前角度,增加随机旋转
  184. // promptAction.showToast({
  185. // message:this.randomAngle.toString()
  186. // })
  187. this.currentAngle += (360 * this.spinDuration/1000)+this.randomAngle
  188. });
  189. }
  190. // 计算每个单元格的角度
  191. private calculateAngles() {
  192. // 根据比例计算总比例,后续迭代需要计算不同比例的角度
  193. const totalProportion = this.cells.reduce((sum, cell) => sum + cell.proportion, 0);
  194. this.cells.forEach(cell => {
  195. cell.angle = (cell.proportion * 360) / totalProportion; // 计算每个单元格的角度
  196. });
  197. let cumulativeAngle = 0; // 累计角度
  198. this.cells.forEach(cell => {
  199. cell.angleStart = cumulativeAngle; // 设置起始角度
  200. cumulativeAngle += cell.angle; // 更新累计角度
  201. cell.angleEnd = cumulativeAngle; // 设置结束角度
  202. cell.rotate = cumulativeAngle - (cell.angle / 2); // 计算旋转角度
  203. });
  204. //手动触发画布,重新画
  205. this.changeCanvas++
  206. }
  207. //不允许重复时,需要在没选中的数组里面选,然后计算角度,使其转到没选中数组的区间当中去
  208. private calculateNoSelectedAngles(){
  209. if(this.UnSelected.length!=0) {
  210. //随机选取一个没有选中的角度范围扇形
  211. const currangle=this.currentAngle%360
  212. const randomIndex = Math.floor(Math.random() * this.UnSelected.length) as number
  213. const ranNumStart = 360-this.UnSelected[randomIndex].angleEnd
  214. const ranNumEnd = 360-this.UnSelected[randomIndex].angleStart
  215. this.randomAngle =Math.floor(Math.random() * (ranNumEnd - ranNumStart) + ranNumStart)-currangle
  216. }
  217. }
  218. //画线
  219. private drawCircleWithCustomRadii() {
  220. // this.context.clearRect(0,0,0,0)
  221. this.context.clearRect(0, 0, 250, 250); // 清空Canvas的内容
  222. const centerX = 125 // 圆心x坐标
  223. const centerY = 125 // 圆心y坐标
  224. const radius = 125 // 圆半径
  225. // 根据自定义角度数组绘制半径线
  226. this.cells.forEach(angle => {
  227. // 将角度转换为弧度(Canvas使用弧度制)
  228. const radians = (angle.angleEnd-90) * Math.PI / 180
  229. // 计算半径线终点坐标
  230. const endX = centerX + radius * Math.cos(radians)
  231. const endY = centerY + radius * Math.sin(radians)
  232. // 绘制半径线
  233. this.context.beginPath()
  234. this.context.moveTo(centerX, centerY)
  235. this.context.lineTo(endX, endY)
  236. this.context.strokeStyle = '#efd4f9' // 红色半径线
  237. this.context.lineWidth = 1.5
  238. this.context.stroke()
  239. })
  240. }
  241. build() {
  242. Stack({alignContent:Alignment.Center}){
  243. Stack({alignContent:Alignment.Top}){
  244. Column() {
  245. // YTHeader({ title: '大转盘', })
  246. Row(){
  247. Image($r('app.media.ic_back'))
  248. .width(24)
  249. .margin({ left: 16 })
  250. .onClick(()=>{
  251. yTRouter.routerBack()
  252. })
  253. Text('大转盘')
  254. .fontSize(18)
  255. .fontWeight(700)
  256. .fontColor(Color.Black)
  257. Image($r('app.media.Subtract'))
  258. .width(24)
  259. .margin({right: 16 })
  260. .onClick(()=>{
  261. this.isShow=true
  262. })
  263. }.width('100%')
  264. .height(84)
  265. .justifyContent(FlexAlign.SpaceBetween)
  266. .padding({ top: 44 })
  267. .margin({bottom:20})
  268. Row(){
  269. Column(){}.width(24).height(24)
  270. // Image($r('[basic].media.voicemuisc')).width(24)
  271. Text('重置').fontColor('rgba(0, 0, 0, 0.65)').onClick(()=>{
  272. //背景色变成白色
  273. this.cells.forEach((item)=>{
  274. item.color='#fff'
  275. })
  276. this.selected=[]
  277. this.UnSelected=[]
  278. this.UnSelected=this.cells
  279. this.DBUnselectCell=this.UnSelected.map(item=>item.id)
  280. this.DBSelectCell=[]
  281. this.DBCells=this.cells.map((item)=>{
  282. return {
  283. id:item.id,
  284. title:item.title,
  285. proportion:item.proportion,
  286. color:item.color
  287. } as CellStorage
  288. })
  289. })
  290. }.width('100%')
  291. .justifyContent(FlexAlign.SpaceBetween)
  292. .padding({left:30,right:30})
  293. // 显示当前状态
  294. Text(this.isAnimating ? '旋转中' : `${this.selectedName}`)
  295. .fontSize(20)
  296. .fontColor("#0b0e15")
  297. .height(40)
  298. .margin({top:100})
  299. Stack() {
  300. Stack() {
  301. // 遍历每个单元格并绘制扇形
  302. ForEach(this.cells, (cell: Cell) => {
  303. Stack() {
  304. Sector({ radius: lpx2px(this.wheelWidth) / 2, angle: cell.angle, color: cell.color }); // 创建扇形
  305. Text(cell.title).fontColor(Color.Black).fontWeight(700).margin({ bottom: this.wheelWidth / 1.4 }); // 显示单元格标题
  306. }.width('100%').height('100%').rotate({ angle: cell.rotate }); // 设置宽度和高度,并旋转
  307. });
  308. }
  309. .borderRadius('50%') // 设置圆角
  310. // .backgroundColor(Color.Gray) // 设置背景颜色
  311. .width(this.wheelWidth) // 设置转盘宽度
  312. .height(this.wheelWidth) // 设置转盘高度
  313. .rotate({ angle: this.currentAngle }); // 旋转转盘
  314. Column() {
  315. Canvas(this.context)
  316. .width(250)
  317. .height(250)
  318. .borderRadius('50%')
  319. .backgroundColor(Color.Transparent)
  320. .onReady(() => {
  321. this.drawCircleWithCustomRadii()
  322. })
  323. } .width(this.wheelWidth) // 设置转盘宽度
  324. .height(this.wheelWidth) // 设置转盘高度
  325. .justifyContent(FlexAlign.Center)
  326. .rotate({ angle: this.currentAngle }) // 旋转转盘
  327. Image($r('app.media.zhizheng'))
  328. .width(63) // 设置按钮宽度
  329. .height(79) // 设置按钮高度
  330. .objectFit(ImageFit.Contain)
  331. .clickEffect({ level: ClickEffectLevel.LIGHT }) // 设置点击效果
  332. }
  333. .width(this.wheelWidth+15)
  334. .height(this.wheelWidth+15)
  335. .backgroundImage($r('app.media.xuanzhuankuang'))
  336. .backgroundImageSize({width:'100%',height:'100%'})
  337. .backgroundImagePosition(Alignment.Center)
  338. Button('转一转').fontColor(Color.White)
  339. .backgroundColor('#fd54e3').width(246)
  340. .height(44).borderRadius(24)
  341. .margin({top:99,bottom:48})
  342. .onClick(()=>{
  343. this.startAnimation()
  344. })
  345. Row({space:15}) {
  346. ForEach([2,3,4,5],(item:number,index:number)=>{
  347. Text((item+1).toString())
  348. .width(40)
  349. .height(40)
  350. .textAlign(TextAlign.Center)
  351. .border({width:1,color:'#000000'})
  352. .borderRadius('50%')
  353. .backgroundColor(this.currSelectNumber==index?'#bff2ff':'#f2f2f2')
  354. .onClick(()=>{
  355. this.currSelectNumber=index
  356. this.number=item+1
  357. const arr=this.cells
  358. this.cells=[]
  359. //如果选中的长度比之前要长
  360. if(this.number>arr.length) {
  361. for (let i = 0; i < this.number; i++) {
  362. if (i < arr.length) {
  363. this.cells.push(arr[i])
  364. }else{
  365. this.cells.push(new Cell(i + 1, 1, "转盘" + (i + 1), '#fff'));
  366. }
  367. }
  368. }else{
  369. //短
  370. for (let i = 0; i < this.number; i++) {
  371. this.cells.push(arr[i])
  372. }
  373. }
  374. //要继承之前没选中的,选中的数组根据id
  375. this.calculateAngles(); // 重新计算角度
  376. //选中的和没选中的也要重新计算角度
  377. //找到id重新过滤
  378. this.selected=[]
  379. this.UnSelected=[]
  380. //每次选择扇形数量都要重新更新一下
  381. let a=[] as number[]
  382. let b=[] as number[]
  383. //先获取选中数组,没选中数组id
  384. a=this.DBSelectCell
  385. b=this.DBUnselectCell
  386. //遍历扇形,获取选中数组和没选中数组
  387. for (let i = 0; i < this.cells.length; i++) {
  388. for(let j=0;j<a.length;j++){
  389. if(a[j]==this.cells[i].id){
  390. this.selected.push(this.cells[i])
  391. continue
  392. }
  393. }
  394. }
  395. //找到选中的id数组
  396. this.DBSelectCell=this.selected.map(item=>item.id)
  397. this.UnSelected=this.cells.filter((item)=>{
  398. return !this.DBSelectCell.includes(item.id)
  399. })
  400. this.DBUnselectCell=this.UnSelected.map(item=>item.id)
  401. this.DBCells=[]
  402. this.DBCells=this.cells.map((item)=>{
  403. return {
  404. id:item.id,
  405. title:item.title,
  406. proportion:item.proportion,
  407. color:item.color
  408. } as CellStorage
  409. })
  410. })
  411. })
  412. Row(){
  413. Image($r('app.media.zidingyi')).width(16)
  414. Text('命名').fontSize(12).fontColor(Color.White)
  415. }.width(64)
  416. .height(40)
  417. .borderRadius(20)
  418. .backgroundColor('rgba(255, 157, 240, 1)')
  419. .justifyContent(FlexAlign.Center)
  420. .onClick(()=>{
  421. this.isshowRename=true
  422. })
  423. }
  424. }.width('100%').padding({ bottom: this.safeBottom })
  425. .justifyContent(FlexAlign.Center).onClick(()=>{
  426. this.isShow=false
  427. })
  428. //是否展示选项设置
  429. if(this.isShow) {
  430. this.BigWheelManagerBuilder()
  431. }
  432. }.height('100%')
  433. .backgroundImage($r('app.media.backimgNumber'))
  434. .backgroundImageSize({width:'100%',height:'100%'})
  435. //重命名设置
  436. if(this.isshowRename) {
  437. this.ReNameBuilder()
  438. }
  439. }
  440. }
  441. @Builder
  442. ReNameBuilder(){
  443. Column() {
  444. Column() {
  445. Row() {
  446. Text('转盘命名').fontSize(20).fontWeight(500).fontColor('#FF1C1C1C').margin({left:110,right:64,top:24})
  447. Column() {
  448. Image($r('app.media.quxiaocl')).width(10)
  449. }.width(24)
  450. .height(24)
  451. .backgroundColor(Color.White)
  452. .justifyContent(FlexAlign.Center)
  453. .borderRadius('50%')
  454. .onClick(() => {
  455. this.isshowRename = false
  456. })
  457. }.width('100%')
  458. Column({space:10}){
  459. ForEach(this.cells,(item:Cell,index:number)=>{
  460. ReNameInput({
  461. text:item.title,
  462. num:index+1,
  463. inputChange:(value:string)=>{
  464. item.title=value
  465. }
  466. })
  467. })
  468. }
  469. }.width(300).height(318)
  470. .justifyContent(FlexAlign.Start)
  471. .borderRadius(20)
  472. .padding({left:16,right:16})
  473. .linearGradient({
  474. angle:135,
  475. colors:[
  476. ['rgba(248, 211, 249, 1)',0.2],
  477. ['rgba(192, 242, 255, 1)',1]
  478. ]
  479. })
  480. }.width('100%').height('100%').justifyContent(FlexAlign.Center).backgroundColor('rgba(30, 30, 30,0.5)')
  481. }
  482. @Builder
  483. BigWheelManagerBuilder(){
  484. Column() {
  485. //允许结果是否重复
  486. Row() {
  487. Row({ space: 10 }) {
  488. Image($r('app.media.qiehuan')).width(24)
  489. Text('允许结果重复').fontWeight(700)
  490. }
  491. Row() {
  492. Toggle({ type: ToggleType.Switch ,isOn:$$this.isRepeat})
  493. .width(38)
  494. .height(20)
  495. .selectedColor('rgba(253, 84, 227, 1)') //打开状态下的背景颜色
  496. .switchStyle({
  497. pointRadius: 8, //圆形滑块半径
  498. trackBorderRadius: 14, //滑轨的圆角
  499. pointColor: Color.White, //圆形滑块颜色 switchPointColor不生效
  500. unselectedColor: 'rgba(233, 233, 234, 1)' //关闭状态的背景颜色
  501. })
  502. .onClick(() => {
  503. this.isRepeat=!this.isRepeat
  504. })
  505. }
  506. }
  507. .width('100%')
  508. .height(40)
  509. .backgroundColor(Color.White)
  510. .borderRadius(8)
  511. .justifyContent(FlexAlign.SpaceBetween)
  512. .padding({ left: 12, right: 12 })
  513. .alignItems(VerticalAlign.Center)
  514. Row() {
  515. Text('每次转动轮盘可能会随机选中相同的选项').fontSize(12).fontColor('rgba(0, 0, 0, 0.45)')
  516. }.width('100%')
  517. .justifyContent(FlexAlign.Start)
  518. .padding({ left: 22 })
  519. .margin({ bottom: 25, top: 10 })
  520. Row() {
  521. Row({ space: 10 }) {
  522. Image($r('app.media.xuanzhuantime')).width(24)
  523. Text('旋转时长').fontWeight(700)
  524. }
  525. Row() {
  526. Counter() {
  527. Text(this.spinDurationTime.toString() + 's').border({ width: 0 })
  528. }
  529. .onInc(() => {
  530. this.spinDurationTime++
  531. this.spinDuration = this.spinDurationTime * 1000
  532. })
  533. .onDec(() => {
  534. if(this.spinDurationTime==1){
  535. IBestToast.show({
  536. message:'秒数最低为1秒'
  537. })
  538. return
  539. }
  540. this.spinDurationTime--
  541. this.spinDuration = this.spinDurationTime * 1000
  542. })
  543. }
  544. }
  545. .width('100%')
  546. .height(40)
  547. .backgroundColor(Color.White)
  548. .borderRadius(8)
  549. .justifyContent(FlexAlign.SpaceBetween)
  550. .padding({ left: 12, right: 12 })
  551. .alignItems(VerticalAlign.Center)
  552. }
  553. .width('100%')
  554. .height(214)
  555. .padding({ left: 22, top: 56, right: 22 })
  556. .borderRadius({ bottomLeft: 20, bottomRight: 20 })
  557. .linearGradient({
  558. angle: 135,
  559. colors: [
  560. ['rgba(239, 144, 237, 1)', 0.2],
  561. ['rgba(191, 242, 255, 1)', 1]
  562. ]
  563. })
  564. }
  565. }