React Native支持任意組件實現(xiàn)下拉刷新功能,并且可以自定義下拉刷新頭部

1.背景

無論是 Androi 還是 ios,下拉刷新都是一個很有必要也很重要的功能。那么在 RN(以下用 RN 表示 React Native )之中,我們該如何實現(xiàn)下拉刷新功能呢?RN 官方提供了一個用于 ScrollView , ListView 等帶有滑動功能組件的下拉刷新組件 RefreshControl。查看 RefreshControl 相關(guān)源碼可以發(fā)現(xiàn),其實它是對原生下拉刷新組件的一個封裝,好處是使用方便快捷。但缺點(diǎn)也很明顯,就是它不可以進(jìn)行自定義下拉刷新頭部,并且只能使用與 ScrollView,ListView 這種帶有滾動功能的組件之中。那么我們該如何去解決這兩個問題呢?
先看下最終實現(xiàn)的效果,這里借助了 ScrollableTabView

ios.gif

android.gif

2.實現(xiàn)原理分析

對于下拉刷新功能,其實它的原理很簡單。就是對要操作的組件進(jìn)行 y 軸方向的位置進(jìn)行判斷。當(dāng)滾動到頂部的時候,此時如果下拉的話,那么就進(jìn)行下拉刷新的操作,如果上拉的話,那么就進(jìn)行原本組件的滾動操作。基于這個原理,找了一些第三方實現(xiàn)的框架,基本上實現(xiàn)方式都是通過 ScrollView,ListView 等的 onScroll 方法進(jìn)行監(jiān)聽回調(diào)。然后設(shè)置 Enable 屬性來控制其是否可以滾動。但在使用的過程中有兩個問題,一個是 onScroll 回調(diào)的頻率不夠,很多時候在滾動到了頂部的時候不能正確回調(diào)數(shù)值。另外一個問題就是 Enable 屬性的問題,當(dāng)在修改 Enable 數(shù)值的時候,當(dāng)前的手勢操作會停止。具體反映到 UI 上的效果就是,完成一次下拉刷新之后,第一次向上滾動的效果不能觸發(fā)。那么,能不能有其他的方式去實現(xiàn) RN 上的下拉刷新呢?

3.實現(xiàn)過程

3.1 判斷組件的滾動位置

在上面的原理分析中,一個重點(diǎn)就是判斷要操作的組件的滾動位置,那么改如何去判斷呢?在這里我們對 RN 的 View,ScrollView,ListView,F(xiàn)latList 進(jìn)行了相關(guān)的判斷,不過要注意的是,F(xiàn)latList 是 RN0.43 版本之后才出現(xiàn)的,所以如果你使用的 RN 版本小于 0.43 的話,那么你就要刪除掉該下拉刷新框架關(guān)于 FlatList 的部分。
我們來看下如何進(jìn)行相關(guān)的判斷。

 onShouldSetPanResponder = (e, gesture) => {
        let y = 0
        if (this.scroll instanceof ListView) { //ListView下的判斷
            y = this.scroll.scrollProperties.offset;
        } else if (this.scroll instanceof FlatList) {//FlatList下的判斷
            y = this.scroll.getScrollMetrics().offset  //這個方法需要自己去源碼里面添加
        }
        //根據(jù)y的值來判斷是否到達(dá)頂部
        this.state.atTop = (y <= 0)
        if (this.state.atTop && index.isDownGesture(gesture.dx, gesture.dy) && this.props.refreshable) {
            this.lastY = this.state.pullPan.y._value;
            return true;
        }
        return false;
    }

首先對于普通的 View,由于它沒有滾動屬性,所以它默認(rèn)處于頂部。而對于 ListView 來說,通過查找它的源碼,發(fā)現(xiàn)它有個 scrollProperties 屬性,里面包含了一些滾動的屬性值,而 scrollProperties.offset 就是表示橫向或者縱向的滾動值。而對于 FlatList 而言,它并沒相關(guān)的屬性。但是發(fā)現(xiàn) VirtualizedList 中存在如下屬性,而 FlatList 是對 VirtualizedList 的一個封裝

 _scrollMetrics = {
        visibleLength: 0, contentLength: 0, offset: 0, dt: 10, velocity: 0, timestamp: 0,
    };

那么很容易想到自己添加方法去獲取。那么在
FlatList(node_modules/react-native/Libraries/Lists/FlatList.js) 添加如下方法

getScrollMetrics = () => {
    return this._listRef.getScrollMetrics()
}

