英文标题: How to implement a user interactivable UIView implicit animation

这一篇教程将结合一个小 demo 讲解, 最终代码在 github 上可以下载到. demo 的运行效果如图所示, 通过手势拖动顶端红色矩形到虚线位置, 拖动距离超过 50% 松手, 矩形会自动移动到目标位置, 少于 50% 松手, 矩形自动归位:

How to implement a user interactive UIView implicit animation

实现一个可交互的动画大概有这么几种思路:

  1. 在响应 GestureRecognizer 的方法中实时更新 view 的状态, 在 GestureRecognizer 结束的时候创建动画完成后续的部分.
  2. 在 GestureRecognizer 开始的时候创建显式动画, 随着 Gesture 变化, 改变动画的 progress, 在 GestureRecognizer 结束之后完成动画.
  3. 在 GestureRecognizer 开始的时候创建隐式动画, 随着 Gesture 变化, 改变动画的 progress, 在 GestureRecognizer 结束之后完成动画.

注意 2 和 3 只有显式和隐式的区别.

对于简单的 view, 单一的动画 (比如 demo 中的这种, 线性移动一个简单的 view), 三种方法差别不大. 而对于复杂的 view (view 中嵌套多个 subView), 多个动画 (view 和 subview 都有各自的动画), 如果在不考虑可交互的前提下, 隐式的动画已经可以满足需求, 那么方法 3 可能会比方法 1 和 2 简洁得多, 本文将对方法 3 进行讲解.

要实现方法 3, 首先要了解 CALayer 的几个属性: speed, timeOffset, beginTime. 对于这几个属性, Controlling Animation Timing 这篇教程图文并茂地给出了直观的解释, 必读, 这里只做简单介绍:

  • speed: 控制动画的速度, 比如设置为 1 (默认), 动画正常运行, 设置为 0 动画暂停, 设置为 -1 动画反向运行.
  • timeOffset: 控制动画在整个动画时间轴上的位置, 比如一个 3 秒的动画, timeOffset 为 1.5, layer 就会处于整个动画过程中间的那个状态.
  • beginTime: 动画的开始时间.

有了这些基础, 我们就可以着手实现了, 首先我们需要建立一个 CALayer 的 Category, 添加一些动画辅助代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (void)gs_pauseAnimation {
    self.speed = 0;
    self.timeOffset = 0;
}
 
- (void)gs_recoverToDefaultState {
    self.speed = 1;
    self.timeOffset = 0;
    self.beginTime = 0;
}
 
- (void)gs_setTimeProgress:(CFTimeInterval)timeProgress {
    self.timeOffset = timeProgress;
}
 
- (void)gs_continueAnimationWithTimeProgress:(CFTimeInterval)timeProgress {
    [self gs_recoverToDefaultState];
 
    self.beginTime = [self convertTime:CACurrentMediaTime() fromLayer:nil] - timeProgress;
}
 
- (void)gs_continueReverseAnimationWithTimeProgress:(CFTimeInterval)timeProgress animationDuration:(CFTimeInterval)duration {
    [self gs_recoverToDefaultState];
 
    self.beginTime = [self convertTime:CACurrentMediaTime() fromLayer:nil] - (duration - timeProgress);
    self.timeOffset = duration;
    self.speed = -1;
}
  • gs_pauseAnimation: 通过将 speed 设置为 0, 来暂停动画.
  • gs_recoverToDefaultState: 将 layer 的各个属性恢复到默认状态
  • gs_setTimeProgress: 在 gs_pauseAnimation 的基础上, 设置动画的时间进度.
  • gs_continueAnimationWithTimeProgress: 从 timeProgress 时间进度开始, 继续播放之前暂停了的动画
    • 这里首先调用了 gs_recoverToDefaultState, 是为了让 convertTime:fromLayer: 得出正确的时间. 
    • [self convertTime:CACurrentMediaTime() fromLayer:nil] 将当前系统时间转换到 layer 的时间上, 减去 timeProgress, 假设这是一个 3 秒的动画, timeProgress 为 1 秒, 则这条代码就表示 layer 的动画在一秒之前就已经开始, 所以动画将从运行到第 1 秒的状态开始继续播放.
    • 如果不能理解, 可以结合一下文档中这篇: How do I pause all animations in a layer tree?
  • gs_continueReverseAnimationWithTimeProgress: 从 timeProgress 时间进度开始, 反向播放之前暂停了的动画.
    • 这里 beginTime 设置为 当前时间 - (整个动画时间 - timeProgress), 再假设这是一个 3 秒的动画, timeProgress 为 1 秒, 因为要让动画反向播放, 所以要把动画反过来看, 由终点到起点运动, timeProgress 这 1 秒, 是动画最后的部分, 前 3 – 1 = 2 秒是已经播放完的, 所以动画要从 第 2 秒开始.
    • 通过将 speed 设置为 -1 来反向播放动画

