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

Adding a custom gesture recognizer for workspace-level interaction. #172

Merged
merged 1 commit into from
Sep 14, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 */; };
30DC64DB1D875C88002D2186 /* BlocklyPanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC64DA1D875C88002D2186 /* 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 @@ -244,6 +245,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>"; };
30DC64DA1D875C88002D2186 /* 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 @@ -574,6 +576,7 @@
children = (
FAA870131C642805000C7C61 /* View Controllers */,
FA4BB5681B7C03EA000980E9 /* Views */,
30DC64DA1D875C88002D2186 /* BlocklyPanGestureRecognizer.swift */,
FAA870111C64276A000C7C61 /* WorkspaceBezierPath.swift */,
);
path = UI;
Expand Down Expand Up @@ -1069,6 +1072,7 @@
FAD652741CB87B2200F73F11 /* DefaultLayoutEngine.swift in Sources */,
FA42D7EB1C605A5F000C8EB4 /* FieldDateView.swift in Sources */,
FA27267A1B83C54900777B49 /* BlockLayout.swift in Sources */,
30DC64DB1D875C88002D2186 /* BlocklyPanGestureRecognizer.swift in Sources */,
FAFAEE531CDABED400698179 /* FieldNumberView.swift in Sources */,
FAA870091C64272C000C7C61 /* Logging.swift in Sources */,
FA3FD1551CF7C886005B6D0F /* XMLConstants.swift in Sources */,
Expand Down
4 changes: 4 additions & 0 deletions Blockly/Code/Control/Dragger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ public class Dragger: NSObject {
return
}

// Set dragging to true, so the block groups displays with correct alpha through changes to the
// group mid-drag
layout.rootBlockGroupLayout?.dragging = true

// Set the connection manager group to "drag mode" to avoid wasting compute cycles during the
// drag
gestureData.connectionGroup.dragMode = true
Expand Down
336 changes: 336 additions & 0 deletions Blockly/Code/UI/BlocklyPanGestureRecognizer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
/*
* 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: class {
/**
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 block: The `BlockView` being touched.
Parameter touch: The `UITouch` hitting the block.
Parameter touchState: The `TouchState` for this individual touch.
*/
func blocklyPanGestureRecognizer(gesture: BlocklyPanGestureRecognizer,
didTouchBlock block: BlockView,
touch: UITouch,
touchState: BlocklyPanGestureRecognizer.TouchState)
}

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

/// 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 states of the individual touches in the `BlocklyPanGestureRecognizer`
*/
@objc
public enum BKYBlocklyPanGestureRecognizerTouchState: Int {
/// Specifies an individual touch has just begun on a `BlockView`
case Began = 0,
/// Specifies an individual touch has just changed on a `BlockView`
Changed,
/// Specifies an individual touch has just ended on a `BlockView`
Ended
}
public typealias TouchState = BKYBlocklyPanGestureRecognizerTouchState

// TODO:(#176) - Replace maximumTouches

/// Maximum number of touches handled by the recognizer
public var maximumTouches = Int.max

/// The minimum distance for the gesture recognizer to count as a pan, in the UIView coordinate
/// system.
public var minimumPanDistance: Float = 2.0

/// The delegate this gestureRecognizer operates on (`WorkbenchViewController` by default).
public weak var targetDelegate: BlocklyPanGestureDelegate?

// MARK: - Initializer

/**
Initializer for the BlocklyPanGestureRecognizer

- Parameter targetDelegate: The object that listens to the gesture recognizer callbacks
*/
public init(targetDelegate: BlocklyPanGestureDelegate)
{
self.targetDelegate = targetDelegate
super.init(target: nil, action: nil)
delaysTouchesBegan = false
}

// MARK: - Super

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

