Flutter插件開發(fā)之HmsScanKit

前沿

從事Flutter開發(fā)以來,一直都是使用已有的插件收津,沒有自己開發(fā)過饿这。最近同事推薦讓我使用華為的掃碼SDK(hms_scan_kit),正好借此機(jī)會來開發(fā)一個Flutter的原生插件朋截。算是對最近的插件學(xué)習(xí)做一個簡單的總結(jié)蛹稍。

效果圖

我們先看一下實現(xiàn)的掃碼效果:點擊LoadScanKit按鈕調(diào)起插件的掃碼功能,掃碼成功后在界面顯示掃碼結(jié)果部服。


效果圖.gif

相關(guān)知識點

1. Flutter Packages

通過使用 package(的模式)可以創(chuàng)建易于共享的模塊化代碼唆姐。一個最基本的 package 由以下內(nèi)容構(gòu)成:

- pubspec.yaml 文件
用于定義 package 名稱、版本號廓八、作者等其他信息的元數(shù)據(jù)文件奉芦。

- lib 目錄
包含共享代碼的 lib 目錄赵抢,其中至少包含一個 <package-name>.dart 文件。

2. Package類別

Package包分為二種:

  1. 純Dart庫(Dart packages)
  • 只用Dart編寫的傳統(tǒng)package声功,比如 path烦却。
  1. 原生插件(Plugin packages)
  • 使用Dart編寫的,按需使用Java或 Kotlin先巴、Objective-C或Swift 分別在Android或iOS平臺實現(xiàn)的package其爵。

3. 原生插件開發(fā)步驟

  1. 創(chuàng)建package
  • 想要創(chuàng)建原生插件 package,請使用帶有 --template=plugin 標(biāo)志的 flutter create 命令
flutter create --org com.example --template=plugin --platforms=android,ios -a kotlin hello
  1. 實現(xiàn)package
    a. 定義 package API(.dart)
    b. 添加 Android/iOS 平臺代碼(.kt/.swift)
    C. 關(guān)聯(lián) API 和平臺代碼

  2. 指定插件支持的平臺伸蚯,比如hms_scan插件就如下定義:

name: flutter_hms_scan
description: A new Flutter project.
version: 0.0.1
homepage:

environment:
  sdk: ">=2.15.1 <3.0.0"
  flutter: ">=2.5.0"
flutter:
  plugin:
    platforms:
      android:
        package: com.fitem.flutter_hms_scan
        pluginClass: HmsScanPlugin
      ios:
        pluginClass: HmsScanPlugin

備注:如果使用IDE(比如Android Studio)直接在創(chuàng)建Flutter項目處選擇Plugin類型即可摩渺,IDE會創(chuàng)建插件模板并實現(xiàn)獲取平臺系統(tǒng)版本的example,無需上面的步驟

  1. Dart對應(yīng)原生類型:
Dart kotlin Swift
null null nil
bool Boolean NSNumber(value: Bool)
int Int NSNumber(value: Int32)
int Long NSNumber(value: Int)
double Double NSNumber(value: Double)
String String String
Uint8List ByteArray FlutterStandardTypedData(bytes: Data)
Int32List IntArray FlutterStandardTypedData(int32: Data)
Int64List LongArray FlutterStandardTypedData(int64: Data)
Float32List FloatArray FlutterStandardTypedData(float32: Data)
Float64List DoubleArray FlutterStandardTypedData(float64: Data)
List List Array
Map HashMap Dictionary
  1. Flutter的plugin通信流程如下:


    PlatformChannels.png

HmsScan插件的實現(xiàn)

前面說了這么多剂邮,終于進(jìn)入正題摇幻,下面我們開始HmsScan插件的開發(fā)吧。

1. 定義 package API:
class FlutterHmsScan {
  // 創(chuàng)建插件
  static const MethodChannel _channel = MethodChannel('hms_scan');
  // 定義調(diào)用方法
  static Future<ScanBean> loadScanKit() async {
    return await _channel
        .invokeMethod("loadScanKit")
        .then((value) => scanBeanFromJson(json.encode(value)));
  }
}
2. Android代碼實現(xiàn):

a. 使用IDE打開Android目錄挥萌,根據(jù)官方SDK導(dǎo)入庫

 // scankitSDK
    implementation 'com.huawei.hms:scanplus:2.4.0.301'

 // 需要在repositories中導(dǎo)入url
   maven {url 'https://developer.huawei.com/repo/'}

b. 繼承FlutterPlugin類绰姻,接入Flutter管道。由于sdk用到權(quán)限請求和onActivityResult的回調(diào)引瀑,因此我們需要繼承ActivityAware對Activity添加監(jiān)聽狂芋。其中registerWith()方法是為了適配老版本Flutter的兼容。

class HmsScanPlugin : FlutterPlugin, ActivityAware {
    /// The MethodChannel that will the communication between Flutter and native Android
    ///
    /// This local reference serves to register the plugin with the Flutter Engine and unregister it
    /// when the Flutter Engine is detached from the Activity
    private lateinit var mScanLauncher: ScanLauncher
    private lateinit var mHandler: MethodCallHandlerImpl

    /**
     * 老版本Flutter兼容
     */
    fun registerWith(registrar: Registrar) {
        mScanLauncher = ScanLauncher(registrar.context(), registrar.activity())
        mHandler = MethodCallHandlerImpl(mScanLauncher)
        mHandler.startService(registrar.messenger())
        registrar.addActivityResultListener(mHandler)
        registrar.addRequestPermissionsResultListener(mHandler)
    }

    override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        mScanLauncher = ScanLauncher(flutterPluginBinding.applicationContext, null)
        mHandler = MethodCallHandlerImpl(mScanLauncher)
        mHandler.startService(flutterPluginBinding.binaryMessenger)
    }

    override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
        mHandler.stopService()
    }

    override fun onAttachedToActivity(binding: ActivityPluginBinding) {
        mScanLauncher.activity = binding.activity
        binding.addActivityResultListener(mHandler)
        binding.addRequestPermissionsResultListener(mHandler)
    }

    override fun onDetachedFromActivity() {
        mScanLauncher.activity = null
    }

    override fun onDetachedFromActivityForConfigChanges() {
        onDetachedFromActivity()
    }

    override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
        onAttachedToActivity(binding)
    }
}

c. 考慮到HmsScanPlugin職責(zé)過多伤疙,這里使用MethodCallHandlerImpl進(jìn)行分離解耦银酗,專門處理Flutter管道的通信辆影。

/**
 * 插件方法監(jiān)聽
 * Created by Fitem on 2022/3/2.
 */
class MethodCallHandlerImpl(var scanLauncher: ScanLauncher) : MethodChannel.MethodCallHandler,
    MethodCallHandlerListener, PluginRegistry.ActivityResultListener,
    PluginRegistry.RequestPermissionsResultListener {

    private lateinit var channel: MethodChannel

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        when (call.method) {
            "getPlatformVersion" -> {
                result.success("Android ${android.os.Build.VERSION.RELEASE}")
            }
            "loadScanKit" -> {
                scanLauncher.loadScanKit(call, result)
            }
            else -> {
                result.notImplemented()
            }
        }
    }

    override fun startService(binaryMessenger: BinaryMessenger) {
        channel = MethodChannel(binaryMessenger, "hms_scan")
        channel.setMethodCallHandler(this)
    }


    override fun stopService() {
        channel.setMethodCallHandler(null)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
        if (resultCode != Activity.RESULT_OK || data == null) {
            return false
        }
        return scanLauncher.onActivityResult(requestCode, resultCode, data)
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>?,
        grantResults: IntArray?
    ): Boolean {
        if (permissions == null || grantResults == null) {
            return false
        }
        return  scanLauncher.onRequestPermissionResult(requestCode, permissions, grantResults)
    }
}

// 管道通信生命周期的綁定
interface MethodCallHandlerListener {

    fun startService(binaryMessenger: BinaryMessenger)

    fun stopService()
}

d. 最后通過ScanLauncher來專門處理掃碼功能的相關(guān)實現(xiàn)

class ScanLauncher(var applicationContext: Context, var activity: Activity?) {

