Files

20 KiB
Raw Permalink Blame History

交互规范

本文档详细说明组件交互设计规范,包括交互状态、动画过渡、反馈机制和拖拽调整。

🎯 交互设计原则

  • 即时反馈 - 所有操作都应有立即的视觉反馈
  • 清晰可预测 - 用户能预期操作的结果
  • 流畅自然 - 动画和过渡平滑流畅
  • 符合直觉 - 遵循用户的使用习惯
  • 宽容错误 - 允许撤销和恢复

🖱️ 交互状态

标准交互状态

所有可交互元素都应该有以下状态:

状态 说明 视觉表现
正常Normal 默认状态 标准样式
悬停Hover 鼠标悬停 背景变化、光标变化
按下Pressed 鼠标按下 背景更暗、轻微缩放
聚焦Focused 键盘聚焦 显示聚焦环
禁用Disabled 不可用 降低透明度、灰色显示
选中Selected 被选中 强调色背景

按钮状态

主要按钮Primary Button

<Button Content="确定"
        Padding="12,6"
        Background="{DynamicResource AccentBrush}"
        Foreground="White">
  
  <Button.Styles>
    <!-- 悬停状态 -->
    <Style Selector="Button:pointerover">
      <Style.Animations>
        <Animation Duration="0:0:0.15" Easing="CubicEaseOut">
          <KeyFrame Cue="100%">
            <Setter Property="Background" 
                    Value="{DynamicResource AccentHoverBrush}"/>
          </KeyFrame>
        </Animation>
      </Style.Animations>
    </Style>
    
    <!-- 按下状态 -->
    <Style Selector="Button:pressed">
      <Style.Animations>
        <Animation Duration="0:0:0.1" Easing="CubicEaseOut">
          <KeyFrame Cue="100%">
            <Setter Property="Background" 
                    Value="{DynamicResource AccentPressedBrush}"/>
            <Setter Property="RenderTransform">
              <ScaleTransform ScaleX="0.98" ScaleY="0.98"/>
            </Setter>
          </KeyFrame>
        </Animation>
      </Style.Animations>
    </Style>
    
    <!-- 禁用状态 -->
    <Style Selector="Button:disabled">
      <Setter Property="Opacity" Value="0.5"/>
    </Style>
    
  </Button.Styles>
  
</Button>

次要按钮Secondary Button

<Button Content="取消"
        Padding="12,6"
        Background="{DynamicResource CardBackgroundSecondaryBrush}"
        Foreground="{DynamicResource TextFillColorPrimaryBrush}">
  
  <Button.Styles>
    <!-- 悬停状态 -->
    <Style Selector="Button:pointerover">
      <Setter Property="Background" Value="#EBEBEB"/>
    </Style>
    
    <!-- 按下状态 -->
    <Style Selector="Button:pressed">
      <Setter Property="Background" Value="#E0E0E0"/>
    </Style>
  </Button.Styles>
  
</Button>

图标按钮

<Button Padding="8"
        Background="Transparent"
        BorderThickness="0">
  <TextBlock Text="🔄" FontSize="16"/>
  
  <Button.Styles>
    <!-- 悬停状态 -->
    <Style Selector="Button:pointerover">
      <Setter Property="Background" 
              Value="{DynamicResource CardBackgroundSecondaryBrush}"/>
    </Style>
    
    <!-- 按下状态 -->
    <Style Selector="Button:pressed">
      <Setter Property="Background" Value="#E0E0E0"/>
      <Setter Property="RenderTransform">
        <ScaleTransform ScaleX="0.95" ScaleY="0.95"/>
      </Setter>
    </Style>
  </Button.Styles>
  
</Button>

输入框状态

<TextBox Text="{Binding InputText}"
         Watermark="请输入内容..."
         Padding="8"
         BorderBrush="{DynamicResource TextBoxBorderBrush}"
         BorderThickness="1">
  
  <TextBox.Styles>
    <!-- 聚焦状态 -->
    <Style Selector="TextBox:focus">
      <Setter Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
      <Setter Property="BorderThickness" Value="2"/>
    </Style>
    
    <!-- 错误状态 -->
    <Style Selector="TextBox.error">
      <Setter Property="BorderBrush" Value="{DynamicResource ErrorBrush}"/>
      <Setter Property="BorderThickness" Value="2"/>
    </Style>
    
    <!-- 禁用状态 -->
    <Style Selector="TextBox:disabled">
      <Setter Property="Opacity" Value="0.5"/>
      <Setter Property="Background" Value="{DynamicResource CardBackgroundSecondaryBrush}"/>
    </Style>
  </TextBox.Styles>
  
