Android MVVM 系列之 Databinding(三)

Android MVVM 系列之 Databinding(三)

所有博文會不定期的更新一下的汤纸,文章有不妥歡迎大家提建議善涨!

這篇文章主要講 Databinding 中注解的使用荒吏,給大家整理個字典出來颅痊,Databinding 中公開的 Api 中有以下注解

  • @Bindable
  • @BindingAdapter
  • @BindingConversion
  • @BindingMethod
  • @BindingMethods
  • @InverseBindingAdapter
  • @InverseBindingMethod
  • @InverseBindingMethods
  • @InverseMethod

[TOC]

@Bindable

使用:

  • 此注解加在需要實現(xiàn)綁定觀察的 get 或 is 開頭的方法上,與 notifyPropertyChanged() 方法配合使用堂污。

  • 實體類需繼承 BaseObseravle 或實現(xiàn) Observable 接口的時候(必須)柒啤,

  • @Bindable 注解的方法必須是 public

添加 @Bindable 注解后倦挂,會在 BR 類中生成對應的字段,然后 與notifyPropertyChanged() 方法配合使用担巩,當該字段中的數(shù)據(jù)被修改時方援,DataBinding 會自動刷新對應view的數(shù)據(jù),而不用我們在拿到新數(shù)據(jù)后手動賦值涛癌。

當然犯戏,這也是實現(xiàn)雙向綁定必須的,UI 改變布局通知 Java 代碼也是靠它實現(xiàn)的

public class User3 extends BaseObservable {
    private String name;
    private int age;

    @Bindable
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
    }

    @Bindable
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
        notifyPropertyChanged(BR.age);
    }

    /**
     * “@Bindable” 注解的方法必須是 public
     * “@Bindable” 注解也可以添加一組參數(shù)的祖很,這樣的話 name 或者 age 發(fā)生變化的時候都會通知 getNameAndAge 方法
     * 需要注意的是這樣做并不等同于 notifyPropertyChanged(BR.nameAndAge); {@link OnPropertyChangedCallback#onPropertyChanged(Observable, int)} 并不會被調用
     * 大家可點進源碼看注釋
     * 那個 onPropertyChanged 方法現(xiàn)在顯然沒什么用笛丙,但是以后不保證你會用它來實現(xiàn)什么騷操作,比如來實現(xiàn) EventBus
     */
    @Bindable({"name", "age"})
    public String getNameAndAge() {
        return name + age;
    }
}

@BindingAdapter

@Target(ElementType.METHOD)
public @interface BindingAdapter {

    /**
     * 返回自定義的屬性數(shù)組.
     */
    String[] value();

    /**
     * 是否需要全部的屬性
     */
    boolean requireAll() default true;
}

這個注解可是 DataBinding 的核心了假颇,用來自定義 View 的屬性胚鸯,注意:

  • 注解用于 public 修飾的方法
  • 方法的第一個參數(shù)必須是 View 或其子類

對于一些View本身就沒有android:xxxx或者app:xxx屬性,或者 View 中對應的 setter 方法不是我們需要的笨鸡,我們使用不了姜钳,使用 BindingAdapter 注解可以幫助我們來解決這個問題。

咱講個 Glide 加載圖片的例子吧

    <ImageView
        app:imageUrl="@{imageUrl}"
        android:layout_width="200dp"
        android:layout_height="200dp" />
@BindingAdapter("imageUrl")
    public static void bindImageUrl(ImageView imageView, String url) {
        GlideApp.with(imageView.getContext())
                .load(url)
                .into(imageView);
    }