    companion object {
        const val CAMERA_REQ_CODE = 111
        const val DEFINED_CODE = 222
        const val BITMAP_CODE = 333
        const val MULTIPROCESSOR_SYN_CODE = 444
        const val MULTIPROCESSOR_ASYN_CODE = 555
        const val GENERATE_CODE = 666
        const val DECODE = 1
        const val GENERATE = 2
        const val REQUEST_CODE_SCAN_ONE = 0X01
        const val REQUEST_CODE_DEFINE = 0X0111
        const val REQUEST_CODE_SCAN_MULTI = 0X011
        const val DECODE_MODE = "decode_mode"
        const val RESULT = "SCAN_RESULT"
        const val SCAN_STATUS = "scanStatus"
        const val CODE_FORMAT = "codeFormat"
        const val RESULT_TYPE = "resultType"
        const val CODE_RESULT = "codeResult"
    }

    private var result: MethodChannel.Result? = null

    /**
     * 掃碼
     */
    fun loadScanKit(call: MethodCall, result: MethodChannel.Result) {
        this.result = result
        requestPermission(CAMERA_REQ_CODE, DECODE)
    }

    /**
     * Apply for permissions.
     */
    private fun requestPermission(requestCode: Int, mode: Int) {
        if (activity == null) {
            result?.success(mapOf(SCAN_STATUS to false))
            return
        }
        if (mode == DECODE) {
            decodePermission(requestCode)
        } else if (mode == GENERATE) {
            generatePermission(requestCode)
        }
    }

    /**
     * Apply for permissions.
     */
    private fun decodePermission(requestCode: Int) {
        ActivityCompat.requestPermissions(
            activity!!,
            arrayOf(Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE),
            requestCode
        )
    }

    /**
     * Apply for permissions.
     */
    private fun generatePermission(requestCode: Int) {
        ActivityCompat.requestPermissions(
            activity!!, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
            requestCode
        )
    }

    fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Boolean {
        //Default View
        if (requestCode == REQUEST_CODE_SCAN_ONE) {
            val obj: HmsScan? = data.getParcelableExtra(ScanUtil.RESULT)
            if (obj != null) {
                result?.success(
                    mapOf(
                        SCAN_STATUS to true,
                        CODE_FORMAT to getCodeFormat(obj.scanType),
                        RESULT_TYPE to getResultType(obj),
                        CODE_RESULT to obj.originalValue
                    )
                )
                return true
            }
            //MultiProcessor & Bitmap
        }
        return false
    }

    fun onRequestPermissionResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ): Boolean {

        if (grantResults.size < 2 || grantResults[0] != PackageManager.PERMISSION_GRANTED || grantResults[1] != PackageManager.PERMISSION_GRANTED) {
            return false
        }
        //Default View Mode
        if (requestCode == CAMERA_REQ_CODE) {
            ScanUtil.startScan(
                activity,
                REQUEST_CODE_SCAN_ONE,
                HmsScanAnalyzerOptions.Creator().create()
            )
            return true
        }
        return false
    }

    /**
     * 獲取CodeFormat
     */
    private fun getCodeFormat(codeFormat: Int): String {
        return when (codeFormat) {
            HmsScan.QRCODE_SCAN_TYPE -> "QR code"
            HmsScan.AZTEC_SCAN_TYPE -> "AZTEC code"
            HmsScan.DATAMATRIX_SCAN_TYPE -> "DATAMATRIX code"
            HmsScan.PDF417_SCAN_TYPE -> "PDF417 code"
            HmsScan.CODE93_SCAN_TYPE -> "CODE93"
            HmsScan.CODE39_SCAN_TYPE -> "CODE39"
            HmsScan.CODE128_SCAN_TYPE -> "CODE128"
            HmsScan.EAN13_SCAN_TYPE -> "EAN13 code"
            HmsScan.EAN8_SCAN_TYPE -> "EAN8 code"
            HmsScan.ITF14_SCAN_TYPE -> "ITF14 code"
            HmsScan.UPCCODE_A_SCAN_TYPE -> "UPCCODE_A"
            HmsScan.UPCCODE_E_SCAN_TYPE -> "UPCCODE_E"
            HmsScan.CODABAR_SCAN_TYPE -> "CODABAR"
            else -> "OTHER"
        }
    }

    /**
     * 獲取ResultType
     */
    private fun getResultType(hmsScan: HmsScan): String {
        return when (hmsScan.scanType) {
            HmsScan.QRCODE_SCAN_TYPE -> when (hmsScan.scanTypeForm) {
                HmsScan.QRCODE_SCAN_TYPE -> "Text"
                HmsScan.EVENT_INFO_FORM -> "Event"
                HmsScan.CONTACT_DETAIL_FORM -> "Contact"
                HmsScan.DRIVER_INFO_FORM -> "License"
                HmsScan.EMAIL_CONTENT_FORM -> "Email"
                HmsScan.LOCATION_COORDINATE_FORM -> "Location"
                HmsScan.TEL_PHONE_NUMBER_FORM -> "Tel"
                HmsScan.SMS_FORM -> "SMS"
                HmsScan.WIFI_CONNECT_INFO_FORM -> "Wi-Fi"
                HmsScan.URL_FORM -> "WebSite"
                HmsScan.URL_FORM -> "WebSite"
                else -> "Text"
            }
            HmsScan.EAN13_SCAN_TYPE -> when (hmsScan.scanTypeForm) {
                HmsScan.ISBN_NUMBER_FORM -> "ISBN"
                HmsScan.ARTICLE_NUMBER_FORM -> "Product"
                else -> "Text"
            }
            HmsScan.EAN8_SCAN_TYPE,
            HmsScan.UPCCODE_A_SCAN_TYPE,
            HmsScan.UPCCODE_E_SCAN_TYPE -> when (hmsScan.scanTypeForm) {
                HmsScan.ARTICLE_NUMBER_FORM -> "Product"
                else -> "Text"

            }
            else -> "Text"
        }
    }
}

