跳转到内容

6.2 界面管理

界面管理是构建复杂 UI 系统的关键。本节将深入讲解如何实现高效的界面管理系统,包括窗口管理、层级控制、状态管理和界面转场。


一、界面管理系统

1.1 界面管理器架构

cpp
// InterfaceManager.h
#pragma once
#include <RmlUi/Core/Context.h>
#include <RmlUi/Core/ElementDocument.h>
#include <string>
#include <vector>
#include <memory>
#include <unordered_map>

enum class InterfaceState
{
    Hidden,
    Showing,
    Visible,
    Hiding
};

struct InterfaceData
{
    std::string name;
    Rml::ElementDocument* document;
    InterfaceState state;
    bool modal;
    bool exclusive;
    int z_index;
    std::string transition_in;
    std::string transition_out;
};

class InterfaceManager : public Rml::EventListener
{
public:
    static InterfaceManager& Instance()
    {
        static InterfaceManager instance;
        return instance;
    }

    void Initialize(Rml::Context* context);
    void Shutdown();

    // 加载界面
    bool LoadInterface(const std::string& name, const std::string& path);
    
    // 显示/隐藏界面
    void ShowInterface(const std::string& name, bool modal = false, bool exclusive = false);
    void HideInterface(const std::string& name);
    
    // 切换界面
    void SwitchInterface(const std::string& from_name, const std::string& to_name);
    
    // 获取界面
    InterfaceData* GetInterface(const std::string& name);
    Rml::ElementDocument* GetDocument(const std::string& name);
    
    // 更新
    void Update(float delta_time);
    
    // 事件处理
    void ProcessEvent(Rml::Event& event) override;

private:
    InterfaceManager() = default;
    
    void UpdateZIndices();
    void PlayTransition(InterfaceData* interface, const std::string& transition);
    void OnInterfaceShown(InterfaceData* interface);
    void OnInterfaceHidden(InterfaceData* interface);

private:
    Rml::Context* context_;
    std::unordered_map<std::string, std::unique_ptr<InterfaceData>> interfaces_;
    std::vector<InterfaceData*> visible_interfaces_;
    int next_z_index_;
};

// InterfaceManager.cpp
void InterfaceManager::Initialize(Rml::Context* context)
{
    context_ = context;
    next_z_index_ = 1000;
}

void InterfaceManager::Shutdown()
{
    for (auto& pair : interfaces_)
    {
        if (pair.second->document)
        {
            pair.second->document->Close();
        }
    }
    interfaces_.clear();
    visible_interfaces_.clear();
}

bool InterfaceManager::LoadInterface(const std::string& name, const std::string& path)
{
    Rml::ElementDocument* doc = context_->LoadDocument(path);
    if (!doc)
    {
        Rml::Log::Message(Rml::Log::LT_ERROR, 
            "Failed to load interface: %s from %s", name.c_str(), path.c_str());
        return false;
    }
    
    auto data = std::make_unique<InterfaceData>();
    data->name = name;
    data->document = doc;
    data->state = InterfaceState::Hidden;
    data->modal = false;
    data->exclusive = false;
    data->z_index = next_z_index_++;
    
    interfaces_[name] = std::move(data);
    return true;
}

void InterfaceManager::ShowInterface(const std::string& name, bool modal, bool exclusive)
{
    auto it = interfaces_.find(name);
    if (it == interfaces_.end())
    {
        Rml::Log::Message(Rml::Log::LT_ERROR, "Interface not found: %s", name.c_str());
        return;
    }
    
    InterfaceData* interface = it->second.get();
    
    // 如果是独占模式,隐藏其他界面
    if (exclusive)
    {
        for (auto* other : visible_interfaces_)
        {
            if (other != interface)
            {
                HideInterface(other->name);
            }
        }
    }
    
    // 如果是模态模式,隐藏非模态界面
    if (modal)
    {
        for (auto* other : visible_interfaces_)
        {
            if (other != interface && !other->modal)
            {
                HideInterface(other->name);
            }
        }
    }
    
    // 如果已经显示,直接返回
    if (interface->state == InterfaceState::Visible)
        return;
    
    // 显示界面
    interface->state = InterfaceState::Showing;
    interface->modal = modal;
    interface->exclusive = exclusive;
    
    // 播放入场动画
    if (!interface->transition_in.empty())
    {
        PlayTransition(interface, interface->transition_in);
    }
    else
    {
        interface->document->Show();
        interface->state = InterfaceState::Visible;
        OnInterfaceShown(interface);
    }
    
    // 更新可见界面列表
    visible_interfaces_.push_back(interface);
    UpdateZIndices();
}

