HarmonyOS鸿蒙Next开发者技术支持-自定义TabBar页签凸起和凹陷案例

HarmonyOS鸿蒙Next开发者技术支持-自定义TabBar页签凸起和凹陷案例

案例概述

1.1 问题背景

在移动应用开发中,TabBar是常见的底部导航组件。传统的TabBar通常采用平面设计,但为了提升用户体验和视觉吸引力,开发者需要实现以下效果:

  1. 凸起效果:中间按钮凸起,吸引用户点击
  2. 凹陷效果:TabBar整体凹陷,增强立体感
  3. 动态交互:点击动画、悬浮效果
  4. 状态管理:选中状态、未选中状态区分
  5. 适配性:兼容不同设备尺寸

1.2 解决方案概述

本案例提供完整的自定义TabBar解决方案,包含:

  • 凸起TabBar:中间按钮凸起,带动画效果
  • 凹陷TabBar:整体凹陷设计,增强立体感
  • 平滑过渡:Tab切换平滑动画
  • 状态管理:完整的Tab状态管理
  • 主题适配:支持深色/浅色模式

实现步骤详解

步骤1:定义数据模型和配置类

// TabBarModels.ets
export interface TabItem {
  id: string;                      // Tab唯一标识
  text: string;                    // 显示文本
  icon: Resource;                  // 图标资源
  activeIcon: Resource;           // 激活图标
  badge?: number | string;        // 角标
  disabled?: boolean;              // 是否禁用
  accessibilityLabel?: string;    // 无障碍标签
}

export interface TabBarMetrics {
  height: number;                  // TabBar高度
  width: number;                   // TabBar宽度
  itemWidth: number;               // 每个Tab宽度
  bulgeHeight: number;            // 凸起高度
  indentDepth: number;             // 凹陷深度
  safeAreaBottom: number;         // 底部安全区域
}

export interface TabAnimationConfig {
  duration: number;                // 动画时长
  curve: number;                  // 动画曲线
  scale: number;                  // 缩放比例
  translateY: number;             // Y轴偏移
}

export interface TabStyle {
  normalColor: ResourceColor;     // 正常颜色
  activeColor: ResourceColor;     // 激活颜色
  backgroundColor: ResourceColor; // 背景色
  borderColor: ResourceColor;     // 边框颜色
  shadowColor: ResourceColor;     // 阴影颜色
  textSize: number;               // 文字大小
  iconSize: number;              // 图标大小
  borderRadius: number;          // 圆角半径
}

export class TabBarConfig {
  // 凸起TabBar配置
  static readonly BULGE_CONFIG = {
    height: 80,                   // 总高度
    bulgeHeight: 20,             // 凸起高度
    bulgeWidth: 60,              // 凸起宽度
    iconSize: 28,                // 图标大小
    textSize: 12,                // 文字大小
    borderRadius: 40,            // 圆角半径
    shadowBlur: 20,              // 阴影模糊
    shadowOffsetY: 4,           // 阴影偏移
    highlightScale: 1.2,         // 高亮缩放
  };

  // 凹陷TabBar配置
  static readonly INDENT_CONFIG = {
    height: 70,                   // 总高度
    indentDepth: 8,              // 凹陷深度
    borderWidth: 1,              // 边框宽度
    iconSize: 24,                // 图标大小
    textSize: 11,                // 文字大小
    borderRadius: 20,            // 圆角半径
    innerPadding: 8,             // 内边距
    highlightDepth: 4,           // 高亮深度
  };

  // 动画配置
  static readonly ANIMATION_CONFIG = {
    tabChange: {
      duration: 300,             // Tab切换动画
      curve: Curve.EaseInOut,
    },
    bulgePress: {
      duration: 200,             // 凸起按钮按下
      curve: Curve.FastOutLinearIn,
    },
    indentPress: {
      duration: 150,             // 凹陷按钮按下
      curve: Curve.EaseOut,
    },
    badgeUpdate: {
      duration: 300,             // 角标更新
      curve: Curve.Spring,
    },
  };