最后在AndroidManifest.xml中添加需要的權(quán)限:

 <!--相機(jī)權(quán)限-->
    <uses-permission android:name="android.permission.CAMERA" />
    <!--文件讀取權(quán)限-->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
3. ios部分的實現(xiàn)

ios原本也是打算使用hms的徒像,但是官方居然2年沒有更新了,并且不支持bitcode版本蛙讥、不支持cocopod锯蛀,demo也無法正常運(yùn)行。經(jīng)過一番嘗試后次慢,決定放棄使用該庫旁涤,換成了MTBBarcodeScanner庫。(ios新人一個迫像,如果有精通IOS的同學(xué)們歡迎指教E蕖)

a. 通過SwiftHmsScanPlugin創(chuàng)建Flutter管道

public class SwiftHmsScanPlugin: NSObject, FlutterPlugin, BarcodeScannerViewControllerDelegate {
    
    private var result: FlutterResult?
    private var hostViewController: UIViewController?
    
    public static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(name: "hms_scan", binaryMessenger: registrar.messenger())
    let instance = SwiftHmsScanPlugin()
    registrar.addMethodCallDelegate(instance, channel: channel)
  }

  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
      self.result = result
      if ("loadScanKit" == call.method) {
          loadScanKit()
      } else {
          result("iOS " + UIDevice.current.systemVersion)
      }
  }
    
    public func loadScanKit() {
        
        if let rootVC = UIApplication.shared.keyWindow?.rootViewController {
                    hostViewController = topViewController(base:rootVC)
                } else if let window = UIApplication.shared.delegate?.window,let rootVC = window?.rootViewController {
                    hostViewController = topViewController(base:rootVC)
                }
        
        let scannerViewController = BarcodeScannerViewController()

        let navigationController = UINavigationController(rootViewController: scannerViewController)
        
        if #available(iOS 13.0, *) {
              navigationController.modalPresentationStyle = .fullScreen
          }
          
        scannerViewController.delegate = self
        hostViewController?.present(navigationController, animated: false)
    }
    
    private func topViewController(base: UIViewController?) -> UIViewController? {
        if let nav = base as? UINavigationController {
            return topViewController(base: nav.visibleViewController)

        } else if let tab = base as? UITabBarController, let selected = tab.selectedViewController {
            return topViewController(base: selected)

        } else if let presented = base?.presentedViewController {
            return topViewController(base: presented)
        }
        return base
    }
    
    func didScanBarcodeWithResult(_ controller: BarcodeScannerViewController?, scanResult: ScanResult) {
        result?(["codeResult":scanResult.rawContent, "scanStatus" : String(true), "resultType": String(scanResult.format.rawValue)])
    }
    
    func didFailWithErrorCode(_ controller: BarcodeScannerViewController?, errorCode: String) {
        result?(["scanStatus" : String(false)])
    }
    
}

b. BarcodeScannerViewController實現(xiàn)掃碼功能

