在下面的示例中,用手势拖动Layer转动,当手势结束时,会播放动画继续让Layer沿着圆的轨道转动一会儿。
这里包括两个动作,以及针对这两个动作的处理。即:
- pan手势,即拖动,这时不播放动画,要确保Layer的运动是按照圆的轨迹来移动,而不是拖动到哪里到哪里
- pan手势的结束,其实应该用swipe手势,这里是简单的监控到pan手势结束,然后按照当前速度,取一个最小值,当超过该值的时候,播放动画继续转动一段时间
这是自定义视图的初始化代码部分:
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame]; if (self) { [self initLayers]; UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(doPanAction:)]; [panGesture setMaximumNumberOfTouches:2]; [self addGestureRecognizer:panGesture]; [panGesture release]; } return self; }- (void) initLayers{
startLayer=[CALayer layer]; UIImage *image=[UIImage imageNamed:@"4.png"]; startLayer.bounds=CGRectMake(0, 0, image.size.width, image.size.height); startLayer.contents=(id)[image CGImage]; startLayer.position=CGPointMake(768/2-RADIAS, 1024/2); [self.layer addSublayer:startLayer]; [image release]; }
这里增加了一个Layer,就是上面看到的小星星。另外,监听pan手势。
当监听到pan手势时,调用:
- (void)doPanAction:(UIPanGestureRecognizer *)gestureRecognizer{
CGPoint locationInView = [gestureRecognizer locationInView:self]; NSLog(@"pan … x: %f, y: %f",locationInView.x,locationInView.y); CALayer *layer=[self.layer hitTest:locationInView]; if (layer==startLayer) { CGPoint velocityInView=[gestureRecognizer velocityInView:self]; NSLog(@"catch it! velocity.x: %f, velocity.y: %f",velocityInView.x,velocityInView.y); struct PanLocationData panData; panData.panLocation=locationInView; panData.currentVelocity=velocityInView; panData.currentLocation=startLayer.position; [CATransaction begin]; [CATransaction setValue:[NSNumber numberWithBool:YES] forKey:kCATransactionDisableActions]; startLayer.position=[self getNextPanLocation:panData]; [CATransaction commit];
这里没有写出该方法的最后几行,主要是那些是用于播放pan后的动画的,暂且忽略。
这里最重要的是拖动的下一个坐标点不是当前pan的坐标点,而是圆的轨迹点。需要另外一个方法来计算:
- (CGPoint) getNextPanLocation:(struct PanLocationData) data{
//防止坐标越界 if (data.panLocation.x<MIN_X) { data.panLocation.x=MIN_X; } if (data.panLocation.x>MAX_X) { data.panLocation.x=MAX_X; } if (data.panLocation.y<MIN_Y) { data.panLocation.y=MIN_Y; } if (data.panLocation.y>MAX_Y) { data.panLocation.y=MAX_Y; } //设置根据x坐标和y坐标的定位点变量 CGPoint xLocation=data.panLocation,yLocation=data.panLocation; //根据x坐标获得y坐标 if (xLocation.y<CENTER_LOCATION_Y) { xLocation.y=-[self getLocationY:xLocation.x]+CENTER_LOCATION_Y; }else{ xLocation.y=[self getLocationY:xLocation.x]+CENTER_LOCATION_Y; } if (yLocation.x<CENTER_LOCATION_Y) { yLocation.x=-[self getLocationX:yLocation.y]+CENTER_LOCATION_X; }else{ yLocation.x=[self getLocationX:yLocation.y]+CENTER_LOCATION_X; } CGPoint returnPoint=xLocation; //在接近x极值时切换到根据y坐标定位x坐标 if (xLocation.x<MIN_X+0.1 || xLocation.x>MAX_X-0.1) { returnPoint= yLocation; } //防止出现跳动回退的情况,即手势向前拖动,图形向后跳动 if (data.currentVelocity.x*(returnPoint.x-data.currentLocation.x)<0) { returnPoint=data.currentLocation; } return returnPoint; }
代码写到这里,发现了个问题,用手势拖拽一个小的layer做弧形移动,问题很大,比如在接近左侧或者右侧边缘时,上下拖动Layer的时候很困难。因为这时x的增量不起作用了,而计算坐标是以x点为基础的。因此在边缘情况下做了个处理,用y坐标来计算x坐标。
计算坐标的方法:
- (float)getLocationY:(float) x{
return RADIAS*sqrt(1-pow((x-CENTER_LOCATION_X)/RADIAS, 2));}- (float)getLocationX:(float) y{
return RADIAS*sqrt(1-pow((y-CENTER_LOCATION_Y)/RADIAS, 2));}
这里的公式原型是:sin2(a)+cos2(a)=1。其实都可以从勾股定理推出。sin是对边比斜边,cos是邻边比斜边。
在做这个代码的时候,把初中高中的一些三角函数方面的知识复习了一下。呵呵。
在pan手势结束,用如下代码做了判断:
if (gestureRecognizer.state==UIGestureRecognizerStateEnded) {
[self doPostPanAction:panData]; }
处理pan结束的方法,主要是播放动画:
- (void)doPostPanAction:(struct PanLocationData) data{
//判断x速度和y速度在圆外切方向是否形成贡献,贡献值是否大于最小值 if (pow(data.currentVelocity.x,2)+pow(data.currentVelocity.y, 2)>pow(MIN_VELOCITY,2)) { BOOL clockwise=FALSE;//顺时针标志 //如下情况顺时针 if ((data.currentLocation.y-CENTER_LOCATION_Y>0 && data.currentVelocity.x<0) || (data.currentLocation.y-CENTER_LOCATION_Y<0 && data.currentVelocity.x>0)) { clockwise=TRUE; } CAKeyframeAnimation *anim=[CAKeyframeAnimation animationWithKeyPath:@"position"]; NSMutableArray *values=[NSMutableArray array]; //计算当前值的角度 float currentArc=atan((startLayer.position.y-CENTER_LOCATION_Y)/(startLayer.position.x-CENTER_LOCATION_X))*360/(2*M_PI); if (startLayer.position.x-CENTER_LOCATION_X<0) { if(currentArc<0){ currentArc=180-abs(currentArc); }else { currentArc=-(180-abs(currentArc)); } } NSLog(@">>>>current arc:%f",currentArc); CGPoint currentPoint; for (int i=0; i<LOOP_COUNT; i++) { if (clockwise) { currentPoint=CGPointMake(RADIAS*cos((currentArc+i)*2*M_PI/360)+CENTER_LOCATION_X, RADIAS*sin((currentArc+i)*2*M_PI/360)+CENTER_LOCATION_Y); }else { currentPoint=CGPointMake(RADIAS*cos((currentArc-i)*2*M_PI/360)+CENTER_LOCATION_X, RADIAS*sin((currentArc-i)*2*M_PI/360)+CENTER_LOCATION_Y); } [values addObject:[NSValue valueWithCGPoint:currentPoint]]; } startLayer.position=currentPoint; anim.values=values; [anim setDuration:2.0]; [startLayer addAnimation:anim forKey:@"demoAnimation"]; } }
这里需要的一些数学知识是,判断用户操作的是顺时针还是逆时针,我没有找到很好的办法,是通过坐标系象限以及手势的x、y方向速度来判断的。
另外,就是动画如何播放。我的思路是,按照角度来,从当前角度开始,每次转动1度。这里需要把弧度转换为角度。我的做法是,已知x、y,因此知道对边和邻边,这样可以得到tan,即正切。然后用arctan取得角度。
这个角度还不能直接用,要判断是在第几象限,有可能需要获取它的补角。
其他的技术点,就是使用关键帧动画。可参照以前的示例。
这个示例不会用于正式生产环境的,因为用户体验很不好。因为Layer太小,手势在操作过程中很容易离开layer。需要用其他方案提到。但是这个示例积累了很多知识。