  // 样式配置
  static readonly STYLE_CONFIG = {
    light: {
      normalColor: '#666666',    // 正常状态颜色
      activeColor: '#0066FF',    // 激活状态颜色
      backgroundColor: '#FFFFFF',  // 背景色
      borderColor: '#F0F0F0',    // 边框颜色
      shadowColor: '#40000000',  // 阴影颜色
      bulgeColor: '#0066FF',     // 凸起按钮颜色
      bulgeShadow: '#400066FF',  // 凸起按钮阴影
    },
    dark: {
      normalColor: '#AAAAAA',
      activeColor: '#4D94FF',
      backgroundColor: '#1C1C1E',
      borderColor: '#2C2C2E',
      shadowColor: '#40000000',
      bulgeColor: '#4D94FF',
      bulgeShadow: '#404D94FF',
    },
  };
}

步骤1是整个自定义TabBar系统的基础架构设计,我们首先建立了一套完整的数据模型和配置体系。这个步骤的目的是为TabBar的各种状态、样式、动画等提供类型安全的定义和可配置的参数。

步骤2:实现TabBar管理器

// TabBarManager.ets
import { hilog } from '@kit.PerformanceAnalysisKit';

export class TabBarManager {
  private static instance: TabBarManager;
  private currentTab: string = '';
  private previousTab: string = '';
  private tabs: Map<string, TabItem> = new Map();
  private tabChangeCallbacks: Array<(tabId: string) => void> = [];
  private badgeUpdateCallbacks: Array<(tabId: string, badge: number | string) => void> = [];
  private animationStateCallbacks: Array<(tabId: string, isAnimating: boolean) => void> = [];
  private theme: 'light' | 'dark' = 'light';
  private metrics: TabBarMetrics = {
    height: 0,
    width: 0,
    itemWidth: 0,
    bulgeHeight: 0,
    indentDepth: 0,
    safeAreaBottom: 0
  };

  private constructor() {
    this.initialize();
  }

  public static getInstance(): TabBarManager {
    if (!TabBarManager.instance) {
      TabBarManager.instance = new TabBarManager();
    }
    return TabBarManager.instance;
  }

  private initialize(): void {
    this.initThemeListener();
    hilog.info(0x0000, 'TabBarManager', 'TabBar管理器初始化完成');
  }

  private initThemeListener(): void {
    // 监听系统主题变化
    try {
      const context = getContext(this) as common.UIAbilityContext;
      // 这里可以添加主题监听逻辑
    } catch (error) {
      hilog.error(0x0000, 'TabBarManager', '初始化主题监听失败: ' + JSON.stringify(error));
    }
  }

  // 注册Tab
  public registerTab(tab: TabItem): void {
    this.tabs.set(tab.id, tab);
    if (!this.currentTab && this.tabs.size > 0) {
      this.currentTab = tab.id;
    }
    hilog.info(0x0000, 'TabBarManager', `注册Tab: ${tab.id} - ${tab.text}`);
  }

  // 切换Tab
  public switchTab(tabId: string, animated: boolean = true): boolean {
    if (!this.tabs.has(tabId)) {
      hilog.warn(0x0000, 'TabBarManager', `Tab不存在: ${tabId}`);
      return false;
    }

    const tab = this.tabs.get(tabId)!;
    if (tab.disabled) {
      hilog.warn(0x0000, 'TabBarManager', `Tab被禁用: ${tabId}`);
      return false;
    }

    if (tabId === this.currentTab) {
      return false;
    }

    this.previousTab = this.currentTab;
    this.currentTab = tabId;

    hilog.info(0x0000, 'TabBarManager', 
      `切换Tab: ${this.previousTab} -> ${this.currentTab}, 动画: ${animated}`);

    // 通知所有监听者
    this.tabChangeCallbacks.forEach(callback => {
      callback(tabId);
    });

    return true;
  }

  // 更新角标
  public updateBadge(tabId: string, badge: number | string): boolean {
    if (!this.tabs.has(tabId)) {
      return false;
    }

    const tab = this.tabs.get(tabId)!;
    tab.badge = badge;

    // 通知角标更新
    this.badgeUpdateCallbacks.forEach(callback => {
      callback(tabId, badge);
    });

    hilog.info(0x0000, 'TabBarManager', `更新角标: ${tabId} = ${badge}`);
    return true;
  }

