AppWidget(桌面小部件)

一、引言

作为一个车机的Launcher开发,总结了下自己认为的难点技术:ApppWidget、拖拽、RemoteAnimation(应用打开/关闭动画)。今天就其中的AppWidget简述一下。

二、AppWidget简介

  • Android widget 也称为桌面插件,其是android系统应用开发层面的一部分,但是又有特殊用途,而且会成为整个android系统的亮点。Android中的AppWidget与google widget和中移动的widget并不是一个概念,这里的AppWidget只是把一个进程的控件嵌入到别外一个进程的窗口里的一种方法。
  • AppWidget的服务核心在AppWidgetService中,它是系统应用,在SystemServer进程中。
  • AppWidget的提供方由应用提供(对大部分应用开发者来说,了解操作这一块就够了)。
  • AppWidget的显示方,基本上运行在Launcher中。
  • AppWidget支持的控件是由局限性的,比如不支持RecyclerView等。
  • RemoteViews 在Android中的使用场景主要有:自定义通知栏和桌面小部件。

如下图红色箭头所指的都是 AppWidget


image.png

三、Launcher3 AppWidget的启动添加流程

1. Launcher3启动添加

Launcher启动onCreate()方法初始化mAppWidgetManager, mAppWidgetHost对象,AppWidgetHost是launcher承载AppWidgetView的宿主。