這樣在 xml 文件中就可以使用自定義的屬性了形耗,大家可能想這種用法好處是什么呢哥桥?先透露一下,在我目前的項目中所使用的控件激涤,基本沒有 id 這個屬性的拟糕,findviewbyid butterknife 什么的都是浮云了,進階的用法在后面的 MVVM 正式篇中會講到的倦踢。這里需要注意的是:

  • BindingAdapter("imageUrl") 里面的 "imageUrl" 就是自定義生成的方法送滞,在 xml 文件種使用時命名控件隨意
  • BindingAdapter("bind:imageUrl") 這種是指定命名空間的寫法,xml 中的屬性必須寫成 bind:imageUrl="@{...}" 這種形式
  • 如果定義的方法重復了辱挥,使用最后一個犁嗅,系統(tǒng)找 BindingAdapter 注解的方法時有它自己的先后順序,先找系統(tǒng)的再找自己定義的晤碘,這樣的話自己定義的方法就能覆蓋系統(tǒng)提供的方法了

如果我們需要定義多個屬性名相同褂微,參數(shù)類型各不相同的方法功蜓,Databinding 在某些方面是支持的,但是我在這里就不列出了(其實是我懶的去一個個驗證了)宠蚂,怕誤人子弟式撼。。

上面的例子是定義單個屬性肥矢,這個注解的方法還可以定義多個屬性端衰,在參數(shù)中傳入多個值,屬性的定義順序要和參數(shù)的順序一致

    <ImageView
        app:url="@{imageUrl}"
        app:holder="@{@drawable/logo}"
        app:error="@{@drawable/logo}"
        android:layout_width="200dp"
        android:layout_height="200dp" />
    @BindingAdapter(value = {"url", "holder", "error"}, requireAll = true)
    public static void bingImgs(ImageView imageView, String url, Drawable holder, Drawable error) {
        //Glide 要求的這些值都是非空的甘改,我這里就 requireAll = true 了,你也可以在這里判空給自定義的默認值灭抑,設置 requireAll = false
        GlideApp.with(imageView.getContext())
                .load(url)
                .placeholder(holder)
                .error(error)
                .into(imageView);
    }

Glide 要求的這些值都是非空的十艾,我這里就 requireAll = true 了,你也可以在這里判空給自定義的默認值腾节,設置 requireAll = false忘嫉。

@BindingMethod 和 @BindingMethods

我們之前了解了 Databinding 在 xml 中使用屬性需要 View 本身提供這個屬性的 set 方法,如果找不到對應的 set 方法就會報錯案腺。
其實這時候我們就想說庆冕,這個不就是上面 BindingAdapter 能解決的問題嗎?

咳劈榨,為了說明這兩個注解存在的意義访递,我強行造了一個例子一個例子:

    android:text='@{user.name}'

我們在給 TextView 設置 text 屬性的時候,TextView 的 Java 類中一定存在一個 setText() 方法

    @android.view.RemotableViewMethod
    public final void setText(CharSequence text) {
        setText(text, mBufferType);
    }

它接收 CharSequence 類型的數(shù)據(jù)同辣,而且我們的 String 類實現(xiàn)了這個接口拷姿,所以 Databinding 才能把字符串給對應的 TextView,大家再看一個

    android:tint="@{@color/colorAccent}"

這個 tint 屬性在 ImageView 的類中是沒有的旱函,它對應的方法實際上是

    public void setImageTintList(@Nullable ColorStateList tint) {
        mDrawableTintList = tint;
        mHasDrawableTint = true;

        applyImageTint();
    }

所以 Databinding 找不到屬性對應的 set 方法响巢,在編譯時就會報錯,這時候 @BindingMethod 與 @BindingMethods 就派上用場了棒妨。

ps:可能會有人說踪古,我沒有做任何處理,直接使用android:tint="@{@color/colorAccent}"也沒有報錯啊券腔,其實是因為dataBinding已經幫我們定義好了轉換方法了伏穆,在你使用android:tint="@{@color/colorAccent}"時,已經幫你自動使用setImageTintList()方法生成代碼了颅眶。

@BindingMethod

有3個字段蜈出,這3個字段都是必填項,少一個都不行:

  • type:要操作的屬性屬于哪個View類涛酗,類型為class對象铡原,比如:ImageView.class
  • attribute:xml屬性偷厦,類型為String ,比如:”android:tint”
  • method:指定xml屬性對應的set方法燕刻,類型為String只泼,比如:”setImageTintList”