  // 获取当前Tab
  public getCurrentTab(): string {
    return this.currentTab;
  }

  // 获取Tab信息
  public getTabInfo(tabId: string): TabItem | undefined {
    return this.tabs.get(tabId);
  }

  // 获取所有Tab
  public getAllTabs(): TabItem[] {
    return Array.from(this.tabs.values());
  }

  // 设置尺寸信息
  public setMetrics(metrics: TabBarMetrics): void {
    this.metrics = metrics;
  }

  // 获取尺寸信息
  public getMetrics(): TabBarMetrics {
    return this.metrics;
  }

  // 设置主题
  public setTheme(theme: 'light' | 'dark'): void {
    this.theme = theme;
    hilog.info(0x0000, 'TabBarManager', `切换主题: ${theme}`);
  }

  // 获取当前主题
  public getCurrentTheme(): 'light' | 'dark' {
    return this.theme;
  }

  // 获取主题样式
  public getThemeStyle(): TabStyle {
    const style = TabBarConfig.STYLE_CONFIG[this.theme];
    return {
      ...style,
      textSize: 14,
      iconSize: 24,
      borderRadius: 20
    };
  }

  // 注册监听器
  public onTabChange(callback: (tabId: string) => void): void {
    this.tabChangeCallbacks.push(callback);
  }

  public onBadgeUpdate(callback: (tabId: string, badge: number | string) => void): void {
    this.badgeUpdateCallbacks.push(callback);
  }

  public onAnimationStateChange(callback: (tabId: string, isAnimating: boolean) => void): void {
    this.animationStateCallbacks.push(callback);
  }

  // 通知动画状态
  public notifyAnimationState(tabId: string, isAnimating: boolean): void {
    this.animationStateCallbacks.forEach(callback => {
      callback(tabId, isAnimating);
    });
  }

  // 清理
  public destroy(): void {
    this.tabs.clear();
    this.tabChangeCallbacks = [];
    this.badgeUpdateCallbacks = [];
    this.animationStateCallbacks = [];
    hilog.info(0x0000, 'TabBarManager', 'TabBar管理器已销毁');
  }
}

步骤2实现了TabBar的核心管理器,这是一个单例模式的协调者,负责管理所有Tab的状态、协调组件间的通信、处理主题切换等全局功能。

步骤3:实现凸起TabBar组件

// BulgeTabBar.ets
[@Component](/user/Component)
export struct BulgeTabBar {
  private tabManager: TabBarManager = TabBarManager.getInstance();
  [@State](/user/State) tabs: TabItem[] = [];
  [@State](/user/State) currentTab: string = '';
  [@State](/user/State) animationStates: Map<string, boolean> = new Map();
  [@State](/user/State) bulgeAnimationValue: number = 1;
  [@State](/user/State) isThemeDark: boolean = false;
  [@Prop](/user/Prop) onTabChange?: (tabId: string) => void;
  [@Prop](/user/Prop) selectedColor?: ResourceColor;
  [@Prop](/user/Prop) normalColor?: ResourceColor;
  [@Prop](/user/Prop) backgroundColor?: ResourceColor;
  [@Prop](/user/Prop) bulgeColor?: ResourceColor;
  [@Prop](/user/Prop) height: number = TabBarConfig.BULGE_CONFIG.height;
  [@Prop](/user/Prop) bulgeHeight: number = TabBarConfig.BULGE_CONFIG.bulgeHeight;
  [@Prop](/user/Prop) showDivider: boolean = true;
  private bulgeAnimation: AnimationController = new AnimationController({ duration: 200 });

  aboutToAppear() {
    this.tabs = this.tabManager.getAllTabs();
    this.currentTab = this.tabManager.getCurrentTab();
    this.setupListeners();
    
    // 初始化动画状态
    this.tabs.forEach(tab => {
      this.animationStates.set(tab.id, false);
    });
  }

  aboutToDisappear() {
    this.bulgeAnimation.reset();
  }

