6 Разработка Mac-приложений на примерах. Multi-touch

Некоторые жесты на multi-touch:

  • Two finger scrolling: This is done by placing two fingers on the trackpad and dragging in a line
  • ‹Tap or pinch to zoom: This is done by tapping once with a single finger, or by placing two fingers on the trackpad and dragging them closer to each other
  • ‹Swipe to navigate: This is done by placing one or more fingers on the trackpad and quickly dragging in any direction followed by lifting all the fingers
  • ‹Rotate: This is done by placing two fingers on the trackpad and turning them in a circular motion while keeping them on the trackpad

Magic trackpad может детектировать и отслеживать все 10 пальцев.

Multi-touch события посылаются объектам NSView.
Custom view явялется подклассом NSView переопределяющим поведение объекта NSView. В первую очередь переопределяется метод drawRect: и некоторые другие управляющие методы.


1. Создадим проект Multi-Finger Paint
2. В MainView.xib увеличим размеры окна до 700 x 600
3. Включим Minimum Size и Maximum Size Constraints
4. Из Object Library перетяним custom view на окно и поставим размеры 400 x 300
5. Добавим новый класс BTSFingerView с subclass от NSView

BTSFingerView.h

#import <Cocoa/Cocoa.h>

// Define the size of the cursor that
// will be drawn in the view for each
// finger on the trackpad
#define D_FINGER_CURSOR_SIZE 20
// Define the color values that will
// be used for the finger cursor
#define D_FINGER_CURSOR_RED 1.0
#define D_FINGER_CURSOR_GREEN 0.0
#define D_FINGER_CURSOR_BLUE 0.0
#define D_FINGER_CURSOR_ALPHA 0.5

@interface BTSFingerView : NSView

// A reference to the object that will
// store the currently active touches
@property (strong) NSMutableDictionary *m_activeTouches;

@end

BTSFingerView.m


#import "BTSFingerView.h"

@implementation BTSFingerView

// Synthesize the object that will
// store the currently active touches
@synthesize m_activeTouches;

- (id)initWithFrame:(NSRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code here.
        
        // Accept trackpad events
        [self setAcceptsTouchEvents: YES];
        
        // Create the mutable dictionary that
        // will hold the list of currently active
        // touch events
        m_activeTouches = [[NSMutableDictionary alloc] init];
    }
    return self;
}

/*
 ** - (void)drawRect:(NSRect)dirtyRect
 **
 ** Draw the view content
 **
 ** Input: dirtyRect - the rectangle to draw
 **
 ** Output: none
 */
- (void)drawRect:(NSRect)dirtyRect
{
    // Drawing code here.
    // Preserve the graphics content
    // so that other things we draw
    // don't get focus rings
    [NSGraphicsContext saveGraphicsState];
    // color the background transparent
    [[NSColor clearColor] set];
    // If this view has accepted first responder
    // it should draw the focus ring
    if ([[self window] firstResponder] == self)
    {
        NSSetFocusRingStyle(NSFocusRingAbove);
    }
    // Fill the view with fully transparent
    // color so that we can see through it
    // to whatever is below
    [[NSBezierPath bezierPathWithRect:[self bounds]] fill];
    // Restore the graphics content
    // so that other things we draw
    // don't get focus rings
    [NSGraphicsContext restoreGraphicsState];
    
    // For each active touch
    for (NSTouch *l_touch in m_activeTouches.allValues)
    {
        // Create a rectangle reference to hold the
        // location of the cursor
        NSRect l_cursor;
        // Determine where the touch point
        NSPoint l_touchNP = [l_touch normalizedPosition];
        // Calculate the pixel position of the touch point
        l_touchNP.x = l_touchNP.x * [self bounds].size.width;
        l_touchNP.y = l_touchNP.y * [self bounds].size.height;
        // Calculate the rectangle around the cursor
        l_cursor.origin.x = l_touchNP.x - (D_FINGER_CURSOR_SIZE /2);
        l_cursor.origin.y = l_touchNP.y - (D_FINGER_CURSOR_SIZE /2);
        l_cursor.size.width = D_FINGER_CURSOR_SIZE;
        l_cursor.size.height = D_FINGER_CURSOR_SIZE;
        // Set the color of the cursor
        [[NSColor colorWithDeviceRed: D_FINGER_CURSOR_RED
                               green: D_FINGER_CURSOR_GREEN
                                blue: D_FINGER_CURSOR_BLUE
                               alpha: D_FINGER_CURSOR_ALPHA] set];
        // Draw the cursor as a circle
        [[NSBezierPath bezierPathWithOvalInRect: l_cursor] fill];
    }
}