void InterfaceManager::HideInterface(const std::string& name)
{
    auto it = interfaces_.find(name);
    if (it == interfaces_.end())
        return;
    
    InterfaceData* interface = it->second.get();
    
    if (interface->state == InterfaceState::Hidden || 
        interface->state == InterfaceState::Hiding)
        return;
    
    // 播放出场动画
    if (!interface->transition_out.empty())
    {
        interface->state = InterfaceState::Hiding;
        PlayTransition(interface, interface->transition_out);
    }
    else
    {
        interface->document->Hide();
        interface->state = InterfaceState::Hidden;
        OnInterfaceHidden(interface);
    }
    
    // 从可见界面列表中移除
    visible_interfaces_.erase(
        std::remove(visible_interfaces_.begin(), visible_interfaces_.end(), interface),
        visible_interfaces_.end()
    );
    UpdateZIndices();
}

void InterfaceManager::SwitchInterface(const std::string& from_name, const std::string& to_name)
{
    HideInterface(from_name);
    ShowInterface(to_name);
}

void InterfaceManager::Update(float delta_time)
{
    // 更新所有可见界面
    for (auto* interface : visible_interfaces_)
    {
        if (interface->state == InterfaceState::Showing || 
            interface->state == InterfaceState::Hiding)
        {
            // 等待动画完成
        }
    }
}

void InterfaceManager::UpdateZIndices()
{
    for (size_t i = 0; i < visible_interfaces_.size(); i++)
    {
        visible_interfaces_[i]->z_index = next_z_index_ + (int)i;
        visible_interfaces_[i]->document->SetProperty(
            Rml::PropertyId::ZIndex, 
            Rml::Property(visible_interfaces_[i]->z_index)
        );
    }
}

void InterfaceManager::PlayTransition(InterfaceData* interface, const std::string& transition)
{
    // 这里可以实现转场动画
    // 例如:fade-in, slide-in, zoom-in 等
    if (transition == "fade-in")
    {
        interface->document->SetProperty(Rml::PropertyId::Opacity, Rml::Property(0.0f));
        interface->document->Show();
        
        // 使用动画淡入
        interface->document->Animate("opacity", "1.0f", 0.3f, Rml::Tween::CubicEaseOut);
        interface->state = InterfaceState::Visible;
        OnInterfaceShown(interface);
    }
    else if (transition == "slide-in")
    {
        interface->document->Show();
        interface->state = InterfaceState::Visible;
        OnInterfaceShown(interface);
    }
    else
    {
        interface->document->Show();
        interface->state = InterfaceState::Visible;
        OnInterfaceShown(interface);
    }
}

void InterfaceManager::OnInterfaceShown(InterfaceData* interface)
{
    Rml::Dictionary params;
    params["interface"] = interface->name;
    interface->document->DispatchEvent(Rml::StringId("interface_shown"), params);
}

void InterfaceManager::OnInterfaceHidden(InterfaceData* interface)
{
    Rml::Dictionary params;
    params["interface"] = interface->name;
    interface->document->DispatchEvent(Rml::StringId("interface_hidden"), params);
}

void InterfaceManager::ProcessEvent(Rml::Event& event)
{
    if (event.GetId() == Rml::StringId("hide_interface"))
    {
        std::string name = event.GetParameter<std::string>("name", "");
        HideInterface(name);
    }
    else if (event.GetId() == Rml::StringId("show_interface"))
    {
        std::string name = event.GetParameter<std::string>("name", "");
        bool modal = event.GetParameter<bool>("modal", false);
        bool exclusive = event.GetParameter<bool>("exclusive", false);
        ShowInterface(name, modal, exclusive);
    }
}

二、窗口管理

2.1 可拖拽窗口系统

cpp
// WindowManager.h
#pragma once
#include <RmlUi/Core/Element.h>
#include <RmlUi/Core/EventListener.h>
#include <vector>

class WindowData
{
public:
    Rml::Element* window;
    Rml::Element* header;
    Rml::Element* close_button;
    Rml::Element* minimize_button;
    Rml::Element* maximize_button;
    
