手机软件新手引导画面设计的思考

最近财富通的新版本做了一个新的新手引导画面,虽然不是我做的,但是我想从APP设计的角度来谈谈这块内容。当然我不是专业人士,大家就权当笑话看吧。

最早的新手引导画面是非常简单的,最最常见的方式是scrollview上面贴了好几张教学图片,用户第一次进入某个界面的时候,就显示这个界面,滑到最后就让引导界面消失。我之前做的“记词助手”便是这样搞的。说实话,我自己用软件的过程中,从来不会去看这些教学图片,弹出新手引导界面的时候,我第一个想找到的就是“跳过”按钮,如果找不到,就会飞快地把它划到最后一页然后让它消失掉。我想很多人应该都是像我这样对待新手引导画面的,我把这个本身视为使用软件的一个障碍。因为这就像中国式灌输教育一样,先把一大堆东西灌输给你,希望你全部掌握之后再去做实用的事情。这对于学习软件操作来说基本上是不可能的,因为一个用户下载好软件之后,肯定希望马上就动手试试,出来一长串新手引导界面,显然是大煞风景,而且我想普通用户肯定不会像背单词一样地去记住每一个操作是怎么做的,因为用软件不是考试。

后来出现的真实界面+蒙版+提示语的引导画面,虽然更加接近实际操作的环境,但我认为在本质上和上面一种新手引导画面没有多少差别。

财富通的新的引导画面则是更加进了一步,因为引导的是新的手势操作功能,所以使用了真实界面+自动操作+手的示意图的组合。这样一来是最贴近实际操作的效果的。

不过我认为这不是新手引导的最佳方式,我认为的最佳方式是没有新手引导界面。这颇有无招胜有招的意思。最佳的情况是——你的软件不需要任何教学,别人上手凭着直觉就会用。这是苹果比较推崇的方式,所以它的iDevice都没有什么特别详细的说明书。不过要做到这点是相当有难度的,完全要看软件设计者的水平,并且有些东西是要做点取舍,这一点,苹果也不总是做得特别好。

我认为要做出这样的设计,需要考虑以下几点:

1.随大流:

有时候随大流不一定代表着平庸,而是代表习惯。当某个操作已经成为大多数软件的共同点时,去改变这个操作的习惯性效果往往使得用户体验非常糟糕。我之前一直在诟病财富通的“返回逻辑”使得返回按钮的点击行为不是正常系统默认的行为,却是想了自己一套复杂的逻辑,可谓是在APP里面独树一帜。但是这样好吗?我觉得非常糟糕,每次我点了返回按钮,页面没有返回却跳到分时图了,我总会觉得一定是我点了不该点的地方。所以遵循系统常用的默认的行为,这是一个软件设计的基本要素,只要遵循了,那么有很多的新手指引可以省去。

不过这里特别强调了“常用”,有些系统行为本身没有设计好,导致很少有用户知道它,那么将它定义为新的意义便是一种创新。这里有个很典型的例子就是“摇一摇”的功能。在苹果最初的设计里,“摇一摇”的含义是“撤销”的意思,有兴趣的同学可以拿苹果的程序(比如“邮件”)来测试一下。然而事实上是这个功能因为比较隐蔽,几乎很少有用户知道它,于是便有微信就重新定义了这个操作为“随机配对”的新含义,取得了非常好的效果。非常巧的是,摇一摇的新手指引非常简单,只需要在那个界面上写“摇一摇”就行了,既不影响用户操作,也不会对用户造成理解上的障碍,“摇一摇”三个字本身其实根本算不上是新手指引画面,而是实体界面的一部分。

所以在软件设计的时候,尽可能设计成用户习惯的操作,这样可以省去新手指引。

2.过渡:

所谓过渡,就是如果想引入一个新交互方式时,需要保留原有的习惯性的交互方式。事实上,UI设计中的拟物派保持的也是这种思路,即通过模仿自然世界的事物存在形式,引入到软件UI操作中去,对于一个电子产品小白来说,拟物化的UI就是一种过渡。

当然过渡不仅仅只有这样的例子。最近很流行手势化划屏菜单操作,它的鼻祖就是Path。

如果将手指向右滑动,左边就会出现被覆盖的菜单。这显然比点击左上方的按钮方便很多。然而path并没有去掉左上方的按钮,也没有对这个功能做新手指引。因为即使用户不知道有手势操作,也一定会习惯性地试试左上方的按钮是干嘛的,那么它并不影响用户的正常使用,所以不需要有用户引导界面。这就是一种过渡的设计。

不过你总是希望用户会学习到这个手势操作,否则这个精巧的设计就白做了,这时候就需要考虑到“暗示”了。

3.暗示:

所谓的暗示,是要通过一些动画效果,提醒用户可能会有某种交互设计,然后使他尝试,这样,便可不制作教程动画。这主要是用于手势交互的教学。再拿上面的path为例,点了左上角的菜单按钮,整个中间的主界面会有一个动画往右移,显露出被遮盖的菜单。那么用户自然会想,是不是拿手指划一下也能划出这个菜单呢。这就是暗示的效果。

再回想一下iOS早先的版本,在App Store安装软件的时候,会有一个动画效果:显示主界面,然后滚动屏幕到软件安装好后图标所在的分页,然后将软件图标显示出来。这个设计一直被人诟病,然后在iOS6里面改掉了。不过我觉得苹果在最初设计这个交互的时候,他们的主要考虑点是暗示iOS系统的主屏是可以滚动的。因为最初拿到iOS设备时只有两个分屏,如果你是世界上第一个iPhone用户,又没有使用说明书的话,也许并不会知道一屏软件装满之后,会产生第二个分屏,新的软件会显示在那里。那么这个交互动画便是暗示性的教学了。而当这个操作已被人所熟知的时候,撤掉这个动画就成了明智的选择。

现在国内的一些软件也都加入了手势操作,比如网易新闻、新浪微博。我认为网易新闻的设计是个非常好的暗示性教学+过渡型设计的范例:网易新闻点击进入一条新闻之后,可以通过向右划屏的手势返回到新闻列表界面。但是它保留了返回键,这是属于过渡。而它进入新闻详情,和电了返回键之后,会有一个类似栈的push, pop动画,会让人联想到是否可以使用手指滑动来操作页面栈,这就是暗示性的教学设计。

网易新闻的暗示性设计

而新浪微博虽然也有手势操作,然而它点击返回按钮时,遵循了最简单的navigation 的back效果,并没有暗示性的效果,这便属于设计失败了。

我认为,一个应用如果在设计上考虑到了上面三点,就能够大幅度地减少引导画面的使用,从而得到一个更好的用户体验。

SAE Python版与Django初体验

昨天新浪SAE的python平台正式对外开放。虽然很久以前我就作为测试用户得到了python平台的使用权,但是一直没去搞过这玩意儿。今天想起来,就拿来试试。

今天只是搞了一下最简单的东西,新建一个app,然后里面建个model,然后注册一个admin site,看它能跑就行了。

本地调试自然没什么问题,但是上传到SAE之后,还是有些问题要解决的。针对SAE新增的一些文件,比如config.yaml跟index.wsgi,在这里就不赘述了,根据新手指南来就行了。

 

继续阅读

Core Data Migration 之拆分Entity

参考文章:http://blog.slalom.com/2010/01/08/splitting-an-entity-in-a-coredata-migration/

最近自己做的背单词软件,在之前的设计上有一个非常大的缺陷就是把单词读音的语音文件放在数据库里面了,而且作为word表里面的一个字段储存。一开始测试的时候没有什么大问题,但是单词越来越多之后查询就变得非常之慢。后面自己加上的一些功能都要频繁地对比数据库,所以做了一个优化就是在core data fetch request里面指定要获取的字段,在这里排除读音字段的话,查询就非常快了,尤其当我把单词本身的string做了index之后。但是代码就很难看了。

core data里面的讲到类似情况的优化时说,需要把大数据文件新建一个Entity独立出来,然后和以前的Entity建立一对一关系。然而"Core Data Model Versioning and Data Migration Programming"这篇文档完全忽略了怎么样拆分Entity这块内容,今天搜到了一篇文章才知道怎么做。

