拖动层并播放动画
在下面的示例中,用手势拖动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。需要用其他方案提到。但是这个示例积累了很多知识。