iOS實現多個可變cell復雜界面的制作

在日常的開發中,有時會遇到內容塊比較多,且又可變的界面:

多個可變cell復雜界面

這個界面中有些內容塊是固定出現的,比如最上面的商品詳情圖片、商品名稱、價格等。而有些內容塊則是不一定出現的,比如促銷(顯然不是每個商品都有促銷)、已選規格(有的商品沒有規格)、店鋪信息(有的商品屬于自營,就沒有店鋪)等。還有些內容要根據情況進行變化,比如評論,這里最多列出4條評論,如果沒有評論,則顯示“暫無評論”且不顯示“查看所有評論”按鈕。

對于這樣的界面,相信很多人第一感覺會用TableView來做,因為中間要列出評論內容,這個用TableView的cell來填充比較合適。但如何處理評論內容之外的其他內容呢?我之前的做法是,評論內容之上的用HeaderView做,下面的用FooterView做,雖然最終實現了功能,但做起來十分麻煩。布局我是用Auto Layout來做的,由于Auto Layout本身的特點,這種做法在控制內容塊View的顯示與否,需要比較多的操作:

  • View的高度約束設置為0
  • 最好還要把子View全部移除,否則,子View里的約束可能會因為View高度約束設置為0而出現約束沖突
  • 如果View本身有距離前面View的間距約束,也需要將間距約束設置為0
  • View的hidden設置為yes,以減少視圖繪制

此外,還有一個麻煩的問題。界面剛進來的時候,是需要請求網絡數據,這時界面就要顯示成一個初始狀態,而顯然初始狀態有些內容塊是不應該顯示的,比如促銷,只有完成了數據請求,才能知道是否有促銷,有的話才顯示促銷內容;比如評論,初始時應該顯示成“暫無評論”,數據請求完成后,才顯示相應的內容。這樣,我們需要處理初始進入和數據請求完成兩種狀態下各個內容塊的顯示,十分復雜繁瑣。
總結來說,用TableView的 HeaderView + 評論內容cell + FooterView + Auto Layout 的方式會帶來如下問題:

  1. 約束在View與View之間是有依賴關系的,對View的顯示與否,需要比較多的操作
  2. 需要處理初始進入和數據請求完成兩種狀態的界面展示,使代碼更加復雜繁瑣
  3. 需要額外計算相應內容的高度,以更新HeaderView、FooterView的高度

可見,這種方式并不是理想的解決方案。可能有人會說,那不要用Auto Layout,直接操作frame來布局就好,這樣或許能減少一些麻煩,但總體上并沒有減少復雜度。也有人說,直接用ScrollView來做,這樣的話,所有的內容包括評論內容的cell,都得自己手動拼接,可以想象這種做法也是比較麻煩的。所以,我們得另辟蹊徑,使用其他方法來達到目的。下面就為大家介紹一種比較簡便的做法,這種做法也是一個前同事分享給我的,我就借花獻佛,分享給大家。


我們還是用TableView來做這個界面,和之前不同的是,我們把每一個可變內容塊做成一個獨立的cell,cell的粒度可以自行控制,比如可以用一個cell囊括商品圖片、標題、副標題、價格,也可以拆得更細,圖片、標題、副標題、價格都各自對應一個cell。這里我們選擇后者,因為圖片內容塊,我們需要按屏幕寬度等比例拉伸;標題、副標題的文字內容可能是一行,也可能是兩行,高度可變,用單獨的cell來控制會更簡單明了,也更加靈活。
下面先定義好各種類型的cell:

//基礎cell,這里為了演示簡便,定義這個cell,其他cell繼承自這個cell
@interface MultipleVariantBasicTableViewCell : UITableViewCell
@property (nonatomic, weak) UILabel *titleTextLabel;
@end

//滾動圖片
@interface CycleImagesTableViewCell : MultipleVariantBasicTableViewCell
@end

//正標題
@interface MainTitleTableViewCell : MultipleVariantBasicTableViewCell
@end

//副標題
@interface SubTitleTableViewCell : MultipleVariantBasicTableViewCell
@end

//價格
@interface PriceTableViewCell : MultipleVariantBasicTableViewCell
@end

// ...其他內容塊的cell聲明


// 各種內容塊cell的實現,這里為了演示簡便,cell中就只放了一個Label
@implementation MultipleVariantBasicTableViewCell

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 320, 44)];
        label.numberOfLines = 0;
        [self.contentView addSubview:label];
        self.titleTextLabel = label;
    }
    return self;
}

