鴻蒙實(shí)現(xiàn)仿騰訊視頻首頁輪播圖

發(fā)現(xiàn)騰訊視頻的首頁輪播圖組件挺有意思,左右滑動(dòng)時(shí),兩張照片不是Swiper傳統(tǒng)的并列顯示,而是拼接在一起,通過滑動(dòng)距離,控制左右照片顯示的比例,今天模仿著實(shí)現(xiàn)一個(gè)。

先看實(shí)現(xiàn)效果

演示.gif

實(shí)現(xiàn)思路

通過觀察,使用Stack布局,將要展示的照片堆疊展示,當(dāng)左右滑動(dòng)時(shí),將最上面的照片裁剪,則露出下面的照片,這樣就實(shí)現(xiàn)了預(yù)期效果,因此,需要實(shí)現(xiàn)照片裁剪和滑動(dòng)監(jiān)聽處理照片的顯示層級。

實(shí)現(xiàn)過程

裁剪照片

使用clipShape接口將組件裁剪為所需的形狀。調(diào)用該接口后,可以保留該形狀覆蓋的組件部分,同時(shí)移除組件的其余部分。裁剪形狀本身是不可見的。

支持裁剪的形狀:

形狀 說明
CircleShape 圓形
EllipseShape 橢圓
PathShape 路徑
RectShape 矩形

使用路徑裁剪

由于裁剪的圖形不是一個(gè)規(guī)則的形狀,所以這里采用路徑裁剪,使用了一個(gè)橢圓加直線的圖形,類似實(shí)現(xiàn)了預(yù)期效果。
SVG路徑描述規(guī)范參數(shù):

命令 說明
M 在給定的(x, y)坐標(biāo)處開始一個(gè)新的子路徑。
L 從當(dāng)前點(diǎn)到給定的(x, y)坐標(biāo)畫一條線,該坐標(biāo)成為新的當(dāng)前點(diǎn)。
H 從當(dāng)前點(diǎn)繪制一條水平線到給定的x坐標(biāo),等效于將y坐標(biāo)指定為當(dāng)前點(diǎn)y坐標(biāo)的L命令。
V 從當(dāng)前點(diǎn)繪制一條垂直線到給定的y坐標(biāo),等效于將x坐標(biāo)指定為當(dāng)前點(diǎn)x坐標(biāo)的L命令。
C 使用(x1, y1)作為曲線起點(diǎn)的控制點(diǎn),(x2, y2)作為曲線終點(diǎn)的控制點(diǎn),從當(dāng)前點(diǎn)到(x, y)繪制三次貝塞爾曲線。
S (x2, y2)作為曲線終點(diǎn)的控制點(diǎn),繪制從當(dāng)前點(diǎn)到(x, y)繪制三次貝塞爾曲線。若前一個(gè)命令是C或S,則起點(diǎn)控制點(diǎn)是上一個(gè)命令的終點(diǎn)控制點(diǎn)相對于起點(diǎn)的映射。
Q 使用(x1, y1)作為控制點(diǎn),從當(dāng)前點(diǎn)到(x, y)繪制二次貝塞爾曲線。
T 繪制從當(dāng)前點(diǎn)到(x, y)繪制二次貝塞爾曲線。若前一個(gè)命令是Q或T,則控制點(diǎn)是上一個(gè)命令的終點(diǎn)控制點(diǎn)相對于起點(diǎn)的映射。
A 從當(dāng)前點(diǎn)到(x, y)繪制一條橢圓弧。
Z 通過將當(dāng)前路徑連接回當(dāng)前子路徑的初始點(diǎn)來關(guān)閉當(dāng)前子路徑。

繪制路徑效:

路徑示例.png