@BindingMethods

因為@BindingMethod注解不能單獨使用,必須要結合@BindingMethods才能發(fā)揮其功效卵洗,所以@BindingMethods注解其實就是一個容器请唱,它內部有一個BindingMethod數(shù)組,存放的是一個一個的BindingMethod过蹂。

我們拿系統(tǒng)提供的類來看看 @BindingMethod 是如何使用的十绑,系統(tǒng)提供的類的位置在

image
@BindingMethods({
        @BindingMethod(type = android.widget.ImageView.class, attribute = "android:tint", method = "setImageTintList"),
        @BindingMethod(type = android.widget.ImageView.class, attribute = "android:tintMode", method = "setImageTintMode"),
})
public class ImageViewBindingAdapter {
    @BindingAdapter("android:src")
    public static void setImageUri(ImageView view, String imageUri) {
        if (imageUri == null) {
            view.setImageURI(null);
        } else {
            view.setImageURI(Uri.parse(imageUri));
        }
    }

    @BindingAdapter("android:src")
    public static void setImageUri(ImageView view, Uri imageUri) {
        view.setImageURI(imageUri);
    }

    @BindingAdapter("android:src")
    public static void setImageDrawable(ImageView view, Drawable drawable) {
        view.setImageDrawable(drawable);
    }
}

我們可以看到系統(tǒng)給我們提供了很多預置的 BindingAdapter 和 BindingMethods,比如看這個 "android:src"酷勺,我們其實可以再寫個直接把 url 傳進去本橙,還可以干其他的,仔細一想脆诉,BindingAdapter 能做的太多了甚亭,本人項目的小伙伴直接把能重復利用的代碼都搞這里了。击胜。亏狰。其實我覺得并不太好,因為寫這里代碼不明顯偶摔,溝通成本就變高了暇唾。

@BindingConversion

  • 注釋方法,用于將表達式類型自動轉換為setter中使用的值啰挪。
  • 轉換器應該有一個參數(shù)信不,表達式類型,返回值應該是setter中使用的目標值類型亡呵。
  • 只要可以應用轉換器抽活,轉換器就會被使用,并且不特定于任何屬性锰什。

問:那到底什么屬性要被轉換下硕?又要把屬性轉換成什么呢?

答:舉個例子汁胆,比如 View 的 android:background="" 屬性,屬性值為 drawable 類型的對象梭姓,如果你要放置一個 @color/black 的屬性值,在不使用 dataBinding 的情況下沒有任何問題嫩码,但是如果你要使用了 dataBinding 方式開發(fā)誉尖,則會報錯,原因在于 android:background="" 的目標值類型為 drawable 铸题,而 databinding 會把 @color/black 先解析成 int 類型的顏色值铡恕,這時如果把 int 類型的顏色值直接賦值到目標為 drawable 類型的參數(shù)中去琢感,那絕逼是要報錯的,如果我們可以在int類型的顏色值賦值之前探熔,讓 int 類型的顏色值自動轉換為 colorDrawable 對象驹针,就可以解決這個問題,而 @BindingConversion 注解就是干這個事的诀艰。
我們先來看一個官網上的示例:

<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

如果只有上面那段代碼柬甥,那肯定會報錯的,原因上面也說了其垄,所以我們需要定義一個轉換方法苛蒲,把整型的顏色值轉換為 drawable 對象:

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
   return new ColorDrawable(color);
}

這時再運行就不會出錯了,因為在 int 型顏色值賦值之前已經被自動轉換為 ColorDrawable 了绿满。
肯定有人會有疑問:

  • 為什么我沒有定義 convertColorToDrawable 這樣的方法撤防,直接寫完布局文件就運行也沒有報錯呢?
  • convertColorToDrawable 這個方法是什么時機被調用的呢棒口?