@end

@implementation CycleImagesTableViewCell
@end

@implementation MainTitleTableViewCell
@end

// ...其他內容塊的cell實現

// 評論內容cell使用Auto Layout,配合iOS 8 TableView的自動算高,實現內容自適應
@implementation CommentContentTableViewCell

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        self.titleTextLabel.translatesAutoresizingMaskIntoConstraints = NO;
        self.titleTextLabel.preferredMaxLayoutWidth = [UIScreen mainScreen].bounds.size.width - 8;
        NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:self.titleTextLabel attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeLeading multiplier:1.0f constant:4.0f];
        NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:self.titleTextLabel attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:-4.0f];
        NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:self.titleTextLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeTop multiplier:1.0f constant:4.0f];
        NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:self.titleTextLabel attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeBottom multiplier:1.0f constant:-4.0f];
        [self.contentView addConstraints:@[leftConstraint, rightConstraint, topConstraint, bottomConstraint]];
    }
    return self;
}

@end

接下來就是重點,就是如何來控制顯示哪些cell及cell顯示的數量。這一步如果處理不好,也會使開發變得復雜。如下面的方式:

// 加載完數據
self.cellCount = 0;
if (存在促銷) {
    self.cellCount++;
}
if (存在規格) {
    self.cellCount++;
}
......

如果以這種方式來記錄cell的數量,那么后續cell的展示、點擊判斷等都會很麻煩。這里我們采用的方式是,使用單獨的類(作為一種數據結構)來保存所要展示的cell信息。

// SKRow.h
@interface SKRow : NSObject

@property (nonatomic, copy) NSString *cellIdentifier;
@property (nonatomic, strong) id data;
@property (nonatomic, assign) float rowHeight;

- (instancetype)initWithCellIdentifier:(NSString *)cellIdentifier
                                  data:(id)data
                             rowHeight:(float)rowHeight;

@end

// SKRow.m
#import "SKRow.h"

@implementation SKRow

- (instancetype)initWithCellIdentifier:(NSString *)cellIdentifier data:(id)data rowHeight:(float)rowHeight {
    if (self = [super init]) {
        self.cellIdentifier = cellIdentifier;
        self.data = data;
        self.rowHeight = rowHeight;
    }
    return self;
}

@end

SKRow用來存儲每個cell所需的信息,包括重用標識、數據項、高度。接下來,我們就開始拼接cell信息。

@interface ViewController () <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) NSMutableArray<NSArray<SKRow *> *> *tableSections;
@end
self.tableSections = [NSMutableArray array];

/* 初始加載數據
 * 初始化時,只顯示滾動圖片、價格、評論頭、無評論
 */
// 滾動圖片(寬高保持比例)
SKRow *cycleImagesRow = [[SKRow alloc] initWithCellIdentifier:@"CycleImagesCellIdentifier" data:@[@"滾動圖片地址"] rowHeight:120*[UIScreen mainScreen].bounds.size.width / 320.f];
// 價格
SKRow *priceRow = [[SKRow alloc] initWithCellIdentifier:@"PriceCellIdentifier" data:@"0" rowHeight:44];
[self.tableSections addObject:@[cycleImagesRow, priceRow]];
// 評論頭
SKRow *commentSummaryRow = [[SKRow alloc] initWithCellIdentifier:@"CommentSummaryCellIdentifier" data:@{@"title":@"商品評價", @"count":@"0"} rowHeight:44];
// 無評論
SKRow *noCommentRow = [[SKRow alloc] initWithCellIdentifier:@"NoCommentCellIdentifier" data:@"暫無評論" rowHeight:44];
[self.tableSections addObject:@[commentSummaryRow, noCommentRow]];