    bool is_dragging;
    bool is_resizing;
    Rml::Vector2f drag_offset;
    Rml::Vector2f original_size;
    Rml::Vector2f resize_direction;
    
    bool is_minimized;
    bool is_maximized;
    Rml::Vector2f restore_position;
    Rml::Vector2f restore_size;
};

class WindowManager : public Rml::EventListener
{
public:
    static WindowManager& Instance()
    {
        static WindowManager instance;
        return instance;
    }

    // 创建窗口
    Rml::Element* CreateWindow(
        const std::string& title,
        float x, float y, float width, float height,
        bool closable = true,
        bool minimizable = true,
        bool maximizable = true);
    
    // 关闭窗口
    void CloseWindow(Rml::Element* window);
    
    // 最小化/最大化/恢复窗口
    void MinimizeWindow(Rml::Element* window);
    void MaximizeWindow(Rml::Element* window);
    void RestoreWindow(Rml::Element* window);
    
    // 激活窗口
    void ActivateWindow(Rml::Element* window);
    
    // 事件处理
    void ProcessEvent(Rml::Event& event) override;

private:
    WindowManager() = default;
    
    WindowData* GetWindowData(Rml::Element* window);
    void SetupWindowEvents(WindowData* data);
    void UpdateZOrder();
    
    std::vector<std::unique_ptr<WindowData>> windows_;
    WindowData* active_window_;
    WindowData* dragged_window_;
    WindowData* resized_window_;
};

// WindowManager.cpp
Rml::Element* WindowManager::CreateWindow(
    const std::string& title,
    float x, float y, float width, float height,
    bool closable, bool minimizable, bool maximizable)
{
    // 创建窗口元素
    Rml::Element* window = context_->CreateElement("div");
    window->SetClass("window");
    window->SetProperty(Rml::PropertyId::Position, Rml::Property(Rml::Style::Position::Absolute));
    window->SetProperty(Rml::PropertyId::Left, Rml::Property(x, Rml::Unit::PX));
    window->SetProperty(Rml::PropertyId::Top, Rml::Property(y, Rml::Unit::PX));
    window->SetProperty(Rml::PropertyId::Width, Rml::Property(width, Rml::Unit::PX));
    window->SetProperty(Rml::PropertyId::Height, Rml::Property(height, Rml::Unit::PX));
    
    // 创建标题栏
    Rml::Element* header = context_->CreateElement("div");
    header->SetClass("window-header");
    header->SetInnerRML(title);
    window->AppendChild(header);
    
    // 创建内容区域
    Rml::Element* content = context_->CreateElement("div");
    content->SetClass("window-content");
    window->AppendChild(content);
    
    // 创建窗口数据
    auto data = std::make_unique<WindowData>();
    data->window = window;
    data->header = header;
    data->is_dragging = false;
    data->is_resizing = false;
    data->is_minimized = false;
    data->is_maximized = false;
    
    // 设置窗口按钮
    if (closable)
    {
        data->close_button = context_->CreateElement("button");
        data->close_button->SetClass("window-close");
        data->close_button->SetInnerRML("×");
        header->AppendChild(data->close_button);
    }
    
    if (minimizable)
    {
        data->minimize_button = context_->CreateElement("button");
        data->minimize_button->SetClass("window-minimize");
        data->minimize_button->SetInnerRML("-");
        header->AppendChild(data->minimize_button);
    }
    
    if (maximizable)
    {
        data->maximize_button = context_->CreateElement("button");
        data->maximize_button->SetClass("window-maximize");
        data->maximize_button->SetInnerRML("□");
        header->AppendChild(data->maximize_button);
    }
    
    // 设置事件
    SetupWindowEvents(data.get());
    
    // 添加到窗口列表
    windows_.push_back(std::move(data));
    ActivateWindow(window);
    
    return window;
}

void WindowManager::SetupWindowEvents(WindowData* data)
{
    // 标题栏拖拽
    data->header->AddEventListener(Rml::EventId::Mousedown, this);
    
    // 按钮事件
    if (data->close_button)
        data->close_button->AddEventListener(Rml::EventId::Click, this);
    if (data->minimize_button)
        data->minimize_button->AddEventListener(Rml::EventId::Click, this);
    if (data->maximize_button)
        data->maximize_button->AddEventListener(Rml::EventId::Click, this);
    
    // 窗口激活
    data->window->AddEventListener(Rml::EventId::Focus, this);
}

