核心动画/Cocoa中的小部件“翻转”行为

9
我正在尝试制作一个Card类,其复制了仪表板小部件的行为,您可以将控件、图像或任何其他东西放在卡片的两侧并在它们之间翻转。
层支持视图具有变换属性,但改变它不会产生我所期望的效果(将图层沿y轴旋转会将其折叠到左侧)。
有人指向了一些未记录的功能和名为cgsprivate.h的.h文件,但我想知道是否有官方的方法来做到这一点?此软件必须被运送,我不希望看到它在Apple的10.6中失败。
谁有任何想法如何做到这一点?对我来说,一个简单的小部件竟然如此难以在核心动画中实现。
提前致谢!
编辑:我可以使用位于图层上的图像完成此行为,但我不知道如何在层上获取更高级的控件/视图/任何其他内容。卡片示例使用图像。

1
我为iPhone编写了一个示例,演示了一种实现此操作的方法。https://github.com/samyzee/CardFlipperSample - user283455
7个回答

11
抱歉,此段内容存在删除线。Mike Lee曾发布过翻转效果的实现示例代码,但很遗憾,这些内容现在已不再提供在线访问。不过Drew McCormack 借鉴了这些内容并开发出自己的实现。他似乎获取了要交换的“背景”和“前景”视图的图层,使用CATransform3D对两个视图进行旋转动画,然后在动画完成后交换视图。通过使用视图的图层,您可以避免需要缓存到位图中,因为这正是图层正在执行的操作。无论如何,他的视图控制器看起来是您所需的好选择。

您提供的链接已经失效了。第一个现在链接到旅游网站,而另一个则是404错误页面。 - Sam Spencer
@RazorSharp - Mike的整个网站似乎已经离线了,所以我能找到的最接近的实现是Drew McCormack基于Mike原始代码设计的类似物。 - Brad Larson

6

像e.James所概述的那样使用核心动画...请注意,这是使用垃圾收集和托管图层:

#import "AnimationWindows.h"

@interface AnimationFlipWindow (PrivateMethods)

NSRect RectToScreen(NSRect aRect, NSView *aView);
NSRect RectFromScreen(NSRect aRect, NSView *aView);
NSRect RectFromViewToView(NSRect aRect, NSView *fromView, NSView *toView);

@end

#pragma mark -

@implementation AnimationFlipWindow

@synthesize flipForward = _flipForward;

- (id) init {

    if ( self = [super init] ) { 
        _flipForward = YES; 
    }

    return self;
}

- (void) finalize {

    // Hint to GC for cleanup
    [[NSGarbageCollector defaultCollector] collectIfNeeded];
    [super finalize];
}

