Skip to content

Instantly share code, notes, and snippets.

@jonlipsky
Created August 21, 2014 18:08
Show Gist options
  • Save jonlipsky/a960e8b91550b5407171 to your computer and use it in GitHub Desktop.
Save jonlipsky/a960e8b91550b5407171 to your computer and use it in GitHub Desktop.
TouchWindow.cs
using System;
using System.Collections.Generic;
using System.Drawing;
using MonoTouch.CoreGraphics;
using MonoTouch.UIKit;
using MonoTouch.Foundation;
using MonoTouch.ObjCRuntime;
/**
* Copyright (c) 2014 Jon Lipsky
*
* Licensed under The MIT License (MIT)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom
* the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*
* This is loosely based on the Objective-C library "Fingertips" which can be found here:
* https://github.com/mapbox/Fingertips
*
* Usage:
*
* Update your app delegate to create a "TouchWindow" as opposed to a "UIWindow"
*
* public override bool FinishedLaunching(UIApplication app, NSDictionary options)
* {
* window = new TouchWindow (UIScreen.MainScreen.Bounds);
*
* applicationController = new ApplicationController ();
* window.RootViewController = applicationController;
* window.MakeKeyAndVisible ();
*
* return true;
* }
*
* By default, it will only display the touchpoints when the screen is mirrored (such as when using AirPlay);
* however you can use the Mode property to configure that.
*/
namespace Elevenworks.MonoTouch
{
/// <summary>
/// The display mode for the TouchWindow.
/// </summary>
public enum TouchWindowMode
{
/// <summary>
/// Never show the touchpoints
/// </summary>
ShowNever,
/// <summary>
/// Only show the touchpoints when the screen is being mirrored.
/// </summary>
ShowWhenMirrored,
/// <summary>
/// Always show the touchpoints.
/// </summary>
ShowAlways
}
/// <summary>
/// A UIWindow subclass that can display the touchpoints on the screen.
/// </summary>
public class TouchWindow : UIWindow, IDisposable
{
private const float AnimationDuration = 0.3f;
private List<NSObject> observers = new List<NSObject>();
private Dictionary<UITouch, TouchView> touchViews = new Dictionary<UITouch, TouchView> ();
private UIImage touchImage;
private UIWindow overlayWindow;
private TouchWindowMode mode = TouchWindowMode.ShowWhenMirrored;
private bool active;
private bool removalScheduled;
public TouchWindow (IntPtr handle) : base (handle)
{
}
public TouchWindow (RectangleF frame) : base (frame)
{
observers.Add(NSNotificationCenter.DefaultCenter.AddObserver(UIScreen.DidConnectNotification, (obj) => { UpdateActive(); } ));
observers.Add(NSNotificationCenter.DefaultCenter.AddObserver(UIScreen.DidDisconnectNotification, (obj) => { UpdateActive(); }));
UpdateActive();
}
public TouchWindowMode Mode
{
get { return mode; }
set
{
mode = value;
UpdateActive ();
}
}
public UIWindow Overlay
{
get
{
if (overlayWindow == null)
{
overlayWindow = new TouchOverlayWindow(Frame);
overlayWindow.UserInteractionEnabled = false;
overlayWindow.WindowLevel = UIWindowLevel.StatusBar;
overlayWindow.BackgroundColor = UIColor.Clear;
overlayWindow.Hidden = false;
}
return overlayWindow;
}
set
{
overlayWindow = value;
}
}
public UIImage TouchImage
{
get
{
if (touchImage == null)
{
UIGraphics.BeginImageContextWithOptions(new SizeF(50,50), false, 0);
var context = UIGraphics.GetCurrentContext ();
context.SetLineWidth (2);
context.SetStrokeColor (new [] { 0f, 0f, 0f, 1f }); // Black
context.SetFillColor (new [] { 0.5f, 0.5f, 0.5f, .5f });
context.AddEllipseInRect (new RectangleF (3, 3, 44, 44));
context.FillPath ();
context.StrokePath ();
touchImage = UIGraphics.GetImageFromCurrentImageContext ();
UIGraphics.EndImageContext();
}
return touchImage;
}
set
{
touchImage = value;
}
}
protected override void Dispose (bool disposing)
{
NSNotificationCenter.DefaultCenter.RemoveObservers(observers);
observers.Clear ();
}
public bool IsMirrored()
{
foreach (UIScreen screen in UIScreen.Screens)
{
if (screen.MirroredScreen != null) return true;
}
return false;
}
private void UpdateActive()
{
bool wasActive = active;
switch (mode)
{
case TouchWindowMode.ShowAlways:
active = true;
break;
case TouchWindowMode.ShowWhenMirrored:
active = IsMirrored ();
break;
default:
active = false;
}
if (wasActive && !active)
{
RemoveAllTouches ();
}
}
public override void SendEvent (UIEvent evt)
{
if (active)
{
var allTouches = evt.AllTouches;
if (allTouches != null)
{
foreach (var touchObject in allTouches)
{
var touch = (UITouch)touchObject;
switch (touch.Phase)
{
case UITouchPhase.Began:
case UITouchPhase.Moved:
case UITouchPhase.Stationary:
{
TouchView touchView;
touchViews.TryGetValue (touch, out touchView);
if (touchView == null)
{
touchView = new TouchView (TouchImage, touch);
Overlay.AddSubview (touchView);
touchViews.Add (touch, touchView);
}
if (!touchView.Removing)
{
touchView.Center = touch.LocationInView (Overlay);
touchView.RemoveAfter = touch.Timestamp + .2;
touchView.RemoveAutomatically = ShouldRemoveAutomatically (touch);
}
break;
}
case UITouchPhase.Ended:
case UITouchPhase.Cancelled:
{
RemoveTouch (touch, true);
break;
}
}
}
}
base.SendEvent (evt);
ScheduleRemoval ();
}
else
{
base.SendEvent (evt);
}
}
private static Selector removeExpiredTouches = new Selector("RemoveExpiredTouches");
public void ScheduleRemoval()
{
if (removalScheduled) return;
removalScheduled = true;
PerformSelector (removeExpiredTouches, this, 0.1f);
}
[Export("RemoveExpiredTouches")]
public void RemoveExpiredTouches()
{
var now = NSProcessInfo.ProcessInfo.SystemUptime;
foreach (var subView in Overlay.Subviews)
{
var touchView = subView as TouchView;
if (touchView != null)
{
if (touchView.RemoveAutomatically)
{
if (now > touchView.RemoveAfter)
{
var touch = touchView.Touch;
RemoveTouch (touch, true);
}
}
}
}
removalScheduled = false;
if (Overlay.Subviews.Length > 0)
{
ScheduleRemoval ();
}
}
public void RemoveAllTouches()
{
foreach (var subView in Overlay.Subviews)
{
var touchView = subView as TouchView;
if (touchView != null)
{
var touch = touchView.Touch;
RemoveTouch (touch, true);
}
}
removalScheduled = false;
}
private void RemoveTouch(UITouch touch, bool animated)
{
TouchView touchView;
if (touchViews.TryGetValue (touch, out touchView))
{
touchViews.Remove (touch);
if (!touchView.Removing)
{
touchView.Removing = true;
UIView.Animate (AnimationDuration, () =>
{
touchView.Frame = new RectangleF (
touchView.Center.X - touchView.Frame.Size.Width,
touchView.Center.Y - touchView.Frame.Size.Height,
touchView.Frame.Size.Width * 2,
touchView.Frame.Size.Height * 2);
touchView.Alpha = 0.0f;
}, () =>
{
touchView.RemoveFromSuperview ();
});
}
}
}
private bool ShouldRemoveAutomatically(UITouch touch)
{
UIView view = touch.View;
if (view != null)
{
var location = touch.LocationInView (view);
view = view.HitTest (location, null);
while (view != null)
{
if (view is UITableViewCell)
{
foreach (UIGestureRecognizer recognizer in touch.GestureRecognizers)
{
if (recognizer is UISwipeGestureRecognizer)
{
return true;
}
}
}
if (view is UITableView)
{
if (touch.GestureRecognizers.Length == 0)
{
return true;
}
}
view = view.Superview;
}
}
return false;
}
}
public class TouchView : UIImageView
{
private readonly UITouch touch;
public TouchView (UIImage image, UITouch touch) : base (image)
{
this.touch = touch;
}
public double RemoveAfter {get; set;}
public bool RemoveAutomatically {get; set;}
public bool Removing {get; set;}
public UITouch Touch
{
get { return touch; }
}
}
public class TouchOverlayWindow : UIWindow
{
public TouchOverlayWindow(RectangleF rect) : base (rect)
{
}
public override UIViewController RootViewController
{
get
{
foreach (var window in UIApplication.SharedApplication.Windows)
{
if (this == window) continue;
var realRootViewController = window.RootViewController;
if (realRootViewController != null)
{
return realRootViewController;
}
}
return base.RootViewController;
}
set
{
base.RootViewController = value;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment