勤之时 - 表示层(一)

应用很早就上线了,欢迎大家下载使用:http://itunes.apple.com/app/id1206687109

源码已经公开,大家可以去https://github.com/Inspirelife96/ILDiligence下载。 喜欢的话Fork或者给个Star,非常感谢。

下面是这一系列的全部帖子:
想法和原型
勤之时 - 架构与工程组织结构
勤之时 - 数据持久层的实现
勤之时 - 网络层的实现
勤之时 - 业务逻辑层
勤之时 - Info.plist的改动
勤之时 - 表示层(一)
勤之时 - 表示层(二)
勤之时 - 表示层(三)
勤之时 - 表示层(四)
勤之时 - 表示层(五)

表示层:由UIKit Framework构成,也就是我们看到的视图,控制器,各种控件以及事件处理等内容。

首先来谈谈表示层的架构,继续推荐大神的iOS应用架构谈 view层的组织和调用方案

说下【勤之时】最后适用的要点:

以下内容摘抄自iOS应用架构谈 view层的组织和调用方案

  • 所有的属性都使用getter和setter
    不要在viewDidLoad里面初始化你的view然后再add,这样代码就很难看。在viewDidload里面只做addSubview的事情,然后在viewWillLayoutSubviews里面做布局的事情,最后在viewDidAppear里面做Notification的监听之类的事情。至于属性的初始化,则交给getter去做。 例如:
#pragma mark - Life Cycle

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self.view addSubview:self.taskScrollView];
    [self.view addSubview:self.pageControl];
}

- (void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];
    
    [self.taskScrollView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.view);
        [self.taskScrollView setContentSize:CGSizeMake(CGRectGetWidth(_taskScrollView.frame) * self.taskIds.count, CGRectGetHeight(_taskScrollView.frame))];
    }];
    
    [self.pageControl mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.view.mas_top).with.offset(30);
        make.centerX.equalTo(self.view);
        make.height.mas_equalTo(28);
        make.width.mas_equalTo(ScreenWidth - 42 * 2);
    }];
}

#pragma mark - Getter and Setter

- (UIScrollView *)taskScrollView {
    if (!_taskScrollView) {
        _taskScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, MainScreenWidth, MainScreenHeight)];
        NSMutableArray *controllers = [[NSMutableArray alloc] init];
        for (NSUInteger i = 0; i < self.taskIds.count; i++)
        {
            [controllers addObject:[NSNull null]];
        }
        self.viewControllers = controllers;
        
        _taskScrollView.pagingEnabled = YES;
        _taskScrollView.contentSize = CGSizeMake(CGRectGetWidth(_taskScrollView.frame) * self.taskIds.count, CGRectGetHeight(_taskScrollView.frame));
        _taskScrollView.showsHorizontalScrollIndicator = NO;
        _taskScrollView.showsVerticalScrollIndicator = NO;
        _taskScrollView.scrollsToTop = NO;
        _taskScrollView.delegate = self;
    }
    
    return _taskScrollView;
}

- (UIPageControl *)pageControl {
    if (!_pageControl) {
        _pageControl = [[UIPageControl alloc] init];
        self.pageControl.numberOfPages = self.taskIds.count;
        self.pageControl.currentPage = 0;
    }
    
    return _pageControl;
}

  • getter和setter全部都放在最后
    因为一个ViewController很有可能会有非常多的view,就像上面给出的代码样例一样,如果getter和setter写在前面,就会把主要逻辑扯到后面去,其他人看的时候就要先划过一长串getter和setter,这样不太好。然后要求业务工程师写代码的时候按照顺序来分配代码块的位置,先是life cycle,然后是Delegate方法实现,然后是event response,然后才是getters and setters。这样后来者阅读代码时就能省力很多。
  • 每一个delegate都把对应的protocol名字带上,delegate方法不要到处乱写,写到一块区域里面去
    比如UITableViewDelegate的方法集就老老实实写上#pragma mark - UITableViewDelegate。这样有个好处就是,当其他人阅读一个他并不熟悉的Delegate实现方法时,他只要按住command然后去点这个protocol名字,Xcode就能够立刻跳转到对应这个Delegate的protocol定义的那部分代码去,就省得他到处找了。
  • event response专门开一个代码区域
    所有button、gestureRecognizer的响应事件都放在这个区域里面,不要到处乱放。
  • 关于private methods,正常情况下ViewController里面不应该写
    不是delegate方法的,不是event response方法的,不是life cycle方法的,就是private method了。对的,正常情况下ViewController里面一般是不会存在private methods的,这个private methods一般是用于日期换算、图片裁剪啥的这种小功能。这种小功能要么把它写成一个category,要么把他做成一个模块,哪怕这个模块只有一个函数也行。
    ViewController基本上是大部分业务的载体,本身代码已经相当复杂,所以跟业务关联不大的东西能不放在ViewController里面就不要放。另外一点,这个private method的功能这时候只是你用得到,但是将来说不定别的地方也会用到,一开始就独立出来,有利于将来的代码复用。

