Monday, May 31, 2010

Wall Paper Editor: Refactoring view controls

The next step after I figured out a few tricks to get views to move around and edit, was to refractor the view controls into a separate class, and place it in my static library (since it's a useful utility that I might want to use elsewhere).

I create a new controller class that will manage the views, it basically implements the basic responder calls for touch events.  It also includes a protocol that defines how it calls back to the parent view.  Note that I made all the methods optional, this causes a little extra work in my class (I have to check for the existence of the method before calling it), but it enables simpler implementation of listeners:


The callback protocol


Note everything is optional, this allows a routine that is only interested in one gesture to only implement that single method.

// Define several protocols that have view level gesture operations.  These can either be from custom code
// or from UIGesture reconizers.
@protocol ViewMoveListener

// Declare all of these optional, so that we can choose to implement only what we need.
@optional
// Called when the view is first selected
- (void) startChangingView:(UIView*) view;
// called when a view is stopped being acted on.
- (void) endChangingView:(UIView*) view isSuccess:(bool) success;
// Called when the view is dragged.
- (void) singleDrag:(UIView*) view from:(CGPoint) startLoc to:(CGPoint) endLoc;
// Called when a pinch action is done.
- (void) pinchAction:(UIView*) view originalRect:(CGRect) orginal newRect:(CGRect) new;
// when a single tap is done on a view.
- (void) singleTap:(UIView*) subViewTapped;
// when a double tap is performed.
- (void) doubleTap:(UIView*) subViewDoubleTapped;
@end



Definition of the controller class



@interface ViewMover : NSObject {
id<ViewMoveListener> delegate;
UIView *parentView;
NSSet *childViews;
UIView *currentView;
CGRect startRect;
CGRect currentRect;
bool inPinch;
}

@property (nonatomic,retain) id delegate;
@property (nonatomic,retain) UIView *parentView;
@property (nonatomic,retain) NSSet *childViews;
@property (nonatomic,retain) UIView *currentView;
@property (nonatomic) CGRect startRect;
@property (nonatomic) CGRect currentRect;
@property (nonatomic) bool inPinch;
- (void)findPinchView:(NSSet*)touches;

- (CGRect) makeRect:(CGPoint) p1 point2: (CGPoint) p2;


// This is not a formal responer, instead you can delegate your responder callbacks to this class.
// Note that for Ipad this will probably implement gestures which are formally hooked into the 
// responder chain by the core classes.   Alas as this time they are not available.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

@end



Implementation of the controller class


The code here is very similar to what I had before, it's just more generic.    Note the use of

if ([delegate respondsToSelector:@selector(endChangingView: isSuccess:)])
{
[delegate endChangingView:currentView isSuccess:true];
}
to check to see if the delegate class implements the required method.





@implementation ViewMover
@synthesize  delegate;
@synthesize  parentView;
@synthesize  childViews;
@synthesize  currentView;
@synthesize  startRect;
@synthesize  currentRect;
@synthesize  inPinch;



- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(@"touches count %d, event touches for my view %d",[touches count],
  [[event touchesForView:parentView] count]);
if ([touches count] == [[event touchesForView:parentView] count])
{
NSLog(@"Ending view");
if ([delegate respondsToSelector:@selector(endChangingView: isSuccess:)])
{
[delegate endChangingView:currentView isSuccess:true];
}

NSLog(@"Event %@",event);
self.currentView=nil;
NSLog(@"In Touches ended count=%d",[touches count]);
inPinch=false;
}
}

- (void)findPinchView:(NSSet *)touches
{
NSArray *objects=[touches allObjects];
UITouch *f1=[objects objectAtIndex:0];
UITouch *f2=[objects objectAtIndex:1];
CGPoint p1=[f1 locationInView:parentView];
CGPoint p2=[f2 locationInView:parentView];
startRect=[self makeRect:p1 point2:p2];
double differenceInArea=10000;
self.currentView=nil;
// Find best match for the starting pinch.  The best match is the rectangle that is closest to the target one.
for (UIView *view in childViews)
{
CGRect intersection=CGRectIntersection(startRect,view.frame);
if (CGRectIsNull(intersection))
continue;
double area=fabs(view.frame.size.width*view.frame.size
.height
intersection.size.width*intersection.size.height);
if (area
{
differenceInArea=area;
self.currentView=view;
}
}
if (currentView!=nil)
inPinch=true;
else {
inPinch=false;
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"In touchesBegan11 count =%d event touches count=%d",[touches count],[[event allTouches] count]);
    // We will do some stuff here.
    // We only support single touches, so anyObject retrieves just that touch from touches
if (parentView==nil)
{
NSLog(@"Parent view is nil");
}
touches=[event allTouches];
if ([touches count]==2)
{
[self findPinchView:touches];
}else if ([touches count]==1)
{
NSLog(@"Child count is %d",[childViews count]);
for (UIView *view in childViews)
{
NSLog(@"Checking view ");
UITouch *touch = [touches anyObject];
if (touch==nil)
{
NSLog(@"NillTouch");
return;
}
CGPoint location = [touch locationInView:parentView ];

if (CGRectContainsPoint(view.frame
, location))
{
NSLog(@"Found point for view");
self.currentView=view;
break;
}
}
if ([delegate respondsToSelector:@selector(startChangingView:)])
{
NSLog(@"Sending start change");
[delegate startChangingView:currentView];
}
}  
}

- (CGRect) makeRect:(CGPoint) p1 point2: (CGPoint) p2;
{
int width= p2.x-p1.x;
int height=p2.y-p1.y;
CGRect returnValue=CGRectMake(p1.x, p1.y, width, height);
return CGRectStandardize(returnValue);
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"In touchesMovedd count =%d event touches count=%d",[touches count],[[event allTouches] count]);
touches=[event allTouches];
if (currentView==nil)
return;
if ([touches count]==2)
{
NSLog(@"Doing pinch");
NSArray *objects=[touches allObjects];
UITouch *f1=[objects objectAtIndex:0];
UITouch *f2=[objects objectAtIndex:1];
CGPoint p1=[f1 locationInView:parentView];
CGPoint p2=[f2 locationInView:parentView];
CGRect lastRect=[self makeRect:p1 point2:p2];
if (!inPinch)
{
[self findPinchView:touches];
// Nothing more to be done here.
return;
}
if ([delegate respondsToSelector:@selector(pinchAction:originalRect:newRect:)])
{
NSLog(@"calling delegate for PINCH");
[delegate pinchAction:currentView originalRect:startRect newRect:lastRect];
}
}else if (!inPinch) {

UITouch *touch = [touches anyObject];
// If the touch was in the placardView, move the placardView to its location
    
NSLog(@"Doing move ");
CGPoint location = [touch locationInView:parentView ];
if ([delegate respondsToSelector:@selector(pinchAction:originalRect:newRect:)])
{
[delegate singleDrag:currentView from:currentView.center to:location];
}
}
}


- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
}



Registering with the controller class


I just overrode ViewDidLoad to register my controller class. I then set the parent view of the controller class to the view that all the work will occur on.  I also set the delegate to myself.  viewMoveHandler is an instance of ViewMover.

- (void) viewDidLoad
{
self.viewMoveHandler=[[ViewMover alloc]init];
viewMoveHandler.parentView=self.image;
viewMoveHandler.delegate=self;
}


Calling the methods of the controller class with touch events


I now delegate touch events to the controller.

- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(@"Touches ended main view");
[viewMoveHandler touchesEnded:touches withEvent:event];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"Touches began, main class");

[viewMoveHandler touchesBegan:touches withEvent:event];
}


- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"In Main Tocuhes moved.");
[viewMoveHandler   touchesMoved:touches withEvent:event];
}



Handling callbacks form the controller class


The implementation now has specific named methods for various actions.  The view controller takes the moves and generates status information about the move/selection.  Then these routines take them and do the required action.  Things are no longer in a big if statement either.


// called when a view is stopped being acted on.
- (void) endChangingView:(UIView*) view isSuccess:(bool) success
{
self.currentPinchView=nil;
}

// Called when the view is dragged.
- (void) singleDrag:(UIView*) view from:(CGPoint) startLoc to:(CGPoint) endLoc
{
NSLog(@"Doing move ");
// If the touch was in the placardView, move the placardView to its location
   
if (view!=nil)
{
view.center=endLoc;
}
    
}

// Called when a pinch action is done.
- (void) pinchAction:(UIView*) view originalRect:(CGRect) orginal newRect:(CGRect) newRect
{
NSLog(@"pinchAction");
// We should be able to scale up evenly this way.
double dheight=newRect.size.height-orginal.size.height;
double dwidth=newRect.size.width-orginal.size.width;
NSLog(@"dh=%lf, dw=%lf",dheight,dwidth);
double newWidth=imageStartSize.size.width+dwidth;
double newHeight=imageStartSize.size.height+dheight;
NSLog(@"1. newWidth=%lf, newHeight=%lf",newWidth,newHeight);
// We calculate scales based on the size of the pinch, but apply the scales to the image being
// manipulated.
if (newWidth<5)
newWidth=5;
if (newHeight<5)
newHeight=5;
if (newWidth>self.view.frame.size.width)
newWidth=self.view.frame.size.width;
if (newHeight>self.view.frame.size.height)
newHeight=self.view.frame.size.height;
NSLog(@"2. newWidth=%lf, newHeight=%lf",newWidth,newHeight);
double dx=CGRectGetMidX(newRect)-CGRectGetMidX(orginal);
double dy=CGRectGetMidY(newRect)-CGRectGetMidY(orginal);
view.frame =CGRectMake(imageStartSize.origin.x+dx,
newRect.origin.y+dy,
newWidth,
newHeight);
}


// Called when the view is first selected
- (void) startChangingView:(UIView*) view
{
NSLog(@"Start changing view ");
imageStartSize=view.frame;
}



What this refactoring did was to take the general case, responding to touch events and handing selection, moving and pinching, and put it in a  separate class.  Then the specifics of WHAT to do with those was left to the existing controller class.

I just added a simple delegate to my controller class.  I probably could register some complicated event forwarding, but I really don't need to do so.

My controller class will eventually use the Apple gesture recognizer classes, I'm sure that they are more robust than my hand generated code, but the nice thing is that I can switch out the gestures without modifying my business case code.

No comments:

Post a Comment