public void onCreate() {
    ...
    //得到AppWidget管理实例 : AppWidgetManager , AppWidgetHost , AppWidgetHostView三个类的关系
    mAppWidgetManager = AppWidgetManagerCompat.getInstance(this);  //1
    mAppWidgetHost = new LauncherAppWidgetHost(this);  //2
    // Host启动监听,监听LauncherProvider中的数据改变
    mAppWidgetHost.startListening();  //3
    ...
}
  1. AppWidgetManagerCompat 管理类是一个单例模式的兼容类
    public static AppWidgetManagerCompat getInstance(Context context) {
        synchronized (sInstanceLock) {
            if (sInstance == null) {
                if (Utilities.ATLEAST_OREO) {
                    sInstance = new AppWidgetManagerCompatVO(context.getApplicationContext());
                } else {
                    sInstance = new AppWidgetManagerCompatVL(context.getApplicationContext());
                }
            }
            return sInstance;
        }
    }
  1. LauncherAppWidgetHost extends AppWidgetHost 由其父类完成初始化对象,创建用于回调的Callbacks服务类IAppWidgetHost.Stub, 绑定服务bindService,得到IAppWidgetService对象,进行launcher和AppWidgetService之间的调用
 public AppWidgetHost(Context context, int hostId, OnClickHandler handler, Looper looper) {
        mContextOpPackageName = context.getOpPackageName();
        mHostId = hostId;
        mOnClickHandler = handler;
        mHandler = new UpdateHandler(looper);
        mCallbacks = new Callbacks(mHandler);
        mDisplayMetrics = context.getResources().getDisplayMetrics();
        bindService(context);
    }

    private static void bindService(Context context) {
        synchronized (sServiceLock) {
            if (sServiceInitialized) {
                return;
            }
            sServiceInitialized = true;
            PackageManager packageManager = context.getPackageManager();
            if (!packageManager.hasSystemFeature(PackageManager.FEATURE_APP_WIDGETS)
                    && !context.getResources().getBoolean(R.bool.config_enableAppWidgetService)) {
                return;
            }
            IBinder b = ServiceManager.getService(Context.APPWIDGET_SERVICE);
            sService = IAppWidgetService.Stub.asInterface(b);
        }
    }
  1. 在startListening 方法中 ,通过IAppWidgetService.startListening 方法解析Launcher中的AppWidget信息保存到系统服务成员变量中。
  public void startListening() {
        if (sService == null) {
            return;
        }
        final int[] idsToUpdate;
        synchronized (mViews) {
            int N = mViews.size();
            idsToUpdate = new int[N];
            for (int i = 0; i < N; i++) {
                idsToUpdate[i] = mViews.keyAt(i);
            }
        }
        List<PendingHostUpdate> updates;
        try {
            updates = sService.startListening(
                    mCallbacks, mContextOpPackageName, mHostId, idsToUpdate).getList();
        }
        catch (RemoteException e) {
            throw new RuntimeException("system server dead?", e);
        }

        int N = updates.size();
        for (int i = 0; i < N; i++) {
            PendingHostUpdate update = updates.get(i);
            switch (update.type) {
                case PendingHostUpdate.TYPE_VIEWS_UPDATE:
                    updateAppWidgetView(update.appWidgetId, update.views);
                    break;
                case PendingHostUpdate.TYPE_PROVIDER_CHANGED:
                    onProviderChanged(update.appWidgetId, update.widgetInfo);
                    break;
                case PendingHostUpdate.TYPE_VIEW_DATA_CHANGED:
                    viewDataChanged(update.appWidgetId, update.viewId);
            }
        }
    }
  1. 当添加AppWidget时,首页返回到Launcher中的onActivityResult中,在handleActivityResult中创建添加小部件意图,之后返回到onActivityForResult,调用completeAddAppWidget,通过IAppWidgetService.getAppWidgetInfo,获取AppWidgetProviderInfo,保存到本地数据库中addItemToDatabase(),并创建AppWidgetHostView 对象,mAppWidgetHost.createView,返回RemoteView对象,IAppWidgetService。getAppWidgetViews(),调用AppWidgetHostView.updateAppWidget(views);更新View到launcher界面上mWorkspace.addInScreen(hostView, launcherInfo);
  @Thunk void completeAddAppWidget(int appWidgetId, ItemInfo itemInfo,
            AppWidgetHostView hostView, LauncherAppWidgetProviderInfo appWidgetInfo) {

        if (appWidgetInfo == null) {
            appWidgetInfo = mAppWidgetManager.getLauncherAppWidgetInfo(appWidgetId);
        }

        LauncherAppWidgetInfo launcherInfo;
        launcherInfo = new LauncherAppWidgetInfo(appWidgetId, appWidgetInfo.provider);
        launcherInfo.spanX = itemInfo.spanX;
        launcherInfo.spanY = itemInfo.spanY;
        launcherInfo.minSpanX = itemInfo.minSpanX;
        launcherInfo.minSpanY = itemInfo.minSpanY;
        launcherInfo.user = appWidgetInfo.getProfile();

        getModelWriter().addItemToDatabase(launcherInfo,
                itemInfo.container, itemInfo.screenId, itemInfo.cellX, itemInfo.cellY);

        if (hostView == null) {
            // Perform actual inflation because we're live
            hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo);
        }
        hostView.setVisibility(View.VISIBLE);
        prepareAppWidget(hostView, launcherInfo);
        mWorkspace.addInScreen(hostView, launcherInfo);
    }
  1. 当AppWidgetProvider获得更新的广播,并执行onUpdate(),onUpdate()中创建了RemoteViews并通过AppWidgetManager.updateAppWidget()更新到AppWidgetService之后,AppWidgetService会通过注册的IAppWidgetHost的回调,执行AppWidgetHost的更新。

2. Lancher3 预置 AppWidget

  • 添加权限
    <uses-permission android:name="android.permission.BIND_APPWIDGET"" />
  • 在res/xml/default_workspace_4x4.xml、default_workspace_5x5.xml等中添加
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2009 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

          http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->

<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3">

    <!-- Hotseat -->
    <include launcher:workspace="@xml/dw_phone_hotseat" />

    <!-- Bottom row -->
    <resolve
        launcher:screen="0"
        launcher:x="0"
        launcher:y="-1" >
        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_EMAIL;end" />
        <favorite launcher:uri="mailto:" />

    </resolve>

    <resolve
        launcher:screen="0"
        launcher:x="1"
        launcher:y="-1" >
        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_GALLERY;end" />
        <favorite launcher:uri="#Intent;type=images/*;end" />

    </resolve>

    <resolve
        launcher:screen="0"
        launcher:x="4"
        launcher:y="-1" >
        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MARKET;end" />
        <favorite launcher:uri="market://details?id=com.android.launcher" />
    </resolve>

     <!--  预置 小组件 -->
    <appwidget
        launcher:packageName="com.example.democollect"
        launcher:className="com.example.democollect.appwidget.MyAppWidgetProvider"
        launcher:screen="0"
        launcher:container="-100"
        launcher:spanX="3"
        launcher:spanY="1"
        launcher:x="2"
        launcher:y="2"/>