void WindowManager::ProcessEvent(Rml::Event& event)
{
    if (event.GetId() == Rml::EventId::Mousedown)
    {
        Rml::Element* target = event.GetTargetElement();
        WindowData* data = GetWindowData(target);
        
        if (data && target == data->header)
        {
            // 开始拖拽
            dragged_window_ = data;
            data->is_dragging = true;
            Rml::Vector2f mouse_pos = event.GetParameter<Rml::Vector2f>("mouse_pos");
            data->drag_offset = mouse_pos - data->window->GetAbsoluteLeftTop();
            ActivateWindow(data->window);
        }
    }
    else if (event.GetId() == Rml::EventId::Mousemove)
    {
        if (dragged_window_ && dragged_window_->is_dragging)
        {
            Rml::Vector2f mouse_pos = event.GetParameter<Rml::Vector2f>("mouse_pos");
            Rml::Vector2f new_pos = mouse_pos - dragged_window_->drag_offset;
            
            dragged_window_->window->SetProperty(
                Rml::PropertyId::Left, 
                Rml::Property(new_pos.x, Rml::Unit::PX)
            );
            dragged_window_->window->SetProperty(
                Rml::PropertyId::Top, 
                Rml::Property(new_pos.y, Rml::Unit::PX)
            );
        }
    }
    else if (event.GetId() == Rml::EventId::Mouseup)
    {
        if (dragged_window_)
        {
            dragged_window_->is_dragging = false;
            dragged_window_ = nullptr;
        }
    }
    else if (event.GetId() == Rml::EventId::Click)
    {
        Rml::Element* target = event.GetTargetElement();
        WindowData* data = GetWindowData(target);
        
        if (data)
        {
            if (target == data->close_button)
                CloseWindow(data->window);
            else if (target == data->minimize_button)
                MinimizeWindow(data->window);
            else if (target == data->maximize_button)
            {
                if (data->is_maximized)
                    RestoreWindow(data->window);
                else
                    MaximizeWindow(data->window);
            }
        }
    }
    else if (event.GetId() == Rml::EventId::Focus)
    {
        Rml::Element* target = event.GetTargetElement();
        WindowData* data = GetWindowData(target);
        if (data)
            ActivateWindow(data->window);
    }
}

void WindowManager::ActivateWindow(Rml::Element* window)
{
    WindowData* data = GetWindowData(window);
    if (!data) return;
    
    active_window_ = data;
    UpdateZOrder();
}

void WindowManager::UpdateZOrder()
{
    int z_index = 1000;
    for (auto& data : windows_)
    {
        data->window->SetProperty(
            Rml::PropertyId::ZIndex, 
            Rml::Property(z_index++)
        );
    }
}

void WindowManager::CloseWindow(Rml::Element* window)
{
    auto it = std::find_if(windows_.begin(), windows_.end(),
        [window](const std::unique_ptr<WindowData>& data) {
            return data->window == window;
        });
    
    if (it != windows_.end())
    {
        windows_.erase(it);
    }
}

void WindowManager::MinimizeWindow(Rml::Element* window)
{
    WindowData* data = GetWindowData(window);
    if (!data) return;
    
    data->is_minimized = true;
    window->SetProperty(Rml::PropertyId::Visibility, Rml::Property(Rml::Style::Visibility::Hidden));
}

void WindowManager::MaximizeWindow(Rml::Element* window)
{
    WindowData* data = GetWindowData(window);
    if (!data) return;
    
    if (data->is_maximized) return;
    
    // 保存当前状态
    data->restore_position = window->GetAbsoluteLeftTop();
    data->restore_size = window->GetBox().GetSize();
    
    // 最大化
    Rml::Vector2f context_size = context_->GetDimensions();
    window->SetProperty(Rml::PropertyId::Left, Rml::Property(0.0f, Rml::Unit::PX));
    window->SetProperty(Rml::PropertyId::Top, Rml::Property(0.0f, Rml::Unit::PX));
    window->SetProperty(Rml::PropertyId::Width, Rml::Property(context_size.x, Rml::Unit::PX));
    window->SetProperty(Rml::PropertyId::Height, Rml::Property(context_size.y, Rml::Unit::PX));
    
    data->is_maximized = true;
}