以下就是一个示范,基于Xcode生成的core data模板程序,并且我假定你已经知道最基本的Model versioning和mapping object的知识。

原始的Model是这样的:

假设我们现在的目标是把address分离到一个独立的entity中去。

现在新建立一个Model Version,假设命名为version2,把它搞成这样:

这样,就把address分离出去了。

接下来要做Migration了,新建一个Mapping Model文件:

根据向导,将source设置为原始的Model文件,target设置为新的Model文件。然后Xcode会生成如下的Mapping文件:

在这个文件基础上,我们只要稍作修改就能完成了。

首先把ENTITY MAPPING下面的Address重命名为EventToAddress(这步可做可不做),然后在右边栏中的Entity Mapping中,设置Source为Event:

接下来改address的Value Expression为$source.address 如下:

然后在左边的ENTITY MAPPINGS栏中切换到EventToEvent,选中Relation Mappings中的address,在右边栏中设置Key Path为$source,设置Mapping Name为EventToAddress(如果刚才没改名,就用刚才的名字)。

然后设置model的current version为version2:

最后一步,在程序中初始化NSPersistentStoreCoordinator的地方,加上NSMigratePersistentStoresAutomaticallyOption的选项,代码类似如下:

 

_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
                             [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    } 

最后,运行程序,在程序初始化的时候,就会自动做migration了。如果要migration的数据量非常大,就会非常的慢。

完整源代码见:https://gitcafe.com/hikui/iOS-Example/tree/master/CoreDataMigrationDemo

iOS6 NavigationController的orientation问题

iOS 6的orientation还是比较奇葩的。我一开始以为只要把之前的- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation 改成 - (BOOL)shouldAutorotate和- (NSUInteger)supportedInterfaceOrientations的组合就行了,但是套了一层UINavigationController之后情况又变了:无论child view controller支持哪些orientation,UINavigationController总是会支持UIInterfaceOrientationMaskAllButUpsideDown。所以要让UINavigationController中的某些viewController通过自身的supportedInterfaceOrientations设置来调整orientation变得不可能。

最后在stackoverflow上面看到一个解决办法,看起来也是比较山寨的,但至少解决了问题:

@implementation UINavigationController (Rotation_IOS6)

-(BOOL)shouldAutorotate
{
    return [[self.viewControllers lastObject] shouldAutorotate];
}

-(NSUInteger)supportedInterfaceOrientations
{
    return [[self.viewControllers lastObject] supportedInterfaceOrientations];
}

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
    return [[self.viewControllers lastObject] preferredInterfaceOrientationForPresentation];
}

@end

一个页面切换效果的拙劣模仿

模仿的是iBooks打开书本时的效果,即缩略图开始放大,然后变成了一本书的内容。包括weico里面打开一个微博的图片也有类似的效果。

但是我的模仿参照了github上面的某一串代码(我现在又搜不到了),进行了一些改进。但是和iBooks以及weico比起来,又少了一些东西,所以比较拙劣。

我的效果是这样的。

原理:

使用两个ViewController。其中DetailViewController当作modalViewController来弹出。并设置detailVC.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;

这时,两个ViewController之间的切换就有渐变过渡了。

然后,detailViewController中设置一个property:originalRect,来保存tableViewController中,点到的cell.imageView.frame相对于顶级View的rect。

在detailViewController的viewWillAppear中,让detailViewController.imageView.frame从originalRect变换到全屏。

这样看上去就好像这个图片从tableViewCell里面飞出来变全屏一样。

但是仔细看和iBooks还是不一样。iBooks应该也是用了modalViewController,并且应该也用了UIModalTransitionStyleCrossDissolve。但是在Dissolve之前,tableViewCell中的image确实做了一点CoreAnimation的东西。但是如何计算我不太清楚。希望各位大神能够告诉我。

关键代码:

ViewController.m

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    UIImageView *imgView = cell.imageView;
    CGRect frame = imgView.frame;
    NSLog(@"original frame:%@",[NSValue valueWithCGRect:frame]);
    CGRect rectInParentView = [imgView convertRect:frame toView:self.view];
    NSLog(@"rect in parent view:%@",[NSValue valueWithCGRect:rectInParentView]);
    
    DetailViewController *detailVC = [[DetailViewController alloc]init];
    detailVC.originalRect = rectInParentView;
    detailVC.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
    [self presentModalViewController:detailVC animated:YES];
}

