Sunday 10 January 2016

Using your Force Touch trackpad.

ForceTouch

Introduction

Not all of the people who use your apps will be fully sighted so it would nice be to provide them with some extra feedback when using visual click & drag applications.

If you have a force touch trackpad the click when you press on it is no longer an function of a physical switch going "clunk" when you press on it. The click is now generated by baby electric relays at the corners that are told to kick the trackpad when the system tells them to because force gauges underneath the track pad have registered a sufficient press force. Enter the matrix!

In OS X 10.11 you have gained an API to allow you trigger that click in certain conditions. NSAlignmentFeedbackFilter

This API lets you generate a feedback click when you line up to a horizontal axis, vertical axis or a point during a mouseDragged[1] event.

[1] It might also be available for mouseMoved event but I havent tried that yet.

Get Started

For this demo to work you need OS X 10.11 , the latest version of Xcode and hardware that has a force touch trackpad, so either a Macbook with one or a standalone Force Touch trackpad

You can download the sample project here.

There is one class that you need to care about here.

BigHapticView - This is a NSView subclass that handles the dragging of its subviews and deals with sending the messages to the NSAlignmentFeedbackFilter

Build and run the project and you'll see something like this.

The colored blobs can be dragged around.

Generating A Feedback Click

For a fairly normal click on an object and drag it somewhere sequence this is what you need to do.

  1. Before you start, create a NSAlignmentFeedbackFilter filter instance.
  2. During mouseDragged work out if the filter can use the event by masking eventTypes.
  3. If the filter accepts the event update the filter with the event.
  4. Request a NSAlignmentFeedbackToken object for your specific axis or point match.
  5. If you get a token then tell the filter perform to feedback just before you move the relevant dragged object by setting its frame to the desired location or axis aligment.
  6. If you dont get a token then just move the object to where the event would normally dictate by setting its frame.

Create a filter

This step is simple. The init requires no parameters so add this line to the top of BigHapticView

class BigHapticView: NSView {
    