关于View的布局

  • 【勤之时】使用了Masonry

何时使用storyboard,何时使用nib,何时使用代码写View

  • 【勤之时】使用代码

关于MVC、MVVM等一大堆思想

  • 【勤之时】使用MVC

我会分好几个篇来说明表示层的开发。虽然这个应用的内容不多,但还是有几个页面的。首先来看看主界面:【勤之时】用来计时学习的界面:

Diligence Layer 1.png

功能描述:

  • 背景为每日一图
  • 主界面四个角落有四个按钮,点击可以进入各自的功能页面,分别为:
  • 任务管理
  • 统计
  • 每日分享
  • 设置
  • 顶部有一个Page Controller,有多少个小圆点就代表有多少个任务。通过左右滑动,可以切换到不同的任务。
  • 中上部为当前任务名称及当前的日历。同样,当左右滑动时,切换为不同的任务。
  • 背景图片在左右滑动是不会移动,每个任务有不同的配色,根据配色会对背景图增加一层对应的遮罩。
  • 中下部为【勤·开始】按钮,点击开始任务计时。
Diligence Layer 2.png
  • 计时时,会显示暂停按钮(若为沉浸模式,则无暂停按钮。)背景音乐开始播放(若背景音乐关闭,则不播放)。中上部圆盘内显示倒计时,圆环同时开始进度显示。
  • 点击暂停按钮,则出现继续/放弃按钮。音乐停,倒计时停。
  • 计时完成时,提示计时完成(蜂鸣+手机震动)。主页面切换为休息建议,并出现【勤·休息】按钮以及跳过按钮。
  • 若按【勤·休息】按钮,进入休息倒计时。
  • 若按跳过按钮,则直接回到默认的【勤·开始】页面。
  • 在倒计时过程中,所有其他额外的按钮都会隐藏。
  • 应用最小化后,音乐和倒计时会继续。
  • 在沉浸模式,倒计时时没有暂停按钮。最小化应用会直接退出倒计时,此时倒计时和应用都会暂停,回到默认的【勤·开始】页面

MVC设计考虑:

Controller:

  • ILDDiligenceViewController:
    page controller结合scrollview,以page的模式来显示ILDDiligenceClockViewController的View的内容。 当然,其他所有的按钮都在这个Controller中定义,包括四角的四个功能按钮,以及开始,暂停,继续,放弃,休息等按钮。

  • ILDDiligenceClockViewController:每一个Page的ViewController,主要包括背景颜色的遮罩,圆环以及圆环内的任务名称,日期,倒计时显示,休息建议等。每一个Page代表一个任务。

Model:

  • ILDDiligenceViewController对应的Model

  • NSArray:所有任务的Ids

  • ILDTaskModel:当前任务的具体内容

  • ILDStoryModel: 背景图片的内容

  • ILDDiligenceClockViewController对应的Model

  • ILDTaskModel:当前任务的具体内容

View:

  • ILDDiligenceClockViewController对应的View
  • ILDDiligenceClockView:用于具体描绘每个ClockView的类