完成了动画控制的实现, 接下来是控制 view 状态的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)switchToState1 {
    [self.moveableView setCenter:CGPointMake(self.view.bounds.size.width / 2, 50)];
    [self.moveableView setTag:1];
}
 
- (void)switchToState2 {
    [self.moveableView setCenter:CGPointMake(self.view.bounds.size.width / 2, 500)];
    [self.moveableView setTag:2];
}
 
- (void)switchState {
    if (self.moveableView.tag == 1) {
        [self switchToState2];
    }
    else {
        [self switchToState1];
    }
}

很简单, 不用过多解释, view 的起始状态是 state1.

然后是手势控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
- (void)handlePanGesture:(UIPanGestureRecognizer *)panGesture {
    if (panGesture.state == UIGestureRecognizerStateBegan) {
        [self.moveableView.layer removeAllAnimations];
 
        [UIView animateWithDuration:kAnimationDuration animations:^{
            if (self.moveableView.tag == 1) {
                [self switchToState2];
            }
            else {
                [self switchToState1];
            }
        } completion:^(BOOL finished) {
            [self.moveableView.layer gs_recoverToDefaultState];
 
            if (self.animationProgress <= 0.5) {
                if (self.moveableView.tag == 1) {
                    [self switchToState2];
                }
                else {
                    [self switchToState1];
                }
            }
        }];
 
        [self.moveableView.layer gs_pauseAnimation];
    }
    else if (panGesture.state == UIGestureRecognizerStateChanged) {
        CGPoint translation = [panGesture translationInView:self.view];
        CGFloat length = ABS(translation.y);
 
        self.animationProgress = MAX(0, MIN(length / 500, 1));
 
        [self.moveableView.layer gs_setTimeProgress:self.animationProgress *  kAnimationDuration];
    }
    else if (panGesture.state == UIGestureRecognizerStateEnded || panGesture.state == UIGestureRecognizerStateFailed || panGesture.state == UIGestureRecognizerStateCancelled) {
 
        if (self.animationProgress > 0.5) {
            [self.moveableView.layer gs_continueAnimationWithTimeProgress:self.animationProgress * kAnimationDuration];
        }
        else {
            [self.moveableView.layer gs_continueReverseAnimationWithTimeProgress:self.animationProgress * kAnimationDuration animationDuration:kAnimationDuration];
        }
    }
}
  1. 在手势开始时, 创建 UIView 隐式动画并暂停;
  2. 随着手势移动, 调整动画的进度
  3. 在手势结束后, 根据当前手势位移来判断是将 view 归位, 还是移动到目标位置.
  4. 在动画结束后, 也就是在手势开始时隐式动画的 completion 中, 恢复 layer 状态, 并在 view 归位的情况下, 设置 view 到初始的状态.

运行一下 (这部分代码在 branch v1), 看起来得到了想要的效果, 但是如果细心观察, 会发现在手势拖动距离不够长, view 移动到原位的时候, view 会闪烁一下. 因为 demo 中 view 太简单了, 所以可能要多试几次才能观察到, 不过也可以在 completion block 里面第一行设置个断点, 可以看到, 执行到断点处时, view 消失了.

这里的原因我没有深入研究, 猜测是因为 speed 设置为 -1, 导致动画运行完成的时候 view 处于一个不存在的状态, 也就无法将这个状态的 view 绘制到屏幕上. 虽然通过将 speed 设置为 -1 来反向运行动画看起来很机智, 但很显然, 这不是一个完美的解决方法.

既然不能让动画自动反向运行, 那就稍微绕个远, 手动反向运行. CADisplayLink 是一个不错的选择, 它与 NSTimer 类似, 但是是在屏幕每一帧刷新之前调用 selector. 新的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
- (void)handlePanGesture:(UIPanGestureRecognizer *)panGesture {
    if (panGesture.state == UIGestureRecognizerStateBegan) {
        [self.moveableView.layer removeAllAnimations];
 
        [self.displayLink invalidate];
 
        [UIView animateWithDuration:kAnimationDuration animations:^{
            [self switchState];
        } completion:^(BOOL finished) {
 
        }];
 
        [self.moveableView.layer gs_pauseAnimation];
    }
    else if (panGesture.state == UIGestureRecognizerStateChanged) {
        CGPoint translation = [panGesture translationInView:self.view];
        CGFloat length = ABS(translation.y);
 
        self.animationProgress = MAX(0, MIN(length / 500, 1));
 
        [self.moveableView.layer gs_setTimeProgress:self.animationProgress *  kAnimationDuration];
    }
    else if (panGesture.state == UIGestureRecognizerStateEnded || panGesture.state == UIGestureRecognizerStateFailed || panGesture.state == UIGestureRecognizerStateCancelled) {
 
        if (self.animationProgress > 0.5) {
            [self.moveableView.layer gs_continueAnimationWithTimeProgress:self.animationProgress * kAnimationDuration];
        }
        else {
            [self.displayLink invalidate];
 
            self.displayLinkStarTime = CACurrentMediaTime();
            self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateAnimation:)];
            [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
        }
    }
}
 
