iOS iPad横屏适配问题梳理

前段时间负责项目横屏适配,做屏幕旋转时遇到了很多问题,在这里梳理一下,主要包括以下内容:

  • 1.屏幕旋转控制优先级

  • 2.屏幕旋转中横竖屏方向枚举说明

  • 3.触发开启屏幕旋转

  • 4.旋转后页面适配问题总结(原生,Flutter)

  • 5.旋转方向不一致引发崩溃 'preferredinterfaceorientationforpresentation 'landscaperight' must match a supported interface orientation: 'portrait'!

一 屏幕旋转控制优先级

这里优先级:工程Target配置(全局权限)= Appdelegate > 根视图控制器 > 普通视图控制器

1. 工程Target配置

在项目中直接勾选设置,项目中只要求iPad适配横竖屏,iPhone只支持竖屏,这里和info.plist中设置是同步的,一边更改另外一个地方也会跟着改变

截屏2023-01-17 14.11.30.png
截屏2023-01-17 14.14.17.png

2. Appdelegate中设置

正常情况下,APP从Appdelegate中启动,这里的设置也是全局有效的。

- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
    LogI(@"appdelegate", @"supportedInterfaceOrientationsForWindow");
    return UIInterfaceOrientationMaskAll;
}

注意:

1.如果实现了这里的方法,全局旋转设置将以这里为准,如果工程Target配置了全部方向,但是这里设置的只支持竖屏,那么只能支持竖屏。

2.如果项目要求从启动就默认是横屏,跟随设备方向,就需要设置工程Target,如果不要求,那么只设置Appdelegate中就可以

3. 开启屏幕旋转的局部权限(视图控制器)

这里视图控制器分为三种:

1.UITabbarViewController

2.UINavigationController

3.UIViewController

上面全局所支持的方向设置后,就要设置下面的局部权限,这里权限最高的是window的根视图控制器。一般情况下都是用UITabbarViewController作为根视图控制器,管理着多个导航控制器,然后由导航控制器管理普通的视图控制器UIViewController

所以这里的优先级是:
UITabbarViewController > UINavigationController > UIViewController

如果高优先级关闭了旋转设置,那么低优先级的控制器是无法旋转的。

另外还需要注意模态视图
模态视图不受这种根视图控制器优先级的限制,因为是隔离出来的,具体设置和普通视图代码相同,如果有UINavigationController需要注意设置UINavigationController

二 屏幕旋转中横竖屏方向枚举

在说明后续的触发屏幕旋转方法前,先说下屏幕旋转中要用到的方向枚举
这里方向枚举一共有三种:UIDeviceOrientation,UIInterfaceOrientation,UIInterfaceOrientationMask。

1. UIDeviceOrientation

UIDeviceOrientation表示设备方向,是指硬件设备本身的当前旋转方向,共有以下七种,设备方向只能取值,不能设置

**typedef** NS_ENUM(NSInteger, UIDeviceOrientation) {

    UIDeviceOrientationUnknown,

    UIDeviceOrientationPortrait,            // Device oriented vertically, home button on the bottom

    UIDeviceOrientationPortraitUpsideDown,  // Device oriented vertically, home button on the top

    UIDeviceOrientationLandscapeLeft,       // Device oriented horizontally, home button on the right

    UIDeviceOrientationLandscapeRight,      // Device oriented horizontally, home button on the left

    UIDeviceOrientationFaceUp,              // Device oriented flat, face up

    UIDeviceOrientationFaceDown             // Device oriented flat, face down

} API_UNAVAILABLE(tvOS);

获取设备当前的旋转方向使用:

UIDevice.currentDevice.orientation

2. UIInterfaceOrientation

UIInterfaceOrientation是开发的程序界面的当前选择方向,是可以设置的,一共有五种类型:

**typedef** NS_ENUM(NSInteger, UIInterfaceOrientation) {

    UIInterfaceOrientationUnknown            = UIDeviceOrientationUnknown,

    UIInterfaceOrientationPortrait           = UIDeviceOrientationPortrait,

    UIInterfaceOrientationPortraitUpsideDown = UIDeviceOrientationPortraitUpsideDown,

    UIInterfaceOrientationLandscapeLeft      = UIDeviceOrientationLandscapeRight,

    UIInterfaceOrientationLandscapeRight     = UIDeviceOrientationLandscapeLeft

} API_UNAVAILABLE(tvOS);

这里需要注意的是左右旋转时是UIInterfaceOrientationLandscapeLeftUIDeviceOrientationLandscapeRight相等,反之亦然,这是因为向左旋转设备需要旋转程序界面右边的内容。
状态栏的方向和这个有关,可以通过以下方法获取