以上是初始狀態時要顯示的cell,我們在ViewController中聲明一個數組,用來存儲TableView各個section要顯示的cell信息。這里我們將cell分成不同的section,實際中,要不要分,分成幾個section都可以自行決定。初始狀態我們有兩個section,第一個section用于顯示基本信息,第二個section用于顯示評論信息,這樣就完成了cell信息的拼接,接下來就是顯示:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    // 這里可以通過判斷cellIdentifier來區分處理各種不同的cell,cell所需的數據從row.data上獲取
    
    SKRow *row = self.tableSections[indexPath.section][indexPath.row];
    if ([row.cellIdentifier isEqualToString:@"CycleImagesCellIdentifier"]) {
        CycleImagesTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
        NSArray<NSString *> *urlStringArray = row.data;
        cell.titleTextLabel.text = [urlStringArray componentsJoinedByString:@"\n"];
        return cell;
    } else if ([row.cellIdentifier isEqualToString:@"MainTitleCellIdentifier"]) {
        MainTitleTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
        cell.titleTextLabel.text = row.data;
        return cell;
    } else if ([row.cellIdentifier isEqualToString:@"PriceCellIdentifier"]) {
        PriceTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
        cell.titleTextLabel.text = [NSString stringWithFormat:@"¥%@", row.data];
        return cell;
    } else if ([row.cellIdentifier isEqualToString:@"SalePromotionCellIdentifier"]) {
        SalePromotionTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
        NSArray<NSString *> *salePromotionStringArray = row.data;
        cell.titleTextLabel.text = [salePromotionStringArray componentsJoinedByString:@"\n"];
        return cell;
    } else if ([row.cellIdentifier isEqualToString:@"SpecificationCellIdentifier"]) {
        SpecificationTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
        cell.titleTextLabel.text = [NSString stringWithFormat:@"已選:%@", row.data];
        return cell;
    } else if ([row.cellIdentifier isEqualToString:@"CommentSummaryCellIdentifier"]) {
        CommentSummaryTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
        NSDictionary *commentSummary = row.data;
        cell.titleTextLabel.text = [NSString stringWithFormat:@"%@(%@)", commentSummary[@"title"], commentSummary[@"count"]];
        return cell;
    } else if ([row.cellIdentifier isEqualToString:@"CommentContentCellIdentifier"]) {
        CommentContentTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
        cell.titleTextLabel.text = row.data;
        return cell;
    } else if ([row.cellIdentifier isEqualToString:@"AllCommentCellIdentifier"]) {
        AllCommentTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
        cell.titleTextLabel.text = row.data;
        return cell;
    } else if ([row.cellIdentifier isEqualToString:@"NoCommentCellIdentifier"]) {
        NoCommentTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
        cell.titleTextLabel.text = row.data;
        return cell;
    }
    return nil;
}

上面的代碼進行了刪減,沒有處理所有類型。雖然稍嫌冗長,但是邏輯非常簡單,就是獲取cell信息,根據重用標識來區分不同類型的內容塊,將數據處理后放到cell中展示。
例如,對于商品圖片,因為是滾動圖片,滾動圖片可以有多張,前面我們傳入的數據就是數組data:@[@"滾動圖片地址"]。后面獲取到數據后,cell.titleTextLabel.text = [urlStringArray componentsJoinedByString:@"\n"];,出于演示,商品圖片cell我們只放了一個Label,所以只是簡單的將地址信息分行顯示出來。在實際的開發中,可以放入一個圖片滾動顯示控件,并將圖片地址的數組數據傳給控件展示。
其他類型的cell處理也是大同小異,出于演示的原因,都只是簡單的數據處理展示。當然,別忘了,設置一下TableView相關的dataSource和delegate:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    SKRow *row = self.tableSections[indexPath.section][indexPath.row];
    return row.rowHeight;
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return self.tableSections.count;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.tableSections[section].count;
}

這樣我們就完成了初始狀態時界面的展示:

初始狀態時界面顯示

完成了cell的顯示處理,接下來我們來模擬一下網絡請求數據后,界面如何顯示所需的cell:

self.tableSections = [NSMutableArray array];

NSMutableArray<SKRow *> *section1 = [NSMutableArray array];
// 滾動圖片(寬高保持比例)
SKRow *cycleImagesRow = [[SKRow alloc] initWithCellIdentifier:@"CycleImagesCellIdentifier" data:@[@"滾動圖片地址1", @"滾動圖片地址2", @"滾動圖片地址3"] rowHeight:120*[UIScreen mainScreen].bounds.size.width / 320.f];
// 主標題
SKRow *mainTitleRow = [[SKRow alloc] initWithCellIdentifier:@"MainTitleCellIdentifier" data:@"商品名稱" rowHeight:44];
// 副標題
SKRow *subTitleRow = [[SKRow alloc] initWithCellIdentifier:@"SubTitleCellIdentifier" data:@"節日促銷,快來買啊" rowHeight:44];
// 價格
SKRow *priceRow = [[SKRow alloc] initWithCellIdentifier:@"PriceCellIdentifier" data:@(arc4random()) rowHeight:44];
[section1 addObjectsFromArray:@[cycleImagesRow, mainTitleRow, subTitleRow, priceRow]];
// 促銷(隨機出現)
if (arc4random() % 2 == 0) {
    SKRow *salePromotionRow = [[SKRow alloc] initWithCellIdentifier:@"SalePromotionCellIdentifier" data:@[@"促銷信息1", @"促銷信息2", @"促銷信息3"] rowHeight:44];
    [section1 addObject:salePromotionRow];
}
[self.tableSections addObject:section1];