class BarcodeScannerViewController: UIViewController {
  private var previewView: UIView?
  private var scanRect: ScannerOverlay?
  private var scanner: MTBBarcodeScanner?
  
  private let formatMap = [
    BarcodeFormat.aztec : AVMetadataObject.ObjectType.aztec,
    BarcodeFormat.code39 : AVMetadataObject.ObjectType.code39,
    BarcodeFormat.code93 : AVMetadataObject.ObjectType.code93,
    BarcodeFormat.code128 : AVMetadataObject.ObjectType.code128,
    BarcodeFormat.dataMatrix : AVMetadataObject.ObjectType.dataMatrix,
    BarcodeFormat.ean8 : AVMetadataObject.ObjectType.ean8,
    BarcodeFormat.ean13 : AVMetadataObject.ObjectType.ean13,
    BarcodeFormat.interleaved2Of5 : AVMetadataObject.ObjectType.interleaved2of5,
    BarcodeFormat.pdf417 : AVMetadataObject.ObjectType.pdf417,
    BarcodeFormat.qr : AVMetadataObject.ObjectType.qr,
    BarcodeFormat.upce : AVMetadataObject.ObjectType.upce,
  ]
  
  var delegate: BarcodeScannerViewControllerDelegate?
  
  private var device: AVCaptureDevice? {
    return AVCaptureDevice.default(for: .video)
  }
  
  private var isFlashOn: Bool {
    return device != nil && (device?.flashMode == AVCaptureDevice.FlashMode.on || device?.torchMode == .on)
  }
  
  private var hasTorch: Bool {
    return device?.hasTorch ?? false
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()

    UIDevice.current.endGeneratingDeviceOrientationNotifications()
    
    #if targetEnvironment(simulator)
    view.backgroundColor = .lightGray
    #endif
    
    previewView = UIView(frame: view.bounds)
    if let previewView = previewView {
      previewView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
      view.addSubview(previewView)
    }
    setupScanRect(view.bounds)
    
    scanner = MTBBarcodeScanner(previewView: previewView)
                                  
    navigationItem.leftBarButtonItem = UIBarButtonItem(title: "cancel",
                                                        style: .plain,
                                                        target: self,
                                                        action: #selector(cancel)
    )
    updateToggleFlashButton()
  }
  
  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    
    if scanner!.isScanning() {
      scanner!.stopScanning()
    }
    
    UIDevice.current.endGeneratingDeviceOrientationNotifications()
    
    scanRect?.startAnimating()
    MTBBarcodeScanner.requestCameraPermission(success: { success in
      if success {
        self.startScan()
      } else {
        #if !targetEnvironment(simulator)
        self.errorResult(errorCode: "PERMISSION_NOT_GRANTED")
        #endif
      }
    })
  }
  
  override func viewWillDisappear(_ animated: Bool) {
    scanner?.stopScanning()
    scanRect?.stopAnimating()
    
    UIDevice.current.beginGeneratingDeviceOrientationNotifications()
    
    if isFlashOn {
      setFlashState(false)
    }
    
    super.viewWillDisappear(animated)
  }
  
  override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    setupScanRect(CGRect(origin: CGPoint(x: 0, y:0),
                         size: size
    ))
  }
  
  private func setupScanRect(_ bounds: CGRect) {
    if scanRect != nil {
      scanRect?.stopAnimating()
      scanRect?.removeFromSuperview()
    }
    scanRect = ScannerOverlay(frame: bounds)
    if let scanRect = scanRect {
      scanRect.translatesAutoresizingMaskIntoConstraints = false
      scanRect.backgroundColor = UIColor.clear
      view.addSubview(scanRect)
      scanRect.startAnimating()
    }
  }
  
  private func startScan() {
    do {
      try scanner!.startScanning(with: cameraFromConfig, resultBlock: { codes in
        if let code = codes?.first {
          let codeType = self.formatMap.first(where: { $0.value == code.type });
          let scanResult = ScanResult.with {
            $0.type = .barcode
            $0.rawContent = code.stringValue ?? ""
            $0.format = codeType?.key ?? .unknown
            $0.formatNote = codeType == nil ? code.type.rawValue : ""
          }
          self.scanner!.stopScanning()
          self.scanResult(scanResult)
        }
      })
    } catch {
      self.scanResult(ScanResult.with {
        $0.type = .error
        $0.rawContent = "\(error)"
        $0.format = .unknown
      })
    }
  }
  
  @objc private func cancel() {
    scanResult( ScanResult.with {
      $0.type = .cancelled
      $0.format = .unknown
    });
  }
  
  @objc private func onToggleFlash() {
    setFlashState(!isFlashOn)
  }
  
  private func updateToggleFlashButton() {
    if !hasTorch {
      return
    }
    
    let buttonText = isFlashOn ? "flash_off" : "flash_on"
    navigationItem.rightBarButtonItem = UIBarButtonItem(title: buttonText,
                                                        style: .plain,
                                                        target: self,
                                                        action: #selector(onToggleFlash)
    )
  }
  
  private func setFlashState(_ on: Bool) {
    if let device = device {
      guard device.hasFlash && device.hasTorch else {
        return
      }
      
      do {
        try device.lockForConfiguration()
      } catch {
        return
      }
      
      device.flashMode = on ? .on : .off
      device.torchMode = on ? .on : .off
      
      device.unlockForConfiguration()
      updateToggleFlashButton()
    }
  }
  
  private func errorResult(errorCode: String){
    delegate?.didFailWithErrorCode(self, errorCode: errorCode)
    dismiss(animated: false)
  }
  
  private func scanResult(_ scanResult: ScanResult){
    self.delegate?.didScanBarcodeWithResult(self, scanResult: scanResult)
    dismiss(animated: false)
  }
  
  private var cameraFromConfig: MTBCamera {
    return .back
  }
}