</favorites>

其中

  • launcher:container="-100",表示添加在 desktop 中,如果是-101那就是在 HotSeat 中,但是这里我们的 widget 是要添加在 desktop,所以是-100;
  • launcher:packageName=“com.android.deskclock”,这个没啥说的,就是widget的包名,我这里添加的是数字时钟,所以这里填写的是 时钟模块 的包名;
  • launcher:className=“com.android.alarmclock.DigitalAppWidgetProvider”,这个是 widget 所在的类,这是是数字时钟,如果要添加 表盘时钟(指针时钟),就填写com.android.alarmclock.AnalogAppWidgetProvider;
  • launcher:screen=“0”,这个是添加在哪一屏;
  • launcher:spanX=“5”,这个表示 widget 在 x 方向上占位多少,我的launcher是 x 方向可以放5个APP图标,所以这里widget是占满整个 x 方向;
  • launcher:spanY=“2”,这个表示 widget 在 y 方向上站位多少,2表示占用相当于两个APP图标的高度;
  • launcher:x=“0”,这个表示 widget 的 x 方向上的位置,这里0表示从屏幕最左侧开始显示;
  • launcher:y=“2”,这个表示 widget 的 y 方向上的位置,这里3表示从上往下第3个位置开始显示(从0开始,所以2就是第3个)。

四、AppWidget的使用

1. 大致思路:

  1. 在AndroidManifest中声明AppWidget。
  2. 在xml目录中定义AppWidget的配置文件。
  3. 在layout目录中定义Widget的布局文件。
  4. 新建一个类,继承AppWidgetProvider类,实现具体的widget业务逻辑

2. 具体使用步骤:

1. 在 AndroidManifest 中声明 App Widget
  <receiver
            android:name=".appwidget.MyAppWidgetProvider"
            android:label="测试小组件">
            <intent-filter>
               <!--所有的窗口小部件都接收android.appwidget.action.APPWIDGET_UPDATE 动作的广播,
                该广播根据android:updatePeriodMillis设定的间隔时间发出广播,用于定时更新桌面上的所有窗口小部件。-->
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
                <!--定义一个自定义的动作广播,可以通过在该广播接收器中注册自定义的动作以使窗口小部件接收自定义的广播。-->
                <action android:name="com.oitsme.REFRESH_WIDGET" />
                <action android:name="com.oitsme.LOCK_ACTION" />
                <action android:name="com.oitsme.UNLOCK_ACTION" />
            </intent-filter>
             <!--声明了 Widget 的 AppWidgetProviderInfo 对应的资源 xml 的位置,用的是 xml 目录下的 example_appwidget_info.xml。-->
            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/appwidget" />
        </receiver>
2. 在 xml 目录定义 App Widget 的初始化 xml 文件
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/appwidget_layout"
    android:minWidth="200dp"
    android:minHeight="100dp"
    android:previewImage="@mipmap/ic_launcher"
    android:resizeMode="vertical|horizontal"
    android:updatePeriodMillis="0"
    android:widgetCategory="home_screen|keyguard" />
  • minWidth & minHeight:定义了 Widget 的最小宽高,当 minWidth 和 minHeight 不是桌面 cell 的整数倍时,Widget 的宽高会被阔至与其最接近的 cells 大小。Google 官方给出了一个大致估算 minWidth & minHeight 的公式,根据 Widget 所占的 cell 数量来计算宽高:70 × n − 30,n 是所占的 cell 数量。
  • updatePeriodMillis:定义了 Widget 的刷新频率,也就是 App Widget Framework 多久请求一次 AppWidgetProvider 的 onUpdate() 回调函数。该时间间隔并不保证精确,出于节约用户电量的考虑,Android 系统默认最小更新周期是 30 分钟,也就是说:如果您的程序需要实时更新数据,设置这个更新周期是 2 秒,那么您的程序是不会每隔 2 秒就收到更新通知的,而是要等到 30 分钟以上才可以,要想实时的更新 Widget,一般可以采用 Service 和 AlarmManager 对 Widget 进行更新。
  • previewImage:当用户选择添加 Widget 时的预览图片。如果该属性没有定义,则展示 application 的 launcher icon。该属性是在 3.0 以后引入的。
  • initialLayout:Widget 的布局 Layout 文件。
  • configure:定义了用户在添加 Widget 时弹出的配置页面的 Activity,用户可以在此进行 Widget 的一些配置,该 Activity 是可选的,如果不需要可以不进行声明。
  • resizeMode:Widget 在水平和垂直方向是否可以调整大小,值可以为:horizontal(水平方向可以调整大小),vertical(垂直方向可以调整大小),none(不可以调整大小),也可以 horizontal|vertical 组合表示水平和垂直方向均可以调整大小。
  • widgetCategory:表示 Widget 可以显示的位置,包括 home_screen(桌面),keyguard(锁屏),keyguard 属性需要 5.0 或以上 Android 版本才可以。
