当前位置: 首页 > news >正文

ArcScroll: 弧形滑动控件

一. 什么是ArcScroll?

ArcScroll是一种基于Scroll控件实现的弧形滑动控件。可以让Scroll内容项沿着一个圆心的轨迹滑动,从而实现内容弧形滑动的效果。如下图:

水平滑动:

垂直滑动:

二. 实现方案

以下,以水平的方向为例,介绍如何实现这种效果

2.1 计算Item的初始位置

前提条件:

(1)以Scroll的左上角为坐标系,每个Item之间的夹角为固定, 值等于a

(2)Scroll的宽度为w, 高度为h

2.1.1 计算Item之间的水平投射距离

大圆半径circleRadius:R

大圆圆心circleCenter坐标:(w/2, h/2 -R)

Scroll中心点screenCenter坐标:(w/2, h/2)

假设两个Item之间的水平投射距离为xOffset,那么根据公式tan(a) = xOffset / R, xOffset = tan(a) * R

代码如下:

 /*** 获取两个Item之间的水平距离* 根据tan公式,计算投射在水平上的距离* @returns*/
private getOffsetOfItem(): number {// 因为ArkTs中tan的参数为弧度,所以这里需要转为弧度const radian = a * Math.PI / 180return Math.tan(radian) * this.arcScrollInfo.circleRadius}

2.1.2 计算Item 在圆上的坐标

已知大圆的圆心坐标(a, b),圆上最低点坐标(x0, y0)和Item之间的夹角α, 根据公式可计算其他Item的坐标,公式如下:

x=a+(x0-a)cosα-(y0-b)sinα

y=b+(x0-a)sinα+(y0-b)cosα

对应到如上坐标系的值,计算如下:

const radian = -(a * Math.PI / 180);
const x = w/2 + (w/2 - w/2)* Math.cos(radian) - (h/2 - (h/2 - R)) * Math.sin(radian);
const y = (h/2 -R) + (w/2 - w/2) * Math.sin(radian) + (h/2 - (h/2 - R)) * Math.cos(radian);

根据如上公式,则可计算出所有Item的中心点位置

2.2 计算Scroll内容的宽度

Scroll本身的宽度为w,只有超过w, Scroll才能开始滑动,所以要想所有的Item都能够滑入Scroll中,则内容宽度为:

w + (Item的个数 - 1)* xOffset;

  /*** 获取Scroll内容的宽度* @returns*/public getScrollContentWidth(): number {const offsetDis = this.getOffsetOfItem() * (this.modelList.length - 1);let width = this.arcScrollInfo.screenWidth;width += +offsetDis;return width;}

2.3 滑动中Item的位置计算

2.3.1 偏移公式

如图所示,所有Item的初始化位置都在Scroll的左上角位置,第一步,将Item从起始点移动到(w/2, h/2)的位置,那么x,y 的translate分别为:

translateX = w/2 - 0, translateY = h/2 - 0

但是移动后,Item的左上角在圆上,中心点并不在圆上,所以,还需要分别减去Item的自身的宽度和高度的一半,最终的计算如下:

translateX = w/2 - 0 - itemWidth/2,

translateY = h/2 - 0 - itemHeight/2

则可以计算出从起始点将Item的中心点移动到(w/2,h/2)的偏移量。那么根据此公式,我们可以计算出圆上任意一点和起始位置的偏移量,假设圆上任意一点为(x0, y0),那么

translateX = x0 - 0 - itemWidth/2,

translateY = y0 - 0 - itemHeight/2

 /*** 获取当前位置和起始位置的偏移值* @param currentPoint 当前位置* @param startPoint 起始位置* @param item Item项* @returns*/private getOffSet(currentPoint: Point, startPoint: Point, item: ItemBean): Offset {const dx = currentPoint.x - startPoint.x - item.width / 2;const dy = currentPoint.y - startPoint.y - item.height / 2;return { dx: dx, dy: dy }}

注:在ArkTs中,translate的移动,永远是从控件自身的左上角位置开始,移动到需要移动的位置,可参考官方文档:translate

2.3.2 Scroll滑动中Item的位置

首先,Scroll在左右滑动的过程中,其内从是会跟着偏移的,那么为了保证Item的起始位置(Scroll左上角)不变,需要将Scroll内容滑动过的偏移量,再使用translate偏移回来

@Component
export struct ArcScrollView {@State totalOffset: number = 0build(){Stack(){Scroll(){// Scroll子控件,需要将滑动的距离偏移会拉Stack(){ForEach(...){// Item项}}.translate({ x: this.totalOffset })}.onWillScroll((xOffset: number, yOffset: number, scrollState: ScrollState) => {this.totalOffset += xOffset;}}}
}

其次,根据水平的移动距离,需要计算Item在圆上的位置坐标,然后从起始位置translate到计算后的位置上即可。

如图,假设Scroll向右滑动了offset的距离,夹角为α,那么需要计算出移动后,Item在圆上的位置(x1, y1)的坐标值。

在2.1.1章节中,我们知道Item之前的夹角为a, 可计算出水平投射距离为xOffset = tan(a) * R, 据此可以计算出Item之间的弧度和水平距离的一个比例,如下:

radio = (a * π / 180) / (tan(a) * R)

根据此比例,可以计算出水平滑动offset后,(x1, y1)和(w/2, h/2)两点之间夹角的弧度值:

radian = offset * radio

根据弧度值radian, 圆上一点的坐标(w/2, h/2), 圆心坐标(w/2, h/2 - R), 可使用2.1.2中的公式,计算出圆上的点(x1, y1)的坐标值。在根据坐标值和起始点,求取Item的偏移量进行移动。

/*** 更新Item的位置* @param offset*/public updateItemPosition(offset: number): void {const radian = offset * this.RADIAN_OFFSET_RADIO;for (let i = 0; i < this.modelList.length; i++) {const item = this.modelList[i];const pos = this.getPointInCircle(radian, item.currentPos);const translate = this.getOffSet(pos, { x: 0, y: 0 }, item);item.translateX = translate.dx as number;item.translateY = translate.dy as number;item.currentPos = pos;}}

以此类推,每次根据Scroll的偏移量计算Item的下一个坐标点,然后偏移,这样就形成了Scroll滑动中,Item沿着圆形路径滑动的效果。

垂直方向的实现方式跟水平一样,把相关x方向移动的距离计算换成y方向的即可,效果如下:

三. ArcScroll支持的能力

3.1 弹簧动画效果

Scroll 设置了手尾的弹簧动画效果,当Scroll滑到开始或结束时,继续往前滑,会有回弹的效果,设置属性如下:

Scroll(){
}
.edgeEffect(EdgeEffect.Spring)

3.2 Scroll限位滑动

为了使Scroll滑动停止时,Item可以显示在Scroll的中心位置,设置了Scroll的限位滑动,如下:

Scroll(){
}
.scrollSnap({snapAlign: ScrollSnapAlign.START,snapPagination: this.arcScrollViewModel?.getSnapPagination()})

限位滑动的距离为Item之间的水平投射距离。

3.3 Scroll属性条件

export class ConditionControl {// 是否显示滑动条public isShowScrollBar?: boolean;// 是否水平滑动public isScrollHorizontal?: boolean;
}

isShowScrollBar: true,显示滚动条,false,不显示

isScrollHorizontal: true: 水平滑动,false:垂直滑动

3.4 ItemView布局

ArcScroll控制Item的移动方式,但是Item自身的布局,则不收ArcScroll控制,所以ArcScroll在Item的参数中提供了传递布局的参数:

// item的布局方法(@Builder方法)public itemView: (item: ItemBean) => void

在初始化ArcScroll时,可以传递每个Item自己的布局方法给ArcScroll,如下:

@Component
struct ItemContainerView {@ObjectLinkitem: ItemBean;@BuilderParamitemView: ($$: ItemBean) => void = this.defaultItemViewBuilder;build() {Stack() {this.itemView(this.item);}.size({ width: this.item.width, height: this.item.height }).translate({ x: this.item.translateX, y: this.item.translateY })}@Builderprivate defaultItemViewBuilder(item: ItemBean): void {}
}@Component
export struct ArcScrollTest {// 将builderItemView传给Itemprivate modelList: ItemBean[] = [new ItemBean(1, px2vp(300), px2vp(300), this.builderItemView),new ItemBean(2, px2vp(300), px2vp(300), this.builderItemView),new ItemBean(3, px2vp(300), px2vp(300), this.builderItemView),new ItemBean(4, px2vp(300), px2vp(300), this.builderItemView),new ItemBean(5, px2vp(300), px2vp(300), this.builderItemView)]private arcScrollInfo: ArcScrollInfo = new ArcScrollInfo();aboutToAppear(): void {this.arcScrollInfo.setConditionControl({ isShowScrollBar: true, isScrollHorizontal: false })}build() {Stack() {ArcScrollView({ modelList: this.modelList, arcScrollInfo: this.arcScrollInfo })}.border({ width: 2, color: Color.Pink })}// Item 的布局方法,可根据自己的实际布局写@Builderprivate builderItemView($$: ItemBean): void {Shape() {Circle().fill(Color.Green).width('100%').height('100%')}.width('100%').height('100%')Text($$.index.toString())}
}

这里需要注意的是,为了使ItemBean中的状态变量能够引起UI刷新,builderItemView中需要按照引入传递参数:

$$: ItemBean

四. 总结

ArcScroll的实现,主要是计算圆形路径上的位置,以及转换水平移动的距离和圆上的角度,计算出移动后Item的位置,然后在根据translate的属性移动,达到弧形移动的效果。

相关文章:

  • 「Mac畅玩AIGC与多模态27」开发篇23 - 多任务摘要合成与提醒工作流示例
  • 大白话解释CPU、NPU和GPU
  • C++(1):整数常量
  • 【C语言】--指针超详解(三)
  • FreeRTOS菜鸟入门(十四)·事件
  • 计算机组成:CU与ALU
  • STL-vector
  • Midjourney-V7:支持参考图片头像或背景生成新保真图
  • 热蛋白质组分析(TPP)技术的优劣势探讨
  • 深入理解 Vue 全局导航守卫:分类、作用与参数详解
  • 资产月报怎么填?资产月报填报指南
  • 报考消防设施操作员需要满足什么条件?
  • RabbitMQ事务机制
  • 鱼眼摄像头(一)多平面格式 单缓冲读取图像并显示
  • robotframe启动ride.py
  • 【NextPilot日志移植】logged_topics.cpp解析
  • 快速开发-基于gin的中间件web项目开发
  • 【速通RAG实战:检索】7.RAG混合检索与重排序技术
  • Conventional Commits 团队使用文档
  • Go语言Stdio传输MCP Server示例【Cline、Roo Code】
  • 深圳市政协原副主席王幼鹏被“双开”
  • 花2万多在海底捞办婚礼,连锁餐企要抢酒楼的婚宴生意?
  • 东方红资管官宣:41岁原国信资管董事长成飞出任新总经理
  • 14岁女生瞒报年龄文身后洗不掉,法院判店铺承担六成责任
  • 马克思主义理论研究教学名师系列访谈|董雅华:让学生感知马克思主义理论存在于社会生活中
  • 正荣地产:前4个月销售14.96亿元,控股股东已获委任联合清盘人