</TextBox>

光标样式

<!-- 可点击元素 -->
<Button Cursor="Hand">点击我</Button>

<!-- 文本输入 -->
<TextBox Cursor="IBeam"/>

<!-- 拖拽元素 -->
<Border Cursor="SizeAll">拖动我</Border>

<!-- 调整大小 -->
<Border Cursor="SizeNWSE">调整大小</Border>

<!-- 禁用元素 -->
<Button IsEnabled="False" Cursor="No">禁用</Button>

🎬 动画与过渡

动画时长标准

类型 时长 使用场景
微交互 100-150ms 悬停、点击
短动画 200-300ms 展开、收起
中动画 300-500ms 页面切换、弹出
长动画 500-800ms 复杂过渡

缓动函数Easing

函数 效果 使用场景
Linear 线性 加载动画、循环动画
CubicEaseOut 快进慢出 大部分交互动画
CubicEaseIn 慢进快出 元素退出
CubicEaseInOut 慢进慢出 平滑过渡
BackEaseOut 回弹效果 强调动画
ElasticEaseOut 弹性效果 有趣的交互

悬停动画

<Border Background="{DynamicResource CardBackgroundBrush}"
        CornerRadius="8"
        Padding="16">
  
  <Border.Styles>
    <Style Selector="Border:pointerover">
      <Style.Animations>
        <!-- 背景色过渡 -->
        <Animation Duration="0:0:0.15" Easing="CubicEaseOut">
          <KeyFrame Cue="100%">
            <Setter Property="Background" 
                    Value="{DynamicResource CardBackgroundSecondaryBrush}"/>
          </KeyFrame>
        </Animation>
        
        <!-- 阴影过渡 -->
        <Animation Duration="0:0:0.15" Easing="CubicEaseOut">
          <KeyFrame Cue="100%">
            <Setter Property="BoxShadow" Value="0 4 16 0 #26000000"/>
          </KeyFrame>
        </Animation>
        
        <!-- 轻微上移 -->
        <Animation Duration="0:0:0.15" Easing="CubicEaseOut">
          <KeyFrame Cue="100%">
            <Setter Property="RenderTransform">
              <TranslateTransform Y="-2"/>
            </Setter>
          </KeyFrame>
        </Animation>
      </Style.Animations>
    </Style>
  </Border.Styles>
  
</Border>

点击动画

<Button Content="点击我" Padding="12,6">
  <Button.Styles>
    <Style Selector="Button:pressed">
      <Style.Animations>
        <!-- 缩放动画 -->
        <Animation Duration="0:0:0.1" Easing="CubicEaseOut">
          <KeyFrame Cue="100%">
            <Setter Property="RenderTransform">
              <ScaleTransform ScaleX="0.95" ScaleY="0.95"/>
            </Setter>
          </KeyFrame>
        </Animation>
      </Style.Animations>
    </Style>
  </Button.Styles>
</Button>

展开/收起动画

<Expander Header="点击展开" IsExpanded="{Binding IsExpanded}">
  <Expander.ContentTransition>
    <CrossFade Duration="0:0:0.3"/>
  </Expander.ContentTransition>
  
  <Border Padding="16">
    <TextBlock Text="展开的内容" TextWrapping="Wrap"/>
  </Border>
</Expander>

淡入/淡出动画

<!-- 元素淡入 -->
<Border Opacity="0">
  <Border.Transitions>
    <Transitions>
      <DoubleTransition Property="Opacity" Duration="0:0:0.3"/>
    </Transitions>
  </Border.Transitions>
  
  <Border.Loaded>
    <EventTrigger>
      <ChangePropertyAction TargetName="Self" Property="Opacity" Value="1"/>
    </EventTrigger>
  </Border.Loaded>
</Border>

旋转动画(加载中)