ILDDiligenceViewController编码

  • 四个角的四个功能按钮,定义
@interface ILDDiligenceViewController () 

@property(nonatomic, strong) UIButton *taskButton;
@property(nonatomic, strong) UIButton *statisticsButton;
@property(nonatomic, strong) UIButton *storyButton;
@property(nonatomic, strong) UIButton *settingButton;

@end

在viewDidLoad中将这些按钮添加到Controller的View中

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self.view addSubview:self.taskButton];
    [self.view addSubview:self.statisticsButton];
    [self.view addSubview:self.storyButton];
    [self.view addSubview:self.settingButton];
}

在viewWillLayoutSubviews中设定Layout

- (void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];
    
      [self.taskButton mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.view.mas_top).with.offset(30);
        make.left.equalTo(self.view.mas_left).with.offset(12);
        make.height.mas_equalTo(28);
        make.width.mas_equalTo(28);
    }];
    
    [self.statisticsButton mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.view.mas_top).with.offset(30);
        make.right.equalTo(self.view.mas_right).with.offset(-12);
        make.height.mas_equalTo(28);
        make.width.mas_equalTo(28);
    }];
    
    [self.storyButton mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.view.mas_bottom).with.offset(-50);
        make.left.equalTo(self.view.mas_left).with.offset(12);
        make.height.mas_equalTo(28);
        make.width.mas_equalTo(28);
    }];
    
    [self.settingButton mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.view.mas_bottom).with.offset(-50);
        make.right.equalTo(self.view.mas_right).with.offset(-12);
        make.height.mas_equalTo(28);
        make.width.mas_equalTo(28);
    }];
}

在get函数中初始化

- (UIButton *)taskButton {
    if (!_taskButton) {
        _taskButton = [UIButton buttonWithType:UIButtonTypeCustom];
        [_taskButton setImage:[UIImage imageNamed:@"menu_task_28x28_"] forState:UIControlStateNormal];
        [_taskButton addTarget:self action:@selector(clickTaskButton:) forControlEvents:UIControlEventTouchUpInside];
    }
    
    return _taskButton;
}

- (UIButton *)statisticsButton {
    if (!_statisticsButton) {
        _statisticsButton = [UIButton buttonWithType:UIButtonTypeCustom];
        [_statisticsButton setBackgroundImage:[UIImage imageNamed:@"menu_statistics_28x28_"] forState:UIControlStateNormal];
        [_statisticsButton addTarget:self action:@selector(clickStatisticsButton:) forControlEvents:UIControlEventTouchUpInside];
    }
    
    return _statisticsButton;
}

- (UIButton *)storyButton {
    if (!_storyButton) {
        _storyButton = [[UIButton alloc] init];
        [_storyButton setBackgroundImage:[UIImage imageNamed:@"menu_story_28x28_"] forState:UIControlStateNormal];
        [_storyButton addTarget:self action:@selector(clickStoryButton:) forControlEvents:UIControlEventTouchUpInside];
    }
    
    return _storyButton;
}

- (UIButton *)settingButton {
    if (!_settingButton) {
        _settingButton = [[UIButton alloc] init];
        [_settingButton setBackgroundImage:[UIImage imageNamed:@"menu_settings_26x26_"] forState:UIControlStateNormal];
        [_settingButton addTarget:self action:@selector(clickSettingButton:) forControlEvents:UIControlEventTouchUpInside];
    }
    
    return _settingButton;
}

按钮对应的Event函数

- (void)clickTaskButton:(id)sender {
    [self copyScreen];
    
    ILDTaskListViewController *taskListVC = [[ILDTaskListViewController alloc] init];
    UINavigationController *taskListNC = [[UINavigationController alloc] initWithRootViewController:taskListVC];
    [self presentViewController:taskListNC animated:YES completion:nil];
}

- (void)clickStatisticsButton:(id)sender {
    [self copyScreen];
    
    ILDStatisticsTodayViewController *statisticsTodayVC = [[ILDStatisticsTodayViewController alloc] init];
    UINavigationController *settingNC = [[UINavigationController alloc] initWithRootViewController:statisticsTodayVC];
    [self presentViewController:settingNC animated:YES completion:nil];
}