- (void)updateAnimation:(id)sender {
    CFTimeInterval timeInterval = CACurrentMediaTime() - self.displayLinkStarTime;
    CFTimeInterval animationTimeProgress = self.animationProgress * kAnimationDuration - timeInterval;
 
    if (animationTimeProgress >= 0) {
        [self.moveableView.layer gs_setTimeProgress:animationTimeProgress];
    }
    else {
        [self.displayLink invalidate];
 
        [self.moveableView.layer gs_continueAnimationWithTimeProgress:kAnimationDuration];
        [self switchState];
    }
}

可以看到, 在手势介素后, 我们创建了一个 CADisplayLink 来手动反向运行动画, 并在动画结束后, 重新设置 view 的状态.

运行, 闪烁消失, Cheers.

–以上–

Tagged with:  

针对含有sub-project的project, 在依赖关系正确设置(关于如何正确设置, 参考本文最后提供的链接)的情况下, 如果直接用下面命令, 基本上编译会出错, 提示找不到sub-project里面的头文件:

xcodebuild -target TestApp -configuration Release clean build

这个命令会在当前目录下创建一个build文件夹, 然后将编译的中间产物和结果放进去, 而Xcode GUI在编译时是将这些放到DerivedData下面, 这一区别导致了在编译时找不到sub-project包含的一些东西.

解决方法很简单, 加上-scheme, 命令如下:

xcodebuild -scheme TestApp -target TestApp -configuration Release clean build

指定了-scheme之后, 命令行编译的行为就跟在Xcode GUI下编译一样了.

如果使用添加header search path的方法, 也能让编译通过, 但其实不是最正确的方法, 因为在GUI下编译不需要这一步.

如何正确设置sub-projct, 及本文参考见这里

Tagged with:  

Objective-C代码格式整理

On 04/07/2014, in iDev, iOS Dev, Mac Dev, by ultragtx

团队合作中代码风格一致比较重要, 像Google NewYorkTimes这些公司都公开了各自的Objective-C代码风格, 有需要的搜索一下就可以找到, 这里主要介绍借助Uncrustify自动整理代码格式的一些技巧.

Uncrustify: Source Code Beautifier for C, C++, C#, ObjectiveC, D, Java, Pawn and VALA, 篇幅及时间有限, 没必要过多介绍, 总之是一款定制性很高的代码格式整理工具, 但高定制性意味着配置起来很烦, 所以这里推荐一个开源图形界面配置文件编辑器UncrustifyX, 这个工具对各个配置都有详细的说明, 并且有直观的预览功能, 可以较为方便地编辑出你需要的配置, 当然更方便的方式是直接搜索别人写好的配置文件, UncrustifyX本身就带一个.

其他的关于如何安装, 如何更方便地在Xcode中使用, 可以参考这篇. 本文不再详细说明.

最后要说一下Uncrustify的一个缺点(Feature):Xcode会自动对代码中的空行添加空格缩进, 这样会对编辑代码带来方便, 而Uncrustify会删除所有行尾多余的空格, 这就意味着如果你用Uncrustify对一个已有的代码进行处理, 所有的空行缩进都会被删除, 产生大量无意义的diff, 同时影响下次编辑. 如果你期待可以通过配置文件来改变这个行为, 那恐怕就要失望了, 而且Uncrustify的作者基本上也明确表示了不会添加这个功能(现在有个issue, 里面有几个人在求这个功能).

这里提供一个简单技巧, 轻松解决这一问题, 在Xcode中, 我们可以通过全选代码, Move Line up(option+command+[) + Move Line Down(option+command+]), 来自动添加回那些消失的空格, 上面说的那个问题也就迎刃而解了. 另外说一下, AppCode自带的代码格式整理, 估计也是用Uncrustify实现的, 所以跟Uncrustify一个毛病.

–以上–

Tagged with:  

UIScrollView及其子类UITableView, UICollectionView为iOS开发带来了极大的方便, 其分页(pagingEnabled)功能也很常用, 但是功能却有些局限, 页只能按UIScrollviewbounds尺寸划分, 如果要实现自定义分页宽度或高度就需要一些技巧.

方法一

类似iOS 6中MobileSafari, 如图所示的分页方法:

HGPageScrollView_Screen_shot_2

 

StackOverflow上提供的实现思路如下:

 

  1. UIScrollviewbounds限制再屏幕中间那一页的位置
  2. 禁用UIScrollviewclipsToBouds, 从而能显示超出bounds的内容

此时已经可以在借助UIScrollview原有分页功能的基础上实现了"视觉上"与MobileSafari类似的效果, 但因为UIScrollview的尺寸限制, 超出范围的触摸事件不会被UIScrollview收到, 影响体验, 要把触摸事件传递给UIScrollview, 方法如下:

  1. 用一个全屏的(或者其他你想要的尺寸)UIView子类, 这里我们命名为CustomUIView来做UIScrollviewsuperView
  2. 重写CustomUIViewhitTest:withEvent方法:

     - (UIView *) hitTest:(CGPoint) point withEvent:(UIEvent *)event {
       if ([self pointInside:point withEvent:event]) {
         return scrollView;
       }
       return nil;
     }
    

    通过重写hitTest, 就可以将CustomUIView接受到的触摸事件传递给UIScrollView

方法二

前一种方法保留了UIScrollView原有的分页功能, 而下面这种更加灵活的自定义分页方法则完全自制了一个分页功能:

DMPagingScrollView就是一个很好的实现, 其自定义分页的思路大致如下(需要参考DMPagingScrollView.m代码):

  1. 用实例变量pageWidth记录自定义分页的宽度
  2. 通过实现UIScrollView的delegate方法, 来记录每次拖动UIScrollView动作的相关参数(位移, 加速度…)
  3. scrollViewDidEndDecelerating:等一些标志拖动结束的delegate方法中调用snapToPage方法, 在snapToPage方法中, 借助之前收集的动作参数计算出contentOffset, 并通过setContentOffset:offset animated:YES来移动.

这种方式的关键就在contentOffset的计算中(DMPagingScrollView.m pageOffsetForComponent:方法), 如果计算的不正确, 就会严重影响拖动UIScrollView的手感.

DMPagingScrollView通过subclassing UIScrollview将自定义分页相关的过程操作封装在DMPagingScrollView里面, 同时支持横向纵向的自定义分页, 十分方便. 类似地, 我们也可以对UITableView, UICollectionView进行同样的处理.

此外, 通过参考pageOffsetForComponent:中的计算方法, 我们甚至可以让每一页的宽度都不同, 从而大大提升了分页的灵活性, 比如我实现的这种:

- (CGFloat)pageOffset {
    CGFloat totalWidth = self.collectionView.contentSize.width;
    CGFloat visibleWidth = self.collectionView.bounds.size.width;

    CGFloat currentOffset = self.collectionView.contentOffset.x;
    CGFloat dragVelocity = _dragVelocity.x;
    CGFloat dragDisplacement = _dragDisplacement.x;

    NSInteger nearestIndex = 0;
    CGFloat minDis = CGFLOAT_MAX;
    CGFloat nearestOffset = 0;
    CGFloat prevOffset = 0;
    CGFloat nextOffset = 0;
    CGFloat totalOffset = 0;

    // self.pagesWidths is an NSArray that contains each page's width
    for (NSInteger i = 0; i < self.pagesWidths.count; i++) {
        CGFloat dis = ABS(totalOffset - currentOffset);

        if (dis < minDis) {
            minDis = dis;

            prevOffset = nearestOffset;
            
            nearestOffset = totalOffset;

            if (i == self.pagesWidths.count - 1) {
                nextOffset = totalOffset;
            }
            else {
                nextOffset = totalOffset + [(NSNumber *)self.pagesWidths[i] floatValue];
            }
        }

        totalOffset += [(NSNumber *)self.pagesWidths[i] floatValue];
    }

    NSInteger lowerIndex;
    NSInteger upperIndex;
    CGFloat lowerOffset;
    CGFloat upperOffset;
    if (currentOffset - nearestOffset < 0) {
        lowerIndex = nearestIndex - 1;
        upperIndex = nearestIndex;

        lowerOffset = prevOffset;
        upperOffset = nearestOffset;
    }
    else {
        lowerIndex = nearestIndex;
        upperIndex = nearestIndex + 1;

        lowerOffset = nearestOffset;
        upperOffset = nextOffset;
    }

    CGFloat newOffset;
    if (ABS(dragDisplacement) < DRAG_DISPLACEMENT_THRESHOLD || dragDisplacement * dragVelocity < 0) {
        if (currentOffset - lowerOffset > upperOffset - currentOffset) {
            newOffset = upperOffset;
        } else {
            newOffset = lowerOffset;
        }
    } else {
        if (dragVelocity > 0) {
            newOffset = upperOffset;
        } else {
            newOffset = lowerOffset;
        }
    }

    if (newOffset > totalWidth - visibleWidth)
        newOffset = totalWidth - visibleWidth;

    if (newOffset < 0)
        newOffset = 0;

    return newOffset;
}

–以上–

Tagged with:  

kernel_task占用大量CPU的解决方案

On 06/23/2013, in Apple, by ultragtx