// If the hit tested view is not an ancestor of a block, cancel the touch(es).
if let attachedView = view,
let hitView = attachedView.hitTest(location, withEvent: event),
let block = owningBlockView(hitView)
where _touches.count < maximumTouches
{
let blockAlreadyTouched = _blocks.contains(block)
_touches.append(touch)
_blocks.append(block)

// 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) && !blockAlreadyTouched {
// Start the drag.
targetDelegate?.blocklyPanGestureRecognizer(self,
didTouchBlock: block,
touch: touch,
touchState: .Began)
}
}
}

// 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)
// If the gesture has yet to start, check if it should start.
if state == .Possible {
for touch in touches {
let touchPosition = touch.locationInView(view)

// Check the distance between the original touch and this one.
let previousPosition = touch.previousLocationInView(view)
let distance = hypotf(Float(previousPosition.x - touchPosition.x),
Float(previousPosition.y - touchPosition.y))
// If the distance is sufficient, begin the gesture.
if distance > minimumPanDistance {
state = .Began
break
// If not, check the next touch to see if it should begin the gesture.
} else {
continue
}
}

// If the gesture still hasn't started, end here.
if state == .Possible {
return
}
// Set the state to changed, so anything listening to the standard gesture recognizer can
// listen to standard gesture events. Note UIGestureRecognizer requires setting the state to
// changed even if it's already there, to fire the correct delegates.
} else if state == .Began || state == .Changed {
state = .Changed
}

// When we begin the gesture, start a touch on every currently-touched block.
if state == .Began {
for touch in _touches {
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
}

targetDelegate?.blocklyPanGestureRecognizer(self,
didTouchBlock: block,
touch: touch,
touchState: .Began)
}
}
} else {
for touch in touches {
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
}

targetDelegate?.blocklyPanGestureRecognizer(self,
didTouchBlock: block,
touch: touch,
touchState: .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
}

// TODO:(#175) Fix blocks jumping from touch to touch when one block is hit by two touches.

targetDelegate?.blocklyPanGestureRecognizer(self,
didTouchBlock: block,
touch: touch,
touchState: .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
}
}
}

/**
Called when touches are cancelled on a workspace.
*/
public override func touchesCancelled(touches: Set<UITouch>, withEvent event: UIEvent) {
super.touchesCancelled(touches, withEvent: event)
for touch in touches {
if let index = _touches.indexOf(touch) {
_touches.removeAtIndex(index)
_blocks.removeAtIndex(index)
}
}

if _touches.count == 0 {
state = .Cancelled
}
}

/**
Manually cancels the touches of the gesture recognizer.
*/
public func cancelAllTouches() {
_touches.removeAll()
_blocks.removeAll()

state = .Cancelled
}

/**
Calculates the delta of the first touch in a given view.

- Parameter view: The view to calculate the location of the touch position.
- Return: The difference between the current position and the previous position.
*/
public func firstTouchDeltaInView(view: UIView?) -> CGPoint {
if _touches.count > 0 {
let currentPosition = _touches[0].locationInView(view)
let previousPosition = _touches[0].previousLocationInView(view)

return currentPosition - previousPosition
}

return CGPointZero
}

/**
Updates the block at the given index, when the `BlockView` has changed (typically when it is
copied to a new workspace.)

- Parameter block: The old `BlockView` to be tracked.
- Parameter newBlock: The new `BlockView` to be tracked.
*/
public func replaceBlock(block: BlockView, withNewBlock newBlock: BlockView) {
guard let touchIndex = _blocks.indexOf(block) else {
return
}

_blocks[touchIndex] = newBlock
}

/**
Checks if any touch handled by the gesture recognizer is inside a given view.

- Parameter view: The `UIView` to be checked against.
*/
public func isTouchingView(otherView: UIView) -> Bool {
for touch in _touches {
let touchPosition = touch.locationInView(otherView)
if CGRectContainsPoint(otherView.bounds, touchPosition) {
return true
}
}

return false
}

// 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 == self.view {
return nil
}
}

return currentView as? BlockView
}
}
Loading