鸿蒙开发实现微信tabs滑动页面时tabbar随着滑动呈现的动态效果

目录

微信和鸿蒙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

 

### 实现微信小程序自定义顶部 TabBar微信小程序中实现自定义顶部 TabBar 可以为应用提供更加个性化的导航体验。通过使用 `scroll-view` 和动态样式绑定,可以创建一个响应式的顶部标签栏。 #### HTML 结构 首先,在 WXML 文件中构建基础结构: ```xml <view class="tab-bar"> <scroll-view scroll-x class="tabs-wrapper"> <view wx:for="{{tabList}}" wx:key="id" data-index="{{index}}" bindtap="switchTab" class="tab-item {{currentIdx==index?'active':''}}"> {{item.name}} </view> </scroll-view> <!-- 动态移动线条 --> <view class="indicator-line" style="left:{{lineLeft}}rpx;width:{{lineWidth}}rpx;"></view> </view> ``` 此部分代码展示了如何利用循环渲染多个标签项,并设置点击事件处理器以及活动状态标记[^3]。 #### 数据模型与逻辑处理 接着,在 JS 文件里初始化数据并编写交互逻辑: ```javascript Page({ data: { tabList: [ { id: 'home', name: '首页' }, { id: 'category', name: '分类' }, { id: 'cart', name: '购物车' } ], currentIdx: 0, lineWidth: 80, // 默认指示器宽度 lineLeft: 0 // 默认左边距位置 }, onLoad() { this.calculateIndicatorPosition(); }, switchTab(e) { const idx = e.currentTarget.dataset.index; this.setData({ currentIdx: idx }, () => { this.calculateIndicatorPosition(); }); }, calculateIndicatorPosition() { let query = wx.createSelectorQuery().in(this); query.select('.tab-item.active').boundingClientRect((rect) => { if (rect !== null && rect.width > 0){ this.setData({ lineWidth: rect.width * 2, // 调整为 rpx 单位 lineLeft: rect.left - 15 // 增加一些偏移量使视觉更佳 }) } }).exec() } }) ``` 上述 JavaScript 代码实现了当用户切换不同标签更新当前选中的索引值,并调用函数重新计算底部滑动条的位置和大小。 #### 样式美化 最后,在 WXSS 中添加必要的 CSS 来增强外观表现力: ```css /* 整体容器 */ .tab-bar{ position:relative; background-color:#fff; } .tabs-wrapper{ white-space: nowrap; padding-bottom:10rpx; } .tab-item{ display:inline-block; height:90rpx; line-height:90rpx; text-align:center; font-size:32rpx; color:#7F7F7F; margin-right:40rpx; } .indicator-line{ position:absolute; bottom:-6rpx; transition:.3s all ease-in-out; } .tab-item.active,.indicator-line{ color:#FFCC00 !important; } ``` 这段样式表设置了各个元素的具体尺寸、颜色以及其他属性,使得整个控件看起来美观大方。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值