forked from MeloNX/MeloNX
上传文件至 src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick #1
@ -0,0 +1,592 @@
|
|||||||
|
//
|
||||||
|
// Joystick4FTG.swift
|
||||||
|
// MeloNX
|
||||||
|
//
|
||||||
|
// Created by RZH on 2025/3/7.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct Joystick4FTG: View {
|
||||||
|
//先声明数据
|
||||||
|
@State var iscool: Bool? = nil
|
||||||
|
@State private var joystickPositionText = ""
|
||||||
|
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||||
|
var dragDiameter: CGFloat {
|
||||||
|
var selfs = CGFloat(160)
|
||||||
|
selfs *= controllerScale
|
||||||
|
if UIDevice.current.systemName.contains("iPadOS") {
|
||||||
|
return selfs * 1.2
|
||||||
|
}
|
||||||
|
return selfs
|
||||||
|
}
|
||||||
|
public var body: some View {
|
||||||
|
VStack{
|
||||||
|
// 新增坐标显示(在摇杆上方)
|
||||||
|
//旧的FTGJoystick:
|
||||||
|
/*FTGJoystickBuilder { relativePosition in
|
||||||
|
|
||||||
|
let scaledX = Double(relativePosition.x )
|
||||||
|
let scaledY = Double(relativePosition.y )
|
||||||
|
|
||||||
|
//print("Joystick Position: (\(scaledX), \(scaledY))")
|
||||||
|
|
||||||
|
*/
|
||||||
|
//新版八向摇杆:
|
||||||
|
/*
|
||||||
|
EnhancedEightWayJoystick{relativePosition in
|
||||||
|
let scaledX = Double(relativePosition.x * 10)
|
||||||
|
let scaledY = Double(relativePosition.y * 10)
|
||||||
|
let formattedText = String(format: "X: %5.2f, Y: %5.2f", scaledX, scaledY)
|
||||||
|
print("Joystick Position: \(formattedText)")
|
||||||
|
// 更新显示文本
|
||||||
|
//self.joystickPositionText = formattedText
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
//新版自由摇杆:
|
||||||
|
FreeRoamJoystick { position in
|
||||||
|
let scaledX = Double(position.x * 10)
|
||||||
|
let scaledY = Double(position.y * 10)
|
||||||
|
//print("Joystick Position: \(formattedText)")
|
||||||
|
// 更新显示文本
|
||||||
|
//self.joystickPositionText = formattedText
|
||||||
|
|
||||||
|
*/
|
||||||
|
//九宫格摇杆:
|
||||||
|
/*
|
||||||
|
NineGridJoystick { position in
|
||||||
|
let scaledX = Double(position.x * 10)
|
||||||
|
let scaledY = Double(position.y * 10)
|
||||||
|
*/
|
||||||
|
|
||||||
|
//新的动态九宫格摇杆
|
||||||
|
DynamicNineGridJoystick{ position in
|
||||||
|
let scaledX = Double(position.x * 10)
|
||||||
|
let scaledY = Double(position.y * 10)
|
||||||
|
|
||||||
|
if iscool != nil {
|
||||||
|
Ryujinx.shared.virtualController.thumbstickMoved(.right, x: scaledX, y: scaledY)
|
||||||
|
} else {
|
||||||
|
Ryujinx.shared.virtualController.thumbstickMoved(.left, x: scaledX, y: scaledY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct EnhancedEightWayJoystick: View {
|
||||||
|
var onDirectionChanged: ((x: Int, y: Int)) -> Void
|
||||||
|
|
||||||
|
@State private var currentDirection: (x: Int, y: Int) = (0, 0)
|
||||||
|
private let radius: CGFloat = 50
|
||||||
|
private let centerPoint = CGPoint(x: 50, y: 50)
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
ZStack {
|
||||||
|
// 触摸区域
|
||||||
|
Circle()
|
||||||
|
.fill(Color.gray.opacity(0.2))
|
||||||
|
.frame(width: 200, height: 200)
|
||||||
|
.contentShape(Circle())
|
||||||
|
|
||||||
|
// 方向指示器
|
||||||
|
Circle()
|
||||||
|
.fill(Color.white.opacity(0.7))
|
||||||
|
.frame(width: 75, height: 75)
|
||||||
|
.offset(x: CGFloat(currentDirection.x), y: CGFloat(currentDirection.y))
|
||||||
|
}
|
||||||
|
.gesture(
|
||||||
|
DragGesture(minimumDistance: 0)
|
||||||
|
.onChanged { gesture in
|
||||||
|
let location = gesture.location
|
||||||
|
//添加使用Geometry计算的dxdy坐标
|
||||||
|
let viewFrame = geometry.frame(in: .local)
|
||||||
|
let center = CGPoint(
|
||||||
|
x: viewFrame.midX,
|
||||||
|
y: viewFrame.midY
|
||||||
|
)
|
||||||
|
|
||||||
|
//let dx = location.x - centerPoint.x
|
||||||
|
//let dy = location.y - centerPoint.y
|
||||||
|
let dx = location.x - center.x
|
||||||
|
let dy = location.y - center.y
|
||||||
|
|
||||||
|
// 计算极坐标
|
||||||
|
let radians = atan2(dy, dx)
|
||||||
|
var degrees = radians * 180 / .pi
|
||||||
|
if degrees < 0 { degrees += 360 }
|
||||||
|
|
||||||
|
// 获取调整后的方向
|
||||||
|
let direction = get45DegreeDirection(from: degrees)
|
||||||
|
|
||||||
|
// 仅在方向变化时更新
|
||||||
|
if direction.x != currentDirection.x || direction.y != currentDirection.y {
|
||||||
|
currentDirection = direction
|
||||||
|
onDirectionChanged(direction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onEnded { _ in
|
||||||
|
currentDirection = (0, 0)
|
||||||
|
onDirectionChanged((0, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 45度均匀分布方向判断
|
||||||
|
//调转Y轴
|
||||||
|
//调整scale到 168
|
||||||
|
private func get45DegreeDirection(from degrees: CGFloat) -> (x: Int, y: Int) {
|
||||||
|
switch degrees {
|
||||||
|
// 右 (337.5°-22.5°)
|
||||||
|
case 337.5...360, 0..<22.5:
|
||||||
|
return (50, 0)
|
||||||
|
|
||||||
|
// 右上 (22.5°-67.5°)
|
||||||
|
case 22.5..<67.5:
|
||||||
|
//return (50, -50)
|
||||||
|
return (50, 50)
|
||||||
|
// 上 (67.5°-112.5°)
|
||||||
|
case 67.5..<112.5:
|
||||||
|
//return (0, -50)
|
||||||
|
return (0, 50)
|
||||||
|
// 左上 (112.5°-157.5°)
|
||||||
|
case 112.5..<157.5:
|
||||||
|
//return (-50, -50)
|
||||||
|
return (-50, 50)
|
||||||
|
// 左 (157.5°-202.5°)
|
||||||
|
case 157.5..<202.5:
|
||||||
|
return (-50, 0)
|
||||||
|
|
||||||
|
// 左下 (202.5°-247.5°)
|
||||||
|
case 202.5..<247.5:
|
||||||
|
//return (-50, 50)
|
||||||
|
return (-50, -50)
|
||||||
|
// 下 (247.5°-292.5°)
|
||||||
|
case 247.5..<292.5:
|
||||||
|
//return (0, 50)
|
||||||
|
return (0, -50)
|
||||||
|
// 右下 (292.5°-337.5°)
|
||||||
|
case 292.5..<337.5:
|
||||||
|
//return (50, 50)
|
||||||
|
return (50, -50)
|
||||||
|
default:
|
||||||
|
return (0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct FreeRoamJoystick: View {
|
||||||
|
var onPositionChanged: (CGPoint) -> Void
|
||||||
|
|
||||||
|
private let trackRadius: CGFloat = 90
|
||||||
|
private let thumbRadius: CGFloat = 75
|
||||||
|
@State private var isEightWay = 0 // 0: 自由模式,1: 八向模式
|
||||||
|
@State private var position = CGPoint.zero
|
||||||
|
@State private var viewCenter: CGPoint = .zero
|
||||||
|
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
ZStack {
|
||||||
|
Spacer()
|
||||||
|
// 摇杆背景(点击区域)
|
||||||
|
Circle()
|
||||||
|
.stroke(Color.gray.opacity(0.5), lineWidth: 4)
|
||||||
|
.frame(width: trackRadius*2, height: trackRadius*2)
|
||||||
|
.position(x: geometry.size.width/2, y: geometry.size.height/2)
|
||||||
|
.contentShape(Circle()) // 关键修改点1:设置点击区域形状
|
||||||
|
// 摇杆头
|
||||||
|
let maxMoveRadius = trackRadius - thumbRadius
|
||||||
|
Circle()
|
||||||
|
.fill(Color.white.opacity(0.4))
|
||||||
|
.frame(width: thumbRadius*2, height: thumbRadius*2)
|
||||||
|
//.offset(x: position.x, y: position.y)
|
||||||
|
.offset(
|
||||||
|
x: min(max(position.x, -maxMoveRadius), maxMoveRadius),
|
||||||
|
y: min(max(position.y, -maxMoveRadius), maxMoveRadius)
|
||||||
|
)
|
||||||
|
.position(x: geometry.size.width/2, y: geometry.size.height/2)
|
||||||
|
}
|
||||||
|
.gesture(
|
||||||
|
DragGesture(minimumDistance: 0) // 关键修改点2:零距离触发
|
||||||
|
.onChanged { gesture in
|
||||||
|
let viewFrame = geometry.frame(in: .local)
|
||||||
|
let touchPoint = gesture.location
|
||||||
|
|
||||||
|
// 计算相对中心坐标
|
||||||
|
let center = CGPoint(
|
||||||
|
x: viewFrame.midX,
|
||||||
|
y: viewFrame.midY
|
||||||
|
)
|
||||||
|
let delta = CGPoint(
|
||||||
|
x: touchPoint.x - center.x,
|
||||||
|
y: touchPoint.y - center.y
|
||||||
|
)
|
||||||
|
|
||||||
|
// 限制在圆形范围内
|
||||||
|
let distance = sqrt(delta.x * delta.x + delta.y * delta.y)
|
||||||
|
let clampedDelta = distance > trackRadius ? CGPoint(
|
||||||
|
x: delta.x * trackRadius / distance,
|
||||||
|
y: delta.y * trackRadius / distance
|
||||||
|
) : delta
|
||||||
|
var test_data = clampedDelta
|
||||||
|
// 更新状态
|
||||||
|
//尝试不同返回值
|
||||||
|
if isEightWay == 1 {
|
||||||
|
test_data = getEightDirection(for: test_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
//position = clampedDelta
|
||||||
|
position = test_data
|
||||||
|
//onPositionChanged(clampedDelta)
|
||||||
|
onPositionChanged(test_data)
|
||||||
|
}
|
||||||
|
.onEnded { _ in
|
||||||
|
|
||||||
|
position = .zero
|
||||||
|
onPositionChanged(.zero)
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.onAppear {
|
||||||
|
viewCenter = CGPoint(
|
||||||
|
x: geometry.size.width/2,
|
||||||
|
y: geometry.size.height/2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: trackRadius, height: trackRadius)
|
||||||
|
//.frame(alignment: .bottomLeading)
|
||||||
|
}
|
||||||
|
private func getEightDirection(for delta: CGPoint) -> CGPoint {
|
||||||
|
|
||||||
|
|
||||||
|
let angle = atan2(-delta.y, delta.x)
|
||||||
|
let degrees = angle * 180 / .pi
|
||||||
|
let normalized = degrees < 0 ? degrees + 360 : degrees
|
||||||
|
|
||||||
|
switch normalized {
|
||||||
|
case 337.5...360, 0..<22.5: // 右
|
||||||
|
return CGPoint(x: trackRadius, y: 0)
|
||||||
|
case 22.5..<67.5: // 右上
|
||||||
|
return CGPoint(x: trackRadius, y: -trackRadius)
|
||||||
|
case 67.5..<112.5: // 上
|
||||||
|
return CGPoint(x: 0, y: -trackRadius)
|
||||||
|
case 112.5..<157.5: // 左上
|
||||||
|
return CGPoint(x: -trackRadius, y: -trackRadius)
|
||||||
|
case 157.5..<202.5: // 左
|
||||||
|
return CGPoint(x: -trackRadius, y: 0)
|
||||||
|
case 202.5..<247.5: // 左下
|
||||||
|
return CGPoint(x: -trackRadius, y: trackRadius)
|
||||||
|
case 247.5..<292.5: // 下
|
||||||
|
return CGPoint(x: 0, y: trackRadius)
|
||||||
|
case 292.5..<337.5: // 右下
|
||||||
|
return CGPoint(x: trackRadius, y: trackRadius)
|
||||||
|
default:
|
||||||
|
return .zero
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
struct NineGridJoystick: View {
|
||||||
|
var onDirectionChanged: (CGPoint) -> Void
|
||||||
|
|
||||||
|
private let trackRadius: CGFloat = 90
|
||||||
|
private let thumbRadius: CGFloat = 35
|
||||||
|
@State private var position = CGPoint.zero
|
||||||
|
@State private var activeColor = Color.white.opacity(0.4)
|
||||||
|
|
||||||
|
// 九宫格配置参数
|
||||||
|
private var gridThreshold: CGFloat {
|
||||||
|
trackRadius / 3 // 56
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
ZStack {
|
||||||
|
// 九宫格背景
|
||||||
|
gridBackgroundView()
|
||||||
|
|
||||||
|
// 摇杆头
|
||||||
|
Circle()
|
||||||
|
.fill(activeColor)
|
||||||
|
.frame(width: thumbRadius*2, height: thumbRadius*2)
|
||||||
|
.offset(x: position.x, y: position.y)
|
||||||
|
.position(x: geometry.size.width/2, y: geometry.size.height/2)
|
||||||
|
//.animation(.spring(), value: position)
|
||||||
|
}
|
||||||
|
.gesture(
|
||||||
|
DragGesture(minimumDistance: 0)
|
||||||
|
.onChanged { gesture in
|
||||||
|
let center = CGPoint(x: geometry.size.width/2, y: geometry.size.height/2)
|
||||||
|
let delta = CGPoint(
|
||||||
|
x: gesture.location.x - center.x,
|
||||||
|
y: gesture.location.y - center.y
|
||||||
|
)
|
||||||
|
|
||||||
|
let direction = getGridDirection(for: delta)
|
||||||
|
updatePosition(to: direction)
|
||||||
|
}
|
||||||
|
.onEnded { _ in
|
||||||
|
resetPosition()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.frame(width: trackRadius*2, height: trackRadius*2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 九宫格视图
|
||||||
|
private func gridBackgroundView() -> some View {
|
||||||
|
ZStack {
|
||||||
|
// 背景网格线
|
||||||
|
Path { path in
|
||||||
|
// 垂直中线
|
||||||
|
path.move(to: CGPoint(x: trackRadius - gridThreshold, y: 0))
|
||||||
|
path.addLine(to: CGPoint(x: trackRadius - gridThreshold, y: trackRadius*2))
|
||||||
|
path.move(to: CGPoint(x: trackRadius + gridThreshold, y: 0))
|
||||||
|
path.addLine(to: CGPoint(x: trackRadius + gridThreshold, y: trackRadius*2))
|
||||||
|
// 水平中线
|
||||||
|
path.move(to: CGPoint(x: 0, y: trackRadius - gridThreshold))
|
||||||
|
path.addLine(to: CGPoint(x: trackRadius*2, y: trackRadius - gridThreshold))
|
||||||
|
path.move(to: CGPoint(x: 0, y: trackRadius + gridThreshold))
|
||||||
|
path.addLine(to: CGPoint(x: trackRadius*2, y: trackRadius + gridThreshold))
|
||||||
|
}
|
||||||
|
.stroke(Color.gray.opacity(0.3), lineWidth: 2)
|
||||||
|
|
||||||
|
// 中心区域高亮
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.gray.opacity(0.1))
|
||||||
|
.frame(width: gridThreshold*2, height: gridThreshold*2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 方向判断逻辑
|
||||||
|
private func getGridDirection(for delta: CGPoint) -> CGPoint {
|
||||||
|
let xVal = delta.x
|
||||||
|
let yVal = delta.y
|
||||||
|
|
||||||
|
switch (xVal, yVal) {
|
||||||
|
case (..<(-gridThreshold), ..<(-gridThreshold)): // 左上
|
||||||
|
return CGPoint(x: -trackRadius, y: -trackRadius)
|
||||||
|
case ((-gridThreshold)...gridThreshold, ..<(-gridThreshold)): // 上
|
||||||
|
return CGPoint(x: 0, y: -trackRadius)
|
||||||
|
case (gridThreshold..., ..<(-gridThreshold)): // 右上
|
||||||
|
return CGPoint(x: trackRadius, y: -trackRadius)
|
||||||
|
case (..<(-gridThreshold), (-gridThreshold)...gridThreshold): // 左
|
||||||
|
return CGPoint(x: -trackRadius, y: 0)
|
||||||
|
case ((-gridThreshold)...gridThreshold, (-gridThreshold)...gridThreshold): // 中
|
||||||
|
return .zero
|
||||||
|
case (gridThreshold..., (-gridThreshold)...gridThreshold): // 右
|
||||||
|
return CGPoint(x: trackRadius, y: 0)
|
||||||
|
case (..<(-gridThreshold), gridThreshold...): // 左下
|
||||||
|
return CGPoint(x: -trackRadius, y: trackRadius)
|
||||||
|
case ((-gridThreshold)...gridThreshold, gridThreshold...): // 下
|
||||||
|
return CGPoint(x: 0, y: trackRadius)
|
||||||
|
case (gridThreshold..., gridThreshold...): // 右下
|
||||||
|
return CGPoint(x: trackRadius, y: trackRadius)
|
||||||
|
default:
|
||||||
|
return .zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 状态更新
|
||||||
|
private func updatePosition(to direction: CGPoint) {
|
||||||
|
//withAnimation(.spring()) {
|
||||||
|
position = direction
|
||||||
|
activeColor = direction == .zero ?
|
||||||
|
Color.white.opacity(0.4) :
|
||||||
|
Color.blue.opacity(0.6)
|
||||||
|
onDirectionChanged(direction)
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resetPosition() {
|
||||||
|
//withAnimation(.spring()) {
|
||||||
|
position = .zero
|
||||||
|
activeColor = Color.white.opacity(0.4)
|
||||||
|
onDirectionChanged(.zero)
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
struct DynamicNineGridJoystick: View {
|
||||||
|
var onDirectionChanged: (CGPoint) -> Void
|
||||||
|
|
||||||
|
private let trackRadius: CGFloat = 100
|
||||||
|
private let thumbRadius: CGFloat = 35
|
||||||
|
@State private var position = CGPoint.zero
|
||||||
|
@State private var activeColor = Color.white.opacity(0.4)
|
||||||
|
@State private var lastDirection: CGPoint = .zero
|
||||||
|
|
||||||
|
// 九宫格配置
|
||||||
|
private var gridThreshold: CGFloat { trackRadius / 3 }
|
||||||
|
private let hapticGenerator = UIImpactFeedbackGenerator(style: .soft)
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
ZStack {
|
||||||
|
// 九宫格背景
|
||||||
|
gridBackgroundView()
|
||||||
|
|
||||||
|
// 摇杆头
|
||||||
|
Circle()
|
||||||
|
.fill(activeColor)
|
||||||
|
.frame(width: thumbRadius*2, height: thumbRadius*2)
|
||||||
|
.offset(x: position.x, y: position.y)
|
||||||
|
.position(x: geometry.size.width/2, y: geometry.size.height/2)
|
||||||
|
//.animation(.spring(), value: position)
|
||||||
|
}
|
||||||
|
// 关键修改点:设置全区域点击识别
|
||||||
|
.contentShape(Rectangle()) // 使用矩形识别区域覆盖整个GeometryReader
|
||||||
|
.gesture(
|
||||||
|
DragGesture(minimumDistance: 0)
|
||||||
|
.onChanged { gesture in
|
||||||
|
handleGestureChange(
|
||||||
|
location: gesture.location,
|
||||||
|
center: CGPoint(x: geometry.size.width/2,
|
||||||
|
y: geometry.size.height/2)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.onEnded { _ in
|
||||||
|
resetPosition()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.frame(width: trackRadius*2, height: trackRadius*2)
|
||||||
|
.onAppear {
|
||||||
|
hapticGenerator.prepare()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 手势处理
|
||||||
|
private func handleGestureChange(location: CGPoint, center: CGPoint) {
|
||||||
|
let delta = CGPoint(
|
||||||
|
x: location.x - center.x,
|
||||||
|
y: location.y - center.y
|
||||||
|
)
|
||||||
|
|
||||||
|
let currentDirection = getGridDirection(for: delta)
|
||||||
|
|
||||||
|
// 仅在新方向变化时触发
|
||||||
|
if currentDirection != lastDirection {
|
||||||
|
updatePosition(to: currentDirection)
|
||||||
|
triggerHapticFeedback()
|
||||||
|
lastDirection = currentDirection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 九宫格方向判断(优化版)
|
||||||
|
private func getGridDirection(for delta: CGPoint) -> CGPoint {
|
||||||
|
let xVal = delta.x
|
||||||
|
let yVal = delta.y
|
||||||
|
|
||||||
|
// 定义九宫格边界
|
||||||
|
let isLeft = xVal < -gridThreshold
|
||||||
|
let isRight = xVal > gridThreshold
|
||||||
|
let isUp = yVal < -gridThreshold
|
||||||
|
let isDown = yVal > gridThreshold
|
||||||
|
|
||||||
|
switch (isLeft, isRight, isUp, isDown) {
|
||||||
|
case (true, false, true, false): // 左上
|
||||||
|
return CGPoint(x: -trackRadius, y: -trackRadius)
|
||||||
|
case (false, false, true, false): // 上
|
||||||
|
return CGPoint(x: 0, y: -trackRadius)
|
||||||
|
case (false, true, true, false): // 右上
|
||||||
|
return CGPoint(x: trackRadius, y: -trackRadius)
|
||||||
|
case (true, false, false, false): // 左
|
||||||
|
return CGPoint(x: -trackRadius, y: 0)
|
||||||
|
case (false, false, false, false): // 中
|
||||||
|
return .zero
|
||||||
|
case (false, true, false, false): // 右
|
||||||
|
return CGPoint(x: trackRadius, y: 0)
|
||||||
|
case (true, false, false, true): // 左下
|
||||||
|
return CGPoint(x: -trackRadius, y: trackRadius)
|
||||||
|
case (false, false, false, true): // 下
|
||||||
|
return CGPoint(x: 0, y: trackRadius)
|
||||||
|
case (false, true, false, true): // 右下
|
||||||
|
return CGPoint(x: trackRadius, y: trackRadius)
|
||||||
|
default: // 处理对角线滑动时的中间状态
|
||||||
|
return calculateEdgeCase(delta: delta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理边缘滑动状态
|
||||||
|
private func calculateEdgeCase(delta: CGPoint) -> CGPoint {
|
||||||
|
let angle = atan2(delta.y, delta.x) * 180 / .pi
|
||||||
|
let normalized = angle < 0 ? angle + 360 : angle
|
||||||
|
|
||||||
|
switch normalized {
|
||||||
|
case 45..<135: // 上象限
|
||||||
|
return CGPoint(x: 0, y: -trackRadius)
|
||||||
|
case 135..<225: // 左象限
|
||||||
|
return CGPoint(x: -trackRadius, y: 0)
|
||||||
|
case 225..<315: // 下象限
|
||||||
|
return CGPoint(x: 0, y: trackRadius)
|
||||||
|
default: // 右象限
|
||||||
|
return CGPoint(x: trackRadius, y: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 视觉与反馈更新
|
||||||
|
private func updatePosition(to direction: CGPoint) {
|
||||||
|
//withAnimation(.interactiveSpring()) {
|
||||||
|
|
||||||
|
position = direction
|
||||||
|
activeColor = direction == .zero ?
|
||||||
|
Color.white.opacity(0.4) :
|
||||||
|
Color.blue.opacity(0.6)
|
||||||
|
onDirectionChanged(direction)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resetPosition() {
|
||||||
|
//withAnimation(.spring()) {
|
||||||
|
position = .zero
|
||||||
|
activeColor = Color.white.opacity(0.4)
|
||||||
|
lastDirection = .zero
|
||||||
|
onDirectionChanged(.zero)
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func triggerHapticFeedback() {
|
||||||
|
guard position != .zero else { return }
|
||||||
|
hapticGenerator.impactOccurred(intensity: 0.7)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 九宫格视图(优化版)
|
||||||
|
private func gridBackgroundView() -> some View {
|
||||||
|
ZStack {
|
||||||
|
// 网格线
|
||||||
|
Path { path in
|
||||||
|
let offsets = [-gridThreshold, 0, gridThreshold]
|
||||||
|
for offset in offsets {
|
||||||
|
path.move(to: CGPoint(x: trackRadius + offset, y: 0))
|
||||||
|
path.addLine(to: CGPoint(x: trackRadius + offset, y: trackRadius*2))
|
||||||
|
path.move(to: CGPoint(x: 0, y: trackRadius + offset))
|
||||||
|
path.addLine(to: CGPoint(x: trackRadius*2, y: trackRadius + offset))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.stroke(Color.gray.opacity(0.3), lineWidth: 2)
|
||||||
|
|
||||||
|
// 中心区域
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.fill(Color.gray.opacity(0.1))
|
||||||
|
.frame(width: gridThreshold*2, height: gridThreshold*2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user