如果你在使用MacBook、MacBook Pro、MacBook Air或者其他可能的Mac设备(我不知道iMAC和Mac mini有没有这个问题)遇到kernel_task长时间占用大量CPU(通常是500%-600%,也有200%,视机型而定)的情况,如果你正在播放Flash、进行高强度运算,或者正值高温天气。那么本文很可能会挽救你于水深火热之中。

首先解释一下kernel_task为什么会占用如此多的CPU。相信你从前文的描述中就可以发现,这是机器过热造成的。事实上kernel_task并不是真的在占用大量CPU,这么做的目的只是为了从那些疯狂消耗CPU的程序中抢夺系统资源,从而达到降低系统开销的目的,进而降低了机器的温度,一旦温度低到一个值,kernel_task也就恢复正常了。这在环境温度比较低的情况下是非常有效的降温手段,而在炎热的夏季,就会有kernel_task迟迟不能恢复正常的情况发生。

根据上面的分析,我们可以发现解决问题的关键就是如何让机器温度快速降低下来。方法自然有很多了,搞个大风扇对着电脑狂吹,在保证机器安全的情况下往机器上浇点水什么的,或者找个调解风扇转速软件(显然我不知道什么软件好用,因为我用了前两种方法)。还可以参考一下这篇文章的内容及相关讨论。

希望本文能帮助到被这个问题困扰着的人。如果本文没有解决你的问题,那么也有可能是其他原因造成kernel_task疯狂占用CPU,你可以尝试一个一个关闭正在运行的程序,看看是哪个造成的问题,不行就去Genius Bar求助吧。

–以上–

Tagged with:  

No Comments

On 06/13/2013, in 未分类, by ultragtx

Sorry_Steve

designed by  Artur Kasimov

 

最近做的一个项目中涉及这样一个需求: 响应用户点击html元素的事件.这其中包含响应单击(single tap/click)和双击(double tap/dblclick).

单击事件

通常单击的解决方案是用javascript实现, 为html元素绑定onclick事件, javascript接收事件后跳转url, 在CocoaTouch中用 webView:shouldStartLoadWithRequest:navigationType: 拦截url跳转, 从而实现单击事件的响应, 大致的代码如下:

Javascript:

function myClick() {
    var e = window.event
    var url = "click:" + e.target.id;
    document.location = url;
}

var ele = document.getElementById(ELEMENT_ID);
ele.onclick = myClick;

CocoaTouch:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {

    NSString *requestString = [[request URL] absoluteString];
    NSArray *components = [requestString componentsSeparatedByString:@":"];
    if (components.count == 2 && [(NSString *)[components objectAtIndex:0] isEqualToString:@"click"]) {
        NSString *eleIdStr = (NSString *)[components objectAtIndex:1];
        NSInteger eleId = [eleIdStr integerValue];

        // Do something

        return NO;   // Do not load request, just receive the event
    }
    return YES;
}

双击事件

Failed 1

使用与上面同样的方法为html元素添加ondblclick来响应双击事件就无法实现, 现象为响应函数从不被调用. 用setTimeout等方法尝试用单击事件来模拟双击事件也无法实现, 无法接收到double tap的第二次点击.

Failed 2

根据之前的现象来看, 显然双击事件在html之外就被拦截了, 要从CocoaTouch层下手才行.

系统的UIWebView中包含了一个UIScrollView, 绑定了UIScrollViewDelayedTouchesBeganGestureRecognizerUIScrollViewPanGestureRecognizer这两个事件, 后者响应拖动事件, 前者相应点击事件.

尝试添给UIScrollView加UITapGestureRecognizer:

UITapGestureRecognizer *dblTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(fakeTapGestureHandler:)];

经测试, fakeTapGestureHandler:从未被调用过.

Success

尝试曲线救国的方式, 用UIGestureRecognizer的delegate方法获取事件:

Javascript:

function eleIdFromPoint(x, y) {
    var ele = document.elementFromPoint(x, y)
    return "" + ele.id;
}

CocoaTouch:

// Add gesture
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(fakeTapGestureHandler:)];

[tapGestureRecognizer setDelegate:self];
[_webView.scrollView addGestureRecognizer:tapGestureRecognizer];

// Delegate

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    CGPoint tapPoint = [touch locationInView:_webView];
    NSString *script = [NSString stringWithFormat:@"eleIdFromPoint(%f, %f)", tapPoint.x, tapPoint.y];
    NSString *eleIdStr = [self stringByEvaluatingJavaScriptFromString:script];
    if (touch.tapCount == 2) {
        if (eleIdStr.length > 0) {
            NSInteger eleId = [eleIdStr integerValue];

            // Do somthing

        }
    }
    return YES; // Return NO to prevent html document from receiving the touch event.
}