同時在 VirtualizedList(node_modules/react-native/Libraries/Lists/VirtualizedList.js) 添加如下方法

getScrollMetrics = () => {
    return this._scrollMetrics
 }

另外,對于 ScrollView 而言,并沒有找到相關(guān)滾動位置的屬性,所以在這里用 ListView 配合 ScrollView 來使用,將 ScrollView 作為
ListView 的一個子控件

//ScrollView 暫時沒有找到比較好的方法去判斷時候滾動到頂部,
//所以這里用ListView配合ScrollView進(jìn)行使用
export default  class PullScrollView extends Pullable {
    getScrollable=()=> {
        return (
            <ListView
                ref={(c) => {this.scroll = c;}}
                renderRow={this.renderRow}
                dataSource={new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}).cloneWithRows([])}
                enableEmptySections={true}
                renderHeader={this._renderHeader}/>
        );
    }

    renderRow = (rowData, sectionID, rowID, highlightRow) => {
        return <View/>
    }

    _renderHeader = () => {
        return (
            <ScrollView
                scrollEnabled={false}>
                {this.props.children}
            </ScrollView>
        )
    }
}

那么當(dāng)要操作的組件滾動到頂部的時候,此時下拉就是下拉刷新操作,而上拉就實現(xiàn)原本的操作邏輯

3.2 組件位置的布局控制

下拉刷新的滾動方式一般有兩種,一種是內(nèi)容跟隨下拉頭部一起下拉滾動,一種是內(nèi)容固定不動,只有下拉頭部在滾動。在這里用isContentScroll屬性來進(jìn)行選擇判斷

render() {
        return (
            <View style={styles.wrap} {...this.panResponder.panHandlers} onLayout={this.onLayout}>
                {this.props.isContentScroll ?
                    <View pointerEvents='box-none'>
                        <Animated.View style={[this.state.pullPan.getLayout()]}>
                            {this.renderTopIndicator()}
                            <View ref={(c) => {this.scrollContainer = c;}}
                                  style={{width: this.state.width, height: this.state.height}}>
                                {this.getScrollable()}
                            </View>
                        </Animated.View>
                    </View> :
                    <View>
                        <View ref={(c) => {this.scrollContainer = c;}}
                              style={{width: this.state.width, height: this.state.height}}>
                            {this.getScrollable()}
                        </View>
                        <View pointerEvents='box-none'
                              style={{position: 'absolute', left: 0, right: 0, top: 0}}>
                            <Animated.View style={[this.state.pullPan.getLayout()]}>
                                {this.renderTopIndicator()}
                            </Animated.View>
                        </View>
                    </View>}
            </View>
        );
    }

從里面可以看到一個方法 this.getScrollable() , 這個就是我們要進(jìn)行下拉刷新的內(nèi)容,這個方法類似我們在 java 中的抽象方法,是一定要實現(xiàn)的,并且操作的內(nèi)容的要指定 ref 為 this.scroll,舉個例子

export default class PullView extends Pullable {

    getScrollable = () => {
        return (
            <View ref={(c) => {this.scroll = c;}}
                {...this.props}>
                {this.props.children}
            </View>
        );
    }
}

3.3 添加默認(rèn)刷新頭部

這里我們添加個默認(rèn)的下拉刷新頭部,用于當(dāng)不添加下拉刷新頭部時候的默認(rèn)的顯示

defaultTopIndicatorRender = () => {
        return (
            <View style={{flexDirection: 'row', justifyContent: 'center', alignItems: 'center', height: index.defaultTopIndicatorHeight}}>
                <ActivityIndicator size="small" color="gray" style={{marginRight: 5}}/>
                <Text ref={(c) => {
                    this.txtPulling = c;
                }} style={styles.hide}>{index.pulling}</Text>
                <Text ref={(c) => {
                    this.txtPullok = c;
                }} style={styles.hide}>{index.pullok}</Text>
                <Text ref={(c) => {
                    this.txtPullrelease = c;
                }} style={styles.hide}>{index.pullrelease}</Text>
            </View>
        );
    }

