Reputation: 1424
Hopefully this isn't too vague for the mods.
I want to make a user interaction similar to the volume controls on some hifi's where you move a dial left or right to change the volume but rather than turning the dial complete revolutions you turn it left or right slightly and the more you turn it the faster the volume changes until you let go and it pings back to the middle.
In my app I want to use a UIPanGestureRecogniser where as the user pans up from the middle the volume goes up, the further from the middle the faster the increase. When they pan below the mid point of the screen the volume goes down, again faster the further from the middle you are.
The area I'm stuck is how to make this happen with out locking up the UI. I can't just use the gestureRecognizer action selector as this is only called when there is movement, for this interaction to work the user will often keep their finger in a single location while waiting for the right volume to be reached.
I feel like I want to set a loop running outside the gesturerecogniser selector and have it monitor a class variable that the gets updated when the gesture moves or ends. If it do this in the gesturerecogniser selector it will get keep running....
If this were an embedded system I would just set up some kind of interrupt based polling to check what the input control was at and keep adding to the volume until it was back to middle - can't find the comparable for iOS here.
Suggestions would be welcome, sorry mods if this is too vague - it's more of a framework methodology question that a specific code issue.
Upvotes: 0
Views: 337
Reputation: 1198
This sounds like a fun challenge. I don't totally understand the hifi thing. But what I'm picturing is a circular knob with a small dot at 9 o'clock almost at the edge. When you turn the knob to the right the dot moves towards 12 oclock and the volume increases, accelerating faster the further the dot is from 9 o'clock. It continues to increase as long as the dot is above 9, just the acceleration of increase changes.
When you turn left, the dot goes towards 6 oclock, and the volume decreases, and the acceleration depends on the radial distance from 9 oclock. If this assumption is correct, I think the following would work..
I would solve this with a little trigonometry. To get the acceleration, you need the angle from the 9 o'oclock axis(negative x axis). A positive angle is increasing the volume, a negative angle is decreasing the volume, and the acceleration depends on the degree of rotation. This angle will also give you a transform that you can apply to the view to change the dot's place. I don't think this will take anything too fancy code wise.. In this case, I have made the maximum rotation to be 90 degree, or pi/2 radians. If you can actually turn the knob more than that, it would take some code changes.
var volume: Double = 0
var maxVolume: Double = 100
var increasing : Bool
var multiplier: Double = 0
var cartesianTransform = CGAffineTransform()
let knobViewFrame = CGRect(x: 50, y: 50, width: 100, height: 100)
let knobRadius: CGFloat = 45
let knob = UIView()
var timer : NSTimer?
func setTransform() {
self.cartesianTransform = CGAffineTransform(a: 1/knobRadius, b: 0, c: 0, d: -1/knobRadius, tx: knobViewFrame.width/2, ty: knobViewFrame.height * 1.5)
// admittedly, I always have to play with these things to get them right, so there may be some errors. This transform should turn the view into a plane with (0,0) at the center, and the knob's circle at (-1,0)
}
func panKnob(pan: UIPanGestureRecognizer) {
let pointInCartesian = CGPointApplyAffineTransform(pan.locationInView(pan.view!), cartesianTransform)
if pan.state == .Began {
increasing = pointInCartesian.y > 0
timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: #selector(increaseVolume), userInfo: nil, repeats: true)
}
let arctangent = CGFloat(M_PI) - atan2(pointInCartesian.y, pointInCartesian.x)
let maxAngle = increasing ? CGFloat(M_PI)/2 : -CGFloat(M_PI)/2
var angle: CGFloat
if increasing {
angle = arctangent > maxAngle ? maxAngle : arctangent
} else {
angle = arctangent < maxAngle ? maxAngle : arctangent
}
knob.transform = CGAffineTransformMakeRotation(angle)
self.multiplier = Double(angle) * 10
if pan.state == .Ended || pan.state == .Cancelled || pan.state == .Failed {
timer?.invalidate()
UIView.animateWithDuration(0.75, delay: 0, options: .CurveEaseIn, animations: {self.knob.transform = CGAffineTransformIdentity}, completion: nil)
}
}
func increaseVolume() {
let newVolume = volume + multiplier
volume = newVolume > maxVolume ? maxVolume : (newVolume < 0 ? 0 : newVolume)
if volume == maxVolume || volume == 0 || multiplier == 0 {
timer?.invalidate()
}
}
I haven't tested the above, but this seemed like a cool puzzle. The multiplier only changes when the angle changes, and the volume keeps adding the multiplier while the timer is valid. If you want the volume to accelerate continuously without angle changing, move the multiplier change to the timer's selector, and keep the angle as a class variable so you know how fast to accelerate it.
edit: You could probably do it without the transform, by just getting the delta between the dot and the locationInView.
Upvotes: 1
Reputation: 1294
Interesting question I wrote a sample for you which must be what you want:
Objective-C code:
#import "ViewController.h"
@interface ViewController ()
@property float theValue;
@property NSTimer *timer;
@property bool needRecord;
@property UIView *dot;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.needRecord = NO;
self.theValue = 0;
UIView *circle = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)];
circle.layer.borderWidth = 3;
circle.layer.borderColor = [UIColor redColor].CGColor;
circle.layer.cornerRadius = 25;
circle.center = self.view.center;
[self.view addSubview:circle];
self.dot = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 40, 40)];
self.dot.backgroundColor = [UIColor redColor];
self.dot.layer.cornerRadius = 20;
self.dot.center = self.view.center;
[self.view addSubview:self.dot];
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panHandle:)];
[self.dot addGestureRecognizer:pan];
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
}
-(void)panHandle:(UIPanGestureRecognizer *)pan{
CGPoint pt = [pan translationInView:self.view];
// NSLog([NSString stringWithFormat:@"pt.y = %f",pt.y]);
switch (pan.state) {
case UIGestureRecognizerStateBegan:
[self draggingStart];
break;
case UIGestureRecognizerStateChanged:
self.dot.center = CGPointMake(self.view.center.x, self.view.center.y + pt.y);
break;
case UIGestureRecognizerStateEnded:
[self draggingEnned];
break;
default:
break;
}
}
-(void)draggingStart{
self.needRecord = YES;
}
-(void)draggingEnned{
self.needRecord = NO;
[UIView animateWithDuration:0.5 animations:^{
self.dot.center = self.view.center;
}];
}
-(void)timerFire{
if (self.needRecord) {
float distance = self.dot.center.y - self.view.center.y;
// NSLog([NSString stringWithFormat:@"distance = %f",distance]);
self.theValue -= distance/1000;
NSLog([NSString stringWithFormat:@"theValue = %f",self.theValue]);
}
}
@end
I'm learning Swift right now, so if you need, this is Swift code:
class ViewController: UIViewController {
var lbInfo:UILabel?;
var theValue:Float?;
var timer:NSTimer?;
var needRecord:Bool?;
var circle:UIView?;
var dot:UIView?;
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
needRecord = false;
theValue = 0;
lbInfo = UILabel(frame: CGRect(x: 50, y: 50, width: UIScreen.mainScreen().bounds.width-100, height: 30));
lbInfo!.textAlignment = NSTextAlignment.Center;
lbInfo!.text = "Look at here!";
self.view.addSubview(lbInfo!);
circle = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50));
circle!.layer.borderWidth = 3;
circle!.layer.borderColor = UIColor.redColor().CGColor;
circle!.layer.cornerRadius = 25;
circle!.center = self.view.center;
self.view.addSubview(circle!);
dot = UIView(frame: CGRect(x: 0, y: 0, width: 40, height: 40));
dot!.backgroundColor = UIColor.redColor();
dot!.layer.cornerRadius = 20;
dot!.center = self.view.center;
self.view.addSubview(dot!);
let pan = UIPanGestureRecognizer(target: self, action: #selector(ViewController.panhandler));
dot!.addGestureRecognizer(pan);
timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: #selector(ViewController.timerFire), userInfo: nil, repeats: true)
}
func panhandler(pan: UIPanGestureRecognizer) -> Void {
let pt = pan.translationInView(self.view);
switch pan.state {
case UIGestureRecognizerState.Began:
draggingStart();
case UIGestureRecognizerState.Changed:
self.dot!.center = CGPoint(x: self.view.center.x, y: self.view.center.y + pt.y);
case UIGestureRecognizerState.Ended:
draggingEnded();
default:
break;
}
}
func draggingStart() -> Void {
needRecord = true;
}
func draggingEnded() -> Void {
needRecord = false;
UIView.animateWithDuration(0.1, animations: {
self.dot!.center = self.view.center;
});
}
@objc func timerFire() -> Void {
if(needRecord!){
let distance:Float = Float(self.dot!.center.y) - Float(self.view.center.y);
theValue! -= distance/1000;
self.lbInfo!.text = String(format: "%.2f", theValue!);
}
}
}
Hope it can help you.
If you still need some advice, just leave it here, I will check it latter.
Upvotes: 2