<TextBlock Text="⏳" FontSize="24">
  <TextBlock.RenderTransform>
    <RotateTransform/>
  </TextBlock.RenderTransform>
  
  <TextBlock.Styles>
    <Style Selector="TextBlock">
      <Style.Animations>
        <Animation Duration="0:0:1" IterationCount="Infinite">
          <KeyFrame Cue="0%">
            <Setter Property="RenderTransform">
              <RotateTransform Angle="0"/>
            </Setter>
          </KeyFrame>
          <KeyFrame Cue="100%">
            <Setter Property="RenderTransform">
              <RotateTransform Angle="360"/>
            </Setter>
          </KeyFrame>
        </Animation>
      </Style.Animations>
    </Style>
  </TextBlock.Styles>
</TextBlock>

脉冲动画(加载中)

<Border Background="{DynamicResource AccentBrush}"
        Width="40" Height="40"
        CornerRadius="20">
  
  <Border.Styles>
    <Style Selector="Border">
      <Style.Animations>
        <Animation Duration="0:0:1.5" 
                   IterationCount="Infinite"
                   Easing="CubicEaseInOut">
          <KeyFrame Cue="0%">
            <Setter Property="Opacity" Value="1"/>
            <Setter Property="RenderTransform">
              <ScaleTransform ScaleX="1" ScaleY="1"/>
            </Setter>
          </KeyFrame>
          <KeyFrame Cue="50%">
            <Setter Property="Opacity" Value="0.5"/>
            <Setter Property="RenderTransform">
              <ScaleTransform ScaleX="0.8" ScaleY="0.8"/>
            </Setter>
          </KeyFrame>
          <KeyFrame Cue="100%">
            <Setter Property="Opacity" Value="1"/>
            <Setter Property="RenderTransform">
              <ScaleTransform ScaleX="1" ScaleY="1"/>
            </Setter>
          </KeyFrame>
        </Animation>
      </Style.Animations>
    </Style>
  </Border.Styles>
  
</Border>

💬 反馈机制

加载状态

加载指示器

<!-- 旋转加载 -->
<StackPanel Spacing="8"
            HorizontalAlignment="Center"
            VerticalAlignment="Center">
  <TextBlock Text="⏳" FontSize="32">
    <!-- 旋转动画(见上文) -->
  </TextBlock>
  <TextBlock Text="加载中..."
             FontSize="14"
             Foreground="{DynamicResource TextFillColorSecondaryBrush}"/>
</StackPanel>

<!-- 进度条 -->
<ProgressBar Value="{Binding Progress}"
             Minimum="0"
             Maximum="100"
             Height="4"
             Foreground="{DynamicResource AccentBrush}"/>

<!-- 不确定进度 -->
<ProgressBar IsIndeterminate="True"
             Height="4"
             Foreground="{DynamicResource AccentBrush}"/>

骨架屏

<StackPanel Spacing="8">
  <!-- 标题骨架 -->
  <Border Width="120" Height="20"
          Background="#F0F0F0"
          CornerRadius="4"/>
  
  <!-- 内容骨架 -->
  <Border Width="200" Height="16"
          Background="#F0F0F0"
          CornerRadius="4"/>
  <Border Width="180" Height="16"
          Background="#F0F0F0"
          CornerRadius="4"/>
  
  <!-- 添加脉冲动画 -->
  <Border.Styles>
    <Style Selector="Border">
      <Style.Animations>
        <Animation Duration="0:0:1.5" 
                   IterationCount="Infinite"
                   Easing="CubicEaseInOut">
          <KeyFrame Cue="0%">
            <Setter Property="Opacity" Value="1"/>
          </KeyFrame>
          <KeyFrame Cue="50%">
            <Setter Property="Opacity" Value="0.5"/>
          </KeyFrame>
          <KeyFrame Cue="100%">
            <Setter Property="Opacity" Value="1"/>
          </KeyFrame>
        </Animation>
      </Style.Animations>
    </Style>
  </Border.Styles>
</StackPanel>

错误状态

<Border Background="{DynamicResource CardBackgroundBrush}"
        BorderBrush="{DynamicResource ErrorBrush}"
        BorderThickness="2"
        CornerRadius="8"
        Padding="16">
  
  <StackPanel Spacing="12">
    <!-- 错误图标 -->
    <TextBlock Text="❌"
               FontSize="32"
               HorizontalAlignment="Center"/>
    
    <!-- 错误信息 -->
    <TextBlock Text="加载失败"
               FontSize="16"
               FontWeight="SemiBold"
               HorizontalAlignment="Center"
               Foreground="{DynamicResource TextFillColorPrimaryBrush}"/>
    
    <TextBlock Text="网络连接失败,请检查网络设置"
               FontSize="14"
               TextWrapping="Wrap"
               HorizontalAlignment="Center"
               Foreground="{DynamicResource TextFillColorSecondaryBrush}"/>
    
    <!-- 重试按钮 -->
    <Button Content="重试"
            Command="{Binding RetryCommand}"
            HorizontalAlignment="Center"
            Padding="16,6"
            Background="{DynamicResource AccentBrush}"
            Foreground="White"/>
  </StackPanel>
  