NSMutableArray<SKRow *> *section2 = [NSMutableArray array];
// 規格(隨機出現)
if (arc4random() % 2 == 0) {
    SKRow *specificationRow = [[SKRow alloc] initWithCellIdentifier:@"SpecificationCellIdentifier" data:@"銀色,13.3英寸" rowHeight:44];
    [section2 addObject:specificationRow];
}
if (section2.count > 0) {
    [self.tableSections addObject:section2];
}

NSMutableArray<SKRow *> *section3 = [NSMutableArray array];
NSArray<NSString *> *commentArray = [NSMutableArray array];
// 評論內容數據(隨機出現)
if (arc4random() % 2 == 0) {
    commentArray = @[@"評論內容1", @"評論內容2", @"2016年6月,蘋果系統iOS 10正式亮相,蘋果為iOS 10帶來了十大項更新。2016年6月13日,蘋果開發者大會WWDC在舊金山召開,會議宣布iOS 10的測試版在2016年夏天推出,正式版將在秋季發布。2016年9月7日,蘋果發布iOS 10。iOS10正式版于9月13日(北京時間9月14日凌晨一點)全面推送。", @"評論內容4"];
}
// 評論頭
SKRow *commentSummaryRow = [[SKRow alloc] initWithCellIdentifier:@"CommentSummaryCellIdentifier" data:@{@"title":@"商品評價", @"count":@(commentArray.count)} rowHeight:44];
[section3 addObject:commentSummaryRow];
if (commentArray.count > 0) {
    for (NSString *commentString in commentArray) {
        // 評論內容需要自適應高度,高度值指定為UITableViewAutomaticDimension
        SKRow *commentContentRow = [[SKRow alloc] initWithCellIdentifier:@"CommentContentCellIdentifier" data:commentString rowHeight:UITableViewAutomaticDimension];
        [section3 addObject:commentContentRow];
    }
    // 查看所有評論
    SKRow *allCommentRow = [[SKRow alloc] initWithCellIdentifier:@"AllCommentCellIdentifier" data:@"查看所有評論" rowHeight:44];
    [section3 addObject:allCommentRow];
} else {
    // 無評論
    SKRow *noCommentRow = [[SKRow alloc] initWithCellIdentifier:@"NoCommentCellIdentifier" data:@"暫無評論" rowHeight:44];
    [section3 addObject:noCommentRow];
}
[self.tableSections addObject:section3];

[self.tableView reloadData];

上面的代碼同樣比較冗長,但邏輯也同樣十分簡單。按顯示順序拼湊cell數據,有些不一定顯示的內容塊,如促銷,則隨機判斷,如果顯示,將數據加入到section數組中[section1 addObject:salePromotionRow];。其他類型的cell也是類似的,不再贅述。要注意的是,評論內容的文本可能有多行,我們將它的cell高設置為UITableViewAutomaticDimension:
[[SKRow alloc] initWithCellIdentifier:@"CommentContentCellIdentifier" data:commentString rowHeight:UITableViewAutomaticDimension];
由于評論內容cell我們使用了Auto Layout,這樣就可以利用iOS 8 TableView的新特性,自動計算cell的高度。拼接完數據后,只要調用[self.tableView reloadData];讓TableView重新加載即可。

好了,這樣就大功告成:

最終效果