- (void)clickStoryButton:(id)sender {
    ILDStoryViewController *storyVC = [[ILDStoryViewController alloc] init];
    [self presentViewController:storyVC animated:YES completion:nil];
}

- (void)clickSettingButton:(id)sender {
    [self copyScreen];
    
    ILDSettingViewController *settingVC = [[ILDSettingViewController alloc] init];
    UINavigationController *settingNC = [[UINavigationController alloc] initWithRootViewController:settingVC];
    [self presentViewController:settingNC animated:YES completion:nil];
}

其他控件的操作基本上同上,以同样的方式处理ScrollView和PageView,然后再添加对应的ScrollViewDeligate

- (void)loadScrollViewWithPage:(NSUInteger)page {
    if (page >= self.taskIds.count) {
        return;
    }
    
    // replace the placeholder if necessary
    ILDDiligenceClockViewController *controller = [self.viewControllers objectAtIndex:page];
    if ((NSNull *)controller == [NSNull null]) {
        controller = [[ILDDiligenceClockViewController alloc] init];
        controller.taskId = self.taskIds[page];
        controller.diligenceClockView.delegate = self;
        controller.isRestMode = NO;
        [self.viewControllers replaceObjectAtIndex:page withObject:controller];
    }
    
    // add the controller's view to the scroll view
    if (controller.view.superview == nil) {
        CGRect frame = self.clockScrollView.frame;
        frame.origin.x = CGRectGetWidth(frame) * page;
        frame.origin.y = 0;
        controller.view.frame = frame;
        
        [self addChildViewController:controller];
        [self.clockScrollView addSubview:controller.view];
        [controller didMoveToParentViewController:self];
    }
}

// at the end of scroll animation, reset the boolean used when scrolls originate from the UIPageControl
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    // switch the indicator when more than 50% of the previous/next page is visible
    CGFloat pageWidth = CGRectGetWidth(self.clockScrollView.frame);
    NSUInteger page = floor((self.clockScrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
    self.pageControl.currentPage = page;
    
    // load the visible page and the page on either side of it (to avoid flashes when the user starts scrolling)
    [self loadScrollViewWithPage:page - 1];
    [self loadScrollViewWithPage:page];
    [self loadScrollViewWithPage:page + 1];
    
    // a possible optimization would be to unload the views+controllers which are no longer visible
}

- (void)gotoPage:(BOOL)animated {
    NSInteger page = self.pageControl.currentPage;
    
    // load the visible page and the page on either side of it (to avoid flashes when the user starts scrolling)
    [self loadScrollViewWithPage:page - 1];
    [self loadScrollViewWithPage:page];
    [self loadScrollViewWithPage:page + 1];
    
    // update the scroll view to the appropriate page
    CGRect bounds = self.clockScrollView.bounds;
    bounds.origin.x = CGRectGetWidth(bounds) * page;
    bounds.origin.y = 0;
    [self.clockScrollView scrollRectToVisible:bounds animated:animated];
}

点击开始按钮是,我们需要播放背景音乐,所以需要引入AVAudioPlayer

@property(nonatomic, strong) AVAudioPlayer *musicPlayer;

- (void)playMusic {
    self.musicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[MusicHelper musicUrlByName:self.currentTaskModel.musicName] error:nil];
    self.musicPlayer.delegate = self;
    self.musicPlayer.numberOfLoops = -1;
    self.musicPlayer.volume = 1;
    [self.musicPlayer prepareToPlay];
    self.musicPlayer.meteringEnabled = YES;
    [self.musicPlayer play];
}

- (void)pauseMusic {
    [self.musicPlayer pause];
}

- (void)stopMusic {
    [self.musicPlayer stop];
}

- (void)resumeMusic {
    [self.musicPlayer play];
}

每次计时完成时,需要有提示声及振动:

- (void)playSystemSound {
    SystemSoundID sound = kSystemSoundID_Vibrate;
    
    //这里使用在上面那个网址找到的铃声,注意格式
    NSString *path = [NSString stringWithFormat:@"/System/Library/Audio/UISounds/%@.%@",@"new-mail",@"caf"];
    if (path) {
        OSStatus error = AudioServicesCreateSystemSoundID((__bridge CFURLRef)[NSURL fileURLWithPath:path],&sound);
        if (error != kAudioServicesNoError) {
            sound = 0;
        }
    }
    
    AudioServicesPlaySystemSound(sound);//播放声音
    AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);//静音模式下震动
}

每次任务完成后,需要把任务的统计信息保存起来

- (void)taskCompleted {
    NSInteger page = self.pageControl.currentPage;
    ILDDiligenceClockViewController *controller = [self.viewControllers objectAtIndex:page];
    
    [self stopMusic];
    [self playSystemSound];
    
    if (controller.isRestMode) {
        [self setToDiligenceStartStatus];
    } else {
        ILDDiligenceModel *diligencModel = [[ILDDiligenceModel alloc] init];
        diligencModel.taskId = self.taskIds[page];
        diligencModel.startDate = self.startDate;
        diligencModel.endDate = [NSDate date];
        diligencModel.breakTimes = [NSNumber numberWithInteger:self.breakTimes];
        diligencModel.diligenceTime = self.currentTaskModel.diligenceTime;
        
        [[ILDDiligenceDataCenter sharedInstance] addDiligence:diligencModel];
        if (self.currentTaskModel.isRestModeEnabled) {
            [self setToRestStartStatus];
        } else {
            [self setToDiligenceStartStatus];
        }
    }
}

应用最小化时,要根据是否是沉浸模式,继续计时和音乐或关闭计时和音乐,此时需要监听UIApplicationDidEnterBackgroundNotification,使用NSNotificationCenter

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterBackground:)name:UIApplicationDidEnterBackgroundNotification object:nil];

需要监听ILDStoryDataCenter的storyDataDictionary成员,当变化时,背景图片也要相应的变化,使用KVO

 [[ILDStoryDataCenter sharedInstance] addObserver:self forKeyPath:@"storyDataDictionary" options:NSKeyValueObservingOptionNew context:NULL];

ILDDiligenceViewController编码
这个类相对简单
首先根据当前的任务设定背景颜色

    self.taskModel = [[ILDTaskDataCenter sharedInstance] taskConfigurationById:self.taskId];
    self.backgroundView.backgroundColor = [ColorHelper colorByName:self.taskModel.color];

其他的事情由ILDDiligenceClockView来处理,他仅需要把ILDDiligenceClockView添加到自己的view中。

ILDDiligenceClockView编码
主要代码就是画时钟及其内容

