HarmonyOS 鸿蒙Next 自定义轮播封装分享
HarmonyOS 鸿蒙Next 自定义轮播封装分享
自定义轮播封装分享
在数字化浪潮汹涌澎湃的当下,鸿蒙技术以其创新性与前瞻性脱颖而出,不仅在技术层面日臻成熟,更在应用拓展的广度与深度上不断突破,持续引领着行业变革的新方向。而与之相伴的,是市场需求的更迭加速,其变化的节奏宛如时代脉搏,跳动得愈发急促且强劲,深刻影响着鸿蒙生态的发展轨迹与前行方向。对展示效果要求越来越高,系统自带的功能无法满足需求,所以需要自定义。
二、功能需求
- 支持左右滑动,中间图片最大,2边的图片较小
- 滑动的时候有动画效果
- 点击中间的图片支持预览效果
三、轮播图的展示效果:
四、实现思路:
1. 根据onGestureSwiper回调中,根据手指滑动实时计算卡片偏移量
2. 在onAnimationStart回调中,计算手指离开屏幕时卡片的偏移量与缩放系数,避免产生突变的偏移量与缩放系数
3. 在onChange回调中提前计算Swiper滑动后卡片的位置与缩放系数
4. 在onAnimationEnd回调中,让startSwiperOffset归0
5. 通过geometryTransition属性绑定两个需要“一镜到底”的组件,结合模态窗口转场即可
核心代码:
@Component
export struct CardSwiperAnimationView {
// 卡片数据源
private data: CardsSource = new CardsSource([]);
// 卡片数据列表
@State private cardsList: CardInfo[] = [];
// 卡片偏移度列表
@State private cardsOffset: number[] = [];
// 屏幕宽度
private displayWidth: number = 0;
// Swiper 两侧的偏移量
private swiperMargin: number = Constants.SWIPER_MARGIN;
// Swiper 当前索引值
@State private currentSwiperIndex: number = 0;
private readonly DEVICESIZE: number =
600; // 依据Navigation的mode属性说明,如使用Auto,窗口宽度>=600vp时,采用Split模式显示;窗口宽度<600vp时,采用Stack模式显示。
// 数据源对应的缩放数组
@State scaleList: number[] = [];
// 最大缩放
private MAX_SCALE: number = 1;
// 最小缩放
private MIN_SCALE: number = 0.75;
// 手势触发时的offset
@State startSwiperOffset: number = 0;
// 图片缩放的比例
@State proportion: number = 0;
@Prop
images: string[] = []
onChangeAction?:(index:number)=>void
aboutToAppear(): void {
// 获取屏幕大小,用于后续计算卡片的偏移量
const displayData: display.Display = display.getDefaultDisplaySync();
this.displayWidth = px2vp(displayData.width);
if ((display.isFoldable() && display.getFoldStatus() === display.FoldStatus.FOLD_STATUS_EXPANDED) ||
this.displayWidth >= this.DEVICESIZE) {
this.displayWidth = px2vp(displayData.width) / 2;
}
logger.info(TAG, `Display width ${this.displayWidth}`);
// 添加卡片数据,来自预置用例,可根据实际情况自行修改。同时初始化偏移量列表。
this.images.forEach((item: string, index: number) => {
const model = {
src: item,
width: 120,
} as CardInfo
this.cardsList.push(model)
this.cardsOffset.push(0)
this.scaleList.push(index === 0 ? this.MAX_SCALE : this.MIN_SCALE);
})
// 初始化懒加载列表
this.data = new CardsSource(this.cardsList);
// 计算当前卡片及关联卡片的偏移量
this.calculateOffset(0);
}
build() {
Column() {
Swiper() {
LazyForEach(this.data, (item: CardInfo, index: number) => {
CardComponent({
cardInfo: item,
cardOffset: this.cardsOffset[index],
cardIndex: index,
showingCard: this.currentSwiperIndex,
scaleList: this.scaleList
})
.border({
width: 1,
color: Color.Transparent
})
})
}
.index($$this.currentSwiperIndex)
.loop(false)
.prevMargin(this.swiperMargin)
.nextMargin(this.swiperMargin)
.duration(Constants.DURATION)
.curve(Curve.Friction)
.indicator(false)
.onChange((index) => {
this.calculateOffset(index);
// index发生变化时,修改数组中对应的缩放系数
this.currentSwiperIndex = index;
this.scaleList[this.currentSwiperIndex] = this.MAX_SCALE;
// 若index为第一张图时,最后一张图片缩放系数为MIN_SCALE,否则index-1缩放系数为MIN_SCALE
if (this.currentSwiperIndex === 0) {
this.scaleList[this.scaleList.length - 1] = this.MIN_SCALE;
} else {
this.scaleList[this.currentSwiperIndex - 1] = this.MIN_SCALE;
}
// 若index为最后一张图时,第一张图缩放系数为MIN_SCALE,否则index+1缩放系数为MIN_SCALE
if (this.currentSwiperIndex === this.scaleList.length - 1) {
this.scaleList[0] = this.MIN_SCALE;
} else {
this.scaleList[this.currentSwiperIndex + 1] = this.MIN_SCALE;
}
this.onChangeAction && this.onChangeAction(index)
})
.onGestureSwipe((index, event) => {
const currentOffset = event.currentOffset;
// 获取当前卡片(居中)的原始偏移量
const maxOffset = this.getMaxOffset(index) * this.proportion / 2;
// 实时维护卡片的偏移量列表,做到跟手效果
if (currentOffset < 0) {
// 向左偏移
/*
* 此处计算原理为:按照比例设置卡片的偏移量。
* 当前卡片居中,向左滑动后将在左边,此时卡片偏移量即为 maxOffset * 2(因为向右对齐)。
* 所以手指能够滑动的最大距离(this.displayWidth)所带来的偏移量即为 maxOffset。
* 易得公式:卡片实时偏移量 = (手指滑动长度 / 屏幕宽度) * 卡片最大可偏移量 + 当前偏移量。
* 之后的计算原理相同,将不再赘述。
*/
this.cardsOffset[index] = (-currentOffset / this.displayWidth) * maxOffset + maxOffset;
if (this.isIndexValid(index + 1)) {
// 下一个卡片的偏移量
const maxOffset = this.getMaxOffset(index + 1) / 2 * this.proportion;
this.cardsOffset[index + 1] = (-currentOffset / this.displayWidth) * maxOffset;
}
if (this.isIndexValid(index - 1)) {
// 上一个卡片的偏移量
const maxOffset = this.getMaxOffset(index - 1) / 2 * this.proportion;
this.cardsOffset[index - 1] = (currentOffset / this.displayWidth) * maxOffset + 2 * maxOffset;
}
} else if (currentOffset > 0) {
// 向右滑动
this.cardsOffset[index] = maxOffset - (currentOffset / this.displayWidth) * maxOffset;
if (this.isIndexValid(index + 1)) {
const maxOffset = this.getMaxOffset(index + 1) / 2 * this.proportion;
this.cardsOffset[index + 1] = (currentOffset / this.displayWidth) * maxOffset;
}
if (this.isIndexValid(index - 1)) {
const maxOffset = this.getMaxOffset(index - 1) / 2 * this.proportion;
this.cardsOffset[index - 1] = 2 * maxOffset - (currentOffset / this.displayWidth) * maxOffset;
}
}
// 页面跟手滑动过程中触发回调,动态计算卡片滑动距离实时计算缩放系数。
this.calculateScaling(index, currentOffset);
})
.onAnimationStart((index, targetIndex) => {
this.calculateOffset(targetIndex);
// 计算手指离开屏幕时卡片的缩放系数
if (index === targetIndex) {
let nextIndex: number = (index === this.scaleList.length - 1 ? 0 : index + 1);
let preIndex: number = (index === 0 ? this.scaleList.length - 1 : index - 1);
this.scaleList[index] = this.MAX_SCALE;
this.scaleList[nextIndex] = this.MIN_SCALE;
this.scaleList[preIndex] = this.MIN_SCALE;
} else {
let nextIndex: number = (targetIndex === this.scaleList.length - 1 ? 0 : targetIndex + 1);
let preIndex: number = (targetIndex === 0 ? this.scaleList.length - 1 : targetIndex - 1);
this.scaleList[targetIndex] = this.MAX_SCALE;
this.scaleList[nextIndex] = this.MIN_SCALE;
this.scaleList[preIndex] = this.MIN_SCALE;
}
})
.onAnimationEnd(() => {
this.startSwiperOffset = 0;
})
.height(160)
.backgroundColor(Color.Grey)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
/**
* 计算卡片偏移量,并维护偏移量列表。
* @param targetIndex { number } swiper target card's index.
*/
calculateOffset(target: number) {
let left = target - 1;
let right = target + 1;
// 计算上一张卡片的偏移值
if (this.isIndexValid(left)) {
this.cardsOffset[left] = this.getMaxOffset(left) - this.cardsList[left].width * (1 - this.MIN_SCALE) / 2;
}
// 计算当前卡片的偏移值
if (this.isIndexValid(target)) {
this.cardsOffset[target] = this.getMaxOffset(target) * this.proportion / 2;
}
// 下一张片的偏移值
if (this.isIndexValid(right)) {
this.cardsOffset[right] = -this.cardsList[right].width * (1 - this.MIN_SCALE) / 2;
}
}
/**
* 检查卡片索引值的合法性。
* @param index {number} input card's index.
* @returns true or false.
*/
isIndexValid(index: number): boolean {
return index >= 0 && index < this.cardsList.length;
}
/**
* 计算指定卡片的最大偏移量。
* @param index {number} target card's index.
* @returns offset value.
*/
getMaxOffset(index: number): number {
/*
* 这里的偏移量指相对容器左侧的值。
* 计算公式为:屏幕宽度 - Swiper两侧突出的偏移量 - 卡片自身的宽度。
* 此值即为卡片可偏移的最大值,也就是卡片右对齐的状态值。
* 如果居中,则将最大偏移量 / 2。
*/
// 原图时最大偏移量
let maxOffset: number = this.displayWidth - this.cardsList[index].width - 2 * this.swiperMargin;
// 缩放时最大偏移量
let maxOffsetScale: number =
this.displayWidth - this.cardsList[index].width * this.MIN_SCALE - 2 * this.swiperMargin;
this.proportion = maxOffset / maxOffsetScale;
return maxOffsetScale;
}
/**
* 根据卡片滑动距离实时计算卡片缩放系数。
* @param index {number} target card's index.
* @param offset {number} current Offset distance.
*/
calculateScaling(index: number, offset: number) {
let currentScale: number = this.scaleList[index];
let nextIndex: number = (index === this.scaleList.length - 1 ? 0 : index + 1);
let preIndex: number = (index === 0 ? this.scaleList.length - 1 : index - 1);
let nextScale: number = this.scaleList[nextIndex];
let preScale: number = this.scaleList[preIndex];
if (this.startSwiperOffset === 0) {
this.startSwiperOffset = offset;
}
// 滑动距离
let distance: number = Math.abs(this.startSwiperOffset - offset);
currentScale = this.MAX_SCALE - Math.min(distance / this.displayWidth, this.MAX_SCALE - this.MIN_SCALE);
// 滑动时实时缩放的比例
if (this.startSwiperOffset > offset) {
nextScale = this.MIN_SCALE + Math.min(distance / this.displayWidth, this.MAX_SCALE - this.MIN_SCALE);
preScale = this.MIN_SCALE;
} else {
preScale = this.MIN_SCALE + Math.min(distance / this.displayWidth, this.MAX_SCALE - this.MIN_SCALE);
nextScale = this.MIN_SCALE;
}
this.scaleList[this.currentSwiperIndex] = currentScale;
this.scaleList[nextIndex] = nextScale;
this.scaleList[preIndex] = preScale;
}
}
五、总结
以上便是达成自定义轮播功能的完整实现思路。其中,最为关键的环节在于精准计算滑动过程中实时缩放的比例,并借助动画这一表现形式将其流畅地展示出来,从而契合预期的视觉效果和交互需求,为用户带来更为出色的体验。
更多关于HarmonyOS 鸿蒙Next 自定义轮播封装分享的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
更多关于HarmonyOS 鸿蒙Next 自定义轮播封装分享的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
针对您分享的HarmonyOS 鸿蒙Next自定义轮播封装,以下是一个简洁的专业回答:
在HarmonyOS鸿蒙Next系统中,自定义轮播封装可以通过使用ArkUI框架的组件和动画功能来实现。首先,需要定义轮播项的布局和样式,这可以通过XML或ETS(Enhanced TypeScript)来完成。每个轮播项可以包含图片、文本或其他自定义组件。
为了实现轮播效果,可以利用动画API来控制轮播项的切换。具体来说,可以使用动画插值器(Interpolator)和定时器(Timer)来定义轮播的平滑度和切换速度。通过监听动画的完成事件,可以实现自动循环播放。
在轮播逻辑中,需要维护一个当前轮播项的索引,并在动画结束时更新该索引以切换到下一个轮播项。同时,需要处理边界情况,如从最后一个轮播项切换到第一个轮播项,或从第一个轮播项切换到最后一个轮播项。
此外,为了提高用户体验,可以添加用户交互功能,如手势滑动切换轮播项或点击停止/继续轮播。这些交互可以通过监听器(Listener)来实现,并根据用户操作来更新轮播状态。
如果问题依旧没法解决请联系官网客服,官网地址是:https://www.itying.com/category-93-b0.html