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

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的属性移动,达到弧形移动的效果。

http://www.xdnf.cn/news/4904.html

相关文章:

  • 初等数论--欧拉函数积性的证明
  • Uniapp Android/IOS 获取手机通讯录
  • 【Linux】自定义shell的编写
  • vllm的技术核心、安装流程和使用教程,以及注意事项
  • 自主独立思考,帮我创造新的方法:vue3 script setup语法中,组件传递值我觉得有些复杂,帮我创造一种简单的方法容易写的方法?
  • 使用Java实现HTTP协议服务:从自定义服务器到内置工具
  • 数据加密方式(对称加密/非对称加密 /数字签名/证书)
  • vue项目的创建
  • 字符串---Spring字符串基本处理
  • 耳机插进电脑只有一边有声音怎么办 解决方法分享
  • 第十六届蓝桥杯B组第二题
  • 什么是分布式光伏系统?屋顶分布式光伏如何并网?
  • 高质量老年生活:从主动健康管理到预防医学的社会价值
  • 在 Spring Boot 中选择合适的 HTTP 客户端
  • 2025年社交APP安全防御指南:抵御DDoS与CC攻击的实战策略
  • NLP基础
  • 支付宝 SEO 优化:提升小程序曝光与流量的完整指南
  • Kotlin中Lambda表达式和匿名函数的区别
  • RabbitMQ消息的重复消费问题如何解决?
  • jenkins 启动报错
  • 从粗放管控到数字治能——安科瑞智能监测系统助力污水厂能耗下降15%+
  • 如何通过C# 获取Excel单元格的数据类型
  • YOLO算法的基本介绍
  • 【react组件】矩形框选小组件,鼠标左键选中 div,键盘 ESC 清空
  • 【Axios】解决Axios下载二进制文件返回空对象的问题
  • 高性能Python Web 框架--FastAPI 学习「基础 → 进阶 → 生产级」
  • [Linux网络_70] ARP协议 | RARP | DNS | ICMP协议
  • 无人机电池储存与操作指南
  • 垃圾分类宣教小程序源码介绍
  • Java——包装类