</Border>

空状态

<StackPanel Spacing="16"
            HorizontalAlignment="Center"
            VerticalAlignment="Center">
  
  <!-- 空状态图标 -->
  <TextBlock Text="📭"
             FontSize="48"
             HorizontalAlignment="Center"/>
  
  <!-- 空状态文字 -->
  <StackPanel Spacing="8">
    <TextBlock Text="暂无数据"
               FontSize="16"
               FontWeight="SemiBold"
               HorizontalAlignment="Center"
               Foreground="{DynamicResource TextFillColorPrimaryBrush}"/>
    
    <TextBlock Text="添加第一个项目开始使用"
               FontSize="14"
               HorizontalAlignment="Center"
               Foreground="{DynamicResource TextFillColorSecondaryBrush}"/>
  </StackPanel>
  
  <!-- 操作按钮 -->
  <Button Content="添加项目"
          Command="{Binding AddCommand}"
          Padding="16,6"
          Background="{DynamicResource AccentBrush}"
          Foreground="White"/>
  
</StackPanel>

成功反馈

<!-- 简短通知Toast -->
<Border Background="{DynamicResource SuccessBrush}"
        CornerRadius="8"
        Padding="12,8"
        BoxShadow="0 4 16 0 #26000000">
  
  <StackPanel Orientation="Horizontal" Spacing="8">
    <TextBlock Text="✅" FontSize="16"/>
    <TextBlock Text="操作成功"
               FontSize="14"
               Foreground="White"/>
  </StackPanel>
  
  <!-- 自动淡出动画 -->
  <Border.Styles>
    <Style Selector="Border">
      <Style.Animations>
        <Animation Duration="0:0:0.3" Delay="0:0:2" FillMode="Forward">
          <KeyFrame Cue="100%">
            <Setter Property="Opacity" Value="0"/>
          </KeyFrame>
        </Animation>
      </Style.Animations>
    </Style>
  </Border.Styles>
  
</Border>

提示信息Tooltip

<Button Content="🔄"
        Padding="8"
        ToolTip.Tip="刷新数据"
        ToolTip.ShowDelay="500">
  <!-- 按钮内容 -->
</Button>

<!-- 自定义 Tooltip -->
<Button Content="⚙️" Padding="8">
  <ToolTip.Tip>
    <Border Background="{DynamicResource CardBackgroundBrush}"
            BorderBrush="{DynamicResource CardBorderBrush}"
            BorderThickness="1"
            CornerRadius="4"
            Padding="8"
            BoxShadow="0 2 8 0 #1A000000">
      <StackPanel Spacing="4">
        <TextBlock Text="设置"
                   FontSize="14"
                   FontWeight="SemiBold"/>
        <TextBlock Text="打开组件设置"
                   FontSize="12"
                   Foreground="{DynamicResource TextFillColorSecondaryBrush}"/>
      </StackPanel>
    </Border>
  </ToolTip.Tip>
</Button>

🖐️ 拖拽与调整

拖拽组件

组件应支持拖拽移动:

public class DraggableComponent : ComponentBase
{
    private Point _dragStartPoint;
    private bool _isDragging;
    
    protected override void OnPointerPressed(PointerPressedEventArgs e)
    {
        _dragStartPoint = e.GetPosition(this);
        _isDragging = true;
        Cursor = new Cursor(StandardCursorType.SizeAll);
    }
    
    protected override void OnPointerMoved(PointerEventArgs e)
    {
        if (_isDragging)
        {
            var currentPosition = e.GetPosition(this.Parent as Visual);
            var offset = currentPosition - _dragStartPoint;
            
            // 更新位置
            Canvas.SetLeft(this, Canvas.GetLeft(this) + offset.X);
            Canvas.SetTop(this, Canvas.GetTop(this) + offset.Y);
        }
    }
    