上面代码通过UIGestureRecognizer的delegate方法成功获取到点击事件, 需要说明的几点:

  • locationInView:的参数为UIWebView, 后面的document.elementFromPoint传递的也是UIWebView中对应的坐标, 不需要算上UIScrollView的Offset.
  • delegate方法中单击事件也可以被拦截到, 这样单击双击就可以在这块儿一起实现, 让代码看起来比较统一.
  • delegate方法返回NO会拦截事件, html文档就不能接受到任何点击事件了.

–以上–

Tagged with:  

这篇属于技巧性文章, 没什么营养.

如果你常写NSTask相关的东西, 可能需要设置一个脚本的权限为可以执行. 有各种Unix方法, 也有只用Cocoa就能实现的:

1
2
3
NSNumber *permissions = [NSNumber numberWithUnsignedLong: 493];
NSDictionary *attributes = [NSDictionary dictionaryWithObject:permissions forKey:NSFilePosixPermissions];
[NSFileManager defaultManager] setAttributes:attributes ofItemAtPath:path error:&amp;err]

简单解释一下那个"493", 这个就是十进制的"755", 也就是"rwx-xr-x", 所以如果你想要"777"的权限, 就用"511".

用NSFileManager创建文件的时候也可以传递上面的attributes, 创建可执行的文件.

–以上–

Tagged with:  

Fusion Drive组建及Bootcamp安装

On 04/09/2013, in 折腾, 硬件, by ultragtx

本教程将涉及

  • 硬件安装
  • 系统安装,Fusion Drive创建,及保留Recovery分区
  • Fusion Drive以后如何Bootcamp装Windows

安装硬件

硬件选购参考(购于Taobao)

FDnBI m5s

Plextor m5s 256G:这个不用说吧,1.0.3固件,东芝颗粒,速度快,便宜。

FDnBI optbay

光驱位硬盘托架 佳翼H125 :做工一般般,不过这个价格也找不出做工好的,需要高质量的去买OWC Data Doubler

FDnBI usbsata

光驱盒替代品:这个小玩意跟上面那个光驱托架一起买只要9.9,特便宜,相对来讲那个光驱盒目测质量不敢恭维,买个这个凑合一下,反正光驱也不常用。另外有一点要注意,拆下来的光驱在读盘的时候会从各种缝隙漏出激光,直射眼睛可能会造成伤害,尤其是刻录的时候,要知道DVD刻录光驱的激光是可以点烟的,所以如果常用光驱还是建议买一下光驱盒

关于SSD,HDD分别放在哪些位置

注意事项:

(其中没有实际测试过的,不能保证100%的正确性)

速度方面:
  • 2011年以后的机器硬盘位支持SATA III 6Gb/s
  • 2012年以后的机器光驱位支持SATA III 6Gb/s
  • 2011年春季的机器光驱位有部分“看起来”支持SATA III 6Gb/s 部分支持SATA II 3Gb/s
  • 2011年秋季的机器光驱位全部“看起来”支持SATA III 6Gb/s
  • 可以在"系统报告/串行ATA/链接速度" 查看是否“看起来”支持SATA III 6Gb/s
  • 2011年光驱位看起来支持SATA III 6Gb/s的机器,将支持SATA III的SSD放入光驱位会有问题,传说Intel的SSD没有问题,具体见这里最后一楼这里的讨论。再次强调,Intel的SSD没有经过测试,其他厂商SSD放在光驱位几乎可以肯定会出问题。
  • SATA III的设备安装在
  • 一些机械硬盘是SATA III 6Gb/s的,这种情况下将机械硬盘放到有问题的光驱位也可能有问题。
  • SATA III的设备理论上是兼容SATA II的,所以只要不是2011年有问题的机器,
安全方面:
  • HDD位有移动感应,据说可以在检测出电脑移动的时候让硬盘磁头跟盘面分离。
  • 视不同的硬盘,有些硬盘内建重力感应,也就是说自带一定减震效果

根据以上条件:

  • 如果你的设备为2011年,光驱位“看起来”支持SATA III的机器,又购买了非Intel的SSD,那么SSD只能放到HDD位,光驱位放原来的硬盘。
  • 如果没有上述问题,尽量将SSD放到光驱位,硬盘放在硬盘位,既保证硬盘的安全,同时硬盘噪音会小很多。

实测“伪”SATA III装SATA III SSD后的问题:

  • 向SSD写数据,会间歇性暂停传输,传输速度为0,如果系统安装在上面就会出现系统间歇性卡顿,难以使用
  • 写入数据速度最高仅200MB/s,正常应达到360MB以上
  • 有时会直接无法写入

拆机

根据上一部分的判断,选择性拆光驱、硬盘。建议先看一下王自如拆机组RAID的视频,做到心里有数,拆的时候不要心急。

以2011 late Macbook Pro(MD318/MD322)为例:

拆光驱(如图所示):

FDnBI breakout

  1. 背盖
  2. 拆绿色部分,顺序无所谓
  3. 紫色部分,撕开与光驱相连的黑色胶布
  4. 蓝色部分,顺序无所谓