使用上述方式制作這種內容塊可變的界面雖然寫起來較為啰嗦,但有如下優點:

  1. 邏輯清晰簡單,易于理解。視圖間不存在像先前HeaderView + Auto Layout + FooterView那種麻煩的約束處理,內容塊的顯示與否處理非常簡便。
  2. 性能比較好。有些cell可以復用,減少開銷。并且只加載需要顯示的View,如果是之前的做法,或者用scrollView來做,雖然最終也是只顯示需要的View,但不需要顯示的View還是要加載進來,有性能損耗。
  3. 易于靜態調整。如果產品經理要求調換內容塊的顯示順序,只要移動下拼湊cell數據的代碼順序即可。如果是去除某個內容塊,代碼上的調整也不復雜。
  4. 易于動態調整內容塊的顯示順序。所謂的動態調整,是指界面要根據接口返回的數據,來決定哪些內容塊顯示在前面,哪些顯示的后面。比如接口返回type=0時,價格項顯示在商品名稱之上,而type=1時,價格項顯示在商品子標題之下。
  5. 易于處理相似但又不同的界面。比如商品有好幾種不同的類型,有特惠專區,有免費專區的。免費專區的商品詳情在價格內容塊上要顯示不一樣的內容。這時,就可以多做一種類型的cell,根據接口返回type進行判斷,如果是免費專區則選取免費專區的cell來顯示。用之前HeaderView + Auto Layout的做法,就要費神地去調整約束,事倍功半。
  6. 易于擴展增加新的內容塊。要增加新的內容塊,只需創建新的cell,在數據拼接時,增加拼接新cell類型的數據代碼,同樣在顯示的地方增加顯示新cell類型的代碼即可,幾乎不需要修改原有的邏輯。

最后,附上Demo工程代碼。注意,這個工程是用XCode 8創建的,低版本的XCode可能運行會有問題(XCode 8的storyboard默認好像不兼容老版本),示例是基于iOS 8,如果要兼容老版本,請自行修改(主要是涉及cell自動算高的部分)。

后記

寫這篇文章之初,只是作為一個note,想著有哪個做iOS的朋友遇到類似的問題,可以給他做個參考。沒想到,竟引來不少關注,還被推上公眾號,收到不少評論,自己也因此打了一些“口水仗”。

從中,我也意識到我少強調了一件“顯而易見”的事件。我想說,我的方法并不適用所有情況,也不是要解決所有的問題。那些持批評觀點的,大多是面臨的問題需求不同所致。就好比我的方法是一把切水果的刀,而你要拿它去剁骨頭,那當然是不行的,它本來就不是用來剁骨頭的。

當然,很慶幸的是,從這些討論批評中,我也發現這種方法的一些不足,也有不少有益的收獲。

不足之處在于:

  • 不大適用于交互比較多的界面。如:點擊某個按鈕顯示/隱藏某個數據項、填寫表單項等


    交互比較多的界面

    像這個界面,選了優惠券后,要更新顯示優惠信息,更新對應的應付款,用這種方法就不方便了

  • 多個接口獲取數據并依次展示內容項。比如基本信息一個接口、促銷信息一個接口、評論信息一個接口,請求到基本信息數據就要展示基本信息,請求到促銷數據就要展示促銷信息,以此類推,那么數據拼接會比較麻煩

收獲在于,因此認識了@sun6boys,多了一個朋友,可謂是不打不相識。雖然他在文章的評論中并沒有詳細展示他的方法,但在私下的討論中,我已經窺探到了他的方法全貌。他對我方法不足的指責是有道理的,他的方法在數據的處理上比我規整,我的方法顯得原始粗暴。對于有交互的界面及從多個接口獲取數據依次展示的處理上,也要更加容易,整體的思路實現也非常簡潔,可以看作是我這種方法的升級改進版。更難能可貴的是,他為此專門寫了一個demo放到了github上:https://github.com/sun6boys/CRVisibleCellsDemo,大家可以學習參考。

同樣,也有其他的解決方法,比如完全用scrollview實現的:《復雜界面開發之所思》。我并不贊同用scrollview來做這種界面,原因在那篇文章也多有評論,但不管如何,多參考下其他的方法也是有益處的,至于如何取舍,就看各位的選擇了。

其實不論用什么方法,都是一種權衡,需要根據自身的情境去考慮。比如你所面臨的需求,是純展示型的界面,還是交互比較多的界面。比如團隊的開發習慣,我的團隊比較不習慣用scrollview,自然解決方法就會向tableview靠。比如團隊技能、學習成本,有的方法對約束的使用要求較高,這就是一種技能要求、一種門檻,會對團隊開發和新人融入產生影響。有人說我的方法偏傻瓜式,這是對的,因為這也是我所追求的。傻瓜式就意味著容易上手,團隊成員可以很容易使用這個方法,不管是接手別人的代碼,還是有新人進入團隊,做這一塊的東西,都不會多高的門檻。并且這個方法也已經足夠解決目前的問題,我覺得這樣就夠了。

所以,我覺得用哪種方法都不奇怪,甚至綜合各種因素后,使用H5去做也可以啊。當然,那樣的話,也就沒iOS多少事了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容