3. layout文件布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_right"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:background="#ccc">

        <ImageView
            android:id="@+id/iv_icon"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_centerVertical="true"
            android:layout_marginEnd="5dp"
            android:layout_marginStart="5dp"
            android:background="@mipmap/ic_launcher_round" />

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_toEndOf="@id/iv_icon"
            android:text="Widget" />

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_alignParentEnd="true"
            android:gravity="center_vertical"
            android:orientation="horizontal">

            <ProgressBar
                android:id="@+id/progress_bar"
                android:layout_width="20dp"
                android:layout_height="20dp"
                android:indeterminateTint="@color/teal_200"
                android:indeterminateTintMode="src_atop"
                android:visibility="gone" />

            <Button
                android:id="@+id/tv_refresh"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="15dp"
                android:text="刷新"
                android:padding="5dp"
                android:textSize="12sp" />
        </LinearLayout>

    </RelativeLayout>
</LinearLayout>

仅支持以下布局类:
FrameLayout、LinearLayout 、RelativeLayout 、GridLayout 、AnalogClock 、Button 、Chronometer 、ImageButton 、ImageView 、ProgressBar 、TextView 、ViewFlipper 、 ListView 、 GridView 、StackView 、AdapterViewFlipper 、 ViewStub 不支持这些类的后代。

4. 自定义一个类 继承 AppWidgetProvider 类

AppWidgetProvider 继承自 BroadcastReceiver,内部逻辑非常简单,就是在 onReceive() 中处理 Widget 相关的广播事件,分发到各个回调函数中(onUpdate(), onDeleted(), onEnabled(), onDisabled, onAppWidgetOptionsChanged())。

  • onUpdate():是最重要的回调函数,根据 updatePeriodMillis 定义的定期刷新操作会调用该函数,此外当用户添加 Widget 时 也会调用该函数,可以在这里进行必要的初始化操作。但如果在<appwidget-provider>中声明了 android:configure 的 Activity,在用户添加 Widget 时,不会调用 onUpdate(),需要由 configure Activity 去负责去调用 AppWidgetManager.updateAppWidget() 完成 Widget 更新,后续的定时更新还是会继续调用 onUpdate() 的。
  • onDeleted():当 Widget 被删除时调用该方法。
  • onEnabled():当 Widget 第一次被添加时调用,例如用户添加了两个你的 Widget,那么只有在添加第一个 Widget 时该方法会被调用。所以该方法比较适合执行你所有 Widgets 只需进行一次的操作。
  • onDisabled():与 onEnabled 恰好相反,当你的最后一个 Widget 被删除时调用该方法,所以这里用来清理之前在 onEnabled() 中进行的操作。
  • onAppWidgetOptionsChanged():当 Widget 第一次被添加或者大小发生变化时调用该方法,可以在此控制 Widget 元素的显示和隐藏。
public class MyAppWidgetProvider extends AppWidgetProvider {

    private static final String TAG = MyAppWidgetProvider.class.getSimpleName();
    public static final String REFRESH_WIDGET = "com.oitsme.REFRESH_WIDGET";
    private Context mContext;