DetailViewController.m

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor scrollViewTexturedBackgroundColor];
    UIImageView *imgView = [[UIImageView alloc]initWithFrame:self.originalRect];
    imgView.contentMode = UIViewContentModeScaleAspectFit;
    imgView.image = [UIImage imageNamed:@"flag.jpg"];
    imgView.userInteractionEnabled = YES;
    UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapImage:)];
    [imgView addGestureRecognizer:tapGesture];
    [self.view addSubview:imgView];
    self.imageView = imgView;
}

- (void)viewWillAppear:(BOOL)animated
{
    [UIView animateWithDuration:0.4 delay:0.1 options:0 animations:^{
        self.imageView.frame = self.view.bounds;
    } completion:nil];
}

- (void)tapImage:(id)sender
{
    NSLog(@"tap image");
    [UIView animateWithDuration:0.4 animations:^{
        self.imageView.frame = self.originalRect;
    }];
    [self performSelector:@selector(dismissModalViewControllerAnimated:) withObject:[NSNumber numberWithBool:YES] afterDelay:0.2];
}

完整代码点此

[摘抄]虾米自动赞Greasemonkey脚本,记之免忘

// ==UserScript==
// @name            Xiami Loop
// @namespace       http://j2nete.com/xiamiloop
// @description     Xiami loop auto
// @author          Owen Liu
// @version         0.1
// @match           http://loop.xiami.com/*
// @match           https://loop.xiami.com/*
// ==/UserScript==


setInterval(auto, 30000);
function auto(){
  var link = document.getElementById("rank_plus");
  link = link.childNodes[0];
  if(document.all) {
    link.click();   
  } else {   
    var evt = document.createEvent("MouseEvents");   
    evt.initEvent("click", true, true);   
    link.dispatchEvent(evt);   
  }
};

自定义对象作为NSDictionary key的一些问题

在Java中把一个自定义的类生成的对象作为HashTable的key是天生可行的,但是在Objective C中,NSDictionary的key使用的是copy方法,所以自定义的类必须要实现copyWithZone才可以。

假设现在有一个CustomClass:

@interface CustomClass : NSObject <NSCopying>

@property (strong) NSString *name;

@end

这个时候,如果不写copyWithZone,那么把它当作NSDictionary key时,就会报错。于是,需要实现copyWithZone方法:

- (id)copyWithZone:(NSZone *)zone
{
    id aCopy = [[[self class] alloc]init];
    if (aCopy) {
        [aCopy setName:[self.name copyWithZone:zone]];
    }
    return aCopy;
}

这样做之后,就可以当作key传到NSDictionary中去了。

但是这还没完,只实现了copyWithZone,只能保证放的进去,却没办法取出来。需要在CustomClass里面另外实现isEqual方法和hash方法才能保证取出来。

比如说我这里定义如果两个obj的name一样就算equal了:

//这是一个错误的设计
- (BOOL)isEqual:(id)object
{
    if ([object isKindOfClass:self.class] && [((CustomClass *)object).name isEqualToString:self.name]) {
        return YES;
    }
    return NO;
}

如果这个时候我不重写hash方法,那么用这样的Class生成的object作为key的时候就会出逻辑上的问题:

 

        CustomClass *a = [[CustomClass alloc]init];

        CustomClass *b = [[CustomClass alloc]init];

        a.name = @"a";

        b.name = @"a";

        NSMutableDictionary *dict = [[NSMutableDictionary alloc]init];

        [dict setObject:@"aaa" forKey:a];

        [dict setObject:@"bbb" forKey:b];

        NSLog(@"%@",[dict objectForKey:b]);

        NSLog(@"dict count: %ld",dict.count);

当两个对象的name是一样时,这个打印出来会是aaa,而且count是1。这是因为NSDictionary在找value的时候,是通过hash值和isEqual共同计算出来的。默认的hash算法会使得a和b返回的hash是一样,而isEqual又是一样的,那么a和b实际上就是相同的key了。