  private setupListeners(): void {
    this.tabManager.onTabChange((tabId: string) => {
      this.currentTab = tabId;
    });

    this.tabManager.onBadgeUpdate((tabId: string, badge: number | string) => {
      const index = this.tabs.findIndex(tab => tab.id === tabId);
      if (index !== -1) {
        this.tabs[index].badge = badge;
        this.tabs = [...this.tabs]; // 触发更新
      }
    });
  }

  private onTabClick(tab: TabItem, index: number): void {
    if (tab.disabled) {
      return;
    }

    // 触发动画
    this.triggerTabAnimation(tab.id);

    // 中间凸起按钮特殊动画
    if (index === Math.floor(this.tabs.length / 2)) {
      this.triggerBulgeAnimation();
    }

    // 切换Tab
    if (this.tabManager.switchTab(tab.id, true)) {
      this.onTabChange?.(tab.id);
    }
  }

  private triggerTabAnimation(tabId: string): void {
    this.animationStates.set(tabId, true);
    this.animationStates = new Map(this.animationStates);
    
    this.tabManager.notifyAnimationState(tabId, true);
    
    setTimeout(() => {
      this.animationStates.set(tabId, false);
      this.animationStates = new Map(this.animationStates);
      this.tabManager.notifyAnimationState(tabId, false);
    }, TabBarConfig.ANIMATION_CONFIG.tabChange.duration);
  }

  private triggerBulgeAnimation(): void {
    this.bulgeAnimation.value = 1;
    this.bulgeAnimation.play();
  }

  [@Builder](/user/Builder)
  private buildTabItem(tab: TabItem, index: number) {
    const isActive = tab.id === this.currentTab;
    const isAnimating = this.animationStates.get(tab.id) || false;
    const isMiddleTab = index === Math.floor(this.tabs.length / 2);
    const themeStyle = this.tabManager.getThemeStyle();
    const config = TabBarConfig.BULGE_CONFIG;

    if (isMiddleTab) {
      // 中间凸起按钮
      this.buildBulgeTab(tab, isActive, isAnimating, themeStyle);
    } else {
      // 普通Tab按钮
      this.buildNormalTab(tab, isActive, isAnimating, themeStyle);
    }
  }