- (void) flip:(NSWindow *)activeWindow 
       toBack:(NSWindow *)targetWindow {

    CGFloat duration = 1.0f * (activeWindow.currentEvent.modifierFlags & NSShiftKeyMask ? 10.0 : 1.0);
    CGFloat zDistance = 1500.0f;

    NSView *activeView = [activeWindow.contentView superview];
    NSView *targetView = [targetWindow.contentView superview];

    // Create an animation window
    CGFloat maxWidth  = MAX(NSWidth(activeWindow.frame), NSWidth(targetWindow.frame)) + 500;
    CGFloat maxHeight = MAX(NSHeight(activeWindow.frame), NSHeight(targetWindow.frame)) + 500;

    CGRect animationFrame = CGRectMake(NSMidX(activeWindow.frame) - (maxWidth / 2), 
                                       NSMidY(activeWindow.frame) - (maxHeight / 2), 
                                       maxWidth, 
                                       maxHeight);

    mAnimationWindow = [NSWindow initForAnimation:NSRectFromCGRect(animationFrame)];

    // Add a touch of perspective
    CATransform3D transform = CATransform3DIdentity; 
    transform.m34 = -1.0 / zDistance;
    [mAnimationWindow.contentView layer].sublayerTransform = transform;

    // Relocate target window near active window
    CGRect targetFrame = CGRectMake(NSMidX(activeWindow.frame) - (NSWidth(targetWindow.frame) / 2 ), 
                                    NSMaxY(activeWindow.frame) - NSHeight(targetWindow.frame),
                                    NSWidth(targetWindow.frame),
                                    NSHeight(targetWindow.frame));

    [targetWindow setFrame:NSRectFromCGRect(targetFrame) display:NO];

    mTargetWindow = targetWindow;

    // New Active/Target Layers
    [CATransaction begin];
    CALayer *activeWindowLayer = [activeView layerFromWindow];
    CALayer *targetWindowLayer = [targetView layerFromWindow];
    [CATransaction commit];

    activeWindowLayer.frame = NSRectToCGRect(RectFromViewToView(activeView.frame, activeView, [mAnimationWindow contentView]));
    targetWindowLayer.frame = NSRectToCGRect(RectFromViewToView(targetView.frame, targetView, [mAnimationWindow contentView]));

    [CATransaction begin];
    [[mAnimationWindow.contentView layer] addSublayer:activeWindowLayer];
    [CATransaction commit];

    [mAnimationWindow orderFront:nil];  

    [CATransaction begin];
    [[mAnimationWindow.contentView layer] addSublayer:targetWindowLayer];
    [CATransaction commit];

    // Animate our new layers
    [CATransaction begin];
    CAAnimation *activeAnim = [CAAnimation animationWithDuration:(duration * 0.5) flip:YES forward:_flipForward];
    CAAnimation *targetAnim = [CAAnimation animationWithDuration:(duration * 0.5) flip:NO  forward:_flipForward];
    [CATransaction commit];

    targetAnim.delegate = self;
    [activeWindow orderOut:nil];

    [CATransaction begin];
    [activeWindowLayer addAnimation:activeAnim forKey:@"flip"];
    [targetWindowLayer addAnimation:targetAnim forKey:@"flip"];
    [CATransaction commit];
}

- (void) animationDidStop:(CAAnimation *)animation finished:(BOOL)flag {

    if (flag) {
        [mTargetWindow makeKeyAndOrderFront:nil];
        [mAnimationWindow orderOut:nil];

        mTargetWindow = nil;
        mAnimationWindow = nil;
    }
}

#pragma mark PrivateMethods:

NSRect RectToScreen(NSRect aRect, NSView *aView) {
    aRect = [aView convertRect:aRect toView:nil];
    aRect.origin = [aView.window convertBaseToScreen:aRect.origin];
    return aRect;
}

NSRect RectFromScreen(NSRect aRect, NSView *aView) {
    aRect.origin = [aView.window convertScreenToBase:aRect.origin];
    aRect = [aView convertRect:aRect fromView:nil];
    return aRect;
}

NSRect RectFromViewToView(NSRect aRect, NSView *fromView, NSView *toView) {

    aRect = RectToScreen(aRect, fromView);
    aRect = RectFromScreen(aRect, toView);

    return aRect;
}

@end

#pragma mark -
#pragma mark CategoryMethods:

@implementation CAAnimation (AnimationFlipWindow)

+ (CAAnimation *) animationWithDuration:(CGFloat)time flip:(BOOL)bFlip forward:(BOOL)forwardFlip{

    CABasicAnimation *flipAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.y"];

    CGFloat startValue, endValue;

    if ( forwardFlip ) {
        startValue = bFlip ? 0.0f : -M_PI;
        endValue = bFlip ? M_PI : 0.0f;
    } else {
        startValue = bFlip ? 0.0f : M_PI;
        endValue = bFlip ? -M_PI : 0.0f;
    }

    flipAnimation.fromValue = [NSNumber numberWithDouble:startValue];
    flipAnimation.toValue = [NSNumber numberWithDouble:endValue];

    CABasicAnimation *shrinkAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
    shrinkAnimation.toValue = [NSNumber numberWithFloat:1.3f];
    shrinkAnimation.duration = time * 0.5;
    shrinkAnimation.autoreverses = YES;

    CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
    animationGroup.animations = [NSArray arrayWithObjects:flipAnimation, shrinkAnimation, nil];
    animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    animationGroup.duration = time;
    animationGroup.fillMode = kCAFillModeForwards;
    animationGroup.removedOnCompletion = NO;

    return animationGroup;
}