效果就是上面的 gif 中除了 View 的 tab 的展示效果,同時需要根據(jù)下拉的狀態(tài)來進(jìn)行頭部效果的切換

 if (this.pullSatte == "pulling") {
                this.txtPulling && this.txtPulling.setNativeProps({style: styles.show});
                this.txtPullok && this.txtPullok.setNativeProps({style: styles.hide});
                this.txtPullrelease && this.txtPullrelease.setNativeProps({style: styles.hide});
            } else if (this.pullSatte == "pullok") {
                this.txtPulling && this.txtPulling.setNativeProps({style: styles.hide});
                this.txtPullok && this.txtPullok.setNativeProps({style: styles.show});
                this.txtPullrelease && this.txtPullrelease.setNativeProps({style: styles.hide});
            } else if (this.pullSatte == "pullrelease") {
                this.txtPulling && this.txtPulling.setNativeProps({style: styles.hide});
                this.txtPullok && this.txtPullok.setNativeProps({style: styles.hide});
                this.txtPullrelease && this.txtPullrelease.setNativeProps({style: styles.show});
            }
const styles = StyleSheet.create({
    wrap: {
        flex: 1,
        flexGrow: 1,
        zIndex: -999,
    },
    hide: {
        position: 'absolute',
        left: 10000,
        backgroundColor: 'transparent'
    },
    show: {
        position: 'relative',
        left: 0,
        backgroundColor: 'transparent'
    }
});

這里借助 setNativeProps 方法來代替 setStat e的使用,減少 render 的次數(shù)

3.4 下拉刷新手勢控制

在下拉刷新之中,手勢的控制是必不可少的一環(huán),至于如何為組件添加手勢,大家可以看下 RN 官網(wǎng)上的介紹

this.panResponder = PanResponder.create({
            onStartShouldSetPanResponder: this.onShouldSetPanResponder,
            onStartShouldSetPanResponderCapture: this.onShouldSetPanResponder,
            onMoveShouldSetPanResponder: this.onShouldSetPanResponder,
            onMoveShouldSetPanResponderCapture: this.onShouldSetPanResponder,
            onPanResponderTerminationRequest: (evt, gestureState) => false, //這個很重要,這邊不放權(quán)
            onPanResponderMove: this.onPanResponderMove,
            onPanResponderRelease: this.onPanResponderRelease,
            onPanResponderTerminate: this.onPanResponderRelease,
        });

這里比較重要的一點(diǎn)就是 onPanResponderTerminationRequest (有其他組件請求使用手勢),這個時候不能將手勢控制交出去

onShouldSetPanResponder = (e, gesture) => {
        let y = 0
        if (this.scroll instanceof ListView) { //ListView下的判斷
            y = this.scroll.scrollProperties.offset;
        } else if (this.scroll instanceof FlatList) {//FlatList下的判斷
            y = this.scroll.getScrollMetrics().offset  //這個方法需要自己去源碼里面添加
        }
        //根據(jù)y的值來判斷是否到達(dá)頂部
        this.state.atTop = (y <= 0)
        if (this.state.atTop && index.isDownGesture(gesture.dx, gesture.dy) && this.props.refreshable) {
            this.lastY = this.state.pullPan.y._value;
            return true;
        }
        return false;
    }

onShouldSetPanResponder方法主要是對當(dāng)前是否進(jìn)行下拉操作進(jìn)行判斷。下拉的前提是內(nèi)容滾動到頂部,下拉手勢并且該內(nèi)容需要下拉刷新操作( refreshable 屬性)

onPanResponderMove = (e, gesture) => {
        if (index.isDownGesture(gesture.dx, gesture.dy) && this.props.refreshable) { //下拉
            this.state.pullPan.setValue({x: this.defaultXY.x, y: this.lastY + gesture.dy / 2});
            this.onPullStateChange(gesture.dy)
        }
    }
 //下拉的時候根據(jù)高度進(jìn)行對應(yīng)的操作
    onPullStateChange = (moveHeight) => {
        //因為返回的moveHeight單位是px,所以要將this.topIndicatorHeight轉(zhuǎn)化為px進(jìn)行計算
        let topHeight = index.dip2px(this.topIndicatorHeight)
        if (moveHeight > 0 && moveHeight < topHeight) { //此時是下拉沒有到位的狀態(tài)
            this.pullSatte = "pulling"
        } else if (moveHeight >= topHeight) { //下拉刷新到位
            this.pullSatte = "pullok"
        } else { //下拉刷新釋放,此時返回的值為-1
            this.pullSatte = "pullrelease"
        }

        if (this.props.topIndicatorRender == null) { //沒有就自己來
            if (this.pullSatte == "pulling") {
                this.txtPulling && this.txtPulling.setNativeProps({style: styles.show});
                this.txtPullok && this.txtPullok.setNativeProps({style: styles.hide});
                this.txtPullrelease && this.txtPullrelease.setNativeProps({style: styles.hide});
            } else if (this.pullSatte == "pullok") {
                this.txtPulling && this.txtPulling.setNativeProps({style: styles.hide});
                this.txtPullok && this.txtPullok.setNativeProps({style: styles.show});
                this.txtPullrelease && this.txtPullrelease.setNativeProps({style: styles.hide});
            } else if (this.pullSatte == "pullrelease") {
                this.txtPulling && this.txtPulling.setNativeProps({style: styles.hide});
                this.txtPullok && this.txtPullok.setNativeProps({style: styles.hide});
                this.txtPullrelease && this.txtPullrelease.setNativeProps({style: styles.show});
            }
        }
        //告訴外界是否要鎖住
        this.props.onPushing && this.props.onPushing(this.pullSatte != "pullrelease")
        //進(jìn)行狀態(tài)和下拉距離的回調(diào)
        this.props.onPullStateChangeHeight && this.props.onPullStateChangeHeight(
            this.pullSatte == "pulling", this.pullSatte == "pullok",
            this.pullSatte == "pullrelease", moveHeight)
    }