虽然不知道apple的NSDictionary具体是怎么工作的,但是通过GNUStep的源码可以知道,它的内部工作原理。它使用了如下的数据结构作为Map

 /*   This is the map                C - array of the buckets
 *   +---------------+             +---------------+
 *   | _GSIMapTable  |      /----->| nodeCount     |  
 *   |---------------|     /       | firstNode ----+--\  
 *   | buckets    ---+----/        | ..........    |  |
 *   | bucketCount  =| size of --> | nodeCount     |  |
 *   | nodeChunks ---+--\          | firstNode     |  |
 *   | chunkCount  =-+\ |          |     .         |  | 
 *   |   ....        || |          |     .         |  |
 *   +---------------+| |          | nodeCount     |  |
 *                    | |          | fistNode      |  | 
 *                    / |          +---------------+  | 
 *         ----------   v                             v
 *       /       +----------+      +---------------------------+ 
 *       |       |  * ------+----->| Node1 | Node2 | Node3 ... | a chunk
 *   chunkCount  |  * ------+--\   +---------------------------+
 *  is size of = |  .       |   \  +-------------------------------+
 *               |  .       |    ->| Node n | Node n + 1 | ...     | another
 *               +----------+      +-------------------------------+
 *               array pointing
 *               to the chunks
 */

key的hash方法返回的值被用来计算bucket所在的位置(通过mod计算),计算出bucket之后,就从node1开始遍历,直到[nodeX->key isEqual: key]为止,再取出nodeX->value。

  • 所以在重写isEqual和hash的时候,如果设计不当,会出现各种问题:
  • 首先按照苹果的文档,要保证两个obj如果isEqual为YES,那么两个obj的hash值必须一样。
  • 并且,因为NSDictionary会copy,所以要保证obj1 copy出来的obj2的hash和obj1必须一样,并且obj1 isEqual:obj2要为YES,否则能放不能取。
  • 并且,hash的算法最好不要依赖于object的内部变量,除非你能保证这个内部变量不变。

基于上述要求,我想到一个比较山寨的解决办法:

加入一个私有的@property (unsafe_unretained) NSUInteger myHash;

当init时,将myHash的值改为(NSUInteger)self;

在copyWithZone时,将拷贝出来的对象的myHash改成原先的myHash。

代码:

 

@interface CustomClass()

@property (unsafe_unretained) NSUInteger myHash;

@end

@implementation CustomClass

- (id)copyWithZone:(NSZone *)zone
{
    id aCopy = [[[self class] alloc]init];
    if (aCopy) {
        [aCopy setName:[self.name copyWithZone:zone]];
        [aCopy setMyHash:self.myHash];
    }
    return aCopy;
}
- (id)init
{
    if (self = [super init]) {
        _myHash = (NSUInteger)self;
    }
    return self;
}
- (NSUInteger)hash
{
    return _myHash;
}
- (BOOL)isEqual:(id)object
{
    return self.myHash == ((CustomClass *)object).myHash;
}
@end

 

 

关于我

第一代90后,双鱼座男生,求女友!!!

前东方财富网的iOS开发工程师,现为墨尔本大学研究生。

主要技能:iOS, Python, CSS, 宅

业余爱好:羽毛球、乒乓球、唱歌、医学、心理学

Gtalk/Gmail: hikuimiao(at)gmail.com

QQ:2253737573

微博: http://weibo.com/hikui

github: https://github.com/hikui

gitcafe: http://gitcafe.com/hikui

 

iOS Orientation一些事(不包括iOS6)

完整代码可在 gitcafe项目主页 中下载。

之前的一篇文章提到对iOS系统键盘的hack,其中有提到新版的东方财富通的数字键盘是自定义键盘。在新的需求里,个股搜索需要支持横屏,本人又比较懒,不想写代码一个键一个键的调frame,就想到了通过判断Orientation从xib载入不同的view直接贴上去。但是碰到了一个不小的问题。

因为代码是属于公司的,我不好直接拿出来贴。在这里我把问题简化地提出来:

如图有两个ViewController,设左边为A,右边为B。点击A时,把B push进navigationController。要求进入B时,如果画面为横屏,则在label显示landscape,如果是竖屏,则显示portrait。在B中,要随着设备的orientation变化,切换label显示landscape或者portrait。

 