首先之所以沒有報錯的原因,其實是因 為convertColorToDrawable 這個方法在 dataBinding 的 jar 包中已經幫我們定義好了辜膝,我們不用再定義了无牵,所以不會報錯。源碼路徑為:android.databinding.adapters.Converters厂抖。
下面再說說調用時機:Android 中的每個 xml 中的屬性都對應著相應的 set/get 方法的茎毁,如果 在xml 中設置的屬性值的類型與對應的 Java 方法的參數(shù)類型不符,這時 dataBinding 就會去尋找可以讓屬性值轉換為正確類型的方法忱辅,而尋找的根據(jù)就是所有被 @BindingConversion 注解標記的方法七蜘,這時 convertColorToDrawable 方法就會被調用了。

如果我們自己定義了一個功能相同的 convertColorToDrawable 方法墙懂,那么 dataBinding 會優(yōu)先使用我們自己定義的方法橡卤。
如果我們自己定義了多個功能相同的convertColorToDrawable方法,比如convertColorToDrawable01损搬,convertColorToDrawable02碧库,convertColorToDrawable03,dataBinding會選擇順序靠后的方法去使用巧勤。

@InverseBindingAdapter

  • 用于雙向綁定
  • 需要與@BindingAdapter配合使用
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
public @interface InverseBindingAdapter {

    String attribute();

    String event() default "";
}
  • attribute:String 類型嵌灰,必填,表示當值發(fā)生變化時颅悉,要從哪個屬性中檢索這個變化的值,示例:"android:text"
  • event: String 類型沽瞭,非必填,如果填寫剩瓶,則使用填寫的內容作為 event 的值驹溃;如果不填城丧,在編譯時會根據(jù) attribute 的屬性名再加上后綴 "AttrChanged" 生成一個新的屬性作為 event 的值,舉個例子:attribute 屬性的值為 "android:text"吠架,那么默認會在 "android:text" 后面追加 "AttrChanged" 字符串芙贫,生成 "android:textAttrChanged" 字符串作為 event 的值.
  • event屬性的作用: 當 View 的值發(fā)生改變時用來通知 dataBinding 值已經發(fā)生改變了。開發(fā)者一般需要使用 @BindingAdapter 創(chuàng)建對應屬性來響應這種改變傍药。

我們找系統(tǒng)提供的 TextViewBindingAdapter 類中的方法來簡單看一下(主要看我寫的注釋磺平,我習慣把內容寫到注釋中):

    // 這里是咱們在 TextView 中使用 android:text="@{xxx}" 功能的具體實現(xiàn),可以看到拐辽,它不是簡單的 textView.setText(xxx)就完事了
    // 這里注意了拣挪,"android:text" 是支持雙向綁定的,雙向綁定會造成死循環(huán)俱诸,瞎面的代碼添加了防止死循環(huán)的邏輯
    // @BindingAdapter("android:text")的作用是:代碼中的值改變了通知布局讓它顯示出來
    @BindingAdapter("android:text")
    public static void setText(TextView view, CharSequence text) {
        final CharSequence oldText = view.getText();
        if (text == oldText || (text == null && oldText.length() == 0)) {
            return;
        }
        if (text instanceof Spanned) {
            if (text.equals(oldText)) {
                return; // No change in the spans, so don't set anything.
            }
        } else if (!haveContentsChanged(text, oldText)) {
            return; // No content changes, so don't set anything.
        }
        view.setText(text);
    }

    /**
    * 注意了菠劝,這就是那個重點要看的,雖然內容就一行睁搭。赶诊。
    * 注解里面的參數(shù)翻譯成大白話就是:
    *   當發(fā)現(xiàn)布局中的 text 發(fā)生改變的時候(EditTextView 用戶輸入),發(fā)一個事件出來("android:textAttrChanged")
    *   然后我們下邊再建個 BindingAdapter 來響應這個改變园骆,以此實現(xiàn)雙向綁定
    */
    @InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
    public static String getTextString(TextView view) {
        return view.getText().toString();
    }
    
    //
    @BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged",
            "android:afterTextChanged", "android:textAttrChanged"}, requireAll = false)
    public static void setTextWatcher(TextView view, final BeforeTextChanged before,
            final OnTextChanged on, final AfterTextChanged after,
            final InverseBindingListener textAttrChanged) {
        final TextWatcher newValue;
        if (before == null && after == null && on == null && textAttrChanged == null) {
            newValue = null;
        } else {
            newValue = new TextWatcher() {
                @Override
                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                    if (before != null) {
                        before.beforeTextChanged(s, start, count, after);
                    }
                }

                @Override
                public void onTextChanged(CharSequence s, int start, int before, int count) {
                    if (on != null) {
                        on.onTextChanged(s, start, before, count);
                    }
                    if (textAttrChanged != null) {
                        // 主要看這里舔痪!
                        // 主要看這里!
                        // 其他自己看锌唾,這里會有個 InverseBindingListener 傳入進來(原理我后邊可能會寫)锄码,
                        // 這個 listener 調用了一個 onChange() 方法,這里就會把布局修改 text 的事情告訴代碼晌涕,然后代碼中的值就變了
                        // onChange 的具體實現(xiàn)在對應的布局生成的 Binding文件中滋捶,想看的先自己研究去吧
                        textAttrChanged.onChange();
                    }
                }

                @Override
                public void afterTextChanged(Editable s) {
                    if (after != null) {
                        after.afterTextChanged(s);
                    }
                }
            };
        }
        final TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher);
        if (oldValue != null) {
            view.removeTextChangedListener(oldValue);
        }
        if (newValue != null) {
            view.addTextChangedListener(newValue);
        }
    }