onPanResponderMove 方法中主要是對下拉時候頭部組件 UI 進(jìn)行判斷,這里有三個狀態(tài)的判斷以及下拉距離的回調(diào)

 onPanResponderRelease = (e, gesture) => {
        if (this.pullSatte == 'pulling') { //沒有下拉到位
            this.resetDefaultXYHandler(); //重置狀態(tài)
        } else if (this.pullSatte == 'pullok') { //已經(jīng)下拉到位了
            //傳入-1,表示此時進(jìn)行的是釋放刷新的操作
            this.onPullStateChange(-1)
            //進(jìn)行下拉刷新的回調(diào)
            this.props.onPullRelease && this.props.onPullRelease();
            //重置刷新的頭部到初始位置
            Animated.timing(this.state.pullPan, {
                toValue: {x: 0, y: 0},
                easing: Easing.linear,
                duration: this.duration
            }).start();
        }
    }
 //重置刷新的操作
    resetDefaultXYHandler = () => {
        Animated.timing(this.state.pullPan, {
            toValue: this.defaultXY,
            easing: Easing.linear,
            duration: this.duration
        }).start(() => {
            //ui要進(jìn)行刷新
            this.onPullStateChange(-1)
        });
    }

onPanResponderRelease 方法中主要是下拉刷新完成或者下拉刷新中斷時候?qū)︻^部 UI 的一個重置,并且有相關(guān)的回調(diào)操作

4.屬性和方法介紹

4.1 屬性

Porp Type Optional Default Description
refreshable bool yes true 是否需要下拉刷新功能
isContentScroll bool yes false 在下拉的時候內(nèi)容時候要一起跟著滾動
onPullRelease func yes 刷新的回調(diào)
topIndicatorRender func yes 下拉刷新頭部的樣式,當(dāng)它為空的時候就使用默認(rèn)的
topIndicatorHeight number yes 下拉刷新頭部的高度,當(dāng)topIndicatorRender不為空的時候要設(shè)置正確的topIndicatorHeight
onPullStateChangeHeight func yes 下拉時候的回調(diào),主要是刷新的狀態(tài)的下拉的距離
onPushing func yes 下拉時候的回調(diào),告訴外界此時是否在下拉刷新

4.2 方法

startRefresh() : 手動調(diào)用下拉刷新功能
finishRefresh() : 結(jié)束下拉刷新

5.最后

該組件已經(jīng)發(fā)布到 npm 倉庫,使用的時候只需要 npm install react-native-rk-pull-to-refresh --save 就可以了,同時需要 react-native link react-native-rk-pull-to-refresh,它的使用Demo已經(jīng)上傳Github了:https://github.com/hzl123456/react-native-rk-pull-to-refresh
另外:在使用過程中不要設(shè)置內(nèi)容組件 Bounce 相關(guān)的屬性為 false ,例如:ScrollView 的 bounces 屬性( ios 特有)

6.更新與2018年1月9日

在使用的過程中,發(fā)現(xiàn)在 Android 中使用的過程中經(jīng)常會出現(xiàn)下拉無法觸發(fā)下拉刷新的問題,所以 Android 的下拉刷新采用原生組件封裝的形式。對 android-Ultra-Pull-To-Refresh 進(jìn)行封裝。調(diào)用主要如下

'use strict';
import React from 'react';
import RefreshLayout from '../view/RefreshLayout'
import RefreshHeader from '../view/RefreshHeader'
import PullRoot from './PullRoot'
import * as index from './info';

export default class Pullable extends PullRoot {