继续阅读

简单即是美

本文并不是一篇技术类的文章,只是用来吐槽一下我们公司的需求组和美工组的。

虽然我不炒股,但是因为在东方财富网工作,所以时常到各个竞争对手的网站上去看看。我觉得东方财富网的网页设计实在是太复杂了,如果让我用,我应该不会选择这个网站。为什么呢?看一下图:

这是网站进去的第一个画面,我的13寸MBP显示的第一眼就是这么些内容。这些内容给人的第一印象是排版格局有问题,广告实在太多了。网站最最上面的竟然是个广告,这可能可以赚很多钱,但是从用户体验的角度来说,是不可接受的。广告在整个版面占的比重实在是太多,会让人误以为进了一个广告页面。更要命的是,文字也太多,密密麻麻的文字扑面而来,我怀疑那些密集恐惧症患者是不是会崩溃掉。

在信息爆炸的时代,信息太多就是没有信息。没有人会耐心地看完这个版面的所有文字,多数的文字产生的效果仅仅是噪声而已。比如logo下面的天气预报,我不知道在这么一个网站上面要天气预报干什么,我觉得没有人回去看这个天气预报,那么这个地方的文字纯粹是背景噪声。然后我也不知道这么多标签性质的链接是干什么用的,很多地方都是重复的,比如行情中心那一条红色栏上面的文字,完全可以到“股票”子页面去,而不是在首页上面,这整一个框完全是原本应该放在“股票”子页面的内容放在了首页的“快捷方式”。但是快捷方式真的快捷吗?如果你试试把你所有的软件都在桌面上面放一个快捷方式看看,你会发现因为内容太多,你根本找不到本来要找的东西,多出来的快捷方式只会充当背景噪声的作用。而页面里面最重要的新闻咨询,因为广告和各种快捷方式的原因,在不滚动的情况下只能看到5行。根据版式设计的原理,需要滚动的部分也许用户永远不会去看,需要滚轮滚两三下才能看到的地方,你即便写dfasfdsaf这样没有意义的东西,恐怕别人也不会去关心的。“最佳免费炒股软件”这几个字放在用户不会看的地方,我估计没有人会点那个链接的。

看看同花顺的网站,我就觉得顺眼很多了

虽然也有广告,但是在第一个版面只占了一行。接下来就是最重要的信息部分——新闻和上证指数的图。页面上也没有各种“快捷方式”了,“软件下载”被放在了用户最容易注意到的地方,首页做到了它真正要做的事情。虽然它也和诸多门户网站一样比较复杂,但是在版式设计上面绝对超过了我们公司的网站。

之前我在携程实习的时候,就是在UED部门,当时看到他们通过技术的手段来绘制用户鼠标的热图,便可以看出哪些地方的信息会被用户看的最多。我不知道我们公司有没有人这么干过,即便不这样,那也希望设计网页版式的同事们看看基本的版式设计书,如果在审美上面连我这个小程序员都不如,那我就没话说了。

另外我还要吐槽一下我们手机客户端需求组,我们每次在需求里都会看到很复杂的用户逻辑,比如在Navigation上面点击“返回”按钮,要根据不同的页面做不同的返回逻辑,有些“返回”直接是pop,有些返回却要到指定的某一页。我们的需求组在定这些需求的时候,总是自认为合情合理,自以为在为用户着想,但是用户一定在潜意识中就认为返回按钮理应是pop,一个复杂的逻辑反而会让他们不知所措。于是我们就效仿大家的做法,在第一次启动的时候加入新手引导界面。实际上根本没有用。我自己用软件的时候,新手引导页完全是一个累赘,一般我看都不看就跳过它。一个好的UI设计,应该能让用户看到第一眼的时候就知道怎么用,这是苹果信奉的观念,也是最好的UED理念。

简单就是美,这是每一个UED工作者都必须要知道要奉行的。简单就是难,对于一个UED工作者来说,要设计出一个让用户觉得简单易用的软件是相当考验自身水平的。所以在这个方面,我们公司必须要好好努力才是。