- (void)drawRect:(CGRect)rect {
    // calculate angle for progress
    if (self.diligenceSeconds == 0) {
        self.endAngle = self.startAngle;
    } else {
        self.endAngle = (1 - self.timeLeft / self.diligenceSeconds) * 2 * M_PI + self.startAngle;
    }
    
    CGFloat radius = (rect.size.width - 20)/2;
    
    // draw circle
    UIBezierPath *circle = [UIBezierPath bezierPath];
    [circle addArcWithCenter:CGPointMake(rect.size.width / 2, rect.size.height / 2)
                      radius:radius
                  startAngle:0
                    endAngle:2 * M_PI
                   clockwise:YES];
    circle.lineWidth = CIRCLE_WIDTH;
    [FlatWhiteDark setStroke];
    [circle stroke];
    
    // draw progress
    UIBezierPath *progress = [UIBezierPath bezierPath];
    [progress addArcWithCenter:CGPointMake(rect.size.width / 2, rect.size.height / 2)
                        radius:radius
                    startAngle:self.startAngle
                      endAngle:self.endAngle
                     clockwise:YES];
    progress.lineWidth = PROGRESS_WIDTH;
    [FlatWhite setStroke];
    [progress stroke];
    
    if (self.isRunning) {
        // if Timer is running, always show time left in the center of the circle
        NSString *textContent = [ILDDateHelper minutesFormatBySeconds:self.timeLeft];
        
        UIFont *textFont = [UIFont fontWithName: @"-" size: TEXT_NAME_SIZE];
        CGSize textSize = [textContent sizeWithAttributes:@{NSFontAttributeName:textFont}];
        CGRect textRect = CGRectMake(rect.size.width / 2 - textSize.width / 2,
                                     rect.size.height / 2 - textSize.height / 2,
                                     textSize.width , textSize.height);
        
        NSMutableParagraphStyle *textStyle = [[NSMutableParagraphStyle defaultParagraphStyle] mutableCopy];
        textStyle.lineBreakMode = NSLineBreakByWordWrapping;
        textStyle.alignment = NSTextAlignmentCenter;
        
        [textContent drawInRect:textRect withAttributes:@{NSFontAttributeName:textFont, NSForegroundColorAttributeName:FlatWhite, NSParagraphStyleAttributeName:textStyle}];
    } else {
        // show task Name or rest suggestion Name
        NSString *taskOrRestName = self.taskName;
        if (self.isRestMode) {
            taskOrRestName = [ILDRestSuggestion randomRestSuggestion];
        }
        
        NSInteger fontSize = TEXT_NAME_SIZE;
        
        UIFont *taskNameFont = [UIFont fontWithName: @"-" size: fontSize];
        CGSize taskNameSize = [taskOrRestName sizeWithAttributes:@{NSFontAttributeName:taskNameFont}];
        
        while (taskNameSize.width > (self.frame.size.width - 20)) {
            fontSize -= 2;
            taskNameFont = [UIFont fontWithName: @"-" size: fontSize];
            taskNameSize = [taskOrRestName sizeWithAttributes:@{NSFontAttributeName:taskNameFont}];
        }
        
        CGFloat taskNameX = 10;
        CGFloat taskNameY = (rect.size.height - taskNameSize.height)/2 - 10;
        CGFloat taskNameWidth = self.frame.size.width - 20;
        CGFloat taskNameHeight = taskNameSize.height;
        
        CGRect taskNameRect = CGRectMake(taskNameX, taskNameY, taskNameWidth, taskNameHeight);
        
        NSMutableParagraphStyle *textStyle = [[NSMutableParagraphStyle defaultParagraphStyle] mutableCopy];
        textStyle.lineBreakMode = NSLineBreakByWordWrapping;
        textStyle.alignment = NSTextAlignmentCenter;
        
        [taskOrRestName drawInRect:taskNameRect withAttributes:@{NSFontAttributeName:taskNameFont, NSForegroundColorAttributeName:FlatWhite, NSParagraphStyleAttributeName:textStyle}];
        
        NSString *dateToday = [ILDDateHelper stringOfDayWithWeekDay:[NSDate date]];
        UIFont *dateFont = [UIFont fontWithName: @"-" size: TEXT_DATE_SIZE];
        CGSize dateSize = [dateToday sizeWithAttributes:@{NSFontAttributeName:dateFont}];
        
        CGFloat dateX = (rect.size.width - dateSize.width)/2;
        CGFloat dateY = taskNameY + taskNameHeight + 5;
        CGFloat dateWidth = dateSize.width;
        CGFloat dateHeight = dateSize.height;
        
        CGRect dateRect = CGRectMake(dateX, dateY, dateWidth, dateHeight);
        
        [dateToday drawInRect:dateRect withAttributes:@{NSFontAttributeName:dateFont, NSForegroundColorAttributeName:FlatWhite, NSParagraphStyleAttributeName:textStyle}];
        
        CGContextRef context = UIGraphicsGetCurrentContext();
        
        [FlatWhite setStroke];
        CGContextMoveToPoint(context, dateX, dateY - 2);
        CGContextAddLineToPoint(context, dateX + dateWidth, dateY - 2);
        CGContextMoveToPoint(context, dateX, dateY + dateHeight + 2);
        CGContextAddLineToPoint(context, dateX + dateWidth, dateY + dateHeight + 2);
        CGContextStrokePath(context);
    }
}

额外的讨论:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容