目录

微信和鸿蒙Tabs效果对比

安卓微信页面滑动效果

Tabs组件效果

Tabs实现滑动动效

简单示例

示例效果

实现类似微信Tabs的图标颜色渐变动效

实现代码

最终效果


微信和鸿蒙Tabs效果对比

在安卓端微信客户端有这样一个效果,向左右滑动页面时底部的tabbar图标由灰色渐变到绿色,本篇博文将要通过ArkUI原生组件Tabs实现类似的效果

安卓微信页面滑动效果

在Tabs组件的使用过程中,如果使用CustomBuilder自定义了tabbar并通过onChange或onContentWillChange亦或onAnimationStart设置当前页面index,在点击tab实现切换可以通过属性动画或转场动画的形式实现tabbar的点击效果,但是如果开启了scrollable想要通过滑动页面的形式来切换TabContent会导致当前页到目标页之间的切换时tabbar的图标没有过渡效果,显得非常生硬。

Tabs组件默认效果(切换效果生硬)

Tabs实现滑动动效

本篇主要介绍通过Tabs组件的onGestureSwipeonAnimationStart来实现页面间滑动时跟随页面滑动的动态效果,首先通过以下示例来简单介绍两个方法如何实现简单切换动效

简单示例

function quantize(value: number) {// 将偏移图标旋转角度进行线性转换
  value = Math.abs(value)// 取绝对值
  const clampedValue = Math.min(480, Math.max(0, value));// 假设480是页面的最大偏移量
  return Math.round(clampedValue * 45 / 480);
}

@Entry
@Component
struct Index {
  private tabsController: TabsController = new TabsController()
  @State currentTarget:number = 480
  @State currentTabIndex: number = 0
  @State comeTabIndex:number = 0

  iconRotate(index:number) : number{
    // 设置激活Icon旋转角度
    let res = 0
    if(index === this.comeTabIndex){
      res = quantize(this.currentTarget)
    }else if(index === this.currentTabIndex){
      res = 45 - quantize(this.currentTarget)
    }
    return res
  }

  @Builder
  tabBar(index: number) {
    Column() {
    }
    .width(36).height(36)
    .backgroundColor(Color.Green)
    .rotate({z:10,angle: this.iconRotate(index)})
    .animation({duration:100,curve:Curve.Linear})
  }

  @Builder
  content(text:string){Column(){
    Text(text)
      .fontSize(30)
      .fontWeight(FontWeight.Bold)
  }.height("100%").width("100%").justifyContent(FlexAlign.End).padding({bottom:40})
  }

  build() {
    Column() {
      Tabs({ controller: this.tabsController }) {
        TabContent() {
          this.content("消息")
        }.tabBar(this.tabBar(0))

        TabContent() {
          this.content("通讯录")
        }.tabBar(this.tabBar(1))

        TabContent() {
          this.content("发现")
        }.tabBar(this.tabBar(2))

        TabContent() {
          this.content("我")
        }.tabBar(this.tabBar(3))
      }
      .onContentWillChange((currentIndex: number, comeIndex: number) => {
        this.currentTabIndex = currentIndex
        this.comeTabIndex = comeIndex
        return true
      })
      .onGestureSwipe((index:number,event:TabsAnimationEvent)=>{
        this.currentTarget = event.currentOffset
      })
      .onAnimationStart((index:number,targetIndex:number,event:TabsAnimationEvent)=>{
        // 当页面滑动停止的时候会触发此回调,所以在这个回调中需要继续刷新currentTabIndex,以避免页面没有成功滑动到目标页的时候让index正常与页面对应
        this.currentTabIndex = index    
        this.comeTabIndex = targetIndex
        this.currentTarget = 480    //假设页面最大偏移量为480
      })
      .barPosition(BarPosition.End)
      .edgeEffect(EdgeEffect.None)
      .scrollable(true)
    }
  }
}

示例效果

以上示例是通过在onGestureSwipe回调中通过得到页面的偏移量,实时将滑动态的值通过线性转换为方块旋转的角度,示例中设定的最大偏移量假设为480,对应图标旋转的角度为45°,实际可通过屏幕宽度或组件Tabs组件组件宽度来设置,其中由两个比较关键的变量,分别是currentTabIndex和comeTabIndex,分别代表的是当前页的inde和目标页的索引,在iconRotate通过当前偏移量分别为所属索引的方块按不同方向旋转。

实现类似微信Tabs的图标颜色渐变动效

实现思路:通过获取页面间滑动时的偏移量线性转换为[0,1](透明度取值范围),在自定义tabbar中使用Stack将激活状态和未激活状态下的两个图标层叠在一起,滑动时将实时偏移量的对应透明度传递给图片组件,激活图标透明度由0到1,未激活图标的透明度由1到0,在视觉上便呈现出了渐显和渐隐的效果