new PathShape({ commands:
`M -100 0 L${this.getUIContext().vp2px(this.imageWidth+this.imageHeight/2)} 0
            A${this.getUIContext().vp2px(this.imageHeight/2)} ${this.getUIContext().vp2px(this.imageHeight*3/8)}0 0 1 ${this.getUIContext().vp2px(this.imageWidth+this.imageHeight/2)} ${this.getUIContext().vp2px(this.imageHeight*3/4)}
            L${this.getUIContext().vp2px(this.imageWidth)} ${this.getUIContext().vp2px(this.imageHeight)}
            L-100 ${this.getUIContext().vp2px(this.imageHeight)} Z`
})

這樣就實(shí)現(xiàn)了圖片的弧形裁剪

通過滑動(dòng)監(jiān)聽改變裁剪的位置

前面有專門介紹過手勢系列,不了解的可以回去看一下前面幾篇,這里主要介紹圖片處理。圖片的排列使用Stack布局,設(shè)置zIndex,使其按照順序?qū)蛹壟帕?,這里通過滑動(dòng)改變zIndex的值,實(shí)現(xiàn)循環(huán)效果。

處理向左滑動(dòng)

當(dāng)圖片向左滑動(dòng)時(shí),可以發(fā)現(xiàn),當(dāng)前顯示的圖片位于最上層,下一張圖片位于下一層,其他圖片默認(rèn)全部位于0最底層。因此將PathShape向左平移手指滑動(dòng)的距離即可。需要注意的是,裁剪的屬性每個(gè)圖片都有,所以需要判斷,只有最上層的一張圖片currentIndex才需要裁剪。并且在滑動(dòng)開始時(shí),設(shè)置下一張要展示的圖片nextIndex。

Stack(){
  Image(item)
    .borderRadius(5)
    .objectFit(ImageFit.Cover)
    .clipShape(new PathShape({ commands:
    `M -100 0 L${this.getUIContext().vp2px(this.imageWidth+this.imageHeight/2)} 0
            A${this.getUIContext().vp2px(this.imageHeight/2)} ${this.getUIContext().vp2px(this.imageHeight*3/8)}0 0 1 ${this.getUIContext().vp2px(this.imageWidth+this.imageHeight/2)} ${this.getUIContext().vp2px(this.imageHeight*3/4)}
            L${this.getUIContext().vp2px(this.imageWidth)} ${this.getUIContext().vp2px(this.imageHeight)}
            L-100 ${this.getUIContext().vp2px(this.imageHeight)} Z`
    }).position({x:index==this.currentIndex?this.clipOffsetX:0}))
}.width(this.imageWidth).height(this.imageHeight)
.borderRadius(5)
.zIndex(index==this.currentIndex?2:index==this.nextIndex?1:0)

開始滑動(dòng)時(shí),設(shè)置下一張要展示的照片

this.nextIndex = (this.currentIndex-1+this.imgs.length)%this.imgs.length

滑動(dòng)結(jié)束后,將裁剪圖形移到最左邊,并且將右側(cè)即將展示的照片設(shè)置為最上層要展示的照片。

if (Math.abs(this.moveOffsetX)>200) {
this.getUIContext().animateTo({
  duration: 300,
  onFinish:()=>{
    // 滑動(dòng)到距離大于200時(shí),松手繼續(xù)向左滑動(dòng)直到不顯示,最后切換照片
    this.currentIndex=(this.currentIndex-1+this.imgs.length)%this.imgs.length
    this.nextIndex = (this.currentIndex-1+this.imgs.length)%this.imgs.length
    this.clipOffsetX= 0
  }
}, () => {
  this.clipOffsetX= this.maxOffsetClipX
});
}

處理向右滑動(dòng)

向右滑動(dòng)和向左滑動(dòng)有點(diǎn)區(qū)別,當(dāng)向右滑動(dòng)時(shí),要將當(dāng)前顯示的照片zIndex設(shè)置為1,左側(cè)要顯示的照片設(shè)置為2,即當(dāng)前顯示的照片為nextIndex,將要顯示的照片設(shè)置為currentIndex。
開始滑動(dòng)時(shí),切換照片顯示層級