void WindowManager::RestoreWindow(Rml::Element* window)
{
    WindowData* data = GetWindowData(window);
    if (!data) return;
    
    if (data->is_maximized)
    {
        // 恢复大小和位置
        window->SetProperty(Rml::PropertyId::Left, Rml::Property(data->restore_position.x, Rml::Unit::PX));
        window->SetProperty(Rml::PropertyId::Top, Rml::Property(data->restore_position.y, Rml::Unit::PX));
        window->SetProperty(Rml::PropertyId::Width, Rml::Property(data->restore_size.x, Rml::Unit::PX));
        window->SetProperty(Rml::PropertyId::Height, Rml::Property(data->restore_size.y, Rml::Unit::PX));
        
        data->is_maximized = false;
    }
    
    if (data->is_minimized)
    {
        data->is_minimized = false;
        window->SetProperty(Rml::PropertyId::Visibility, Rml::Property(Rml::Style::Visibility::Visible));
        ActivateWindow(window);
    }
}

WindowData* WindowManager::GetWindowData(Rml::Element* element)
{
    for (auto& data : windows_)
    {
        if (data->window == element || 
            data->header == element || 
            data->close_button == element ||
            data->minimize_button == element ||
            data->maximize_button == element)
        {
            return data.get();
        }
    }
    return nullptr;
}

三、界面状态管理

3.1 状态机管理

cpp
// UIStateManager.h
#pragma once
#include <RmlUi/Core/Types.h>
#include <functional>
#include <unordered_map>

enum class UIState
{
    None,
    MainMenu,
    Game,
    Pause,
    Settings,
    Loading,
    GameOver,
    Victory
};

class UIStateManager
{
public:
    using StateCallback = std::function<void(UIState, UIState)>;

    static UIStateManager& Instance()
    {
        static UIStateManager instance;
        return instance;
    }

    // 设置当前状态
    void SetState(UIState state);
    
    // 获取当前状态
    UIState GetCurrentState() const { return current_state_; }
    
    // 注册状态变化回调
    void OnStateChange(StateCallback callback);
    
    // 检查是否可以转换到指定状态
    bool CanTransitionTo(UIState state) const;

private:
    UIStateManager() = default;
    
    void ExecuteTransition(UIState from_state, UIState to_state);

private:
    UIState current_state_;
    std::vector<StateCallback> state_change_callbacks_;
    
    // 状态转换规则
    std::unordered_map<UIState, std::vector<UIState>> allowed_transitions_;
};

// UIStateManager.cpp
void UIStateManager::SetState(UIState state)
{
    if (state == current_state_)
        return;
    
    if (!CanTransitionTo(state))
    {
        Rml::Log::Message(Rml::Log::LT_WARNING, 
            "Cannot transition from state %d to %d", 
            (int)current_state_, (int)state);
        return;
    }
    
    UIState from_state = current_state_;
    current_state_ = state;
    
    // 执行转换
    ExecuteTransition(from_state, state);
    
    // 通知回调
    for (auto& callback : state_change_callbacks_)
    {
        callback(from_state, state);
    }
}

bool UIStateManager::CanTransitionTo(UIState state) const
{
    auto it = allowed_transitions_.find(current_state_);
    if (it == allowed_transitions_.end())
        return true;
    
    return std::find(it->second.begin(), it->second.end(), state) != it->second.end();
}

void UIStateManager::ExecuteTransition(UIState from_state, UIState to_state)
{
    // 这里可以执行状态转换时的特定逻辑
    switch (to_state)
    {
        case UIState::MainMenu:
            InterfaceManager::Instance().ShowInterface("main_menu");
            break;
            
        case UIState::Game:
            InterfaceManager::Instance().HideInterface("main_menu");
            InterfaceManager::Instance().ShowInterface("game_ui");
            break;
            
        case UIState::Pause:
            InterfaceManager::Instance().ShowInterface("pause_menu", true);
            break;
            
        case UIState::Settings:
            InterfaceManager::Instance().ShowInterface("settings");
            break;
            
        case UIState::GameOver:
            InterfaceManager::Instance().ShowInterface("game_over");
            break;
            
        default:
            break;
    }
}

四、界面转场动画

4.1 转场管理器

cpp
// TransitionManager.h
#pragma once
#include <RmlUi/Core/Element.h>
#include <RmlUi/Core/EventListener.h>
#include <string>

