如何处理滑动与点击手势的冲突?

如何处理滑动与点击手势的冲突?

在同一组件上同时监听点击(TapGesture)和滑动(SwipeGesture)时,轻微滑动会误触发点击事件。例如,设计一个可左右滑动切换页面的组件,同时每个页面内有可点击的元素。如何精确区分滑动和点击意图,避免冲突?

阅读 322
1 个回答
import SwiftUI

// MARK: - 手势管理器
class GestureManager: ObservableObject {
    @Published var isSwipeGestureActive = false
    @Published var isTapGestureEnabled = true
    
    private let swipeThreshold: CGFloat = 20
    private let velocityThreshold: CGFloat = 300
    
    func evaluateGesture(translation: CGSize, velocity: CGSize) -> GestureType {
        let horizontalMovement = abs(translation.x)
        let verticalMovement = abs(translation.y)
        let horizontalVelocity = abs(velocity.x)
        
        // 判断是否为滑动手势
        if horizontalMovement > swipeThreshold || horizontalVelocity > velocityThreshold {
            if horizontalMovement > verticalMovement {
                return .swipe
            }
        }
        
        // 判断移动距离是否在点击范围内
        let totalDistance = sqrt(pow(translation.x, 2) + pow(translation.y, 2))
        if totalDistance <= 10 {
            return .tap
        }
        
        return .unknown
    }
}

enum GestureType {
    case tap
    case swipe
    case unknown
}

// MARK: - 主视图
struct AdvancedGestureView: View {
    @StateObject private var gestureManager = GestureManager()
    @State private var currentPage = 0
    @State private var dragOffset: CGFloat = 0
    
    var body: some View {
        NavigationView {
            GeometryReader { geometry in
                HStack(spacing: 0) {
                    ForEach(0..<4) { index in
                        AdvancedPageView(
                            pageIndex: index,
                            gestureManager: gestureManager
                        )
                        .frame(width: geometry.size.width)
                    }
                }
                .offset(x: -CGFloat(currentPage) * geometry.size.width + dragOffset)
                .gesture(
                    DragGesture()
                        .onChanged { value in
                            gestureManager.isSwipeGestureActive = true
                            gestureManager.isTapGestureEnabled = false
                            dragOffset = value.translation.x
                        }
                        .onEnded { value in
                            let gestureType = gestureManager.evaluateGesture(
                                translation: value.translation,
                                velocity: value.velocity
                            )
                            
                            switch gestureType {
                            case .swipe:
                                handleSwipe(value: value, screenWidth: geometry.size.width)
                            case .tap, .unknown:
                                // 重置拖拽偏移
                                withAnimation(.easeOut(duration: 0.2)) {
                                    dragOffset = 0
                                }
                            }
                            
                            // 重置手势状态
                            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                                gestureManager.isSwipeGestureActive = false
                                gestureManager.isTapGestureEnabled = true
                            }
                        }
                )
            }
            .navigationTitle("高级手势处理")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
    
    private func handleSwipe(value: DragGesture.Value, screenWidth: CGFloat) {
        let threshold: CGFloat = screenWidth * 0.2
        let velocity = value.velocity.x
        
        withAnimation(.easeOut(duration: 0.3)) {
            if value.translation.x > threshold || velocity > 500 {
                currentPage = max(0, currentPage - 1)
            } else if value.translation.x < -threshold || velocity < -500 {
                currentPage = min(3, currentPage + 1)
            }
            dragOffset = 0
        }
    }
}

// MARK: - 高级页面视图
struct AdvancedPageView: View {
    let pageIndex: Int
    @ObservedObject var gestureManager: GestureManager
    
    var body: some View {
        ScrollView {
            VStack(spacing: 25) {
                HeaderView(pageIndex: pageIndex)
                
                CardGridView(gestureManager: gestureManager)
                
                InteractiveButtonsView(gestureManager: gestureManager)
                
                StatusView(gestureManager: gestureManager)
            }
            .padding()
        }
        .background(
            LinearGradient(
                gradient: Gradient(colors: [
                    Color(hue: Double(pageIndex) * 0.25, saturation: 0.3, brightness: 0.95),
                    Color(hue: Double(pageIndex) * 0.25, saturation: 0.2, brightness: 0.98)
                ]),
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            )
        )
    }
}

struct HeaderView: View {
    let pageIndex: Int
    
    var body: some View {
        VStack {
            Text("页面 \(pageIndex + 1)")
                .font(.largeTitle)
                .fontWeight(.bold)
            
            Text("左右滑动切换页面")
                .font(.caption)
                .foregroundColor(.secondary)
        }
        .padding()
    }
}

struct CardGridView: View {
    @ObservedObject var gestureManager: GestureManager
    
    let cards = [
        ("⭐️", "收藏", Color.yellow),
        ("❤️", "喜欢", Color.red),
        ("📚", "阅读", Color.blue),
        ("🎵", "音乐", Color.purple)
    ]
    
