Skip to content
This repository has been archived by the owner on Jan 10, 2023. It is now read-only.

Commit

Permalink
Adding a custom gesture recognizer for workspace-level interaction.
Browse files Browse the repository at this point in the history
This should reduce overhead for gesture recognition and allow for better
multi-touch interactions (like dragging children of currently-dragged blocks.)
  • Loading branch information
CoryDCode committed Sep 8, 2016
1 parent b165b6b commit 0d532ed
Show file tree
Hide file tree
Showing 4 changed files with 373 additions and 129 deletions.
4 changes: 4 additions & 0 deletions Blockly/Blockly.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
300CABD11D5CF816000E43B2 /* ConnectionValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 300CABD01D5CF816000E43B2 /* ConnectionValidator.swift */; };
300CABE81D5E8606000E43B2 /* DefaultConnectionValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 300CABE71D5E8606000E43B2 /* DefaultConnectionValidator.swift */; };
30707E271D667B770000E418 /* BlocklyPanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30707E261D667B770000E418 /* BlocklyPanGestureRecognizer.swift */; };
F98FF7E11BB2036A00A4F8E5 /* BlockFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F98FF7E01BB2036A00A4F8E5 /* BlockFactory.swift */; };
F98FF7E51BB208EB00A4F8E5 /* BlockFactoryJSONTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F98FF7E41BB208EB00A4F8E5 /* BlockFactoryJSONTest.swift */; };
F98FF7E71BB4911500A4F8E5 /* BlockBuilderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F98FF7E61BB4911500A4F8E5 /* BlockBuilderTest.swift */; };
Expand Down Expand Up @@ -243,6 +244,7 @@
/* Begin PBXFileReference section */
300CABD01D5CF816000E43B2 /* ConnectionValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionValidator.swift; sourceTree = "<group>"; };
300CABE71D5E8606000E43B2 /* DefaultConnectionValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultConnectionValidator.swift; sourceTree = "<group>"; };
30707E261D667B770000E418 /* BlocklyPanGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlocklyPanGestureRecognizer.swift; sourceTree = "<group>"; };
F98FF7E01BB2036A00A4F8E5 /* BlockFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockFactory.swift; sourceTree = "<group>"; };
F98FF7E21BB2087700A4F8E5 /* block_factory_json_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = block_factory_json_test.json; sourceTree = "<group>"; };
F98FF7E41BB208EB00A4F8E5 /* BlockFactoryJSONTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BlockFactoryJSONTest.swift; path = JSON/BlockFactoryJSONTest.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -780,6 +782,7 @@
FAA870161C642805000C7C61 /* TrashCanViewController.swift */,
FAA870171C642805000C7C61 /* WorkbenchViewController.swift */,
FA8425211D35D4140092CDDC /* WorkspaceViewController.swift */,
30707E261D667B770000E418 /* BlocklyPanGestureRecognizer.swift */,
);
path = "View Controllers";
sourceTree = "<group>";
Expand Down Expand Up @@ -1065,6 +1068,7 @@
FAD652741CB87B2200F73F11 /* DefaultLayoutEngine.swift in Sources */,
FA42D7EB1C605A5F000C8EB4 /* FieldDateView.swift in Sources */,
FA27267A1B83C54900777B49 /* BlockLayout.swift in Sources */,
30707E271D667B770000E418 /* BlocklyPanGestureRecognizer.swift in Sources */,
FAFAEE531CDABED400698179 /* FieldNumberView.swift in Sources */,
FAA870091C64272C000C7C61 /* Logging.swift in Sources */,
FA3FD1551CF7C886005B6D0F /* XMLConstants.swift in Sources */,
Expand Down
235 changes: 235 additions & 0 deletions Blockly/Code/UI/View Controllers/BlocklyPanGestureRecognizer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
/*
* Copyright 2016 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import Foundation
import UIKit
import UIKit.UIGestureRecognizerSubclass

/**
The delegate protocol for BlocklyPanGestureRecognizer.
*/
public protocol BlocklyPanGestureDelegate {
/**
The callback that's called when the BlocklyPanGestureRecognizer detects a valid block pan. Note:
This function returns a `BlockView`, in case this function changes the view that's passed in,
typically copying the view onto a new workspace.

Parameter gesture: The gesture calling this function.
Parameter touchPosition: The UI space position of the touch.
Parameter block: The `BlockView` being touched.
Parameter state: The `UIGestureRecognizerState` for this individual touch.
Return: The `BlockView` currently being touched. Typically, this will be block, but blockTouched
might copy it into a new view.
*/
func blockTouched(gesture: BlocklyPanGestureRecognizer, touchPosition: CGPoint, block:BlockView,
state: UIGestureRecognizerState) -> BlockView;
}