@InverseBindingMethod 和 @InverseBindingMethods

@InverseBindingMethods 和 @BindingMethods 相似,都只是一個容器余黎,用法也一樣重窟,真正的功能在 @InverseBindingMethod 中

@Target(ElementType.ANNOTATION_TYPE)
public @interface InverseBindingMethod {

    Class type();

    /**
     * 要支持雙向綁定的屬性名  例如 android:text 這個 text 就是屬性名
     */
    String attribute();

    /**
     * event 不是必須的,event 代表當屬性發(fā)生變化的時候通知哪個 Binding 事件驯耻,默認 attribute() + "AttrChanged"
     * 如果在 InverseBindingMethod 的 event 里面任意定義了一個方法 “xxx”亲族,那么必須在該注解類有一個同名方法如 setXxx() 或者 @BindingAdapter 注解一個 “xxx” 方法,然后在這里面調用 InverseBindingListener 的 onChange()可缚。
     * 當然霎迫,如果不是自定義的,默認的 setter 或者 BindingAdapter 提供也得有
     */
    String event() default "";

    /**
     * 這個是屬性調用的 getter 的方法帘靡,默認調用基于屬性名的知给,也可以自己指定
     */
    String method() default "";
}

例子來個簡單的:

@InverseBindingMethods({
        @InverseBindingMethod(type = RatingBar.class, attribute = "android:rating"),
})
public class RatingBarBindingAdapter {
    @BindingAdapter("android:rating")
    public static void setRating(RatingBar view, float rating) {
        if (view.getRating() != rating) {
            view.setRating(rating);
        }
    }

    // 這里和 InverseBindingAdapter 一樣添加了 InverseBindingListener 進去,兩個注解都可實現(xiàn)雙向綁定
    @BindingAdapter(value = {"android:onRatingChanged", "android:ratingAttrChanged"},
            requireAll = false)
    public static void setListeners(RatingBar view, final OnRatingBarChangeListener listener,
            final InverseBindingListener ratingChange) {
        if (ratingChange == null) {
            view.setOnRatingBarChangeListener(listener);
        } else {
            view.setOnRatingBarChangeListener(new OnRatingBarChangeListener() {
                @Override
                public void onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser) {
                    if (listener != null) {
                        listener.onRatingChanged(ratingBar, rating, fromUser);
                    }
                    ratingChange.onChange();
                }
            });
        }
    }
}