/*
 ** - (BOOL) acceptsFirstResponder
 **
 ** Make sure the view will receive
 ** events.
 **
 ** Input: none
 **
 ** Output: YES to accept, NO to reject
 */
- (BOOL) acceptsFirstResponder
{
    return YES;
}

/**
 ** - (void)touchesBeganWithEvent:(NSEvent *)event
 **
 ** Invoked when a finger touches the trackpad
 **
 ** Input: event - the touch event
 **
 ** Output: none
 */
- (void)touchesBeganWithEvent:(NSEvent *)event
{
    // Get the set of began touches
    NSSet *l_touches =
    [event touchesMatchingPhase:NSTouchPhaseBegan
                         inView:self];
    // For each began touch, add the touch
    // to the active touches dictionary
    // using its identity as the key
    for (NSTouch *l_touch in l_touches)
    {
        [m_activeTouches setObject:l_touch forKey:l_touch.
         identity];
    }
    // Redisplay the view
    [self setNeedsDisplay:YES];
}

/**
 ** - (void)touchesMovedWithEvent:(NSEvent *)event
 **
 ** Invoked when a finger moves on the trackpad
 **
 ** Input: event - the touch event
 **
 ** Output: none
 */
- (void)touchesMovedWithEvent:(NSEvent *)event
{
    // Get the set of move touches
    NSSet *l_touches =
    [event touchesMatchingPhase:NSTouchPhaseMoved
                         inView:self];
    // For each move touch, update the touch
    // in the active touches dictionary
    // using its identity as the key
    for (NSTouch *l_touch in l_touches)
    {
        // Update the touch only if it is found
        // in the active touches dictionary
        if ([m_activeTouches objectForKey:l_touch.identity])
        {
            [m_activeTouches setObject:l_touch
                                forKey:l_touch.identity];
        } }
    // Redisplay the view
    [self setNeedsDisplay:YES];
}

/**
 ** - (void)touchesEndedWithEvent:(NSEvent *)event
 **
 ** Invoked when a finger lifts off the trackpad
 **
 ** Input: event - the touch event
 **
 ** Output: none
 */
- (void)touchesEndedWithEvent:(NSEvent *)event
{
    // Get the set of ended touches
    NSSet *l_touches =
    [event touchesMatchingPhase:NSTouchPhaseEnded
                         inView:self];
    // For each ended touch, remove the touch
    // from the active touches dictionary
    // using its identity as the key
    for (NSTouch *l_touch in l_touches)
    {
        [m_activeTouches removeObjectForKey:l_touch.identity];
    }
    // Redisplay the view
    [self setNeedsDisplay:YES];
}

/**
 ** - (void)touchesCancelledWithEvent:(NSEvent *)event
 **
 ** Invoked when a touch is cancelled
 **
 ** Input: event - the touch event
 **
 ** Output: none
 */
- (void)touchesCancelledWithEvent:(NSEvent *)event
{
    // Get the set of cancelled touches
    NSSet *l_touches =
    [event touchesMatchingPhase:NSTouchPhaseCancelled
                         inView:self];
    // For each cancelled touch, remove the touch
    // from the active touches dictionary
    // using its identity as the key
    for (NSTouch *l_touch in l_touches)
    {
        [m_activeTouches removeObjectForKey:l_touch.identity];
    }
    // Redisplay the view
    [self setNeedsDisplay:YES];
}

@end

6. Переходим в .xib файл, выбираем custom view и в Identify Inspector -> Custom Class -> Class прописываем BTSFingerView вместо NSView

7. Запуск


To be continued...