/**
The blockly gesture recognizer, which detects pan gestures on blocks in the workspace.
*/
public class BlocklyPanGestureRecognizer: UIGestureRecognizer {
// MARK: - Properties

/// The delegate this gestureRecognizer operates on (`WorkbenchViewController` by default).
private var _target: BlocklyPanGestureDelegate

/// The minimum distance for the gesture recognizer to count as a pan.
private let _minPanDistance: Float = 2.0

/// An ordered list of touches being handled by the recognizer.
private var _touches: [UITouch]

/// An ordered list of blocks being dragged by the recognizer.
private var _blocks: [BlockView]

/**
The container view blocks to be dragged are currently - either the "main" workspace, or a
toolbox/trash container.
*/
private let _originView: UIView

/**
The container view blocks will end up in - either the same as the origin view, or the
workspace blocks are being copied into.
*/
private let _destinationView: UIView

// MARK: - Initializer

/**
Initializer for the BlocklyPanGestureRecognizer

- Parameter target: The object that listens to the gesture recognizer callbacks
- Parameter action: The action to be performed on recognizer callbacks
- Parameter workbench: The workbench being operated on
*/
public init(target: BlocklyPanGestureDelegate, action: Selector, originView: UIView,
destView: UIView, workbench: WorkbenchViewController)
{
_target = workbench
_blocks = [BlockView]()
_touches = [UITouch]()
_originView = originView
_destinationView = destView
super.init(target: target as? AnyObject, action: action)
delaysTouchesBegan = false
}

// MARK: - Super

/**
Called when touches begin on the workspace.
*/
public override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent) {
for touch in touches {
let location = touch.locationInView(_originView)

// If the hit tested view is not an ancestor of a block, cancel the touch(es).
if let hitView = _originView.hitTest(location, withEvent: event),
var block = owningBlockView(hitView)
{
super.touchesBegan(touches, withEvent:event)
// Begin a new touch immediately if there is another touch being handled. Otherwise, the
// touch will begin once a touch has been moved enough to trigger a pan.
if state == .Began || state == .Changed {
// Check that we're not already dragging the block with a different touch
if !_blocks.contains(block) {
// Start the drag.
let touchPosition = touch.locationInView(_destinationView)
block = _target.blockTouched(self,
touchPosition: touchPosition,
block: block,
state: .Began)
}
}

_touches.append(touch)
_blocks.append(block)
}
}

// If none of the touches have hit a block, cancel the gesture.
if _touches.count == 0 {
state = .Cancelled
}
}

/**
Called when touches are moved on the workspace.
*/
public override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent) {
super.touchesMoved(touches, withEvent:event)
for touch in touches {
let touchPosition = touch.locationInView(_destinationView)

if state == .Possible {
// Check the distance between the original touch and this one.
let previousPosition = touch.previousLocationInView(_destinationView)
let distance = hypotf(Float(previousPosition.x - touchPosition.x),
Float(previousPosition.y - touchPosition.y))
// If the distance is sufficient, begin the touch.
if distance > _minPanDistance {
state = .Began
} else {
continue
}
} else if state == .Began || state == .Changed {
// Set the state to changed, so anything listening to the standard gesture recognizer can
// listen to standard gesture events.
state = .Changed
}

if let index = _touches.indexOf(touch) {
let block = _blocks[index]
// Ignore any touch beyond the first, if multiple are touching the same block.
if _blocks.indexOf(block) < index {
continue
}

if state == .Began {
// If the gesture just began, begin a touch on the delegate.
_blocks[index] = _target.blockTouched(self,
touchPosition: touchPosition,
block: block,
state: .Began)
continue
}

_blocks[index] = _target.blockTouched(self,
touchPosition: touchPosition,
block: block,
state: .Changed)
}
}
}

/**
Called when touches end on a workspace.
*/
public override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent) {
super.touchesEnded(touches, withEvent:event)
for touch in touches {
if let index = _touches.indexOf(touch) {
let block = _blocks[index]

_touches.removeAtIndex(index)
_blocks.removeAtIndex(index)

// Only end the drag if no other touches are dragging the block.
if _blocks.contains(block) {
continue
}

let touchPosition = touch.locationInView(_destinationView)
_target.blockTouched(self, touchPosition: touchPosition, block: block, state: .Ended)
}
}

if _touches.count == 0 {
if state == .Changed {
// If the gesture succeeded, end the gesture.
state = .Ended
} else {
// If the gesture never began, cancel the gesture.
state = .Cancelled
}
}
}

// MARK: - Private

/**
Utility function for finding the first ancestor that is a `BlockView`.

- Parameter view: The view to find an ancestor of
- Return: The first ancestor of the `UIView` that is a `BlockView`
*/
private func owningBlockView(view: UIView?) -> BlockView? {
var currentView = view
while !(currentView is BlockView) {
currentView = currentView?.superview
if currentView == nil {
return nil
}

if currentView == _originView {
return nil
}
}

return currentView as? BlockView
}
}
Loading

0 comments on commit 0d532ed

Please sign in to comment.