    let alignmentFilter = NSAlignmentFeedbackFilter()

Does the filter accept the event?

Now you are going to mask the mouseDragged event with the set of events that the NSAlignmentFeedbackFilter accepts so look inside the function override func mouseDragged(theEvent: NSEvent) and change the loop here...

if let lockedView = lockedView {
                
    let oldFrame = lockedView.frame
            
    let newLocation = NSPoint(x:oldFrame.origin.x + theEvent.deltaX,y:oldFrame.origin.y - theEvent.deltaY)

    lockedView.frame = NSRect(x: newLocation.x, y:newLocation.y, width: oldFrame.size.width, height: oldFrame.size.height)
                
}

to this

if let lockedView = lockedView {
                
    let oldFrame = lockedView.frame
            
     let newLocation = NSPoint(x:oldFrame.origin.x + theEvent.deltaX,y:oldFrame.origin.y - theEvent.deltaY)
     
     //mask the event against the class filter           
     let alignmentFilterAcceptsEvent = NSAlignmentFeedbackFilter.inputEventMask().rawValue & UInt64(theEvent.type.rawValue) != 0
                
     if alignmentFilterAcceptsEvent  {
                    
         lockedView.frame = NSRect(x: newLocation.x, y:newLocation.y, width: oldFrame.size.width, height: oldFrame.size.height)
     }
     else {
                    
          lockedView.frame = NSRect(x: newLocation.x, y:newLocation.y, width: oldFrame.size.width, height: oldFrame.size.height)
     }              
}

Here you are saying that if the class handles the event type then route the flow through that part of the loop otherwise just work normally.

Update the Filter

Now you are going to update the filter object with the event. So add this line

if alignmentFilterAcceptsEvent  {
    
    //update filter
    alignmentFilter.updateWithEvent(theEvent) 
            
    lockedView.frame = NSRect(x: newLocation.x, y:newLocation.y, width: oldFrame.size.width, height: oldFrame.size.height)

Request a NSAlignmentFeedbackToken

A NSAlignmentFeedbackToken is an opaque object that the NSAlignmentFeedbackFilter returns when requesting alignment feedback.

If you have alignment to the center cross hairs of the view you should recieve a token.

Add the following lines to your function.

var desiredAlignment = NSZeroPoint

if alignmentFilterAcceptsEvent  {

    alignmentFilter.updateWithEvent(theEvent)
                    
    let oldLocation = oldFrame.origin
    
    let midx = NSMidX(bounds) - NSWidth(lockedView.bounds)/2
    let midy = NSMidY(bounds) - NSHeight(lockedView.bounds)/2
    
    desiredAlignment = NSPoint(x:midx,y:midy)
    
    //request a token for the event
    let torigin = alignmentFilter.alignmentFeedbackTokenForMovementInView(self, previousPoint: oldLocation, alignedPoint: desiredAlignment, defaultPoint: newLocation)
    
    if let torigin = torigin {
        gotHlock = true
        gotVlock = true
        tokens.append(torigin)
        Swift.print("center lock")
    }
    
    
     lockedView.frame = NSRect(x: newLocation.x, y:newLocation.y, width: oldFrame.size.width, height: oldFrame.size.height)
}

Build and run the project and drag the blobs near the center hairs and you will see the cross hair light up red when the center of the blobs get near the view center marks.

Tell the filter perform to feedback

The last thing to do is ask the filter to perform feedback when you receive a token and set the frame of the object when this occurs.

Change the function to this

var desiredAlignment = NSZeroPoint
if alignmentFilterAcceptsEvent  {

    alignmentFilter.updateWithEvent(theEvent) 

    let oldLocation = oldFrame.origin
    
    let midx = NSMidX(bounds) - NSWidth(lockedView.bounds)/2
    let midy = NSMidY(bounds) - NSHeight(lockedView.bounds)/2
    desiredAlignment = NSPoint(x:midx,y:midy)

    let torigin = alignmentFilter.alignmentFeedbackTokenForMovementInView(self, previousPoint: oldLocation, alignedPoint: desiredAlignment, defaultPoint: newLocation)
    
    if let torigin = torigin {
        gotHlock = true
        gotVlock = true
        tokens.append(torigin)
        Swift.print("center lock")
    }
    
    //IMPORTANT - Remove the set frame from here!
    
} 
   
if tokens.count > 0 {
    
    //Here is where you ask for feedback to be performed - JUST BEFORE SETTING FRAME
    alignmentFilter.performFeedback(tokens, performanceTime: .Default)
    lockedView.frame = NSRect(x: desiredAlignment.x, y:desiredAlignment.y, width: oldFrame.size.width, height: oldFrame.size.height)
    
} else {
    lockedView.frame = NSRect(x: newLocation.x, y:newLocation.y, width: oldFrame.size.width, height: oldFrame.size.height)
}

Build and run and a couple of things will happen

  1. The blob will leap to the center when you get it near.
  2. You will feel a light click through your track pad when this occurs.
  3. The drag is sticky and it takes a little movement to drag it away from the center mark.

Once you get it working you will notice that the effect is fairly subtle and currently you cant change the strength of the feedback (AFAIK).

Whether or not you get a token and hence align with markers depends on various reasons like the speed of your drag. Experiment with dragging faster across the marker to see this.

Improve the implementation.

You might want to align to just a single axis and the API provides for this also but the general rule is that you dont submit more than one token per axis.

Here you'll implement this rule - If you didnt align to the center check the x and y axis.

Change mouseDragged function one last time.

if let torigin = torigin {
    gotHlock = true
    gotVlock = true
    tokens.append(torigin)
    Swift.print("center lock")
}
//New part to check individual axes
else { 
    
    let thorizontal = alignmentFilter.alignmentFeedbackTokenForHorizontalMovementInView(self, previousX: oldLocation.x, alignedX: desiredAlignment.x, defaultX: newLocation.x)
    
    if let thorizontal = thorizontal {
        gotHlock = true
        tokens.append(thorizontal)
        desiredAlignment.y = newLocation.y
    }
    else {
    
        let tvertical = alignmentFilter.alignmentFeedbackTokenForVerticalMovementInView(self, previousY: oldLocation.y, alignedY: desiredAlignment.y, defaultY: newLocation.y)
        
        if let tvertical = tvertical {
            gotVlock = true
            tokens.append(tvertical)
            desiredAlignment.x = newLocation.x  
        }
    }
    
} 
Build and run and now the blobs will also align to the vertical or horizontal cross hairs. Theres bound to be plenty of uses you can put this API to. Probably even some cool game applications. Have fun.

No comments:

Post a Comment