    constructor(props) {
        super(props);
        this.pullState = 'pulling'; //pulling,pullok,pullrelease
        this.topIndicatorHeight = this.props.topIndicatorHeight ? this.props.topIndicatorHeight : index.defaultTopIndicatorHeight;
    }

    render() {
        return (
            <RefreshLayout
                {...this.props}
                style={{flex: 1}}
                ref={(c) => this.refresh = c}>

                <RefreshHeader
                    style={{flex: 1, height: this.topIndicatorHeight}}
                    viewHeight={index.dip2px(this.topIndicatorHeight)}
                    onPushingState={(e) => this.onPushingState(e)}>
                    {this.renderTopIndicator()}
                </RefreshHeader>

                {this.getScrollable()}
            </RefreshLayout>
        )
    }


    onPushingState = (event) => {
        let moveHeight = event.nativeEvent.moveHeight
        let state = event.nativeEvent.state
        //因為返回的moveHeight單位是px,所以要將this.topIndicatorHeight轉(zhuǎn)化為px進(jìn)行計算
        let topHeight = index.dip2px(this.topIndicatorHeight)
        if (moveHeight > 0 && moveHeight < topHeight) { //此時是下拉沒有到位的狀態(tài)
            this.pullState = "pulling"
        } else if (moveHeight >= topHeight) { //下拉刷新到位
            this.pullState = "pullok"
        } else { //下拉刷新釋放,此時返回的值為-1
            this.pullState = "pullrelease"
        }
        //此時處于刷新中的狀態(tài)
        if (state == 3) {
            this.pullState = "pullrelease"
        }
        //默認(rèn)的設(shè)置
        this.defaultTopSetting()
        //告訴外界是否要鎖住
        this.props.onPushing && this.props.onPushing(this.pullState != "pullrelease")
        //進(jìn)行狀態(tài)和下拉距離的回調(diào)
        this.props.onPullStateChangeHeight && this.props.onPullStateChangeHeight(this.pullState, moveHeight)
    }

    finishRefresh = () => {
        this.refresh && this.refresh.finishRefresh()
    }

    startRefresh = () => {
        this.refresh && this.refresh.startRefresh()
    }
}

同時修改了主動調(diào)用下拉刷新的的方法為 startRefresh() , 結(jié)束刷新的方法為 finishRefresh() , 其他的使用方式和方法沒有修改

7.更新于2018年5月14日

由于 React Native 版本的更新,移除了 React.PropTypes ,更新了 PropTypes 的引入方式,改動如下(基于 RN 0.55.4 版本):
1.使用 import PropTypes from 'prop-types' 引入 PropTypes
2.修改 FlatList 滑動距離的判斷,這樣你就不需要再修改源碼了

let y = 0
if (this.scroll instanceof ListView) { //ListView下的判斷
     y = this.scroll.scrollProperties.offset;
} else if (this.scroll instanceof FlatList) {//FlatList下的判斷
    y = this.scroll._listRef._getScrollMetrics().offset
}

8.更新于2019年2月15日

最近升級了 React Native 到 0.58.1 版本,發(fā)現(xiàn) android 的下拉刷新頭部無法隱藏,一直顯示在最頂端,排查 RN 的源碼發(fā)現(xiàn)。

  public ReactViewGroup(Context context) {
    super(context);
    setClipChildren(false);
    mDrawingOrderHelper = new ViewGroupDrawingOrderHelper(this);
  }

ReactViewGroup 默認(rèn)調(diào)用了setClipChildren(false)方法,這樣子 View 將可以超出父 View 的布局范圍,也就導(dǎo)致了我們的下拉刷新頭部無法隱藏的問題。修改如下:

//設(shè)置所有的parent的clip屬性為true,為了兼容RN的view默認(rèn)為false的bug
        setViewClipChildren(getParent());
private void setViewClipChildren(ViewParent rootView) {
        if (rootView != null && rootView instanceof ViewGroup) {
            ViewGroup viewGroup = ((ViewGroup) rootView);
            viewGroup.setClipChildren(true);
            setViewClipChildren(viewGroup.getParent());
        }
    }

在 onFinishInflate() 的最后調(diào)用 setViewClipChildren(getParent()) 方法,修改下拉刷新控件的所有父 View 的 clipChildren 屬性為 true,可以解決這個 bug。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,224評論 6 529
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 97,916評論 3 413
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,014評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,466評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,245評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,795評論 1 320
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,869評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,010評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,524評論 1 331
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,487評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,634評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,173評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,884評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,282評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,541評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,236評論 3 388
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,623評論 2 370

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