一些说明:

  • 红色部分的线跟视频中位置不同,如图所示情况下不需要拆,实际只要不会影响取出光驱就没必要拆这根线,而且这根也较难拆
  • 蓝色部分除7之外,所有的都是6角螺丝,需要特殊的螺丝刀
  • 绿色部分除1之外,所有的都是十字螺丝
  • 蓝4,在红色部分线下面,需要将红线移开一些才能接触到
  • 蓝5,需要拿开上面的黑色塑料模块才能看到

拆硬盘

  • 跟视频中的方法一样,注意排线。

安装OSX

本文创作时,OSX最新版本为10.8.3,一切内容均以此版本为基准。

安装OSX 10.8.3 Mountain Lion

安装流程:

WARNING

以下操作会清空所有数据,切记在执行前备份重要数据!

  1. 制作安装光盘/U盘

  2. 重启,安装盘引导,查看磁盘列表

     diskutil list 
  3. 创建逻辑卷组(Logical volume group)。其中,cs为coreStorage简写;FusionDrive为卷组名字,随便起;disk0为SSD,disk1为HDD,根据上一步的结果自行修改为合适的值

     diskutil cs create FusionDrive disk0 disk1 
  4. 查看创建结果,复制逻辑卷组的UUID(eg. "+– Logical Volume Group 2B4EDB74-5842-40E4-8398-1567CD879127",这一行最后一串数字字母)

     diskutil cs list 
  5. 创建卷。后面2B4…127为之前复制的UUID,FusionDrive为名字,100%为容量

     diskutil coreStorage createVolume 2B4EDB74-5842-40E4-8398-1567CD879127 jhfs+ FusionDrive 100% 
  6. 在Fusion Drive上安装系统

  7. 装好系统想着开启Trim:方法(适用于10.8.3,其他未测试)

参考链接:

关于Recovery HD问题

虽说按上面教程安装后用开机按option看不到Recovery分区,但开机按Command + R还是可以进入Recovery的。

这个链接 DIY Fusion Drive: Adding Recovery HD to a CoreStorage Volume Group里有创建Recovery分区的方法,实测@robmathers的方案在开机Option中还是看不到Recovery,也可能是我操作有问题。

Bootcamp

几点说明:

  • Macbook Pro通过Bootcamp安装Windows需要内置光驱,外置以及USB引导安装盘都无解
  • Fusion Drive后,Bootcamp分区是从HDD上分割出来的,不会用到SSD
  • 在Bootcamp 5下,光驱位HDD上的Windows是可以引导的(有传言不能引导)

准备工作:

  • Virtualbox,虚拟机,用于制作Windows磁盘镜像。
  • Windows 7 64bit,或Win 8 64bit 安装镜像。32位Bootcamp说不支持,未测试。
  • Winclone,用于将磁盘镜像写入Bootcamp分区

安装

创建虚拟磁盘

  1. VirtualBox新建虚拟机,命名为“Windows”,磁盘文件类型VDI,动态分配,大小10G(不要给大,影响后面转换的速度)。
  2. 虚拟机设置里面设置好Windows安装光盘镜像
  3. 启动进入安装界面,Shift+F12打开CMD,输入下面命令,手动创建分区,避免Windows自动创建100MB分区

     diskpart select disk 0 create partition primary exit exit 
  4. 开始安装,选择刚才创建的分区,继续

  5. 第一次重启的时候关闭虚拟机,注意是关闭不是暂停
  6. VirtualBox菜单/管理/虚拟介质管理,选择“Windows.vdi”,复制,文件格式选择vdi,固定大小,命名为“Windows_copy”
  7. 上一步复制完成后,打开终端,进入复制后文件所在目录,输入下面内容,转换虚拟磁盘:

     sudo VBoxManage internalcommands converttoraw Windows_copy.vdi Windows.raw 
  8. 转换结束后,终端接着输入下面内容,挂载虚拟磁盘:

     sudo hdiutil attach -imagekey diskimage-class=CRawDiskImage Windows.raw 

创建Bootcamp分区

  • 使用Bootcamp助理创建Bootcamp分区(需要Windows安装光盘,据说加载个镜像就可以,未测试,我用的是Bootcamp助理制作的USB安装盘)
  • 关于如何通过Bootcamp助理制作USB Windos安装盘(适用于Bootcamp 5):

    • Finder 找到 /应用程序/实用工具/Boot Camp 助理,显示包内容
    • 复制Contens/Info.plist到桌面,修改“PreUSBBootSupportedModels”下面的“MacBookPro8,3”为“MacBookPro7,3”,移动回原来的位置,替换。
    • 重启Bootcamp助理,可以看到制作USB Windos安装盘的选项
  • 创建Bootcamp分区后记得重启,你可以尝试不重启,但是可能会有个小问题,导致下面的步骤报错,重启能简单解决。