UIInterfaceOrientation interfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation];

3. UIInterfaceOrientationMask

UIInterfaceOrientationMask是iOS6之前增加的一种枚举,也是表示页面方向,是为了集成多种UIInterfaceOrientation而定义的类型

**typedef** NS_OPTIONS(NSUInteger, UIInterfaceOrientationMask) {

    UIInterfaceOrientationMaskPortrait = (1 << UIInterfaceOrientationPortrait),

    UIInterfaceOrientationMaskLandscapeLeft = (1 << UIInterfaceOrientationLandscapeLeft),

    UIInterfaceOrientationMaskLandscapeRight = (1 << UIInterfaceOrientationLandscapeRight),

    UIInterfaceOrientationMaskPortraitUpsideDown = (1 << UIInterfaceOrientationPortraitUpsideDown),

    UIInterfaceOrientationMaskLandscape = (UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight),

    UIInterfaceOrientationMaskAll = (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight | UIInterfaceOrientationMaskPortraitUpsideDown),

    UIInterfaceOrientationMaskAllButUpsideDown = (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight),

} API_UNAVAILABLE(tvOS);

三 触发开启屏幕旋转

控制页面的旋转我们一般通过以下三个方法来控制

1. shouldAutorotate 当前界面是否开启自动转屏
2. preferredInterfaceOrientationForPresentation 返回进入界面默认的显示方向
3. supportedInterfaceOrientations 返回支持的旋转方向,这里支持的旋转方向必须包含进入界面默认的显示方向,要不旋转中会报错

在16以下版本shouldAutorotate决定当前界面是否开启自动转屏,如果返回未NO,后面的两个方法也不会调用,但是需要注意的是在16及以上系统,这里的API有较大的变化,需要特别注意,shouldAutorotate方法在16及以上系统会不起作用,而是需要控制supportedInterfaceOrientations方法才有效果,系统中代码注释如下。

// Applications should use supportedInterfaceOrientations and/or shouldAutorotate.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation API_DEPRECATED("", ios(2.0, 6.0)) API_UNAVAILABLE(tvOS);

上面我们说到了旋转的优先级,根据这个原理我们可以逐级设置各视图控制器,让高优先级跟随低优先级控制器的旋转配置。

UITabbarViewController中设置:

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

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return [self.selectedViewController supportedInterfaceOrientations];
}

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

导航控制器 UINavigationController中设置:

// 控制 vc present进来的横竖屏和进入方向 ,支持的旋转方向必须包含该返回值的方向
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
    return [[self.viewControllers lastObject] preferredInterfaceOrientationForPresentation];
}

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

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

UIViewController中设置(开启方向控制):

- (BOOL)shouldAutorotate {
    return YES;
}

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
    return UIInterfaceOrientationPortrait;
}

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskAll;
}

强制旋转

屏幕旋转分为两种,一种是跟随设备旋转方向自动旋转,一种是强制旋转。在iOS16以下,强制旋转可以直接通过[[UIDevice currentDevice] setValue:@(orientation) forKey:@"orientation"];来设置,但是在16以上会失效,所以需要添加版本兼容。iOS16新增了一个方法,在你想要转屏的时候需要通知UIViewController 表示你已经准备好要转屏的方向了,然后再调用转屏方法,这里用强制旋转的时候最好是升级xcode14,有些系统api方法在xcode14以下没有提供。
/// Notifies the view controller that a change occurred that affects supported interface orientations or the preferred interface orientation for presentation.

/// By default, this will animate any changes to orientation. To perform a non-animated update, call within `[UIView performWithoutAnimation:]`.

- (void)setNeedsUpdateOfSupportedInterfaceOrientations API_AVAILABLE(ios(16.0));
强制旋转方法
+ (void)forceUIDeviceOrientation:(UIDeviceOrientation)orientation {
    if (orientation == UIDeviceOrientationUnknown) {
        orientation = UIDeviceOrientationPortrait;
    }
    if (@available(iOS 16.0, *)) {
        [[EduAppUtil topMostController] setNeedsUpdateOfSupportedInterfaceOrientations];

        NSArray *array = [[[UIApplication sharedApplication] connectedScenes] allObjects];
        if (array.count == 0) {
            return;
        }

        id firstObject = [array firstObject];
        if (![firstObject isKindOfClass:[UIWindowScene class]]) {
            return;
        }

        UIWindowScene *scene = (UIWindowScene *)firstObject;
        UIWindowSceneGeometryPreferencesIOS *geometryPreferences = [[UIWindowSceneGeometryPreferencesIOS alloc] init];
        geometryPreferences.interfaceOrientations = orientation;
        [scene requestGeometryUpdateWithPreferences:geometryPreferences errorHandler:^(NSError *_Nonnull error) {
            //业务代码
        }];
    } else {
        [UIViewController attemptRotationToDeviceOrientation];
        [[UIDevice currentDevice] setValue:@(orientation) forKey:@"orientation"];
    }

}

四 旋转后页面适配

屏幕旋转后就要考虑页面的适配问题,横屏和竖屏下界面需要重新调整视图布局。

1. 先说下原生这边的适配

1.1 使用Masnory自动布局

如果项目中页面是使用Masonry/SnapKit来做页面布局的,当发生屏幕旋转时不需要重新调整布局。推荐后续新页面尽量都是用自动布局的形式

1.2 使用frame布局

项目中的页面几乎全是使用frame来布局的,接下来说下这部分的适配,这里可以采用Autoresizing布局的形式,除此之外,VC中有三种方法调用会在屏幕旋转后触发,可以在这里实现逻辑。

Autoresizing

Autoresizing是苹果早期的布局适配方法,但是注意它只能相对父控件布局。因为项目中大部分都是frame布局,父控件改变后,子控件可以使用这种方式随着更改,但是需要注意不要使用不变的屏幕宽高来计算设置。
Autoresizing一共有6种枚举值,可以组合使用。但是比较局限,根据实际情况使用。
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
    UIViewAutoresizingFlexibleWidth        = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
    UIViewAutoresizingFlexibleHeight       = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
项目中的使用
_videoView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin;

VC中有三种方法在屏幕旋转后会触发,可以在这里做自己的相关操作

1当设备方向发生变化时会触发UIDeviceOrientationDidChangeNotification通知方法,我们可以通过监听通知在这里做相关的操作

//监听屏幕旋转通知
[EduNotificationObserver observeName:UIDeviceOrientationDidChangeNotification target:self action:@selector(deviceOrientationChanged:) object:nil];
监听到通知后操作,这里要通过[UIDevice currentDevice].orientation获取设备方向,根据方向来做对应的设置
- (void)deviceOrientationChanged:(NSNotification *)notification {

    //获取设备当前方向

    UIDevice *device = [UIDevice currentDevice];

    if (UIDeviceOrientationIsPortrait(device.orientation)) {

        //竖屏

    } else if (UIDeviceOrientationIsLandscape(device.orientation)) {

        //横屏

    } else if (device.orientation == UIDeviceOrientationFaceUp) {

        //屏幕朝上平躺

    } else if (device.orientation == UIDeviceOrientationFaceDown) {

        //屏幕朝下平躺

    }

}

2. 通过 viewWillTransitionToSize方法,通过注释说明可以看到当屏幕旋转或者大小发生改变时触发

/* 

 This method is called when the view controller's view's size is changed by its parent (i.e. for the root view controller when its window rotates or is resized). 

 If you override this method, you should either call super to propagate the change to children or manually forward the change to children.

 */

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator API_AVAILABLE(ios(8.0));

3. 通过viewWillLayoutSubviews方法

这个方法中也可以监听到方向的变化来做view的改变,但是使用时需要慎重,因为会调用多次,可能拉垮程序,如果要使用,建议添加view的size是否更改,保证只在需要的时候调用

1.3 项目中在做布局时,通常会使用这几种size来做对应的更新布局:self.view.frame.size.widthSCREEN_WIDTHSCREEN_WIDTH_ORIUIScreen.mainScreen.bounds.size.width,在上面不同的方法中,这几种size的大小也会有不同,而且在16以上和16以下版本中表现也不一样,通过日志打印来看一下

方法调用
-(void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];
    NSLog(@"屏幕发生旋转==viewWillLayoutSubviews==%f==%d==%d==%f",self.view.frame.size.width,SCREEN_WIDTH,SCREEN_WIDTH_ORI,UIScreen.mainScreen.bounds.size.width);
}
-(void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
    NSLog(@"屏幕发生旋转==viewWillTransitionToSize==%f==%d==%d==%f",self.view.frame.size.width,SCREEN_WIDTH,SCREEN_WIDTH_ORI,UIScreen.mainScreen.bounds.size.width);
}