this.currentIndex = (this.currentIndex+1+this.imgs.length)%this.imgs.length
this.nextIndex = (this.currentIndex-1+this.imgs.length)%this.imgs.length

滑動(dòng)結(jié)束后,當(dāng)前顯示照片即為最上層照片,因此只需將裁剪圖形的偏移設(shè)置0即不裁剪效果。

onActionEnd(() => {
  this.isMove=false
  if (Math.abs(this.moveOffsetX)>200) {  //觸發(fā)切換動(dòng)畫
    this.getUIContext().animateTo({
      duration: 300,
      onFinish:()=>{
        if (this.clipOffsetX>0) {  //向左移到結(jié)束后 重新設(shè)置圖片顯示
          // 滑動(dòng)到距離大于200時(shí),松手繼續(xù)向左滑動(dòng)直到不顯示,最后切換照片
          this.currentIndex=(this.currentIndex-1+this.imgs.length)%this.imgs.length
          this.nextIndex = (this.currentIndex-1+this.imgs.length)%this.imgs.length
          this.clipOffsetX= 0
        }
      }
    }, () => {
      if(this.moveOffsetX>0){ //向右移到
        this.clipOffsetX=0
      }else {
        this.clipOffsetX= this.maxOffsetClipX
      }
    });
  }
}

處理滑動(dòng)取消,不觸發(fā)切換

當(dāng)滑動(dòng)距離小于設(shè)置的閾值時(shí),不觸發(fā)切換,只需將裁剪圖形偏移歸位即可。由于向右滑動(dòng)時(shí),改變了圖片的顯示層級,將左側(cè)要顯示的照片設(shè)置成了currentIndex,因此這里需要再將下一張照片設(shè)置成最上層顯示的。

if(this.moveOffsetX>0){ //向右移到
  this.currentIndex = (this.currentIndex-1+this.imgs.length)%this.imgs.length
  this.clipOffsetX=0
}else {
  this.clipOffsetX=  0
}

處理左滑觸發(fā)后又觸發(fā)右滑

當(dāng)開始向左滑動(dòng)觸發(fā)后,又向右滑動(dòng)更多距離,這時(shí),需要將當(dāng)前顯示的圖片設(shè)為nextIndex,因此需要記錄一下開始滑動(dòng)方向。

if(!this.rightDirection&&event.offsetX>0){ // 開始向左滑動(dòng) 變?yōu)橄蛴一瑒?dòng)
  this.rightDirection = true
  this.currentIndex = (this.currentIndex+1+this.imgs.length)%this.imgs.length
  this.nextIndex = (this.currentIndex-1+this.imgs.length)%this.imgs.length
}

處理右滑觸發(fā)后又觸發(fā)左滑

同理,當(dāng)觸發(fā)相反方向滑動(dòng)后,需要調(diào)整照片顯示層級。

if(this.rightDirection&&event.offsetX<0){
  this.rightDirection = false
  this.currentIndex = (this.currentIndex-1+this.imgs.length)%this.imgs.length
  this.nextIndex = (this.currentIndex-1+this.imgs.length)%this.imgs.length
}

完成源碼

import { PathShape } from '@kit.ArkUI'
import { MyNavigation } from '../utils/MyAttributeModifier'

@Entry
@ComponentV2
struct ClipShapeSwiperTest{
  private imgs:Resource[]=[$r('app.media.img_gallery_1'),$r('app.media.img_gallery_4'),$r('app.media.img_gallery_5')]
  private maxOffsetClipX:number =320+134
  @Local imageWidth:number=320
  @Local imageHeight:number=200
  @Local clipOffsetX:number=0  //裁剪圖形偏移
  @Local moveOffsetX:number=0  //手指移到偏移
  @Local currentIndex:number =this.imgs.length-1
  @Local nextIndex:number=this.currentIndex-1
  @Local isMove:boolean = false  //是否在滑動(dòng)中
  @Local rightDirection:boolean = false  // 是否向右滑動(dòng)
  build() {
    Column(){
      Stack(){
        ForEach(this.imgs,(item: ResourceStr, index: number)=>{
          Stack(){
            Image(item)
              .borderRadius(5)
              .objectFit(ImageFit.Cover)
              .clipShape(new PathShape({ commands:
              `M -100 0 L${this.getUIContext().vp2px(this.imageWidth+this.imageHeight/2)} 0
            A${this.getUIContext().vp2px(this.imageHeight/2)} ${this.getUIContext().vp2px(this.imageHeight*3/8)}0 0 1 ${this.getUIContext().vp2px(this.imageWidth+this.imageHeight/2)} ${this.getUIContext().vp2px(this.imageHeight*3/4)}
            L${this.getUIContext().vp2px(this.imageWidth)} ${this.getUIContext().vp2px(this.imageHeight)}
            L-100 ${this.getUIContext().vp2px(this.imageHeight)} Z`
              }).position({x:index==this.currentIndex?this.clipOffsetX:0}))
          }.width(this.imageWidth).height(this.imageHeight)
          .borderRadius(5)
          .zIndex(index==this.currentIndex?2:index==this.nextIndex?1:0)
        })
      }.width('100%').height(this.imageHeight).alignContent(Alignment.Center)
      .gesture(
        PanGesture({ direction: PanDirection.Horizontal })
          .onActionStart((event: GestureEvent) => {
            this.isMove=true
            if (event.offsetX>0) {
              this.rightDirection = true //開始向右滑動(dòng)
              this.currentIndex = (this.currentIndex+1+this.imgs.length)%this.imgs.length
              this.nextIndex = (this.currentIndex-1+this.imgs.length)%this.imgs.length
            }else {
              this.rightDirection = false //開始向左滑動(dòng)
              this.nextIndex = (this.currentIndex-1+this.imgs.length)%this.imgs.length
            }
          })
          .onActionUpdate((event: GestureEvent) => {
            this.moveOffsetX = event.offsetX

            if(event.offsetX>0){
              this.clipOffsetX=-this.maxOffsetClipX+event.offsetX
            }else {
              this.clipOffsetX=event.offsetX
            }

            if(this.rightDirection&&event.offsetX<0){
              this.rightDirection = false
              this.currentIndex = (this.currentIndex-1+this.imgs.length)%this.imgs.length
              this.nextIndex = (this.currentIndex-1+this.imgs.length)%this.imgs.length
            }
            if(!this.rightDirection&&event.offsetX>0){ // 開始向左滑動(dòng) 變?yōu)橄蛴一瑒?dòng)
              this.rightDirection = true
              this.currentIndex = (this.currentIndex+1+this.imgs.length)%this.imgs.length
              this.nextIndex = (this.currentIndex-1+this.imgs.length)%this.imgs.length
            }

          })
          .onActionEnd(() => {
            this.isMove=false
            if (Math.abs(this.moveOffsetX)>200) {  //觸發(fā)切換動(dòng)畫
              this.getUIContext().animateTo({
                duration: 300,
                onFinish:()=>{
                  if (this.clipOffsetX>0) { //向左移到結(jié)束后 重新設(shè)置圖片顯示
                    // 滑動(dòng)到距離大于200時(shí),松手繼續(xù)向左滑動(dòng)直到不顯示,最后切換照片
                    this.currentIndex=(this.currentIndex-1+this.imgs.length)%this.imgs.length
                    this.nextIndex = (this.currentIndex-1+this.imgs.length)%this.imgs.length
                    this.clipOffsetX= 0
                  }
                }
              }, () => {
                if(this.moveOffsetX>0){
                  this.clipOffsetX=0
                }else {
                  this.clipOffsetX= this.maxOffsetClipX
                }
              });

            }else {
              if(this.moveOffsetX>0){ //向右移到
                this.currentIndex = (this.currentIndex-1+this.imgs.length)%this.imgs.length
                this.clipOffsetX=0
              }else {
                this.clipOffsetX=  0
              }
            }
          })
      )
    }
  }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容