class TransitionManager : public Rml::EventListener
{
public:
    static TransitionManager& Instance()
    {
        static TransitionManager instance;
        return instance;
    }

    // 播放转场动画
    void PlayTransition(
        Rml::Element* element,
        const std::string& transition,
        float duration = 0.3f,
        std::function<void()> callback = nullptr);
    
    // 预定义转场
    void FadeIn(Rml::Element* element, float duration = 0.3f, std::function<void()> callback = nullptr);
    void FadeOut(Rml::Element* element, float duration = 0.3f, std::function<void()> callback = nullptr);
    void SlideIn(Rml::Element* element, const std::string& direction = "left", float duration = 0.3f);
    void SlideOut(Rml::Element* element, const std::string& direction = "right", float duration = 0.3f);
    void ScaleIn(Rml::Element* element, float duration = 0.3f);
    void ScaleOut(Rml::Element* element, float duration = 0.3f);

private:
    TransitionManager() = default;
    
    void OnTransitionComplete(Rml::Event& event);
};

// TransitionManager.cpp
void TransitionManager::PlayTransition(
    Rml::Element* element,
    const std::string& transition,
    float duration,
    std::function<void()> callback)
{
    if (transition == "fade-in")
    {
        FadeIn(element, duration, callback);
    }
    else if (transition == "fade-out")
    {
        FadeOut(element, duration, callback);
    }
    else if (transition == "slide-in")
    {
        SlideIn(element, "left", duration);
    }
    else if (transition == "slide-out")
    {
        SlideOut(element, "right", duration);
    }
    else if (transition == "scale-in")
    {
        ScaleIn(element, duration);
    }
    else if (transition == "scale-out")
    {
        ScaleOut(element, duration);
    }
}

void TransitionManager::FadeIn(Rml::Element* element, float duration, std::function<void()> callback)
{
    element->SetProperty(Rml::PropertyId::Opacity, Rml::Property(0.0f));
    
    // 使用 CSS 动画
    element->Animate(
        "opacity", 
        "1.0f", 
        duration, 
        Rml::Tween::CubicEaseOut,
        [callback](Rml::Element* el, const Rml::String& property, const Rml::Variant& value) {
            if (callback) callback();
        }
    );
}

void TransitionManager::FadeOut(Rml::Element* element, float duration, std::function<void()> callback)
{
    element->Animate(
        "opacity", 
        "0.0f", 
        duration, 
        Rml::Tween::CubicEaseIn,
        [callback](Rml::Element* el, const Rml::String& property, const Rml::Variant& value) {
            if (callback) callback();
        }
    );
}

void TransitionManager::SlideIn(Rml::Element* element, const std::string& direction, float duration)
{
    float offset = element->GetClientWidth();
    
    if (direction == "left")
    {
        element->SetProperty(Rml::PropertyId::Transform, 
            Rml::Property(Rml::Transform::TranslateX(-offset)));
    }
    else if (direction == "right")
    {
        element->SetProperty(Rml::PropertyId::Transform, 
            Rml::Property(Rml::Transform::TranslateX(offset)));
    }
    
    element->Animate(
        "transform", 
        "translateX(0)", 
        duration, 
        Rml::Tween::CubicEaseOut
    );
}

void TransitionManager::SlideOut(Rml::Element* element, const std::string& direction, float duration)
{
    float offset = element->GetClientWidth();
    
    if (direction == "left")
    {
        element->Animate(
            "transform", 
            "translateX(-" + std::to_string(offset) + ")", 
            duration, 
            Rml::Tween::CubicEaseIn
        );
    }
    else if (direction == "right")
    {
        element->Animate(
            "transform", 
            "translateX(" + std::to_string(offset) + ")", 
            duration, 
            Rml::Tween::CubicEaseIn
        );
    }
}

五、实战练习

练习 1:实现多标签页界面管理

创建一个支持多个标签页的界面管理系统:

  • 标签页切换
  • 标签页关闭
  • 标签页拖拽重排

练习 2:实现界面堆栈管理

创建一个界面堆栈管理器:

  • 界面推入/弹出
  • 返回按钮功能
  • 界面历史记录

练习 3:实现界面动画库

创建一个完整的界面动画库:

  • 多种预定义动画
  • 自定义动画参数
  • 动画队列管理

六、下一步

继续学习 本地化 来实现多语言支持。


📝 检查清单

基于 MIT 许可发布