//处理监听屏幕旋转通知
- (void)handleDeviceOrientationDidChange:(NSNotification *)notification {
    NSLog(@"屏幕发生旋转==handleDeviceOrientationDidChange==%f==%d==%d==%f",self.view.frame.size.width,SCREEN_WIDTH,SCREEN_WIDTH_ORI,UIScreen.mainScreen.bounds.size.width);
}

16及以上系统(该设备竖屏下,宽:810,高:1080)

竖屏旋转到横屏打印结果如下:
2023-01-18 18:56:24.797152+0800 Test[12171:3262879] 屏幕发生旋转==handleDeviceOrientationDidChange==810.000000==810==1080==810.000000
2023-01-18 18:56:24.797950+0800 Test[12171:3262879] 屏幕发生旋转==viewWillTransitionToSize==810.000000==810==1080==1080.000000
2023-01-18 18:56:24.814537+0800 Test[12171:3262879] 屏幕发生旋转==viewWillLayoutSubviews==1080.000000==810==1080==1080.000000
横屏转竖屏:
2023-01-18 18:58:19.281700+0800 Test[12171:3262879] 屏幕发生旋转==handleDeviceOrientationDidChange==1080.000000==810==810==1080.000000
2023-01-18 18:58:19.282561+0800 Test[12171:3262879] 屏幕发生旋转==viewWillTransitionToSize==1080.000000==810==810==810.000000
2023-01-18 18:58:19.293358+0800 Test[12171:3262879] 屏幕发生旋转==viewWillLayoutSubviews==810.000000==810==810==810.000000

16以下系统(该设备竖屏下,宽:768,高:1024)

竖屏旋转到横屏打印结果如下:
2023-01-19 13:24:19.248161+0800 Test[829:30050] 屏幕发生旋转==viewWillTransitionToSize==768.000000==768==768==768.000000
2023-01-19 13:24:19.296882+0800 Test[829:30050] 屏幕发生旋转==viewWillLayoutSubviews==1024.000000==768==1024==1024.000000
r2023-01-19 13:24:19.368117+0800 Test[829:30050] 屏幕发生旋转==handleDeviceOrientationDidChange==1024.000000==768==1024==1024.000000
横屏转竖屏:
2023-01-19 13:24:43.075147+0800 Test[829:30050] 屏幕发生旋转==viewWillTransitionToSize==1024.000000==768==1024==1024.000000
2023-01-19 13:24:43.101307+0800 Test[829:30050] 屏幕发生旋转==viewWillLayoutSubviews==768.000000==768==768==768.000000
2023-01-19 13:24:43.164203+0800 Test[829:30050] 屏幕发生旋转==handleDeviceOrientationDidChange==768.000000==768==768==768.000000

总结:

1. 从上述可以看出三个方法中当发生屏幕旋转时SCREEN_WIDTH不会发生变化,尽量不要使用这个和SCREEN_HEIGHT,如果需要固定值的话可以使用,比如设置整个全屏横屏播放器的宽高,其他情况下尽量不要使用(项目中之前使用的地方很多)。

2. self.view.frame.size和UIScreen.mainScreen.bounds.size在屏幕发生变化的时候会更新,但是在上面的三个方法中表现不同,16和16以下差异较大,使用起来还是要格外注意。除了SCREEN_WIDTH外的三个值,在viewWillLayoutSubviews方法中都已经切换为旋转后的值,在handleDeviceOrientationDidChange通知监听中,16以下版本也都切换为旋转后的值,在16及以上的版本中,除了SCREEN_WIDTH_ORI切换成功后,其他都较慢,所以使用时需要根据情况选择对应的方法具体的时机,是个坑点。

1.4 补充

除了VC外,还有一些view的更新适配,比如自定义的view,cell,这时候需要用到layoutSubviews方法,当屏幕旋转时会触发这个方法,可以在这里做一些更新操作
- (void)layoutSubviews {
    [super layoutSubviews];
}
另外还有layer的更新,这里有一个坑点,layer不会自动随着view的更新而更新,需要我们另外操作,即当图层的bounds发生改变的时候,layoutSublayers方法会被调用,我们可以在这里重新调整图层的大小,而不能像view的autoresizingMask和constraints属性做到自适应屏幕旋转,不过这个只有view才有这个方法,如果是VC,同上述说明的三种方法,在屏幕旋转时重新设置图层的bounds
    override func layoutSublayers(of layer: CALayer) {
        super.layoutSublayers(of: layer)
        //ipad屏幕旋转造成宽度改变时刷新layer图层
        if (iPadAdaptLandscapeIsOpen()){
            let bounds = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: self.bounds.height)
            let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: [UIRectCorner.topLeft, UIRectCorner.topRight], cornerRadii: CGSize(width: 12, height: 12))
            self.contentView.layer.mask?.frame = bounds
            let maskLayer = self.contentView.layer.mask as? CAShapeLayer
            maskLayer?.path = path.cgPath
        }
    }

