勤之時 - 表示層(一)

應用很早就上線了,歡迎大家下載使用: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閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容