Halide:专业控制,易于使用

Halide为何赢得2022年Apple Design Award:基于手势的相机控制、智能激活、胶片风格UX。包含SwiftUI实现模式。

5 分钟阅读 170 字
Halide:专业控制,易于使用 screenshot

Halide:让专业控制触手可及

"复杂性依然存在——只是不会让你感到不知所措。当你能以一种相对易于接受的方式进入这个世界时,它可以点燃热情而非带来恐惧。"

Halide 证明了专业工具不需要专业界面。作为 2022 年 Apple 设计大奖得主,它将单反级别的控制隐藏在直观的手势背后,只有当你需要时才会展现其强大功能。


为何 Halide 如此重要

Halide 由前 Apple 设计师 Sebastiaan de With 和前 Twitter 工程师 Ben Sandofsky 共同创建,它成功地在 Apple 简单的相机应用与令人生畏的专业应用之间架起了一座桥梁。

主要成就: - 2022 年 Apple 设计大奖 - 通过 Instant RAW 让 RAW 摄影变得触手可及 - 开创了基于手势的相机控制方式 - 在所有 iPhone 尺寸上实现单手操作 - 超过 10,000 条五星评价


核心要点

  1. 隐藏复杂性,而非移除它 - 专业功能存在但保持隐形,直到被召唤;"飞行模拟器"式界面并非展现强大功能的唯一方式
  2. 智能激活优于开关按钮 - 在交互时出现的工具(如拖动时出现的对焦放大镜)比手动显示/隐藏控件更直观
  3. 手势应有物理感 - 垂直滑动调整曝光、水平滑动调整对焦,直接映射真实相机转盘的操作行为
  4. 在使用中学习 - 状态变化时的简短标签帮助用户自然地学习摄影术语,无需单独的教程
  5. 单手操作是一种设计约束 - 为拇指可达范围而设计,迫使形成良好的信息层级并优先考虑核心控件

核心设计理念

"不要挡道"

Halide 的指导原则:工具应在需要时出现,不需要时消失。

OTHER PRO CAMERA APPS                 HALIDE'S APPROACH
───────────────────────────────────────────────────────────────────
"飞行模拟器"式界面                    简洁的取景器
所有控件始终可见                      控件按需出现
使用前先学界面                        边用边学
固定布局                              可自定义工具栏
仅手动工作流                          自动模式+手动模式兼备

关键洞察:"其他相机应用看起来像飞行模拟器,布满了各种转盘,这让人望而生畏,即使对于像我这样热爱胶片相机的人也是如此。"

胶片相机的启发

Halide 将模拟相机的触觉乐趣转化为触屏手势。

FILM CAMERA                          HALIDE TRANSLATION
───────────────────────────────────────────────────────────────────
转动光圈环                            垂直滑动调整曝光
转动对焦环                            水平滑动调整对焦
机械卡位感                            触觉反馈
物理取景器                            全屏构图
测光表指针                            数字直方图
手动/自动切换                         AF 按钮切换

设计目标:"给孩子一台相机,他们会把玩光圈环、转盘和开关。也许我们可以在这块玻璃上的应用中带来一丝那样的愉悦感。"


模式库

1. 智能激活

控件在需要时根据上下文出现,然后消失。无需手动显示/隐藏。

示例:对焦放大镜

DEFAULT STATE
┌─────────────────────────────────────────────┐
│                                             │
│              [取景器]                        │
│                                             │
│                                             │
│                                             │
│  [底部对焦转盘]                              │
└─────────────────────────────────────────────┘

WHEN USER TOUCHES FOCUS DIAL
┌─────────────────────────────────────────────┐
│  ┌──────────┐                               │
│  │ 对焦      │  ← 对焦放大镜自动出现          │
│  │ 放大镜   │                                │
│  │ (放大)   │                                │
│  └──────────┘                               │
│                                             │
│  [对焦转盘激活]                              │
└─────────────────────────────────────────────┘

WHEN USER RELEASES
┌─────────────────────────────────────────────┐
│                                             │
│              [取景器]                        │
│                   ↑                         │
│           放大镜淡出                         │
│                                             │
│  [对焦转盘静止]                              │
└─────────────────────────────────────────────┘

SwiftUI 实现概念:

struct IntelligentActivation<Content: View, Tool: View>: View {
    @Binding var isInteracting: Bool
    let content: Content
    let tool: Tool

    var body: some View {
        ZStack {
            content

            tool
                .opacity(isInteracting ? 1 : 0)
                .animation(.easeInOut(duration: 0.2), value: isInteracting)
        }
    }
}

struct FocusControl: View {
    @State private var isDragging = false
    @State private var focusValue: Double = 0.5