    protected override void OnPointerReleased(PointerReleasedEventArgs e)
    {
        _isDragging = false;
        Cursor = new Cursor(StandardCursorType.Arrow);
        
        // 保存位置
        SavePosition();
    }
}

调整大小

组件应支持调整尺寸:

<Border Width="{Binding Width}"
        Height="{Binding Height}"
        Background="{DynamicResource CardBackgroundBrush}">
  
  <!-- 组件内容 -->
  <Grid>
    <!-- ... -->
  </Grid>
  
  <!-- 调整大小手柄 -->
  <Grid>
    <!-- 右下角手柄 -->
    <Border Width="12" Height="12"
            Background="{DynamicResource AccentBrush}"
            CornerRadius="6"
            HorizontalAlignment="Right"
            VerticalAlignment="Bottom"
            Cursor="SizeNWSE"
            PointerPressed="OnResizeHandlePressed"
            PointerMoved="OnResizeHandleMoved"
            PointerReleased="OnResizeHandleReleased"/>
  </Grid>
  
</Border>
private void OnResizeHandlePressed(object sender, PointerPressedEventArgs e)
{
    _isResizing = true;
    _resizeStartPoint = e.GetPosition(this.Parent as Visual);
    _initialWidth = Width;
    _initialHeight = Height;
}

private void OnResizeHandleMoved(object sender, PointerEventArgs e)
{
    if (_isResizing)
    {
        var currentPoint = e.GetPosition(this.Parent as Visual);
        var delta = currentPoint - _resizeStartPoint;
        
        Width = Math.Max(MinWidth, _initialWidth + delta.X);
        Height = Math.Max(MinHeight, _initialHeight + delta.Y);
    }
}

private void OnResizeHandleReleased(object sender, PointerReleasedEventArgs e)
{
    _isResizing = false;
    SaveSize();
}

拖拽反馈

<!-- 拖拽时显示阴影 -->
<Border.Styles>
  <Style Selector="Border.dragging">
    <Setter Property="BoxShadow" Value="0 8 24 0 #33000000"/>
    <Setter Property="Opacity" Value="0.8"/>
  </Style>
</Border.Styles>

⌨️ 键盘交互

快捷键

常用快捷键:

快捷键 操作
Enter 确认、提交
Esc 取消、关闭
Tab 焦点切换
Space 激活按钮
方向键 导航、选择
Ctrl+S 保存
Ctrl+Z 撤销
Ctrl+Y 重做

实现快捷键

protected override void OnKeyDown(KeyEventArgs e)
{
    switch (e.Key)
    {
        case Key.Enter:
            ConfirmAction();
            e.Handled = true;
            break;
            
        case Key.Escape:
            CancelAction();
            e.Handled = true;
            break;
            
        case Key.S when e.KeyModifiers.HasFlag(KeyModifiers.Control):
            SaveAction();
            e.Handled = true;
            break;
    }
    
    base.OnKeyDown(e);
}

焦点管理

<!-- 设置初始焦点 -->
<TextBox Name="UsernameBox"
         Text="{Binding Username}"
         Loaded="OnLoaded"/>

<!-- 代码设置焦点 -->
private void OnLoaded(object sender, RoutedEventArgs e)
{
    UsernameBox.Focus();
}

<!-- Tab 顺序 -->
<StackPanel>
  <TextBox TabIndex="1"/>
  <TextBox TabIndex="2"/>
  <Button TabIndex="3"/>
</StackPanel>

交互检查清单

发布前请检查:

状态反馈

  • 所有按钮有悬停状态
  • 所有按钮有按下状态
  • 禁用状态清晰可见
  • 加载状态有明确提示
  • 错误状态有友好说明

动画

  • 动画流畅不卡顿
  • 动画时长合适100-500ms
  • 使用合适的缓动函数
  • 不影响性能
  • 可以禁用动画

反馈

  • 操作成功有提示
  • 操作失败有说明
  • 空状态有引导
  • 加载中有指示
  • 提示信息清晰

拖拽与调整

  • 组件可拖拽移动
  • 组件可调整大小
  • 拖拽有视觉反馈
  • 调整大小有限制
  • 位置和尺寸可保存

键盘交互

  • Tab 键可切换焦点
  • Enter 键可确认操作
  • Esc 键可取消操作
  • 快捷键正常工作
  • 焦点状态清晰可见

📖 相关文档


记住: 即时反馈、流畅动画、清晰提示、直觉交互。