  [@Builder](/user/Builder)
  private buildNormalTab(tab: TabItem, isActive: boolean, isAnimating: boolean, style: TabStyle) {
    const config = TabBarConfig.BULGE_CONFIG;
    const scale = isAnimating ? config.highlightScale : 1;
    const opacity = isAnimating ? 0.8 : 1;
    const translateY = isAnimating ? -5 : 0;

    Column() {
      // 角标
      if (tab.badge !== undefined) {
        this.buildBadge(tab.badge);
      }

      // 图标
      Stack({ alignContent: Alignment.Center }) {
        Image(isActive ? tab.activeIcon : tab.icon)
          .width(config.iconSize)
          .height(config.iconSize)
          .objectFit(ImageFit.Contain)
          .interpolation(ImageInterpolation.High)
      }
      .width(config.iconSize + 20)
      .height(config.iconSize + 20)
      .borderRadius((config.iconSize + 20) / 2)
      .backgroundColor(isActive ? `${style.activeColor}20` : Color.Transparent)

      // 文本
      Text(tab.text)
        .fontSize(config.textSize)
        .fontColor(isActive ? style.activeColor : style.normalColor)
        .fontWeight(isActive ? FontWeight.Medium : FontWeight.Normal)
        .margin({ top: 4 })
        .opacity(isActive ? 1 : 0.8)
    }
    .scale({ x: scale, y: scale })
    .opacity(opacity)
    .translate({ y: translateY })
    .animation({
      duration: TabBarConfig.ANIMATION_CONFIG.tabChange.duration,
      curve: TabBarConfig.ANIMATION_CONFIG.tabChange.curve
    })
    .width('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 8, bottom: 8 })
    .onClick(() => this.onTabClick(tab, index))
  }

  [@Builder](/user/Builder)
  private buildBulgeTab(tab: TabItem, isActive: boolean, isAnimating: boolean, style: TabStyle) {
    const config = TabBarConfig.BULGE_CONFIG;
    const bulgeScale = isAnimating ? 1.1 : 1;
    const shadowOpacity = isAnimating ? 0.6 : 0.4;
    const translateY = isAnimating ? -config.bulgeHeight * 0.5 : -config.bulgeHeight * 0.3;

    Column() {
      // 凸起背景
      Circle()
        .width(config.iconSize + 40)
        .height(config.iconSize + 40)
        .fill(this.bulgeColor || style.bulgeColor)
        .shadow({
          radius: config.shadowBlur,
          color: style.bulgeShadow,
          offsetY: config.shadowOffsetY
        })

更多关于HarmonyOS鸿蒙Next开发者技术支持-自定义TabBar页签凸起和凹陷案例的实战教程也可以访问 https://www.itying.com/category-93-b0.html

3 回复

学习

更多关于HarmonyOS鸿蒙Next开发者技术支持-自定义TabBar页签凸起和凹陷案例的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


鸿蒙Next自定义TabBar页签凸起和凹陷可通过ArkUI组件实现。使用@Builder装饰器构建自定义TabBar,结合StackPositioned布局控制凸起位置。通过borderRadiusclip属性设置圆角与裁剪效果,利用offsetmargin调整凸起高度。凹陷效果可使用Column嵌套并设置内边距或背景色差实现。具体样式需在aboutToAppear生命周期中动态计算位置。

这是一个非常专业且完整的HarmonyOS Next自定义TabBar实现案例。代码结构清晰,功能完备,体现了良好的ArkTS开发实践。以下是对该实现方案的几点技术点评:

架构设计优秀:

  1. 分层清晰:通过TabBarManager单例统一管理状态、主题和通信,实现了业务逻辑与UI组件的解耦。
  2. 配置驱动TabBarConfig集中管理尺寸、动画、样式参数,提高了可维护性和主题切换能力。
  3. 类型安全:使用TypeScript接口(TabItem, TabStyle等)明确定义数据结构,减少了运行时错误。

ArkTS特性运用得当:

  1. 状态管理:正确使用@State@Prop装饰器驱动UI更新,在BulgeTabBarIndentTabBar中通过状态变化触发动画。
  2. 动画系统:合理使用AnimationController和属性动画(scaletranslate等),实现了凸起按压、凹陷深度变化等细腻的交互反馈。
  3. @Builder方法:将Tab项、角标等UI片段封装为Builder方法,提升了代码的可复用性和可读性。

视觉与交互细节到位:

  1. 凸起效果:通过Circle叠加、阴影偏移和Y轴平移实现了真实的“凸起”立体感。
  2. 凹陷效果:利用背景色差、内阴影和边框叠加,模拟出嵌入屏幕的视觉效果。
  3. 交互反馈:不仅有点击动画,IndentTabBar还实现了长按加深凹陷、触摸时深度变化的细节,增强了用户体验。

性能与工程化考虑:

  1. 资源管理:在aboutToDisappear中清理动画控制器和定时器,避免了内存泄漏。
  2. 日志记录:使用hilog在关键节点输出日志,便于调试。
  3. 无障碍支持TabItem中定义了accessibilityLabel,体现了对无障碍功能的重视。

可扩展性: 当前架构易于扩展,例如:

  • 添加新的TabBar样式(如半透明、磨砂效果)。
  • 集成更复杂的动画曲线。
  • 支持动态Tab项增减。

注意事项:

  1. TabBarManager中的initThemeListener方法目前为空实现,在实际项目中需要接入系统的主题监听服务(如appearance)。
  2. 动态切换TabBar类型(凸起/凹陷)时,确保TabBarManager中的Tab状态保持一致。
  3. 对于更复杂的场景,可以考虑将动画配置进一步抽离,支持运行时动态修改。

总的来说,这是一个生产级的自定义TabBar实现方案,涵盖了从架构设计到视觉细节的各个方面,可以直接用于HarmonyOS Next应用开发,也为开发者学习ArkUI高级组件定制提供了优秀范本。

回到顶部