实现代码

const MAX_OFFSET:number = 378 //假设页面最大偏移量为378

function linearMap(value:number, origMin:number, origMax:number, targetMin = 0, targetMax = 1) {
  // value取绝对值
  value = Math.abs(value)
  // 处理原始区间无效的情况
  if (origMin === origMax) {
    return (targetMin + targetMax) / 2; // 返回目标区间中值
  }
  // 计算原始区间比例
  const normalized = (value - origMin) / (origMax - origMin);
  // 扩展到目标区间
  const result = normalized * (targetMax - targetMin) + targetMin;
  // 约束结果在目标区间(自动处理反向区间)
  let bind:number[] = [Math.min(targetMin, targetMax), Math.max(targetMin, targetMax)];
  return Math.min(bind[1], Math.max(bind[0], result));
}

@Entry
@Component
struct Index {
  private tabsController: TabsController = new TabsController()
  @State currentOffset:number = MAX_OFFSET
  @State currentTabIndex: number = 0
  @State comeTabIndex:number = 0

  // 通过target动态改变透明度
  imageOpacity(index: number, isSelectIcon: boolean): number {
    const lmValue = linearMap(this.currentOffset, 0, MAX_OFFSET);
    let res = isSelectIcon ? 0 : 1;

    if (index === this.currentTabIndex) {
      res = isSelectIcon ? 1 - lmValue : lmValue;
    }
    if (index === this.comeTabIndex) {
      res = isSelectIcon ? lmValue : 1 - lmValue;
    }
    return res;
  }

  @Styles
  imageStyle(){
    .width("100%").height("100%")
    .animation({duration:100,curve:Curve.Linear})
  }

  @Builder
  customTabBar(index: number,selectIcon:ResourceStr,unSelectIcon:ResourceStr,label:string) {
    Column() {
      Stack(){
        Image(selectIcon).imageStyle()
          .opacity(this.imageOpacity(index,true))
        Image(unSelectIcon).imageStyle()
          .opacity(this.imageOpacity(index,false))
      }.width(28).height(28)
      Stack(){   // label同样可以使用图标的方式来实现颜色渐变
        Text(label)
          .fontSize(14).fontColor("#45c01a")
          .animation({duration:100,curve:Curve.Linear})
          .opacity(this.imageOpacity(index,true))
        Text(label)
          .fontSize(14).fontColor("#252525")
          .opacity(this.imageOpacity(index,false))
          .animation({duration:100,curve:Curve.Linear})
      }.margin({top:6})
    }
  }

  @Builder
  content(text:string){Column(){
    Text(text)
      .fontSize(30)
      .fontWeight(FontWeight.Bold)
  }.height("100%").width("100%").justifyContent(FlexAlign.End).padding({bottom:40})
  }

  build() {
    Column() {
      Tabs({ controller: this.tabsController }) {
        TabContent() {
          this.content("消息")
        }.tabBar(this.customTabBar(0,$r("app.media.al_"),$r("app.media.ala"),"消息"))

        TabContent() {
          this.content("通讯录")
        }.tabBar(this.customTabBar(1,$r("app.media.al8"),$r("app.media.al9"),"通讯录"))

        TabContent() {
          this.content("发现")
        }.tabBar(this.customTabBar(2,$r("app.media.alb"),$r("app.media.alc"),"发现"))

        TabContent() {
          this.content("我")
        }.tabBar(this.customTabBar(3,$r("app.media.ald"),$r("app.media.ale"),"我"))
      }
      .onContentWillChange((currentIndex: number, comeIndex: number) => {
        this.currentTabIndex = currentIndex
        this.comeTabIndex = comeIndex
        return true
      })
      .onGestureSwipe((index:number,event:TabsAnimationEvent)=>{
        this.currentOffset = event.currentOffset
      })
      .onAnimationStart((index:number,targetIndex:number,event:TabsAnimationEvent)=>{
        // 当页面滑动停止的时候会触发此回调,所以在这个回调中需要继续刷新currentTabIndex,以避免页面没有成功滑动到目标页的时候让index正常与页面对应
        this.currentTabIndex = index
        this.comeTabIndex = targetIndex
        this.currentOffset = MAX_OFFSET
      })
      .barPosition(BarPosition.End)
      .edgeEffect(EdgeEffect.None)  // 关闭页面首尾页的滑动效果
      .scrollable(true)
    }
  }
}

最终效果

完整代码 https://gitcode.com/mtyee/swipeTabsAnimation.git

 

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