介紹
本方案做的是采用Swiper組件實現(xiàn)容器視圖居中完全展示尿背,兩邊等長露出棚唆,并且跟手滑動效果邑闺。
效果圖預(yù)覽
實現(xiàn)思路
本解決方案通過維護所有卡片偏移的數(shù)組季俩,實時更新卡片的偏移量,以實現(xiàn)swiper子組件內(nèi)圖片居中展示雀瓢,兩邊等長露出。
- 左右露出效果靜態(tài)實現(xiàn)玉掸。
Swiper組件基礎(chǔ)視圖效果如下刃麸。
如果所有子組件卡片大小一樣,子組件內(nèi)卡片居中展示即可實現(xiàn)效果司浪。但是當(dāng)子組件的卡片大小不一樣時泊业,無法通過簡單的設(shè)置居中布局實現(xiàn)左右的等長露出把沼。
此時需要計算當(dāng)前狀態(tài)下的卡片的偏移量。
/**
* 計算指定卡片的最大偏移量吁伺。
* @param index {number} target card's index.
* @returns offset value.
*/
getMaxOffset(index: number): number {
/*
* 這里的偏移量指相對容器左側(cè)的值饮睬。
* 計算公式為:屏幕寬度 - Swiper兩側(cè)突出的偏移量 - 卡片自身的寬度。
* 此值即為卡片可偏移的最大值篮奄,也就是卡片右對齊的狀態(tài)值捆愁。
* 如果居中,則將最大偏移量 / 2窟却。
*/
return this.displayWidth - this.cardsList[index].width - 2 * this.swiperMargin;
}
/**
* 計算卡片偏移量昼丑,并維護偏移量列表。
* @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);
}
// 計算當(dāng)前卡片的偏移值
if (this.isIndexValid(target)) {
this.cardsOffset[target] = this.getMaxOffset(target) / 2;
}
// 下一張片的偏移值
if (this.isIndexValid(right)) {
this.cardsOffset[right] = 0;
}
}
- 滑動跟手實現(xiàn)
滑動swiper組件動態(tài)位置更新原理和上一步靜態(tài)位置獲取原理一樣夸赫,只不過在滑動過程通過相應(yīng)的回調(diào)函數(shù)實時位置更新菩帝。
在以下這三個swiper回調(diào)接口中,分別實現(xiàn)卡片跟手茬腿、離手呼奢、導(dǎo)航點切換時的卡片偏移量更新。
接口名 | 基本功能 |
---|---|
onGestureSwipe | 在頁面跟手滑動過程中切平,逐幀觸發(fā)該回調(diào)握础。 |
onAnimationStart | 切換動畫開始時觸發(fā)該回調(diào)。 |
onChange | 子組件索引變化時觸發(fā)該事件揭绑。 |
具體api接口信息查看:Swiper事件弓候。
- 在onGestureSwiper回調(diào)中,根據(jù)手指滑動的距離實時維護卡片的偏移量他匪。
.onGestureSwipe((index, event) => {
let currentOffset = event.currentOffset;
// 獲取當(dāng)前卡片(居中)的原始偏移量
let maxOffset = this.getMaxOffset(index) / 2;
// 實時維護卡片的偏移量列表菇存,做到跟手效果
if (currentOffset < 0) {
// 向左偏移
/*
* 此處計算原理為:按照比例設(shè)置卡片的偏移量。
* 當(dāng)前卡片居中邦蜜,向左滑動后將在左邊依鸥,此時卡片偏移量即為 maxOffset * 2(因為向右對齊)。
* 所以手指能夠滑動的最大距離(this.displayWidth)所帶來的偏移量即為 maxOffset悼沈。
* 易得公式:卡片實時偏移量 = (手指滑動長度 / 屏幕寬度) * 卡片最大可偏移量 + 當(dāng)前偏移量贱迟。
* 之后的計算原理相同,將不再贅述絮供。
*/
this.cardsOffset[index] = (-currentOffset / this.displayWidth) * maxOffset + maxOffset;
if (this.isIndexValid(index + 1)) {
// 下一個卡片的偏移量
let maxOffset = this.getMaxOffset(index + 1) / 2;
this.cardsOffset[index + 1] = (-currentOffset / this.displayWidth) * maxOffset;
}
if (this.isIndexValid(index - 1)) {
// 上一個卡片的偏移量
let maxOffset = this.getMaxOffset(index - 1) / 2;
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)) {
let maxOffset = this.getMaxOffset(index + 1) / 2;
this.cardsOffset[index + 1] = (currentOffset / this.displayWidth) * maxOffset;
}
if (this.isIndexValid(index - 1)) {
let maxOffset = this.getMaxOffset(index -1) / 2;
this.cardsOffset[index - 1] = 2 * maxOffset - (currentOffset / this.displayWidth) * maxOffset;
}
}
})
- 在onAnimationStart回調(diào)中衣吠,計算手指離開屏幕時卡片的偏移量,避免產(chǎn)生突變的偏移量壤靶。
.onAnimationStart((_, targetIndex) => {
this.calculateOffset(targetIndex);
})
這里的 calculateOffset 函數(shù)即步驟1中維護卡片偏移量的函數(shù)缚俏。
- 在onChange回調(diào)中提前計算Swiper滑動后卡片的位置。
.onChange((index) => {
logger.info(TAG, `Target index: ${index}`);
this.calculateOffset(index);
})
計算方式同上一步。
- 圖片預(yù)覽效果實現(xiàn)
圖片預(yù)覽動效是通過共享元素轉(zhuǎn)場結(jié)合全屏模態(tài)實現(xiàn)的忧换。
通過geometryTransition屬性綁定兩個需要“一鏡到底”的組件(本案例中的圖片)恬惯,結(jié)合模態(tài)窗口轉(zhuǎn)場即可。
// 以下代碼僅展示關(guān)鍵部分亚茬,詳請查看源碼
Row() {
Image(this.cardInfo.src)
.objectFit(ImageFit.Cover)
.borderRadius($r('app.integer.photo_radius'))
// TODO 知識點:geometryTransition通過id參數(shù)綁定兩個組件轉(zhuǎn)場關(guān)系酪耳,實現(xiàn)一鏡到底動畫
.geometryTransition(this.cardIndex.toString(), { follow: true })
.transition(TransitionEffect.OPACITY.animation({ duration: Constants.DURATION, curve: Curve.Friction }))
}
...
.bindContentCover(
this.isPhotoShow,
this.photoShowBuilder(this.cardInfo.src, this.cardIndex.toString()),
{ backgroundColor: $r('app.color.photo_preview_build_background'), modalTransition: ModalTransition.ALPHA }
)
...
// 全屏模態(tài)組件
@Builder photoShowBuilder(img: Resource, id: string) {
Column() {
Image(img)
.borderRadius($r('app.integer.photo_radius'))
.geometryTransition(id, { follow: true })
.width($r('app.string.photo_preview_width'))
.transition(TransitionEffect.opacity(Constants.OPACITY))
}
...
.onClick(() => {
animateTo({
duration: Constants.DURATION,
curve: Curve.Friction
}, () => {
this.isPhotoShow = !this.isPhotoShow;
})
})
}
高性能知識點
- 本示例使用了LazyForEach進行數(shù)據(jù)懶加載以降低內(nèi)存占用。
- Swiper組件的onGestureSwipe事件是個高頻回調(diào)刹缝,注意在里面不要調(diào)用冗余操作和耗時操作碗暗。
工程結(jié)構(gòu)&模塊類型
cardswiperanimation // har包
├─components
│ ├─mainpage
│ │ └─ CardSwiper.ets // 卡片滑動組件入口
│ ├─model
│ │ └─ CardModel.ets // 定義卡片類型
│ ├─viewmodel
│ └─ CardViewModel.ets // 定義卡片組件
├─utils
│ ├─ Constants.ets // 常量數(shù)據(jù)
寫在最后
如果你覺得這篇內(nèi)容對你還蠻有幫助,我想邀請你幫我三個小忙:
- 點贊赞草,轉(zhuǎn)發(fā)讹堤,有你們的 『點贊和評論』,才是我創(chuàng)造的動力厨疙。
- 關(guān)注小編洲守,同時可以期待后續(xù)文章ing??,不定期分享原創(chuàng)知識沾凄。
- 想要獲取更多完整鴻蒙最新學(xué)習(xí)知識點梗醇,請移步前往小編:
https://gitee.com/MNxiaona/733GH/blob/master/jianshu