    private static final Handler mHandler = new Handler();
    private final Runnable runnable = new Runnable() {
        @Override
        public void run() {
            hideLoading(mContext);
        }
    };
    @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);
        String action = intent.getAction();
        Log.i(TAG, "onReceive");
        if (action.equals(REFRESH_WIDGET)) {
            // 接受“bt_refresh”的点击事件的广播
            showLoading(context);
            mHandler.postDelayed(runnable, 2000);
        }
    }

    /**
     * 到达指定的更新时间或者当用户向桌面添加AppWidget时被调用
     * appWidgetIds:桌面上所有的widget都会被分配一个唯一的ID标识,这个数组就是他们的列表
     *
     * @param context
     * @param appWidgetManager
     * @param appWidgetIds
     */
    @Override
  @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        this.mContext = context;
        Log.i(TAG, "onUpdate");
        // 获取AppWidget对应的视图
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.appwidget_layout);
        // 设置响应 “按钮(bt_refresh)” 的intent
        Intent btIntent = new Intent(context, MyAppWidgetProvider.class);
        btIntent.setAction(REFRESH_WIDGET);
//            btIntent.putExtra(REFRESH_WIDGET,"REFRESH_WIDGET");
        PendingIntent btPendingIntent = PendingIntent.getBroadcast(context, 0, btIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        remoteViews.setOnClickPendingIntent(R.id.tv_refresh, btPendingIntent);
        // 调用集合管理器对集合进行更新
        appWidgetManager.updateAppWidget(appWidgetIds, remoteViews);
    }

    /**
     * 显示加载loading
     */
    private void showLoading(Context context) {
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.appwidget_layout);
        remoteViews.setViewVisibility(R.id.tv_refresh, View.VISIBLE);
        remoteViews.setViewVisibility(R.id.progress_bar, View.VISIBLE);
        remoteViews.setTextViewText(R.id.tv_refresh, "正在刷新...");
        refreshWidget(context, remoteViews, false);
    }
    /**
     * 隐藏加载loading
     */
    private void hideLoading(Context context) {
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.appwidget_layout);
        remoteViews.setViewVisibility(R.id.progress_bar, View.GONE);
        remoteViews.setTextViewText(R.id.tv_refresh, "刷新");
        refreshWidget(context, remoteViews, false);
    }
    /**
     * 刷新Widget
     */
    private void refreshWidget(Context context, RemoteViews remoteViews, boolean refreshList) {
        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
        ComponentName componentName = new ComponentName(context, MyAppWidgetProvider.class);
        appWidgetManager.updateAppWidget(componentName, remoteViews);
    }
}

  1. onUpdate()方法中首先需要new一个RemoteViews,构造方法里需要传递两个参数,一个是包名(context.getPacakgeName),一个是布局文件(layout_widget)。
    然后通过remoteViews.setOnClickPendingIntent()设置按钮的点击事件。setOnClickPendingIntent()中需要传递两个参数:一个是id(比如需要被点击的button),一个是PendingIntent。PendingIntent是未来的意图。
    于是我们需要事先构造一个PendingIntent,这个需要通过 PendingIntent.getBroadcast()来构造。getBroadcast()方法中需要传递四个参数,其中有一个是Intent。于是我们需要构造一个Intent。在intent里发送广播,并设置Action。按钮点击完了之后,记得调用appWidgetManager.updateAppWidget(int[] appWidgetIds, RemoteViews views)方法更新一下,第一个参数就是onUpdate方法中的参数,代表的是所有的控件。在onUpdate()方法中通过intent发送按钮点击时间的广播之后,我们需要在onReceive()方法中进行广播的接收。
  2. onReceive()方法中当intent的action匹配成功时,开始执行做点击时间之后的setText,不过这里需要重新new 一个 RemoteViews,而不能共用onUpdate()方法中的RemoteViews(这是一个很大的坑)。执行完点击事件之后的setText之后,记得调用appWidgetManager.updateAppWidget(ComponentName, RemoteViews)方法,第一个参数为组件名,需要我们自己new一下,第二个参数很好解释。
5. 如何显示在桌面
  1. 桌面长按桌面空白部分弹框选择 Widgets


    image.png
  2. 选择自己的小组件长按拖拽到桌面


    image.png

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

推荐阅读更多精彩内容