@end

#pragma mark -

@implementation NSWindow (AnimationFlipWindow)

+ (NSWindow *) initForAnimation:(NSRect)aFrame {

    NSWindow *window =  [[NSWindow alloc] initWithContentRect:aFrame 
                                                    styleMask:NSBorderlessWindowMask 
                                                      backing:NSBackingStoreBuffered 
                                                        defer:NO];
    [window setOpaque:NO];
    [window setHasShadow:NO];
    [window setBackgroundColor:[NSColor clearColor]];
    [window.contentView setWantsLayer:YES];

    return window;
}

@end

#pragma mark -

@implementation NSView (AnimationFlipWindow)

- (CALayer *) layerFromWindow {

    NSBitmapImageRep *image = [self bitmapImageRepForCachingDisplayInRect:self.bounds];
    [self cacheDisplayInRect:self.bounds toBitmapImageRep:image];

    CALayer *layer = [CALayer layer];
    layer.contents = (id)image.CGImage;
    layer.doubleSided = NO;

    // Shadow settings based upon Mac OS X 10.6
    [layer setShadowOpacity:0.5f];
    [layer setShadowOffset:CGSizeMake(0,-10)];
    [layer setShadowRadius:15.0f];


    return layer;
}

@end

头文件:

@interface AnimationFlipWindow : NSObject {

    BOOL _flipForward;

    NSWindow *mAnimationWindow;
    NSWindow *mTargetWindow;
}

// Direction of flip animation (property)
@property (readwrite, getter=isFlipForward) BOOL flipForward;

- (void) flip:(NSWindow *)activeWindow 
       toBack:(NSWindow *)targetWindow;
@end

#pragma mark -
#pragma mark CategoryMethods:

@interface CAAnimation (AnimationFlipWindow)
+ (CAAnimation *) animationWithDuration:(CGFloat)time 
                                   flip:(BOOL)bFlip // Flip for each side
                                forward:(BOOL)forwardFlip; // Direction of flip
@end

@interface NSWindow (AnimationFlipWindow)
+ (NSWindow *) initForAnimation:(NSRect)aFrame;
@end

@interface NSView (AnimationFlipWindow)
- (CALayer *) layerFromWindow;
@end

编辑:这将以翻转的方式从一个窗口动画到另一个窗口。您可以将相同的原则应用于视图。


3

对您的需求而言,这可能过于复杂(因为它包含一个相当完整的棋牌类游戏参考应用程序),但可以查看来自ADC的样例。其中包括的纸牌游戏可以很好地实现翻转效果。


这个示例中的卡片是将图像作为其内容提供的图层。我需要在这些图层上放置控件(按钮、文本字段、视图),但似乎找不到一个好的方法来实现。 - user46747

2
如果您能使用图像来完成这个操作,也许您可以将所有控件放在一个NSView对象中(与平常一样),然后使用cacheDisplayInRect:toBitmapImageRep:NSView渲染成位图图像,在执行翻转效果之前。步骤如下:
  1. NSView渲染成位图
  2. 在适合翻转效果的层中显示该位图
  3. 隐藏NSView并显示图像层
  4. 执行翻转效果

是的。那正好是我试图避免的解决方案,或者至少避免自己实现它。我怀疑这正是仪表板小部件所做的,因为你可以看到它的冻结和更新。我只是希望有一种已经建立的方法来实现它。感谢NSView方法! - user46747

1

0

-3

可能在 2008 年提出这个问题时不是这种情况,但如今这很容易:

[UIView animateWithDuration:0.5 animations:^{
    [UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:self.iconView cache:YES];
    /* changes to the view made here will be reflected on the flipped to side */
}];

注意:显然,这仅适用于iOS。

问题是针对 MacOS,不是 iOS。 - Borzh
@Borzh 真的有点荒谬,因为在 iOS 上想要做这件事情的人和在 OSX 上想要做这件事情的人一样可能会找到这个问题,无论 OP 在 2008 年使用了什么。 - aepryus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接