- fixed memory leaks caused by on-screen button auto-tap timer

- feat: Inertial RsvPad/LsvPad
This commit is contained in:
TrueZhuanjia committed 2025-12-09 16:38:28 +08:00
1 parent adafacbf81
commit 7caa778ff8
11 files changed
+213 -71

No files matched your search

+4
View File
@@ -25,6 +25,7 @@
18951D1D2EBB6D2300A86E12 /* TouchPadGestureHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */; }; 18951D1D2EBB6D2300A86E12 /* TouchPadGestureHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */; };
189AD29A2DE7446000E24B88 /* HostCollectionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 189AD2992DE7446000E24B88 /* HostCollectionViewController.m */; }; 189AD29A2DE7446000E24B88 /* HostCollectionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 189AD2992DE7446000E24B88 /* HostCollectionViewController.m */; };
18A765BD2ECD51FB004784AA /* ControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */; }; 18A765BD2ECD51FB004784AA /* ControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */; };
18B317032EE67A3A009B62C2 /* InertialScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B317022EE67A39009B62C2 /* InertialScroller.swift */; };
18E4BCEA2E891895000A8657 /* MotionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E4BCE92E891895000A8657 /* MotionHandler.swift */; }; 18E4BCEA2E891895000A8657 /* MotionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E4BCE92E891895000A8657 /* MotionHandler.swift */; };
840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */; }; 840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */; };
841BAC922DE35D6B00DA639E /* ThemeManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 841BAC912DE35D6B00DA639E /* ThemeManager.m */; }; 841BAC922DE35D6B00DA639E /* ThemeManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 841BAC912DE35D6B00DA639E /* ThemeManager.m */; };
@@ -182,6 +183,7 @@
189AD2962DE743B700E24B88 /* HostCollectionViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HostCollectionViewController.h; sourceTree = "<group>"; }; 189AD2962DE743B700E24B88 /* HostCollectionViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HostCollectionViewController.h; sourceTree = "<group>"; };
189AD2992DE7446000E24B88 /* HostCollectionViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HostCollectionViewController.m; sourceTree = "<group>"; }; 189AD2992DE7446000E24B88 /* HostCollectionViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HostCollectionViewController.m; sourceTree = "<group>"; };
18A765BC2ECD51FB004784AA /* ControllerUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerUtil.swift; sourceTree = "<group>"; }; 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerUtil.swift; sourceTree = "<group>"; };
18B317022EE67A39009B62C2 /* InertialScroller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InertialScroller.swift; sourceTree = "<group>"; };
18BF141D2E0D7A00009DEE4F /* VoidLink v1.0.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "VoidLink v1.0.xcdatamodel"; sourceTree = "<group>"; }; 18BF141D2E0D7A00009DEE4F /* VoidLink v1.0.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "VoidLink v1.0.xcdatamodel"; sourceTree = "<group>"; };
18E4BCE92E891895000A8657 /* MotionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionHandler.swift; sourceTree = "<group>"; }; 18E4BCE92E891895000A8657 /* MotionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionHandler.swift; sourceTree = "<group>"; };
693B3A9A218638CD00982F7B /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; }; 693B3A9A218638CD00982F7B /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
@@ -714,6 +716,7 @@
988FCD3F293B091B003050E2 /* KeyboardInputField.h */, 988FCD3F293B091B003050E2 /* KeyboardInputField.h */,
988FCD40293B091B003050E2 /* KeyboardInputField.m */, 988FCD40293B091B003050E2 /* KeyboardInputField.m */,
18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */, 18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */,
18B317022EE67A39009B62C2 /* InertialScroller.swift */,
18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */, 18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */,
18A765BC2ECD51FB004784AA /* ControllerUtil.swift */, 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */,
1850C8292E667E4000845308 /* MicHandler.swift */, 1850C8292E667E4000845308 /* MicHandler.swift */,
@@ -1106,6 +1109,7 @@
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */, 1888C1922E24C33100108730 /* AboutViewController.swift in Sources */,
FBD349621A0089F6002D2A60 /* DataManager.m in Sources */, FBD349621A0089F6002D2A60 /* DataManager.m in Sources */,
97E20DE72E216510000D5DCA /* ImGuiRenderer.mm in Sources */, 97E20DE72E216510000D5DCA /* ImGuiRenderer.mm in Sources */,
18B317032EE67A3A009B62C2 /* InertialScroller.swift in Sources */,
FB4A23B81A9D3637004D2EF2 /* LoadingFrameViewController.m in Sources */, FB4A23B81A9D3637004D2EF2 /* LoadingFrameViewController.m in Sources */,
FB9AFD3A1A7E05CE00872C98 /* ServerInfoResponse.m in Sources */, FB9AFD3A1A7E05CE00872C98 /* ServerInfoResponse.m in Sources */,
84351B2B2C29AD6100A77989 /* LayoutOnScreenControlsViewController.m in Sources */, 84351B2B2C29AD6100A77989 /* LayoutOnScreenControlsViewController.m in Sources */,
+1
View File
@@ -125,6 +125,7 @@ import UIKit
@objc public static let stickTouchPads: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD"] @objc public static let stickTouchPads: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD"]
@objc public static let nonVectorStickPads: [String] = ["LSPAD", "RSPAD"] @objc public static let nonVectorStickPads: [String] = ["LSPAD", "RSPAD"]
@objc public static let vectorTouchPads: [String] = ["LSVPAD", "RSVPAD", "MOUSEPAD", "TRACKBALL"] @objc public static let vectorTouchPads: [String] = ["LSVPAD", "RSVPAD", "MOUSEPAD", "TRACKBALL"]
@objc public static let inertialTouchPads: [String] = ["LSVPAD", "RSVPAD", "TRACKBALL"]
@objc public static let verticalTouchPads: [String] = ["LTPAD", "RTPAD", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL"] @objc public static let verticalTouchPads: [String] = ["LTPAD", "RTPAD", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL"]
@objc public static let bidirectionalVerticalTouchPads: [String] = ["LTPAD", "RTPAD", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL"] @objc public static let bidirectionalVerticalTouchPads: [String] = ["LTPAD", "RTPAD", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL"]
@objc public static let functionalButtonCmds: [String] = ["SETTINGS", "TOOLBOX", "WIDGETTOOL", "WIDGETPROFILES", "PROFILES", "SOFTKEYBOARD", "ABSTCHDRAG"] @objc public static let functionalButtonCmds: [String] = ["SETTINGS", "TOOLBOX", "WIDGETTOOL", "WIDGETPROFILES", "PROFILES", "SOFTKEYBOARD", "ABSTCHDRAG"]
-1
View File
@@ -1014,7 +1014,6 @@ double rc_expo(double x, double expo) {
- (void)sendStickToMouseMoveEventWithStickX:(float)stickX stickY:(float)stickY expo:(float)expo { - (void)sendStickToMouseMoveEventWithStickX:(float)stickX stickY:(float)stickY expo:(float)expo {
CGFloat mouseDeltaX = _stickToMouseVelocity*rc_expo(stickX, expo); CGFloat mouseDeltaX = _stickToMouseVelocity*rc_expo(stickX, expo);
CGFloat mouseDeltaY = _stickToMouseVelocity*rc_expo(stickY, expo); CGFloat mouseDeltaY = _stickToMouseVelocity*rc_expo(stickY, expo);
NSLog(@"mouseDeltaX %f, mouseDeltaY %f, %f, %ld", mouseDeltaX, mouseDeltaY, CACurrentMediaTime(), (long)_displayLink.preferredFramesPerSecond);
LiSendMouseMoveEvent(mouseDeltaX, -mouseDeltaY); LiSendMouseMoveEvent(mouseDeltaX, -mouseDeltaY);
} }
@@ -406,7 +406,7 @@ static CGRect layoutViewBounds;
buttonState.yawFactor = widgetView.yawFactor; buttonState.yawFactor = widgetView.yawFactor;
buttonState.pitchFactor = widgetView.pitchFactor; buttonState.pitchFactor = widgetView.pitchFactor;
buttonState.rollFactor = widgetView.rollFactor; buttonState.rollFactor = widgetView.rollFactor;
buttonState.decelerationRate = widgetView.trackballDecelerationRate; buttonState.decelerationRate = widgetView.decelerationRate;
buttonState.stickIndicatorOffset = widgetView.stickIndicatorOffset; buttonState.stickIndicatorOffset = widgetView.stickIndicatorOffset;
buttonState.widgetShape = widgetView.shape; buttonState.widgetShape = widgetView.shape;
buttonState.minStickOffset = widgetView.minStickOffset; buttonState.minStickOffset = widgetView.minStickOffset;
+35
View File
@@ -0,0 +1,35 @@
//
// InertialScroller.swift
// VoidLink
//
// Created by True砖家 on 2025/11/5.
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
//
class InertialScroller {
public var decelerationRate: CGFloat
public lazy var timer: SafeTimer? = {
SafeTimer(interval: 1/displayLinkRate) { [weak self] in
guard let self = self, handler != nil else {return}
let deceratedDeltaX = self.vector.dx * decelerationRate
let deceratedDeltaY = self.vector.dy * decelerationRate
self.vector = CGVector(dx: deceratedDeltaX, dy: deceratedDeltaY)
if abs(deceratedDeltaX) < self.timerSuspendThreshold && abs(deceratedDeltaY) < self.timerSuspendThreshold {
self.timer?.pause()
}
(self.handler ?? {})()
}
}()
public var vector: CGVector = .zero
public var displayLinkRate: CGFloat
public var timerSuspendThreshold: CGFloat = 0.06
public var handler: (() -> Void)?
init(decelerationRate: CGFloat = 0.93, displayLinkRate: CGFloat = 60, handler: (() -> Void)? = nil) {
self.decelerationRate = decelerationRate
self.displayLinkRate = displayLinkRate
self.handler = handler
}
}
+2 -2
View File
@@ -162,7 +162,7 @@ public class MicHandler: NSObject {
@objc public func startTapping() { @objc public func startTapping() {
// recordedBuffers.removeAll() // recordedBuffers.removeAll()
isRecording = true isRecording = true
self.timer?.resume() self.timer?.start()
/* /*
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { [weak self] in
guard let self = self else { return } guard let self = self else { return }
@@ -175,7 +175,7 @@ public class MicHandler: NSObject {
@objc public func stopTapping(stopEngine:Bool) { @objc public func stopTapping(stopEngine:Bool) {
isRecording = false isRecording = false
self.timer?.suspend() self.timer?.pause()
// engine.inputNode.removeTap(onBus: 0) // engine.inputNode.removeTap(onBus: 0)
if(stopEngine){ if(stopEngine){
playerNode.stop() playerNode.stop()
+92 -28
View File
@@ -42,9 +42,14 @@ import UIKit
case touchPad case touchPad
} }
private static let MinAutotapInterval:Int = 50
private let oscProfileMan: OSCProfilesManager = OSCProfilesManager.sharedManager(CGRectZero) private let oscProfileMan: OSCProfilesManager = OSCProfilesManager.sharedManager(CGRectZero)
private var oscProfile: OSCProfile private var oscProfile: OSCProfile
private let dataMan: DataManager = DataManager()
private let tempSettings: TemporarySettings
@objc public var widgetType: WidgetTypeEnum = WidgetTypeEnum.uninitialized @objc public var widgetType: WidgetTypeEnum = WidgetTypeEnum.uninitialized
@objc static public var editMode: Bool = false @objc static public var editMode: Bool = false
@@ -93,6 +98,9 @@ import UIKit
private var vibrationGenerator = UIImpactFeedbackGenerator(style: .light) private var vibrationGenerator = UIImpactFeedbackGenerator(style: .light)
private var vibrationOn: Bool = false private var vibrationOn: Bool = false
private var inertialScroller:InertialScroller
@objc public var displayLinkRate:CGFloat = 60;
// for movable buttons during streaming // for movable buttons during streaming
@objc public var relocatedDuringStreaming: Bool = false @objc public var relocatedDuringStreaming: Bool = false
@@ -120,7 +128,7 @@ import UIKit
@objc public var hasAutoTap: Bool = false @objc public var hasAutoTap: Bool = false
@objc public var isMousePadWithButtonActions: Bool = false @objc public var isMousePadWithButtonActions: Bool = false
@objc public var hasTrackBall: Bool = false @objc public var hasInertia: Bool = false
@objc public var isFuncationalButton: Bool = false @objc public var isFuncationalButton: Bool = false
@objc public var hasHapticFeedback: Bool = false @objc public var hasHapticFeedback: Bool = false
@objc public var isDirectionPad: Bool = false @objc public var isDirectionPad: Bool = false
@@ -206,7 +214,7 @@ import UIKit
// trackball // trackball
private var trackballVelocity: CGPoint = .zero private var trackballVelocity: CGPoint = .zero
private var trackballDecelerationTimer: Timer? private var trackballDecelerationTimer: Timer?
@objc public var trackballDecelerationRate: CGFloat = 0.93 @objc public var decelerationRate: CGFloat = 0.93
private let trackballVelocityThreshold: CGFloat = 0.1 private let trackballVelocityThreshold: CGFloat = 0.1
@objc public var slideThreshold: CGFloat = 6.0 @objc public var slideThreshold: CGFloat = 6.0
@@ -324,6 +332,8 @@ import UIKit
} }
self.activePointerIds = [] self.activePointerIds = []
self.oscProfile = oscProfileMan.getSelectedProfile() self.oscProfile = oscProfileMan.getSelectedProfile()
self.tempSettings = dataMan.getSettings()
self.inertialScroller = InertialScroller()
super.init(frame: .zero) super.init(frame: .zero)
// helps widget panel to hide/show stacks // helps widget panel to hide/show stacks
@@ -375,7 +385,7 @@ import UIKit
self.hasAutoTap = self.widgetType == WidgetTypeEnum.button && self.functionalButtonString == "" && self.motionControlButtonString == "" self.hasAutoTap = self.widgetType == WidgetTypeEnum.button && self.functionalButtonString == "" && self.motionControlButtonString == ""
self.isMousePadWithButtonActions = CommandManager.mousePadWithButtonActions.contains(self.touchPadString) && widgetType == WidgetTypeEnum.touchPad self.isMousePadWithButtonActions = CommandManager.mousePadWithButtonActions.contains(self.touchPadString) && widgetType == WidgetTypeEnum.touchPad
self.hasTrackBall = self.touchPadString == "TRACKBALL" self.hasInertia = CommandManager.inertialTouchPads.contains(self.touchPadString)
self.isFuncationalButton = self.functionalButtonString != "" self.isFuncationalButton = self.functionalButtonString != ""
self.hasHapticFeedback = !self.comboButtonStrings.isEmpty || CommandManager.directionPads.contains(self.touchPadString) self.hasHapticFeedback = !self.comboButtonStrings.isEmpty || CommandManager.directionPads.contains(self.touchPadString)
self.isDirectionPad = self.widgetType == WidgetTypeEnum.touchPad && CommandManager.directionPads.contains(self.touchPadString) self.isDirectionPad = self.widgetType == WidgetTypeEnum.touchPad && CommandManager.directionPads.contains(self.touchPadString)
@@ -384,8 +394,9 @@ import UIKit
// ====================================================================================================== // ======================================================================================================
@objc public func setupAutoTapTimer() { @objc public func setupAutoTapTimer() {
if self.widgetType == WidgetTypeEnum.button { if self.widgetType == WidgetTypeEnum.button, autoTapInterval >= OnScreenWidgetView.MinAutotapInterval {
self.autoTapTimer = SafeTimer(interval:0.001 * Double(autoTapInterval)) { self.autoTapTimer = SafeTimer(interval:0.001 * Double(autoTapInterval)) {
// print("timer instance \(Unmanaged.passUnretained(self.autoTapTimer!).toOpaque()), \(CACurrentMediaTime())")
self.handleButtonDown() self.handleButtonDown()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.025) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.025) {
self.handleButtonUp() self.handleButtonUp()
@@ -394,6 +405,12 @@ import UIKit
} }
} }
@objc public func setupInertialScroller() {
if self.hasInertia {
self.inertialScroller = InertialScroller(decelerationRate: self.decelerationRate, displayLinkRate: CGFloat(self.tempSettings.framerate.intValue))
}
}
@objc public func setVibration(style: Int) { @objc public func setVibration(style: Int) {
if #available(iOS 13.0, *) { if #available(iOS 13.0, *) {
vibrationOn = style < UIImpactFeedbackGenerator.FeedbackStyle.rigid.rawValue + 1 vibrationOn = style < UIImpactFeedbackGenerator.FeedbackStyle.rigid.rawValue + 1
@@ -1106,29 +1123,22 @@ import UIKit
//mousepad-trackball behavior======================================================== //mousepad-trackball behavior========================================================
private func startTrackballMomentum() { private func startTrackballMomentum() {
stopTrackballMomentum() // stopTrackballMomentum()
trackballDecelerationTimer = Timer.scheduledTimer(withTimeInterval: 1/Double(UIScreen.main.maximumFramesPerSecond), repeats: true) { [weak self] _ in if self.inertialScroller.handler == nil {
guard let self = self else { return } self.inertialScroller.handler = {
LiSendMouseMoveEvent(
LiSendMouseMoveEvent( Int16(self.inertialScroller.vector.dx*1.7),
Int16(truncatingIfNeeded: Int(self.trackballVelocity.x)), Int16(self.inertialScroller.vector.dy*1.7)
Int16(truncatingIfNeeded: Int(self.trackballVelocity.y)) )
)
self.trackballVelocity.x *= self.trackballDecelerationRate
self.trackballVelocity.y *= self.trackballDecelerationRate
if abs(self.trackballVelocity.x) < self.trackballVelocityThreshold &&
abs(self.trackballVelocity.y) < self.trackballVelocityThreshold {
self.stopTrackballMomentum()
} }
} }
self.inertialScroller.timer?.restart()
} }
private func stopTrackballMomentum() { private func stopTrackballMomentum() {
trackballDecelerationTimer?.invalidate() self.inertialScroller.timer?.pause()
trackballDecelerationTimer = nil
} }
@@ -1136,7 +1146,7 @@ import UIKit
//==== Button widget tap down============================================= //==== Button widget tap down=============================================
private func handleTapDownOrSlidein() { private func handleTapDownOrSlidein() {
if autoTapInterval < 50 { if autoTapInterval < OnScreenWidgetView.MinAutotapInterval {
handleButtonDown() handleButtonDown()
} }
else{ else{
@@ -1145,11 +1155,11 @@ import UIKit
} }
private func handleFingerUpOrSlideout() { private func handleFingerUpOrSlideout() {
if autoTapInterval < 50 { if autoTapInterval < OnScreenWidgetView.MinAutotapInterval {
handleButtonUp() handleButtonUp()
} }
else{ else{
self.autoTapTimer?.suspend() self.autoTapTimer?.pause()
self.handleButtonUp() self.handleButtonUp()
} }
} }
@@ -1448,7 +1458,19 @@ import UIKit
if quickDoubleTapDetected { if quickDoubleTapDetected {
self.showl3r3Indicator() self.showl3r3Indicator()
self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)} self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)}
case "LSVPAD","RSVPAD","DS4TOUCH": case "LSVPAD":
self.clearLeftStickTouchPadFlag()
self.inertialScroller.timer?.pause()
if quickDoubleTapDetected {
self.showl3r3Indicator()
self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)}
case "RSVPAD":
self.clearRightStickTouchPadFlag()
self.inertialScroller.timer?.pause()
if quickDoubleTapDetected {
self.showl3r3Indicator()
self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)}
case "DS4TOUCH":
if quickDoubleTapDetected { if quickDoubleTapDetected {
self.showl3r3Indicator() self.showl3r3Indicator()
self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)} self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)}
@@ -1694,6 +1716,11 @@ import UIKit
self.deltaX = currentTouchLocation.x - self.latestTouchLocation.x self.deltaX = currentTouchLocation.x - self.latestTouchLocation.x
self.deltaY = currentTouchLocation.y - self.latestTouchLocation.y self.deltaY = currentTouchLocation.y - self.latestTouchLocation.y
if self.hasInertia && firstTouchMoved {
self.inertialScroller.vector = UITouchUtil.vector(of: touch, in: self)
}
self.offSetX = currentTouchLocation.x - self.touchBeganLocation.x self.offSetX = currentTouchLocation.x - self.touchBeganLocation.x
self.offSetY = currentTouchLocation.y - self.touchBeganLocation.y self.offSetY = currentTouchLocation.y - self.touchBeganLocation.y
} }
@@ -1988,6 +2015,7 @@ import UIKit
self.handleMousePadButtonActionUp() self.handleMousePadButtonActionUp()
} }
/*
if !OnScreenWidgetView.editMode && self.widgetType == WidgetTypeEnum.touchPad && self.touchPadString == "TRACKBALL" && allSpawnedTouchesCount == 1 && !twoTouchesDetected { if !OnScreenWidgetView.editMode && self.widgetType == WidgetTypeEnum.touchPad && self.touchPadString == "TRACKBALL" && allSpawnedTouchesCount == 1 && !twoTouchesDetected {
if(mousePointerMoved){ if(mousePointerMoved){
self.startTrackballMomentum() self.startTrackballMomentum()
@@ -1996,7 +2024,7 @@ import UIKit
else{ else{
self.stopTrackballMomentum() self.stopTrackballMomentum()
} }
} }*/
if !OnScreenWidgetView.editMode && self.widgetType == WidgetTypeEnum.touchPad && CommandManager.mousePadWithButtonActions.contains(self.touchPadString) && twoTouchesDetected && touches.count == allSpawnedTouchesCount { // need to enable multi-touch first if !OnScreenWidgetView.editMode && self.widgetType == WidgetTypeEnum.touchPad && CommandManager.mousePadWithButtonActions.contains(self.touchPadString) && twoTouchesDetected && touches.count == allSpawnedTouchesCount { // need to enable multi-touch first
// touches.count == allCapturedTouchesCount means allfingers are lifting // touches.count == allCapturedTouchesCount means allfingers are lifting
@@ -2015,9 +2043,41 @@ import UIKit
self.clearRightStickTouchPadFlag() self.clearRightStickTouchPadFlag()
if widgetType == WidgetTypeEnum.touchPad {self.resetStickBallPositionAndHideIndicator()} if widgetType == WidgetTypeEnum.touchPad {self.resetStickBallPositionAndHideIndicator()}
case "LSVPAD": case "LSVPAD":
self.clearLeftStickTouchPadFlag() if self.decelerationRate > 0.50001 {
if(!firstTouchMoved) {self.inertialScroller.vector = CGVector(dx: 0, dy: 0)}
if self.inertialScroller.handler == nil {
self.inertialScroller.handler = {
let weightedDeltaX = self.inertialScroller.vector.dx*1.5167*self.sensitivityFactorX
let weightedDeltaY = self.inertialScroller.vector.dy*1.5167*self.sensitivityFactorY
self.sendLeftStickTouchPadEvent(inputX: weightedDeltaX, inputY: weightedDeltaY)
}
}
self.inertialScroller.timer?.restart()
}
else {self.clearLeftStickTouchPadFlag()}
case "RSVPAD": case "RSVPAD":
self.clearRightStickTouchPadFlag() if self.decelerationRate > 0.50001 {
if(!firstTouchMoved) {self.inertialScroller.vector = CGVector(dx: 0, dy: 0)}
if self.inertialScroller.handler == nil {
self.inertialScroller.handler = {
let weightedDeltaX = self.inertialScroller.vector.dx*1.5167*self.sensitivityFactorX
let weightedDeltaY = self.inertialScroller.vector.dy*1.5167*self.sensitivityFactorY
self.sendRightStickTouchPadEvent(inputX: weightedDeltaX, inputY: weightedDeltaY)
}
}
self.inertialScroller.timer?.restart()
}
else {self.clearRightStickTouchPadFlag()}
case "TRACKBALL":
if allSpawnedTouchesCount == 1, self.decelerationRate > 0 {
if(mousePointerMoved){
self.startTrackballMomentum()
mousePointerMoved = false //reset flag
}
else{
self.stopTrackballMomentum()
}
}
case "LTPAD": case "LTPAD":
self.onScreenControls.updateLeftTrigger(0x00) self.onScreenControls.updateLeftTrigger(0x00)
case "RTPAD": case "RTPAD":
@@ -2205,6 +2265,10 @@ import UIKit
leftIndicator.removeFromSuperlayer() leftIndicator.removeFromSuperlayer()
rightIndicator.removeFromSuperlayer() rightIndicator.removeFromSuperlayer()
lrudIndicatorBall.removeFromSuperlayer() lrudIndicatorBall.removeFromSuperlayer()
self.inertialScroller.timer?.clean()
self.clearLeftStickTouchPadFlag()
self.clearRightStickTouchPadFlag()
self.autoTapTimer?.clean()
} }
else{ else{
self.superViewWidth = (superview?.bounds.size.width)! self.superViewWidth = (superview?.bounds.size.width)!
+69 -31
View File
@@ -6,47 +6,85 @@
// Copyright © 2025 True砖家@Bilibili. All rights reserved. // Copyright © 2025 True砖家@Bilibili. All rights reserved.
// //
import Foundation
import QuartzCore
class SafeTimer { /// SafeTimer: 高精度、可暂停/重启、可彻底清理
private var timer: DispatchSourceTimer final class SafeTimer {
private(set) var isRunning = false private var timer: DispatchSourceTimer?
private var interval: TimeInterval = 0 private let timerQueue: DispatchQueue // 私有串行队列
private var delay: TimeInterval = 0 private let userHandler: () -> Void
private let interval: TimeInterval
private let delay: TimeInterval
private var shouldRunHandler: Bool = false // 控制逻辑上的暂停/恢复
private var isCleaned: Bool = false // 是否已经 clean
init(interval: TimeInterval = 1.0,
delay: TimeInterval = 0,
queueLabel: String = "com.example.safetimer",
handler: @escaping () -> Void) {
init(interval: TimeInterval = 1.0, queue: DispatchQueue = .global(), delay: TimeInterval = 0, handler: @escaping () -> Void) {
timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now() + delay, repeating: interval)
timer.setEventHandler(handler: handler)
self.interval = interval self.interval = interval
self.delay = delay self.delay = delay
// 注意:这里不调用 resume,保持 suspended,等 start() 时才运行 self.userHandler = handler
} self.timerQueue = DispatchQueue(label: queueLabel)
public func resume() { let t = DispatchSource.makeTimerSource(queue: timerQueue)
guard !isRunning else { return } t.schedule(deadline: .now() + delay, repeating: interval)
timer.resume()
isRunning = true
}
public func suspend() { t.setEventHandler { [weak self] in
guard isRunning else { return } guard let self = self, !self.isCleaned else { return }
timer.suspend() if !self.shouldRunHandler { return }
isRunning = false self.userHandler()
}
func restart() {
if isRunning {
timer.suspend()
isRunning = false
} }
timer.schedule(deadline: .now() + self.delay, repeating: self.interval) // 重置时间
timer.resume() t.resume() // timer 一开始就 resume,避免 suspend/resume 的计数问题
isRunning = true self.timer = t
} }
deinit { deinit {
if !timer.isCancelled { clean()
timer.cancel() }
/// 开始逻辑上的计时
func start() {
timerQueue.async { [weak self] in
guard let self = self, !self.isCleaned else { return }
self.shouldRunHandler = true
}
}
/// 暂停逻辑上的计时
func pause() {
timerQueue.async { [weak self] in
guard let self = self, !self.isCleaned else { return }
self.shouldRunHandler = false
}
}
/// 重置下一次触发时间,并开始执行 handler
func restart() {
timerQueue.async { [weak self] in
guard let self = self, let t = self.timer, !self.isCleaned else { return }
t.schedule(deadline: .now() + self.delay, repeating: self.interval)
self.shouldRunHandler = true
}
}
/// 彻底清理 timer,返回后保证 handler 不再执行
func clean() {
if isCleaned { return }
// 同步在 timerQueue 执行,保证已排队事件全部处理完(或跳过)
timerQueue.sync {
self.isCleaned = true
self.shouldRunHandler = false
if let t = self.timer {
t.setEventHandler {} // 清空 handler
t.cancel() // 取消 timer
self.timer = nil
}
} }
} }
} }
+2 -3
View File
@@ -568,7 +568,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
widgetView.yawFactor = buttonState.yawFactor; widgetView.yawFactor = buttonState.yawFactor;
widgetView.pitchFactor = buttonState.pitchFactor; widgetView.pitchFactor = buttonState.pitchFactor;
widgetView.rollFactor = buttonState.rollFactor; widgetView.rollFactor = buttonState.rollFactor;
widgetView.trackballDecelerationRate = buttonState.decelerationRate; widgetView.decelerationRate = buttonState.decelerationRate;
widgetView.stickIndicatorOffset = buttonState.stickIndicatorOffset; widgetView.stickIndicatorOffset = buttonState.stickIndicatorOffset;
widgetView.minStickOffset = buttonState.minStickOffset; widgetView.minStickOffset = buttonState.minStickOffset;
widgetView.buttonMode = buttonState.buttonMode; widgetView.buttonMode = buttonState.buttonMode;
@@ -585,6 +585,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
[widgetView tweakLabelAlphaWithAlpha:buttonState.labelAlpha]; [widgetView tweakLabelAlphaWithAlpha:buttonState.labelAlpha];
[widgetView tweakBorderAlphaWithAlpha:buttonState.borderAlpha]; [widgetView tweakBorderAlphaWithAlpha:buttonState.borderAlpha];
[widgetView setupAutoTapTimer]; [widgetView setupAutoTapTimer];
[widgetView setupInertialScroller];
} }
} }
} }
@@ -838,8 +839,6 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
- (BOOL)handleMouseButtonEvent:(int)buttonAction forTouches:(NSSet *)touches withEvent:(UIEvent *)event { - (BOOL)handleMouseButtonEvent:(int)buttonAction forTouches:(NSSet *)touches withEvent:(UIEvent *)event {
// NSLog(@"mouse click time: %f", CACurrentMediaTime()*1000);
#if !TARGET_OS_TV #if !TARGET_OS_TV
if (@available(iOS 13.4, *)) { if (@available(iOS 13.4, *)) {
UITouch* touch = [touches anyObject]; UITouch* touch = [touches anyObject];
@@ -157,7 +157,7 @@ typedef NS_ENUM(NSUInteger, BorderWidthSliderMode) {
widgetView.yawFactor = buttonState.yawFactor; widgetView.yawFactor = buttonState.yawFactor;
widgetView.pitchFactor = buttonState.pitchFactor; widgetView.pitchFactor = buttonState.pitchFactor;
widgetView.rollFactor = buttonState.rollFactor; widgetView.rollFactor = buttonState.rollFactor;
widgetView.trackballDecelerationRate = buttonState.decelerationRate; widgetView.decelerationRate = buttonState.decelerationRate;
widgetView.stickIndicatorOffset = buttonState.stickIndicatorOffset; widgetView.stickIndicatorOffset = buttonState.stickIndicatorOffset;
widgetView.minStickOffset = buttonState.minStickOffset; widgetView.minStickOffset = buttonState.minStickOffset;
widgetView.buttonMode = buttonState.buttonMode; widgetView.buttonMode = buttonState.buttonMode;
@@ -731,7 +731,7 @@ typedef NS_ENUM(NSUInteger, BorderWidthSliderMode) {
newWidget.yawFactor = widget.yawFactor; newWidget.yawFactor = widget.yawFactor;
newWidget.pitchFactor = widget.pitchFactor; newWidget.pitchFactor = widget.pitchFactor;
newWidget.rollFactor = widget.rollFactor; newWidget.rollFactor = widget.rollFactor;
newWidget.trackballDecelerationRate = widget.trackballDecelerationRate; newWidget.decelerationRate = widget.decelerationRate;
newWidget.stickIndicatorOffset = widget.stickIndicatorOffset; newWidget.stickIndicatorOffset = widget.stickIndicatorOffset;
newWidget.minStickOffset = widget.minStickOffset; newWidget.minStickOffset = widget.minStickOffset;
[newWidget setVibrationWithStyle:widget.vibrationStyle]; [newWidget setVibrationWithStyle:widget.vibrationStyle];
@@ -977,8 +977,8 @@ typedef NS_ENUM(NSUInteger, BorderWidthSliderMode) {
}]; }];
[self autoFitLabel:self.autoTapLabel]; [self autoFitLabel:self.autoTapLabel];
self.decelerationRateStack.hidden = !selectedWidgetView.hasTrackBall; self.decelerationRateStack.hidden = !selectedWidgetView.hasInertia;
[self.decelerationRateSlider setValue:selectedWidgetView.trackballDecelerationRate]; [self.decelerationRateSlider setValue:selectedWidgetView.decelerationRate];
[self autoFitLabel:self.decelerationRateLabel]; [self autoFitLabel:self.decelerationRateLabel];
[self decelerationRateSliderMoved:self.decelerationRateSlider]; [self decelerationRateSliderMoved:self.decelerationRateSlider];
@@ -1210,7 +1210,7 @@ typedef NS_ENUM(NSUInteger, BorderWidthSliderMode) {
- (void)decelerationRateSliderMoved:(UISlider* )sender{ - (void)decelerationRateSliderMoved:(UISlider* )sender{
[self.decelerationRateLabel setText:[LocalizationHelper localizedStringForKey:@"Deceleration Rate: %.3f ", sender.value]]; [self.decelerationRateLabel setText:[LocalizationHelper localizedStringForKey:@"Deceleration Rate: %.3f ", sender.value]];
if(self->selectedWidgetView != nil && self->widgetViewSelected) self->selectedWidgetView.trackballDecelerationRate = sender.value; if(self->selectedWidgetView != nil && self->widgetViewSelected) self->selectedWidgetView.decelerationRate = sender.value;
return; return;
} }
@@ -927,6 +927,8 @@
} }
- (void) returnToMainFrame { - (void) returnToMainFrame {
[_streamView clearOnScreenWidgets];
// Reset display mode back to default // Reset display mode back to default
[self updatePreferredDisplayMode:NO]; [self updatePreferredDisplayMode:NO];
if (@available(iOS 13.0, *)) { if (@available(iOS 13.0, *)) {