c. 最后需要在example的ios目錄Info.plist文件中添加相機(jī)權(quán)限:

// example/ios/Runner/Info.plist
<key>NSCameraUsageDescription</key>
<string>Camera permission is required for barcode scanning.</string>

至此,一個簡單的應(yīng)用于Android闻妓、iOS的plugin插件已完成菌羽。

4. 需要注意的點
  1. 使用Android Studio右鍵選擇Flutter即可通過Android Studio和Xcode打開項目,如圖:


    WX20220408-114305@2x.png
  2. Android目錄打開后由缆,若看不到插件module注祖,可以選擇Project Files模式下查看猾蒂,如圖:


  3. ios目錄打開前,需要進(jìn)入example目錄輸入命令 flutter build ios是晨,待編譯完成后再通過Xcode打開肚菠。

總結(jié)

Plugin原生插件其實就是基于Flutter提供的管道進(jìn)行通信,和原生開發(fā)的使用并無太大區(qū)別罩缴。但需要我們對原生代碼的調(diào)用有一個基本的了解蚊逢,然后引入其他原生開發(fā)庫進(jìn)行調(diào)用。最后附上項目地址:flutter_hms_scan

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末箫章,一起剝皮案震驚了整個濱河市时捌,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌炉抒,老刑警劉巖奢讨,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異焰薄,居然都是意外死亡拿诸,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門塞茅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來亩码,“玉大人,你說我怎么就攤上這事野瘦∶韫担” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵鞭光,是天一觀的道長吏廉。 經(jīng)常有香客問我,道長惰许,這世上最難降的妖魔是什么席覆? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮汹买,結(jié)果婚禮上佩伤,老公的妹妹穿的比我還像新娘。我一直安慰自己晦毙,他們只是感情好生巡,可當(dāng)我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著见妒,像睡著了一般孤荣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天垃环,我揣著相機(jī)與錄音邀层,去河邊找鬼。 笑死遂庄,一個胖子當(dāng)著我的面吹牛寥院,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播涛目,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼秸谢,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了霹肝?” 一聲冷哼從身側(cè)響起估蹄,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎沫换,沒想到半個月后臭蚁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡讯赏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年垮兑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片漱挎。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡系枪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出磕谅,到底是詐尸還是另有隱情私爷,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布膊夹,位于F島的核電站衬浑,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏割疾。R本人自食惡果不足惜嚎卫,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一嘉栓、第九天 我趴在偏房一處隱蔽的房頂上張望宏榕。 院中可真熱鬧,春花似錦侵佃、人聲如沸麻昼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抚芦。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間叉抡,已是汗流浹背尔崔。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留褥民,地道東北人季春。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像消返,于是被迫代替她去往敵國和親载弄。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容