    var body: some View {
        ZStack {
            // 取景器
            CameraPreview()

            // 对焦放大镜 - 拖动时出现
            IntelligentActivation(isInteracting: $isDragging, content: EmptyView()) {
                FocusLoupeView(zoomLevel: 3.0)
                    .frame(width: 120, height: 120)
                    .position(x: 80, y: 80)
            }

            // 对焦转盘
            VStack {
                Spacer()
                FocusDial(value: $focusValue)
                    .gesture(
                        DragGesture()
                            .onChanged { _ in
                                isDragging = true
                            }
                            .onEnded { _ in
                                isDragging = false
                            }
                    )
            }
        }
    }
}

2. 渐进式展示

默认简洁,需要时复杂。界面根据模式进行变换。

模式转换:

AUTO MODE(默认)
┌─────────────────────────────────────────────┐
│                                             │
│              [取景器]                        │
│                                             │
│                                             │
│ ┌───┐                               ┌───┐   │
│ │ [F] │                               │ AF │   │
│ └───┘                               └───┘   │
│              ┌───────────┐                  │
│              │   (●)     │  ← 快门          │
│              └───────────┘                  │
└─────────────────────────────────────────────┘
极简控件。拍就对了。

MANUAL MODE(点击 "AF" → "MF" 后)
┌─────────────────────────────────────────────┐
│  [直方图]                        [对焦峰值]  │
│                                             │
│              [取景器]                        │
│                                             │
│ ┌───┐ ┌───┐ ┌───┐              ┌────┐      │
│ │ [F] │ │WB │ │ISO│              │ MF │      │
│ └───┘ └───┘ └───┘              └────┘      │
│              ┌───────────┐                  │
│  [────────────────●───────] ← 对焦转盘      │
│              │   (●)     │                  │
│              └───────────┘                  │
└─────────────────────────────────────────────┘
完整控件展现。专业工具触手可及。

实现方式:

enum CameraMode {
    case auto
    case manual

    var visibleControls: [CameraControl] {
        switch self {
        case .auto:
            return [.flash, .shutter, .autoFocus]
        case .manual:
            return [.flash, .whiteBalance, .iso, .shutter,
                    .manualFocus, .histogram, .focusPeaking]
        }
    }
}

struct AdaptiveToolbar: View {
    @Binding var mode: CameraMode

    var body: some View {
        HStack {
            ForEach(mode.visibleControls, id: \.self) { control in
                ControlButton(control: control)
                    .transition(.asymmetric(
                        insertion: .scale.combined(with: .opacity),
                        removal: .scale.combined(with: .opacity)
                    ))
            }
        }
        .animation(.spring(response: 0.3), value: mode)
    }
}

3. 基于手势的控制

曝光和对焦通过滑动操作控制,类似于实体相机的拨盘。

EXPOSURE CONTROL (Vertical swipe anywhere)
           ↑ Swipe up = brighter
           │
    ───────┼─────── Current exposure
           │
           ↓ Swipe down = darker

FOCUS CONTROL (Horizontal swipe on dial)
    Near ←────────●────────→ Far
                  │
            Current focus

QUICK ACTIONS (Edge swipes)
    ← Left edge: Switch to photo library
    → Right edge: Open manual controls panel

实现:

struct GestureCamera: View {
    @State private var exposure: Double = 0
    @State private var focus: Double = 0.5

    var body: some View {
        ZStack {
            CameraPreview()

            // Exposure gesture (anywhere on screen)
            Color.clear
                .contentShape(Rectangle())
                .gesture(
                    DragGesture()
                        .onChanged { value in
                            // Vertical translation maps to exposure
                            let delta = -value.translation.height / 500
                            exposure = max(-2, min(2, exposure + delta))
                            HapticsEngine.impact(.light)
                        }
                )

            // Visual feedback
            VStack {
                Spacer()
                ExposureIndicator(value: exposure)
                    .opacity(exposure != 0 ? 1 : 0)
            }
        }
    }
}

struct ExposureIndicator: View {
    let value: Double

    var body: some View {
        HStack {
            Image(systemName: value > 0 ? "sun.max.fill" : "moon.fill")
            Text(String(format: "%+.1f", value))
                .monospacedDigit()
        }
        .font(.caption)
        .padding(8)
        .background(.ultraThinMaterial)
        .clipShape(Capsule())
    }
}

4. 教育性微文案

当用户与控件交互时,简短的标签会帮助他们学习相关术语。

USER TAPS "AF" → Switches to "MF"
┌────────────────────────────────┐
│  Manual Focus                  │  ← Appears briefly
│  Swipe to adjust distance      │
└────────────────────────────────┘
   Fades after 2 seconds

USER ENABLES FOCUS PEAKING
┌────────────────────────────────┐
│  Focus Peaking                 │
│  Red highlights = in focus     │
└────────────────────────────────┘

实现:

struct EducationalToast: View {
    let title: String
    let description: String
    @Binding var isVisible: Bool

    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text(title)
                .font(.headline)
            Text(description)
                .font(.caption)
                .foregroundStyle(.secondary)
        }
        .padding()
        .background(.ultraThinMaterial)
        .clipShape(RoundedRectangle(cornerRadius: 12))
        .opacity(isVisible ? 1 : 0)
        .onAppear {
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                withAnimation { isVisible = false }
            }
        }
    }
}

// Usage
struct FocusModeToggle: View {
    @State private var showToast = false
    @Binding var isManual: Bool

    var body: some View {
        Button(isManual ? "MF" : "AF") {
            isManual.toggle()
            showToast = true
        }
        .overlay(alignment: .top) {
            EducationalToast(
                title: isManual ? "Manual Focus" : "Auto Focus",
                description: isManual
                    ? "Swipe to adjust distance"
                    : "Tap to focus on subject",
                isVisible: $showToast
            )
            .offset(y: -60)
        }
    }
}

5. 专业工具(直方图、焦点峰值、斑马纹)

提供专业的可视化工具,但不会让界面显得过于复杂。

直方图选项:

放置选项
┌─────────────────────────────────────────────┐
│ [▁▂▃▅▇█▅▃▁]                                 │  ← 顶部角落(简洁模式)
│                                             │
│              [取景器]                        │
│                                             │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│              [取景器]                        │
│                                             │
│   ┌─────────────────────────────────────┐   │
│   │ ▁▂▃▅▇██▅▃▂▁ RGB                     │   │  ← 底部(详细模式)
│   └─────────────────────────────────────┘   │
└─────────────────────────────────────────────┘

直方图类型(点击切换)
1. 亮度(单色)
2. RGB(颜色通道)
3. 波形图(垂直分布)

焦点峰值叠加层:

struct FocusPeakingOverlay: View {
    let enabled: Bool
    let peakingColor: Color = .red

    var body: some View {
        if enabled {
            // 生产环境中会使用 Metal 着色器
            // 高亮显示焦平面处的边缘
            GeometryReader { _ in
                // 高亮显示对焦区域的叠加层
                // 基于当前对焦距离的边缘检测
            }
            .blendMode(.plusLighter)
        }
    }
}

struct ZebraOverlay: View {
    let threshold: Double  // 0.0 到 1.0
    let pattern: ZebraPattern = .diagonal

    enum ZebraPattern {
        case diagonal
        case horizontal
    }

    var body: some View {
        // 在过曝区域显示条纹叠加层
        // 帮助在拍摄前识别高光溢出
    }
}

6. 可自定义工具栏

用户可以根据自己的工作流程重新排列工具。

默认工具栏
[闪光灯] [网格] [定时器] ─(●)─ [RAW] [AF] [设置]

长按自定义
┌─────────────────────────────────────────────┐
│  拖动以重新排序                              │
│                                             │
│  [闪光灯]   [网格]   [定时器]                │
│     ↕         ↕         ↕                   │
│  [RAW]    [微距]  [设置]                     │
│                                             │
│  已隐藏:                                    │
│  [斑马纹] [波形图] [景深]                    │
│                                             │
│  [恢复默认]                       [完成]     │
└─────────────────────────────────────────────┘

实现:

struct CustomizableToolbar: View {
    @State private var tools: [CameraTool] = CameraTool.defaults
    @State private var isEditing = false

    var body: some View {
        HStack {
            ForEach(tools) { tool in
                ToolButton(tool: tool)
                    .draggable(tool) // iOS 16+
            }
        }
        .onLongPressGesture {
            isEditing = true
        }
        .sheet(isPresented: $isEditing) {
            ToolbarEditor(tools: $tools)
        }
    }
}

struct ToolbarEditor: View {
    @Binding var tools: [CameraTool]

    var body: some View {
        NavigationStack {
            List {
                Section("Active Tools") {
                    ForEach($tools) { $tool in
                        ToolRow(tool: tool)
                    }
                    .onMove { from, to in
                        tools.move(fromOffsets: from, toOffset: to)
                    }
                }

                Section("Available Tools") {
                    ForEach(CameraTool.hidden(from: tools)) { tool in
                        ToolRow(tool: tool)
                            .onTapGesture {
                                tools.append(tool)
                            }
                    }
                }
            }
            .navigationTitle("Customize Toolbar")
            .toolbar {
                Button("Done") { /* dismiss */ }
            }
        }
    }
}

视觉设计系统

色彩方案

extension Color {
    // Halide 在 UI 装饰元素中使用极简配色
    static let halideBlack = Color(hex: "#000000")
    static let halideWhite = Color(hex: "#FFFFFF")
    static let halideGray = Color(hex: "#8E8E93")

    // 激活状态的强调色
    static let halideYellow = Color(hex: "#FFD60A")  // 激活指示器