這里我感覺兩種實現(xiàn)雙向綁定的方式看起來沒什么太大區(qū)別,InverseBindingMethod 多了可以指定 getter 方法涩赢,InverseBindingAdapter 可以自己實現(xiàn) getter 方法戈次,如果有什么說的不對的地方,希望大家指正筒扒,我目前項目中沒有用到自定義的雙向綁定怯邪,因為覺得這些方法雖然方便了使用,但是增加了溝通交流成本花墩,所以我盡量使用系統(tǒng)提供的方法來開發(fā)悬秉。

@InverseMethod

這個方法的用途就是為一個方法提供一個相反的方法。

    public @interface InverseMethod {
        /**
         * @return 這個參數(shù)是需要轉換的方法名冰蘑,被注解的和被轉換的需要在同一個類中
         */
        String value();
    }

注意:

  • 轉換的方法的入?yún)⑹潜晦D換方法的出參和泌,反之亦然
  • 這么拗口

比如下面:

    // 因為被轉換的方法傳入 int 所以這個方法返回 int
    // 因為被轉換的方法返回 String 所以這個方法傳入 String
    @InverseMethod("convertIntToString")
    public static int convertStringToInt(String value) {
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            return -1;
        }
    }

    // 這個方法提供了從 String 轉 int 的功能
    // 我們在布局中給一個 EditTextView 傳了個 int,但是 android:text 只接收 String 類型的值祠肥,
    // 但我們用這個參數(shù)的時候又需要從 EditTextView 拿到 int 類型的值武氓,
    // 為了避免手動轉來轉去,這個就是個非常 nice 的功能
    public static String convertIntToString(int value) {
        return String.valueOf(value);
    }
    
    <EditText
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="@={vm.convertIntToString(vm.persion.age)}"/>

教程還有好多想寫的仇箱,這里講的也不全县恕,差不多是按照我學習的先后順序來了,后面給大家講一些 Google jetpack 開發(fā)組件(Architecture components)剂桥,源碼的解析暫時不會寫了弱睦,主要是自己沒有全部看完,相信你真正看完這些注解的時候源碼已經看懂很多了渊额。

在這里附上參考博文:

DataBinding使用教程(三):各個注解詳解

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市垒拢,隨后出現(xiàn)的幾起案子旬迹,更是在濱河造成了極大的恐慌,老刑警劉巖求类,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奔垦,死亡現(xiàn)場離奇詭異,居然都是意外死亡尸疆,警方通過查閱死者的電腦和手機椿猎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來寿弱,“玉大人犯眠,你說我怎么就攤上這事≈⒏铮” “怎么了筐咧?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我量蕊,道長铺罢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任残炮,我火速辦了婚禮韭赘,結果婚禮上,老公的妹妹穿的比我還像新娘势就。我一直安慰自己泉瞻,他們只是感情好,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布蛋勺。 她就那樣靜靜地躺著瓦灶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪抱完。 梳的紋絲不亂的頭發(fā)上贼陶,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音巧娱,去河邊找鬼碉怔。 笑死,一個胖子當著我的面吹牛禁添,可吹牛的內容都是我干的撮胧。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼老翘,長吁一口氣:“原來是場噩夢啊……” “哼芹啥!你這毒婦竟也來了?” 一聲冷哼從身側響起铺峭,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤墓怀,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后卫键,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體傀履,經...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年莉炉,在試婚紗的時候發(fā)現(xiàn)自己被綠了钓账。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡絮宁,死狀恐怖梆暮,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情绍昂,我是刑警寧澤惕蹄,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響卖陵,放射性物質發(fā)生泄漏遭顶。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一泪蔫、第九天 我趴在偏房一處隱蔽的房頂上張望棒旗。 院中可真熱鬧,春花似錦撩荣、人聲如沸铣揉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽逛拱。三九已至,卻和暖如春台猴,著一層夾襖步出監(jiān)牢的瞬間朽合,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工饱狂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留曹步,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓休讳,卻偏偏與公主長得像讲婚,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子俊柔,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345