    var body: some View {
        LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 15) {
            ForEach(Array(cards.enumerated()), id: \.offset) { index, card in
                SmartCardView(
                    icon: card.0,
                    title: card.1,
                    color: card.2,
                    gestureManager: gestureManager
                ) {
                    print("\(card.1) 卡片被点击")
                }
            }
        }
    }
}

struct SmartCardView: View {
    let icon: String
    let title: String
    let color: Color
    @ObservedObject var gestureManager: GestureManager
    let action: () -> Void
    
    @State private var isPressed = false
    @State private var dragStartTime: Date = Date()
    @State private var dragStartLocation: CGPoint = .zero
    
    var body: some View {
        VStack(spacing: 8) {
            Text(icon)
                .font(.system(size: 30))
            
            Text(title)
                .font(.headline)
                .foregroundColor(.primary)
        }
        .frame(height: 80)
        .frame(maxWidth: .infinity)
        .background(color.opacity(0.2))
        .cornerRadius(12)
        .overlay(
            RoundedRectangle(cornerRadius: 12)
                .stroke(color.opacity(0.3), lineWidth: 1)
        )
        .scaleEffect(isPressed ? 0.95 : 1.0)
        .animation(.easeInOut(duration: 0.1), value: isPressed)
        .simultaneousGesture(
            DragGesture(minimumDistance: 0)
                .onChanged { value in
                    if !gestureManager.isSwipeGestureActive && !isPressed {
                        isPressed = true
                        dragStartTime = Date()
                        dragStartLocation = value.startLocation
                    }
                }
                .onEnded { value in
                    if isPressed {
                        isPressed = false
                        
                        // 只有在非滑动状态下才处理点击
                        if !gestureManager.isSwipeGestureActive {
                            let distance = sqrt(
                                pow(value.location.x - dragStartLocation.x, 2) +
                                pow(value.location.y - dragStartLocation.y, 2)
                            )
                            let duration = Date().timeIntervalSince(dragStartTime)
                            
                            if distance <= 15 && duration <= 0.5 {
                                action()
                            }
                        }
                    }
                }
        )
    }
}

struct InteractiveButtonsView: View {
    @ObservedObject var gestureManager: GestureManager
    
    var body: some View {
        VStack(spacing: 15) {
            Text("交互按钮区域")
                .font(.headline)
                .padding(.bottom, 5)
            
            HStack(spacing: 15) {
                PrecisionButton(title: "确认", color: .green, gestureManager: gestureManager) {
                    print("确认按钮被点击")
                }
                
                PrecisionButton(title: "取消", color: .red, gestureManager: gestureManager) {
                    print("取消按钮被点击")
                }
            }
            
            PrecisionButton(title: "详细设置", color: .blue, gestureManager: gestureManager) {
                print("详细设置被点击")
            }
        }
        .padding()
        .background(Color(UIColor.systemBackground))
        .cornerRadius(15)
        .shadow(radius: 2)
    }
}

struct PrecisionButton: View {
    let title: String
    let color: Color
    @ObservedObject var gestureManager: GestureManager
    let action: () -> Void
    
    @State private var isPressed = false
    @State private var touchStartTime: Date = Date()
    
    var body: some View {
        Button(action: {}) {
            Text(title)
                .fontWeight(.medium)
                .foregroundColor(.white)
                .frame(maxWidth: .infinity)
                .padding(.vertical, 12)
                .background(color)
                .cornerRadius(8)
        }
        .scaleEffect(isPressed ? 0.98 : 1.0)
        .brightness(isPressed ? -0.1 : 0)
        .animation(.easeInOut(duration: 0.1), value: isPressed)
        .simultaneousGesture(
            DragGesture(minimumDistance: 0)
                .onChanged { _ in
                    if gestureManager.isTapGestureEnabled && !isPressed {
                        isPressed = true
                        touchStartTime = Date()
                    }
                }
                .onEnded { value in
                    if isPressed {
                        isPressed = false
                        
                        let duration = Date().timeIntervalSince(touchStartTime)
                        let distance = sqrt(pow(value.translation.x, 2) + pow(value.translation.y, 2))
                        
                        if gestureManager.isTapGestureEnabled && distance <= 10 && duration <= 0.5 {
                            action()
                        }
                    }
                }
        )
    }
}

struct StatusView: View {
    @ObservedObject var gestureManager: GestureManager
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("手势状态监控")
                .font(.headline)
            
            HStack {
                StatusIndicator(
                    title: "滑动手势",
                    isActive: gestureManager.isSwipeGestureActive,
                    color: .orange
                )
                
                Spacer()
                
                StatusIndicator(
                    title: "点击可用",
                    isActive: gestureManager.isTapGestureEnabled,
                    color: .green
                )
            }
        }
        .padding()
        .background(Color.gray.opacity(0.1))
        .cornerRadius(10)
    }
}

struct StatusIndicator: View {
    let title: String
    let isActive: Bool
    let color: Color
    
    var body: some View {
        HStack(spacing: 6) {
            Circle()
                .fill(isActive ? color : Color.gray.opacity(0.3))
                .frame(width: 8, height: 8)
            
            Text(title)
                .font(.caption)
                .foregroundColor(isActive ? .primary : .secondary)
        }
    }
}
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
宣传栏