    // 对焦峰值
    static let halideFocusPeak = Color(hex: "#FF3B30")  // 红色叠加层
    static let halideZebra = Color(hex: "#FFFFFF").opacity(0.5)
}

字体排版

struct HalideTypography {
    // UI 标签(小字、大写)
    static let controlLabel = Font.system(size: 10, weight: .bold, design: .rounded)
        .smallCaps()

    // 数值显示(数字使用等宽字体)
    static let valueDisplay = Font.system(size: 14, weight: .medium, design: .monospaced)

    // 教学提示文字
    static let tooltipTitle = Font.system(size: 14, weight: .semibold)
    static let tooltipBody = Font.system(size: 12, weight: .regular)
}

动画与触觉反馈

触觉反馈系统

struct HapticsEngine {
    static func impact(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
        let generator = UIImpactFeedbackGenerator(style: style)
        generator.impactOccurred()
    }

    static func exposureChange() {
        // 曝光调节时使用轻触感
        impact(.light)
    }

    static func focusLock() {
        // 对焦锁定时使用中等触感
        impact(.medium)
    }

    static func shutterPress() {
        // 快门按下时使用强触感
        impact(.heavy)
    }

    static func modeSwitch() {
        // 模式切换时使用选择反馈
        let generator = UISelectionFeedbackGenerator()
        generator.selectionChanged()
    }
}

控件动画

// 转盘旋转动画
struct FocusDial: View {
    @Binding var value: Double

    var body: some View {
        GeometryReader { geo in
            // Dial visual with tick marks
            Circle()
                .stroke(Color.white.opacity(0.3), lineWidth: 2)
                .overlay {
                    // Tick marks rotate with value
                    ForEach(0..<12) { i in
                        Rectangle()
                            .fill(Color.white)
                            .frame(width: 2, height: 10)
                            .offset(y: -geo.size.height / 2 + 15)
                            .rotationEffect(.degrees(Double(i) * 30))
                    }
                }
                .rotationEffect(.degrees(value * 360))
                .animation(.spring(response: 0.2, dampingFraction: 0.8), value: value)
        }
    }
}

对我们工作的启示

1. 隐藏复杂性,而非移除它

专业功能存在但保持隐藏,直到被召唤时才显现。

2. 智能激活优于开关按钮

交互时自动出现的工具(如拖动时显示的对焦放大镜)胜过手动显示/隐藏。

3. 手势应具有物理质感

垂直滑动调节曝光,水平滑动调节对焦——这与真实相机转盘的操作方式相映射。

4. 在使用中教学

状态变化时的简短标签帮助用户自然地学习专业术语。

5. 单手操作是一项设计约束

为拇指可及范围而设计,迫使设计者建立良好的信息层级。


常见问题

Halide 与其他专业相机应用有何不同?

大多数专业相机应用会同时显示所有控件,形成 Halide 创作者所称的"飞行模拟器"界面。Halide 从简洁的取景器开始,仅在需要时才显示控件。自动模式显示最少的 UI;切换到手动模式时会逐步显示直方图、对焦峰值、ISO 和白平衡控件。这种方法使单反级别的功能变得易于使用,同时不会让新用户感到不知所措。

什么是智能激活,为什么它很重要?

智能激活意味着当你与相关控件交互时,工具会自动出现,停止交互时则消失。例如,当你触摸对焦转盘时对焦放大镜会出现,松开时则淡出。这消除了显示/隐藏辅助视图的开关按钮需求,减少了视觉杂乱和认知负担,同时确保工具在需要时始终可用。

Halide 的手势控制如何工作?

Halide 将触摸屏手势映射到物理相机控件。在屏幕任意位置垂直滑动可调节曝光(如同光圈环),在对焦转盘上水平滑动可调节对焦距离(如同对焦环)。这些手势包含触觉反馈,模拟真实相机转盘的机械档位感。边缘滑动可快速访问照片库和手动控制面板。

Halide 包含哪些专业可视化工具?

Halide 提供直方图(亮度、RGB 或波形模式)、对焦峰值(在对焦边缘显示红色高亮)和斑马纹(在过曝区域显示斜线)。这些工具以叠加层形式显示在取景器上,可放置在不同位置。每个工具都是可选的,可从可自定义的工具栏访问,用户可以重新排列以匹配自己的工作流程。

为什么 Halide 在界面中使用胶片相机的隐喻?

Halide 的设计师观察到,孩子们会本能地把玩模拟相机上的光圈环、转盘和开关。触摸屏应用缺少这种触觉乐趣。通过将机械相机控件转化为带有触觉反馈的滑动手势,Halide 重新创造了部分物理感。这种方法还利用了数十年相机用户体验的演进,使用熟悉的心智模型,而非发明新的范式。