将虚拟磁盘镜像写入Bootcamp分区

  • 打开Winclone,应该可以看到一个Untitled,一个Bootcamp
  • 选择Untitled,写映像…,一切操作选择默认选项
  • 上一步完成后,选择创建好的winclone文件,选择Bootcamp分区为目的磁盘,恢复…,一切操作选择默认选项
  • 系统偏好设置,选择启动磁盘为windows
  • 重启进入Windows,继续安装,完成

参考链接:

Tagged with:  

Leap Motion Controller Hands-on

On 01/25/2013, in 评测, by ultragtx

Product hero 3610e3d732bde725cd2254ff8f67851e

昨日入手了开发版Leap Motion Controller,既然不要钱就不能白拿,先写个简单的试用体验,之后会研究研究开发一些应用出来(如果你还不知道这个东西是干什么的,请点前面的链接到官网看看)。关于Leap Motion Controller比较面向消费者的介绍可以看下ifanr的文章,本文主要以Geek和开发角度评价。

我手上的Leap Motion版本应该可能是Rev6(虽然盒子上写的Rev5),先贴两张我自己照的图,这里先说明一下,图中Leap Motion上3浅紫色的光源用肉眼看只是3个很暗的红色光源,只有通过相机镜头才会看到图中这样的效果,原因应该就是相机的感光范围比肉眼广吧。

LeapMotion 001

LeapMotion 002

基本工作原理

借助相机的这个特殊效果,我们也能大概推测一下Leap Motion的工作原理,仔细看上面第二张图,能发现一些均匀排列的光点(如下图所示),在最下面的官方“解剖”图中还可以看到主板上的两个镜头,知道了这些,Leap Motion Controller大概的工作原理我也不用再多说什么了。

LeapMotion 002 1

Overview2

工作模式及识别效果

Leap Motion有3种主工作模式,Balanced、Precision、High-Speed,此外在这三种基础之上还有Low-Resource模式,Robust模式两种辅助模式,Balanced,Precision、High-Speed这三者之间的区别在于如何权衡识别精度和识别速度,精度越高,速度(帧数)就越慢,具体参见下表:

Modes USB 2.0 USB 3.0
Precision 60  fps 80  fps
Balanced 120 fps 150 fps
High-Speed 214 fps 295 fps

 

至于Low-Resource模式,主要用来降低CPU使用率,当然相应帧数也会有所降低。Robust模式则是针对poor lighting环境做出一些优化同时也会牺牲一些识别的范围和精度(这里的poor lighting我推测是在强光环境下)。

LeapMotion 003

用自带的Visualizer(如上图)实际体验,三种主模式主要的区别就在帧数上有不同,具体要针对不同的应用做选择,只通过这个Visualizer看不出来太多东西。Low-Resource模式关闭的情况下,CPU占用持续维持在50%左右(MBP 15 Late 2011),开启Low-Resource后降低到20%,如果把Leap Moution Controller当做一个像鼠标、触摸板这种日常使用的输入设备的话,不开启Low-Resouce模式,这种持续的高百分比CPU占用基本是不能忍受的,开启Low-Resource也就勉勉强强可以接受,不知道官方准不准备优化这一“问题”。Robust没测试,反正官方也说这个模式在今后开启的机会会越来越少。至于识别方面,Leap可以识别手指,手,以及类似笔这种细长形状的工具,识别精度很好,手指的轻微移动都可以准确反映出来,不足之处是只有手水平放置才能正确识别,垂直就不行,从官方的演示视频也可以看出,演示者一直都在避免让手指纵向排列,不知道这一问题会不会对最终的用户体验造成太多影响,另外要准确识别所有手指需要把手指伸直,时间长了可能会比较累。

开发

目前的SDK(0.7.1)虽然各种语言、平台覆盖的还算全面,但是功能比较弱,只能得到每一帧识别出的手指手掌等的坐标和所指方向等信息,还没有一套封装好的手势,这就导致了开发者如果对手势识别技术不熟悉,就没法开发出体验很好的应用,不同的开发者开发出来的应用的“手感”也会有差距,用户就很难适应,更何况一个优秀的手势识别系统不是轻易就能开发出来的,幸好官方已经将手势识别提上日程,对于大部分开发者来说也就只能“坐等”更新了。

第三方应用暂时都在Demo级别,数量也很少,但是开发者的热情普遍很高,相信随着更多的开发者收到设备,目前的状态会很快得到改变。

总结

从目前来看,Leap Motion Controller还只能算是一个新颖的产品,距离“创造新时代”还是有些遥远。虽然有很多令人称赞之处,但如果没有一套优秀的手势识别系统支撑,加上纵向识别的不足,用户输入的错误率就会比较高,不能带来像苹果的触摸板那样优秀的体验,能否真正成为一款跨时代的产品还需要硬件商和开发者的共同努力。

–以上–

Tagged with: