# TabBar

> 标签栏，带动画指示器，内容可水平滚动并按页对齐吸附。

Source: https://crossfly.dev/zh/docs/ui/widgets/tabbar — CrossFly documentation. Full docs as one file: https://crossfly.dev/llms-full.txt

# TabBar

`TabBar` 是一个标签栏：一条按钮（tab strip）+ 一个**可水平滚动**并**按页对齐**的内容面板（通过 `PageScrollEngine`）。触摸某个标签会将内容滚动到对应页面；**拖动**内容会切换活动标签。一个**动画指示器**跟随活动标签。

美学效果（指示器、标签在顶部或底部、颜色）来自活动的 `Skin`（`TabBarStyle`）。每个标签由一个 `TabBar.Tab`（文本、图标或两者）描述。

> 🟡 · **包：** `dev.crossfly.ui` · **继承：** `Component` · **样式：** `TabBarStyle`（通过 `getSkin().tabBar()`）

## 何时使用
- 在一个屏幕的 2–5 个区块之间导航（首页 / 搜索 / 个人资料）。
- 分页内容，配合页面之间的滑动手势 + 同步的标签。

## 何时不使用
- 放不下的过多区块（strip 会滚动，但可读性下降）→ 考虑其他导航方式。

## 外观
等宽的标签条（`1/n`）；如果它们放不下，strip 会变为可水平滚动，标签则改为自动宽度。指示器在活动标签下方（如果 `tabsAtBottom` 则在上方）滑动。内容面板一次显示一个页面，带对齐。

## 完整示例（复制、粘贴并运行）
```java
package com.exemplo;

import dev.crossfly.ui.*;
import dev.crossfly.ui.layout.*;

/** 可执行的主界面。将 launcher 指向此类（classname）并运行：gradlew :lwjgl3:run */
public class MainWindow extends Window
{
   @Override
   public void onShow() throws Exception
   {
      super.onShow();
      setLayout(new VerticalLayout());

      // 每个标签页一个内容 view（数量必须与标签页数量一致）。
      Label resumo    = new Label("Conteúdo do Resumo");
      Label detalhes  = new Label("Conteúdo dos Detalhes");
      Label historico = new Label("Conteúdo do Histórico");

      TabBar tabs = new TabBar(
         new TabBar.Tab("Resumo"),
         new TabBar.Tab("Detalhes"),
         new TabBar.Tab("Histórico"));
      tabs.setContent(resumo, detalhes, historico);
      tabs.onTabChanged(i -> System.out.println("aba " + i));

      add(LAY_FILL, tabs);   // TabBar 吸收剩余的高度
   }
}
```

> 下面的片段**只展示核心部分**（放入 `onShow()` 内的内容）。要运行它们，请将它们放入上面的 `MainWindow` 骨架中。

## 基本用法
```java
TabBar tabs = new TabBar(
   new TabBar.Tab("Home",   new MaterialIcon("")),
   new TabBar.Tab("Buscar", new MaterialIcon("")),
   new TabBar.Tab("Perfil", new MaterialIcon("")));
tabs.setContent(homeView, searchView, profileView);
tabs.onTabChanged(i -> System.out.println("aba " + i));
add(LAY_FILL, tabs);
```

## 构造函数
| 构造函数 | 描述 |
|---|---|
| `TabBar(Tab... tabs)` | 用这些标签创建栏（`activeIndex = 0`） |

### `TabBar.Tab`（标签描述符）
| 构造函数 | 结果 |
|---|---|
| `Tab(String text)` | 仅文本（`TextPos.NONE`） |
| `Tab(Icon icon)` | 仅图标（`TextPos.NONE`） |
| `Tab(String text, Icon icon)` | 文本 + 图标（`TextPos.TEXT_AT_BOTTOM`） |
| `Tab(String text, Icon icon, TextPos textPos)` | 带自定义位置的文本 + 图标 |

## 属性与自定义
| 属性 / 方法 | 类型 | 作用 |
|---|---|---|
| `setContent(Component...)` | `TabBar` | 各标签的内容（数量**必须**与标签匹配，否则抛出 `IllegalArgumentException`） |
| `setActiveTab(int)` | `TabBar` | 以编程方式激活一个标签（带动画/滚动）；若无效或已激活则为 no-op |
| `getActiveIndex()` | `int` | 活动标签的索引 |
| `getTabs()` | `Tab[]` | 标签的描述符 |
| `getStrip()` / `getTabButtons()` / `getContentPanel()` | — | 访问内部组件 |
| `getIndicatorX()` / `getIndicatorW()` | `float` | 指示器的当前位置/宽度（用于在 `Design` 中绘制） |

## 回调与事件
| 回调 | 何时触发 |
|---|---|
| `onTabChanged(TabChangedListener)` | 切换标签时，通过触摸、滑动或 `setActiveTab(...)` |

`TabChangedListener` 是函数式的：`void onTabChanged(int index)`。

## 状态
- **活动** —— 一次只有一个标签；指示器动画到它（持续时间 `TabBarStyle.switchDuration`）。
- **用户滑动** —— 指示器在相邻标签之间连续插值；跨越到另一页面时，`activeIndex` 切换并触发 `onTabChanged`。
- **编程式** —— `setActiveTab`/触摸会锁定同步，直到滚动动画到达目标。

### 仅文本的标签
```java
TabBar tabs = new TabBar(
   new TabBar.Tab("Resumo"),
   new TabBar.Tab("Detalhes"),
   new TabBar.Tab("Histórico"));
tabs.setContent(resumoView, detalhesView, historicoView);
add(LAY_FILL, tabs);
```

### 编程式切换标签
```java
TabBar tabs = new TabBar(new TabBar.Tab("A"), new TabBar.Tab("B"), new TabBar.Tab("C"));
tabs.setContent(viewA, viewB, viewC);
add(LAY_FILL, tabs);
// ... 稍后：
tabs.setActiveTab(2);  // 动画切换到标签页 C
```

## 最佳实践
- `setContent(...)` 的数量**必须**与标签的数量吻合。
- 对于底部标签（iOS 风格），使用一个 `TabBarStyle.tabsAtBottom = true` 的 `Skin` —— 布局和指示器会自行调整。
- 不要强制标签固定宽度：如果它们放不下，strip 会自行滚动。

## 常见错误
- **`setContent` 数量错误** → `IllegalArgumentException`。每个标签正好传入一个 view。
- **期待 `setActiveTab` 对已激活的索引触发 `onTabChanged`** —— 当 `index == activeIndex` 时它是 no-op。

## 另见
- 领域指南：[`ui/`](/docs/ui) · 食谱：[`cookbook/`](/docs/cookbook)