代码说明注释

#define SCREEN_WIDTH [UIScreenHelper screenWidth]

#define SCREEN_WIDTH_ORI [UIScreenHelper screenWidthOri] //支持横屏竖屏

#define SCREEN_HEIGHT [UIScreenHelper screenHeight]

#define SCREEN_HEIGHT_ORI [UIScreenHelper screenHeightOri] //支持横屏竖屏

+ (int)screenWidth {
    static int s_scrWidth = 0;
    if (s_scrWidth == 0) {
        CGRect screenFrame = [UIScreen mainScreen].bounds;
        s_scrWidth = MIN(screenFrame.size.width, screenFrame.size.height);
       
    }
    return s_scrWidth;
}

+ (int)screenHeight {
    static int s_scrHeight = 0;
    if (s_scrHeight == 0) {
        CGRect screenFrame = [UIScreen mainScreen].bounds;
        s_scrHeight = MAX(screenFrame.size.width, screenFrame.size.height);
    }
    return s_scrHeight;
}

+ (int)screenWidthOri {
    static int s_scrWidth = 0;

    CGRect screenFrame = [UIScreen mainScreen].bounds;
    UIInterfaceOrientation interfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation];

    if (UIInterfaceOrientationIsLandscape(interfaceOrientation)) {
        //横屏
        s_scrWidth = MAX(screenFrame.size.width, screenFrame.size.height);
        if (isIPad() && [EduChatViewLayoutMgr shareInstance].isiPadSidebar) {
            s_scrWidth = 375;
        }
    } else {
        s_scrWidth = MIN(screenFrame.size.width, screenFrame.size.height);
    }

    return s_scrWidth;
}

+ (int)screenHeightOri {
    static int s_scrHeight = 0;
    CGRect screenFrame = [UIScreen mainScreen].bounds;
    UIInterfaceOrientation interfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation];
    if (UIInterfaceOrientationIsLandscape(interfaceOrientation)) {
        //横屏
        s_scrHeight = MIN(screenFrame.size.width, screenFrame.size.height);
    } else {
        s_scrHeight = MAX(screenFrame.size.width, screenFrame.size.height);
    }

    return s_scrHeight;
}

2. Flutter旋转适配

Flutter工程中,当应用尺寸改变的时候会触发didChangeMetrics方法,屏幕旋转时可以在这里做相应的更新操作。
混入WidgetsBindingObserver

class MiniCourseDetailCoverWidgetState
    extends State<MiniCourseDetailCoverWidget> with WidgetsBindingObserver {

添加观察者

  @override
  void initState() {
    super.initState();
    _initCoverSize();
    WidgetsBinding.instance?.addObserver(this);

  }

在这里更新对应的size布局,不需要setState,会自动调用

  @override
  void didChangeMetrics() {
    super.didChangeMetrics();
    _initCoverSize();
  }

最后记得要移除

  @override
  void dispose() {
    super.dispose();
    WidgetsBinding.instance?.removeObserver(this);
  }

五 旋转方向不一致引发常见崩溃

在做横竖屏切换的时候,有一个crash,之前开发时没有留意到

'preferredinterfaceorientationforpresentation 'landscaperight' must match a supported interface orientation: 'portrait'!

因为supportedInterfaceOrientationspreferredInterfaceOrientationForPresentation返回内容不符,特别是preferredInterfaceOrientationForPresentation返回的是当前状态栏的方向UIApplication.shared.statusBarOrientation时,比如当前页面方向仅支持竖屏,模态弹出一个横屏页面,当退出模态,也就是调用dismiss的时候,会调用preferredInterfaceOrientationForPresentation方法,横屏模态还没完全旋转到当前竖屏页面时,当前状态栏方向为landscaperight,supportedInterfaceOrientations返回仅支持竖屏,这时候就会崩溃。

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskPortrait;
}

有两种修复方式,一种是在调用横屏的dismiss方法时,更新当前竖屏页面的supportedInterfaceOrientations方法,但是这种有个问题是如果调起的方法在其他工具类里时要记录这个状态可能有些麻烦,也可以使用第二种方式,修改preferredinterfaceorientationforpresentation方法,竖屏页面仅设置当前preferredinterfaceorientationforpresentation方法返回的是UIInterfaceOrientationMaskPortrait也可修复。

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

推荐阅读更多精彩内容