Android車載Launcher開(kāi)發(fā)(1) - 顯示W(wǎng)idget

1.Launcher簡(jiǎn)介

Launcher是安卓系統(tǒng)中的桌面啟動(dòng)器牙丽,安卓系統(tǒng)的桌面UI統(tǒng)稱為L(zhǎng)auncher。Launcher是安卓系統(tǒng)中的主要程序組件之一,安卓系統(tǒng)中如果沒(méi)有Launcher就無(wú)法啟動(dòng)安卓桌面抒寂。作為車機(jī)開(kāi)機(jī)后用戶接觸到的第一個(gè)帶有界面的系統(tǒng)級(jí)APP,和普通APP一樣掠剑,它的界面也是在Activity上繪制出來(lái)的屈芜。

車機(jī)上Launcher一般分為兩個(gè)界面,首頁(yè)和應(yīng)用列表界面朴译。

首頁(yè)一般包括用戶信息井佑、常用應(yīng)用快捷方式、3D車模和widget卡片眠寿,widget卡片有:地圖躬翁、天氣、音樂(lè)播放器盯拱、時(shí)鐘等盒发;

圖1-比亞迪漢車機(jī)上的嘟嘟桌面

應(yīng)用列表界面就是啟動(dòng)APP的列表界面,單擊APP的Icon可進(jìn)入App狡逢,長(zhǎng)按APP的Icon可以進(jìn)入編輯模式宁舰,編輯模式下APP可以進(jìn)行拖拽、合并文件夾奢浑、刪除等功能蛮艰。

圖2-吉利繽越的應(yīng)用列表界面

(ps:頂部狀態(tài)欄status bar和底部導(dǎo)航欄navigation bar屬于System UI,中間才屬于Launcher部分)

2.Widget概述

參考資料:應(yīng)用微件概覽

Widget雀彼,又稱為微件或者小部件壤蚜。我們可以把它當(dāng)作是一個(gè)微型應(yīng)用程序視圖,用以嵌入到其他應(yīng)用程序中(一般來(lái)說(shuō)就是桌面Launcher)并接收周期性的更新徊哑。這樣用戶就可以方便查看應(yīng)用程序的重點(diǎn)信息或者進(jìn)行應(yīng)用程序的快捷控制袜刷。

圖3-天氣widget

Widget類型官方分為信息微件、集合微件莺丑、控制微件和混合微件著蟹。開(kāi)發(fā)Widget是由各自應(yīng)用程序(如天氣、導(dǎo)航、音樂(lè))開(kāi)發(fā)人員開(kāi)發(fā)草则,不是本篇的重點(diǎn)內(nèi)容钢拧,網(wǎng)上有很多關(guān)于Widget開(kāi)發(fā)的例子。如何使車載Launcher具有擺放Widget的能力炕横,是我們關(guān)注的重點(diǎn)源内!

3.Launcher開(kāi)發(fā)如何顯示W(wǎng)idget

3.1 使Launcher App成為系統(tǒng)級(jí)App

  • Q:為什么在顯示W(wǎng)idget的時(shí)候要把Launcher App聲明為系統(tǒng)級(jí)的App呢?

  • A:開(kāi)發(fā)Launcher App時(shí)肯定會(huì)聲明其為系統(tǒng)級(jí)App份殿。而顯示W(wǎng)idget時(shí)需要App是系統(tǒng)級(jí)的原因是:Widget顯示需要我們獲取到AppWidgetManager對(duì)象并調(diào)用public boolean bindAppWidgetIdIfAllowed(int appWidgetId, ComponentName provider)方法膜钓,而此方法返回值要為true就需要App是系統(tǒng)級(jí)App。

private AppWidgetProviderInfo createAppWidgetInfo(ComponentName component) {
    //分配新的widgetId
    int widgetId = LauncherApplication.getContext().getWidgetHost().allocateAppWidgetId();
    //將widgetId和ComponentName綁定
    boolean isBindAppWidgetIdIfAllowed = LauncherApplication.getContext()
            .getWidgetManager().bindAppWidgetIdIfAllowed(widgetId, component);
    LogUtil.info(TAG, "createAppWidgetInfo bindAppWidgetIdIfAllowed = "
            + isBindAppWidgetIdIfAllowed);
    //獲取AppWidgetProviderInfo
    AppWidgetProviderInfo appWidgetInfo = LauncherApplication.getContext()
            .getWidgetManager().getAppWidgetInfo(widgetId);
    //存儲(chǔ)widgetId卿嘲、包名颂斜、類名到數(shù)據(jù)庫(kù)
    WidgetInfoEntity entity = new WidgetInfoEntity(widgetId, component.getPackageName(),
            component.getClassName(), checkWidgetDisplay(component.getPackageName()));
    saveWidgetInfo(entity);
    return appWidgetInfo;
}

Launcher未聲明為系統(tǒng)級(jí)App時(shí)截取的Log:

圖4-未聲明為系統(tǒng)級(jí)App時(shí)bindAppWidgetIdIfAllowed的返回值

將App聲明為系統(tǒng)級(jí)App的步驟:

  1. 將車機(jī)系統(tǒng)簽名放到項(xiàng)目中,創(chuàng)建一個(gè)keystore目錄放置簽名文件:
圖5-放置系統(tǒng)簽名文件
  1. app目錄下的build.gradle文件配置簽名文件,在android{}內(nèi)加上簽名文件的配置信息拾枣,然后sync一下:
android {
    ...
    signingConfigs {
        config {
            storeFile file('../keystore/platform.jks')
            storePassword 'android'
            keyAlias 'androiddebugkey'
            keyPassword 'android'
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 
'proguard-rules.pro'
            signingConfig signingConfigs.config
        }
        debug {
            signingConfig signingConfigs.config
        }
    }
    ...
}

3.在AndroidManifest.xml文件添加android:sharedUserId=”android.uid.system”沃疮,讓程序運(yùn)行在系統(tǒng)進(jìn)程中。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.yx.yxlauncher"
    android:sharedUserId="android.uid.system">
    //定義查詢權(quán)限梅肤,查詢系統(tǒng)中的所有widget廣播需要
    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
        tools:ignore="QueryAllPackagesPermission" />
    ...
</manifest>

通過(guò)以上步驟司蔬,相當(dāng)于把我們自己的開(kāi)發(fā)的Launcher聲明成系統(tǒng)級(jí)App了。

3.2 定義并初始化AppWidgetHost對(duì)象

定義類繼承Application姨蝴,在Application初始化的時(shí)候定義好AppWidgetHost對(duì)象并且調(diào)用startListening()方法

public class YxApplication extends Application {
    private static final String TAG = "Yx_YxApplication";

    private AppWidgetHost mWidgetHost;
    //自定義一個(gè)APPWIDGET_HOST_ID
    private static final int APPWIDGET_HOST_ID = 0x300;
    private static YxApplication sApplication;

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate: ");
        sApplication = this;
        initWidgetHost();
    }

    private void initWidgetHost() {
        //初始化WidgetHost并且開(kāi)始接收onAppWidgetChanged()的回調(diào)
        mWidgetHost = new AppWidgetHost(YxApplication.getContext(), APPWIDGET_HOST_ID);
        mWidgetHost.startListening();

        //初始化數(shù)據(jù)庫(kù)里存儲(chǔ)的widget信息列表俊啼,后面會(huì)介紹數(shù)據(jù)庫(kù)存儲(chǔ)的內(nèi)容
        WidgetInfoManager.getInstance().initializeWidget();
        //初始化Widget廣播的ResolveInfo列表
        WidgetInfoManager.getInstance().initializeWidgetResolveInfo();
    }

    public static YxApplication getContext() {
        return sApplication;
    }

    public static Context getDirectBootContext() {
        return getContext().getBaseContext().createDeviceProtectedStorageContext();
    }

    public AppWidgetManager getWidgetManager() {
        return AppWidgetManager.getInstance(YxApplication.getContext());
    }

    public AppWidgetHost getWidgetHost() {
        return mWidgetHost;
    }

3.3 數(shù)據(jù)庫(kù)存儲(chǔ)widgetId

有了我們的AppWidgetHost,我們就可以調(diào)用allocateAppWidgetId()方法獲取widgetId左医,并且將其存入數(shù)據(jù)庫(kù)授帕,定義實(shí)體類WidgetInfoEntity,我用的是room數(shù)據(jù)庫(kù)浮梢,存儲(chǔ)了widgetId跛十、包名、類名:

@Entity(tableName = "widget_info")
public class WidgetInfoEntity {
    @PrimaryKey
    @ColumnInfo(name = "widgetId")
    private int widgetId;

    @ColumnInfo(name = "packageName")
    private String packageName;

    @ColumnInfo(name = "className")
    private String className;

    /**
     * Construction method.
     */
    public WidgetInfoEntity(int widgetId, String packageName, String className) {
        this.widgetId = widgetId;
        this.packageName = packageName;
        this.className = className;
    }

    public int getWidgetId() {
        return widgetId;
    }

    public String getPackageName() {
        return packageName;
    }

    public String getClassName() {
        return className;
    }

    @Override
    public String toString() {
        return "WidgetInfoEntity{" +
                "widgetId=" + widgetId +
                ", packageName='" + packageName + '\'' +
                ", className='" + className + '\'' +
                '}';
    }
}

dao層定義黔寇,將訪問(wèn)數(shù)據(jù)庫(kù)里的widget信息的代碼封裝起來(lái):

@Dao
public interface WidgetInfoDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void insertWidgetInfo(WidgetInfoEntity... infoEntity);

    @Query("SELECT * FROM " + "widget_info" + " ORDER BY " + "widgetId" + " ASC")
    List<WidgetInfoEntity> queryAllWidgetInfos();

    @Delete
    void deleteWidgetInfo(WidgetInfoEntity entity);
}

db定義偶器,數(shù)據(jù)庫(kù)工具類,包含創(chuàng)建數(shù)據(jù)庫(kù)缝裤、打開(kāi)數(shù)據(jù)庫(kù)、數(shù)據(jù)庫(kù)操作的對(duì)外方法等:

@Database(entities = {WidgetInfoEntity.class}, version = 1, exportSchema = false)
public abstract class DatabaseUtil extends RoomDatabase {
    private static final String TAG = "Yx_DatabaseUtil";

    private static DatabaseUtil sInstance;

    private final ExecutorService mExecutor;

    private final WidgetInfoDao mWidgetInfoDao;

    public DatabaseUtil() {
        mExecutor = Executors.newSingleThreadExecutor();
        mWidgetInfoDao = widgetInfoDao();
    }

    /**
     * get DatabaseUtil Singleton.
     *
     * @return DatabaseUtil
     */
    public static DatabaseUtil getInstance() {
        if (sInstance == null) {
            synchronized (DatabaseUtil.class) {
                create();
            }
        }
        return sInstance;
    }

    private static void create() {
        Log.i(TAG, "create: ");
        sInstance = Room.databaseBuilder(YxApplication.getDirectBootContext(),
                        DatabaseUtil.class, "yx_launcher_db")
                       .addCallback(new RoomDatabase.Callback() {
                    @Override
                    public void onCreate(@NonNull SupportSQLiteDatabase db) {
                        super.onCreate(db);
                        Log.d(TAG, "onCreate database: " + db.getPath());
                    }

                    @Override
                    public void onOpen(@NonNull SupportSQLiteDatabase db) {
                        super.onOpen(db);
                        Log.d(TAG, "onOpen database: " + db.getPath());
                    }
                }).allowMainThreadQueries()
                .fallbackToDestructiveMigration()
                .build();
    }

    /**
     * Create instance of WidgetInfoDao.
     *
     * @return WidgetInfoDao.
     */
    public abstract WidgetInfoDao widgetInfoDao();
    /**
     * Query all widgetInfo.
     *
     * @return widgetInfos
     */
    public List<WidgetInfoEntity> queryAllWidgetInfos() {
        Log.d(TAG, "queryAllWidgetInfos: ");
        return mWidgetInfoDao.queryAllWidgetInfos();
    }

    /**
     * insert WidgetInfoEntity.
     *
     * @param infoEntity WidgetInfoEntity
     */
    public void insertWidgetInfos(WidgetInfoEntity infoEntity) {
        Log.d(TAG, "insertWidgetInfos: infoEntity = " + infoEntity.toString());
        mExecutor.execute(() -> mWidgetInfoDao.insertWidgetInfo(infoEntity));
    }

    /**
     * Delete WidgetInfo.
     *
     * @param entity WidgetInfoEntity
     */
    public void deleteWidgetInfo(WidgetInfoEntity entity) {
        Log.d(TAG, "deleteWidgetInfo: entity = " + entity);
        mExecutor.execute(() -> mWidgetInfoDao.deleteWidgetInfo(entity));
    }
}

3.4 定義WidgetInfoManager類處理widget

包含的內(nèi)容:

  1. 查詢系統(tǒng)里所有Widget廣播的ResolveInfo列表用于獲取其ComponentName
  2. 根據(jù)存儲(chǔ)的widgetId獲取或者新創(chuàng)建AppWidgetProviderInfo
  3. 保存新創(chuàng)建的widgetId到數(shù)據(jù)庫(kù)颊郎,刪除數(shù)據(jù)庫(kù)里數(shù)據(jù)或者去重

其實(shí)憋飞,這個(gè)類最主要的目的就是拿到AppWidgetProviderInfo對(duì)象,有了這個(gè)對(duì)象才能獲取AppWidgetHostView用于顯示:

public class WidgetInfoManager {

    private static final String TAG = "Yx_WidgetInfoManager";
    private static final long RELOAD_DELAY = 100;

    private final List<WidgetInfoEntity> mWidgetInfoList = new ArrayList<>();
    private final List<ResolveInfo> mAllWidgetResolveInfo = new ArrayList<>();
    private final Handler mHandler = new Handler(YxApplication.getContext().getMainLooper());

    private final Runnable mReloadWidgetResolveInfoRunnable
            = this::initializeWidgetResolveInfo;

    private static class SingletonHolder {
        // Static initializer, thread safety is guaranteed by JVM
        private static WidgetInfoManager instance = new WidgetInfoManager();
    }

    /**
     * Privatization construction method.
     */
    private WidgetInfoManager() {
    }

    /**
     * getInstance.
     *
     * @return WidgetInfoManager
     */
    public static WidgetInfoManager getInstance() {
        return SingletonHolder.instance;
    }

    /**
     * initializeWidgetResolveInfo.
     */
    @SuppressLint("QueryPermissionsNeeded")
    public void initializeWidgetResolveInfo() {
        mAllWidgetResolveInfo.clear();
        mAllWidgetResolveInfo.addAll(YxApplication.getContext().getPackageManager()
                .queryBroadcastReceivers(new Intent(
                      "android.intent.action.WidgetProvider"), 0));
        if (mAllWidgetResolveInfo.size() == 0) {
            mHandler.postDelayed(mReloadWidgetResolveInfoRunnable, RELOAD_DELAY);
            Log.i(TAG, "mAllWidgetResolveInfo is null, reload after 100ms");
        } else {
            mHandler.removeCallbacks(mReloadWidgetResolveInfoRunnable);
            Log.i(TAG, "initializeWidgetResolveInfo: mAllWidgetResolveInfo = "
                    + Arrays.toString(mAllWidgetResolveInfo.toArray()));
        }
    }

    public void initializeWidget() {
        mWidgetInfoList.addAll(DatabaseUtil.getInstance().queryAllWidgetInfos());
        Log.i(TAG, "WidgetInfoManager: size = " + mWidgetInfoList.size());
    }

    /**
     * Get AppWidgetProviderInfo by package name.
     * @param pkg package name
     * @return AppWidgetProviderInfo
     */
    public AppWidgetProviderInfo getAppWidgetProviderInfo(String pkg) {
        Log.i(TAG, "getAppWidgetProviderInfo: pkg = " + pkg);
        int widgetId = -1;
        AppWidgetProviderInfo appWidgetInfo;

        // 1. 根據(jù)包名獲取 ComponentName
        ComponentName component = getComponent(pkg);
        if (component == null) {
            Log.w(TAG, "getAppWidgetProviderInfo: component is null !!!");
            return null;
        }

        // 2. 根據(jù) ComponentName 獲取已保存的 WidgetId
        for (WidgetInfoEntity entity : mWidgetInfoList) {
            if (component.getPackageName().equals(entity.getPackageName())
                    && component.getClassName().equals(entity.getClassName())) {
                widgetId = entity.getWidgetId();
                break;
            }
        }

        // 3. 判斷獲取的widgetId是否有效姆吭,如果有效就使用widgetId去拿AppWidgetProviderInfo; 
        //如果無(wú)效就執(zhí)行4
        if (widgetId != -1) {
            appWidgetInfo = YxApplication.getContext()
                    .getWidgetManager().getAppWidgetInfo(widgetId);
            // 3.1 如果獲取的AppWidgetProviderInfo為null榛做,則執(zhí)行4
            if (appWidgetInfo == null) {
                Log.w(TAG, "getAppWidgetProviderInfo: appWidgetInfo is null !!! widgetId = "
                        + widgetId);
                // 移除無(wú)效值
                removeWidgetByPkg(component.getPackageName());
                // 創(chuàng)建新的AppWidgetProviderInfo
                appWidgetInfo = createAppWidgetInfo(component);
            }
        } else {
            Log.w(TAG, "getAppWidgetProviderInfo: widgetId is -1 !!!");
            // 4. 重新創(chuàng)建widgetId -> 綁定widget -> 生成新的 AppWidgetProviderInfo
            // 移除無(wú)效值
            removeWidgetByPkg(component.getPackageName());
            // 創(chuàng)建新的 AppWidgetProviderInfo
            appWidgetInfo = createAppWidgetInfo(component);
        }
        Log.i(TAG, "getAppWidgetProviderInfo: appWidgetInfo = " + appWidgetInfo);
        return appWidgetInfo;
    }

    private AppWidgetProviderInfo createAppWidgetInfo(ComponentName component) {
        Log.i(TAG, "createAppWidgetInfo: component = " + component.toString());
        int widgetId = YxApplication.getContext().getWidgetHost().allocateAppWidgetId();
        boolean isBindAppWidgetIdIfAllowed = YxApplication.getContext()
                .getWidgetManager().bindAppWidgetIdIfAllowed(widgetId, component);
        Log.i(TAG, "createAppWidgetInfo bindAppWidgetIdIfAllowed = "
                + isBindAppWidgetIdIfAllowed);
        AppWidgetProviderInfo appWidgetInfo = YxApplication.getContext()
                .getWidgetManager().getAppWidgetInfo(widgetId);
        WidgetInfoEntity entity = new WidgetInfoEntity(widgetId, component.getPackageName(),
                component.getClassName());
        saveWidgetInfo(entity);
        return appWidgetInfo;
    }

    private ComponentName getComponent(String pkg) {
        for (ResolveInfo info : mAllWidgetResolveInfo) {
            if (info.activityInfo.packageName.equals(pkg)) {
                return new ComponentName(
                               info.activityInfo.packageName, info.activityInfo.name);
            }
        }
        Log.w(TAG, pkg + " ComponentName is null ! "
                + " mAllWidgetResolveInfo.size = " + mAllWidgetResolveInfo.size());
        return null;
    }

    /**
     * Get widget id by pkg name.
     * @param pkg package name
     * @return widgetId
     */
    public int getWidgetId(String pkg) {
        for (WidgetInfoEntity entity : mWidgetInfoList) {
            if (entity.getPackageName().equals(pkg)) {
                return entity.getWidgetId();
            }
        }
        return -1;
    }

    /**
     * saveWidgetInfo.
     *
     * @param entity WidgetInfoEntity
     */
    private void saveWidgetInfo(WidgetInfoEntity entity) {
        Log.d(TAG, "saveWidgetInfo: entity = " + entity.toString());
        // 去重,移除臟數(shù)據(jù)(入?yún)⒌膚idgetId是新生成的,可信的),保證 widgetId 的唯一性
        removeDuplicateWidget(entity.getWidgetId());

        mWidgetInfoList.add(entity);
        DatabaseUtil.getInstance().insertWidgetInfos(entity);
    }

    private void removeDuplicateWidget(int widgetId) {
        Iterator<WidgetInfoEntity> iterator = mWidgetInfoList.iterator();
        while (iterator.hasNext()) {
            WidgetInfoEntity entity = iterator.next();
            if (widgetId == entity.getWidgetId()) {
                iterator.remove();
                DatabaseUtil.getInstance().deleteWidgetInfo(entity);
            }
        }
    }

    /**
     * Remove widget by package name.
     *
     * @param pkg package name
     */
    public void removeWidgetByPkg(String pkg) {
        Iterator<WidgetInfoEntity> iterator = mWidgetInfoList.iterator();
        while (iterator.hasNext()) {
            WidgetInfoEntity entity = iterator.next();
            if (entity.getPackageName().equals(pkg)) {
                iterator.remove();
                DatabaseUtil.getInstance().deleteWidgetInfo(entity);
                YxApplication.getContext().getWidgetHost()
                        .deleteAppWidgetId(entity.getWidgetId());
                break;
            }
        }
    }
}

3.5 獲取AppWidgetHostView并顯示

我車機(jī)里有另外一個(gè)app提供了widget-provider检眯,包名為"com.yx.mywidget"厘擂,最終可以看到widget顯示在Launcher App中:

...
private void initView() {
        mWidgetFrameLayout = findViewById(R.id.widget_test_fl);
        mWidgetFrameLayout.addView(getWidgetView("com.yx.mywidget"));
    }

    /**
     * Get widget view.
     * @param pkg package name
     * @return widget view
     */
    private View getWidgetView(String pkg) {
        Log.d(TAG, "getWidgetView: pkg: " + pkg);
        AppWidgetProviderInfo appWidgetInfo = WidgetInfoManager.getInstance()
                .getAppWidgetProviderInfo(pkg);
        int widgetId = WidgetInfoManager.getInstance().getWidgetId(pkg);
        Log.i(TAG, "getWidgetView: appWidgetInfo = " + appWidgetInfo
                + " widgetId = " + widgetId);
        if (appWidgetInfo != null && widgetId != -1) {
            AppWidgetHostView hostView = YxApplication.getContext().getWidgetHost()
                    .createView(YxApplication.getContext(), widgetId, appWidgetInfo);
            // Remove HostView's default padding value
            Log.i(TAG, "getWidgetView: pkg = " + pkg + " hostView = " + hostView);
            return hostView;
        }
        return null;
    }
...
圖6-另外一個(gè)app的widget顯示到了Launcher上

4.總結(jié)

可以看到,想要widget顯示到Launcher上其實(shí)并不復(fù)雜锰瘸,主要流程就是:

  1. 定義widgetHost并startListening
  2. 獲取系統(tǒng)里所有widget-provider廣播刽严,拿到其ComponentName
  3. 獲取AppWidgetProviderInfo,如果首次沒(méi)有widgetId就創(chuàng)建并存儲(chǔ)
  4. 通過(guò)widgetId和AppWidgetProviderInfo獲取AppWidgetHostView并顯示

本文是我首次進(jìn)行技術(shù)性文檔的總結(jié)并發(fā)布到網(wǎng)上避凝,感謝你的閱讀舞萄。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市管削,隨后出現(xiàn)的幾起案子倒脓,更是在濱河造成了極大的恐慌,老刑警劉巖含思,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件崎弃,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡含潘,警方通過(guò)查閱死者的電腦和手機(jī)吊履,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)调鬓,“玉大人艇炎,你說(shuō)我怎么就攤上這事√谖眩” “怎么了缀踪?”我有些...
    開(kāi)封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)虹脯。 經(jīng)常有香客問(wèn)我驴娃,道長(zhǎng),這世上最難降的妖魔是什么循集? 我笑而不...
    開(kāi)封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任唇敞,我火速辦了婚禮,結(jié)果婚禮上咒彤,老公的妹妹穿的比我還像新娘疆柔。我一直安慰自己,他們只是感情好镶柱,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布旷档。 她就那樣靜靜地躺著,像睡著了一般歇拆。 火紅的嫁衣襯著肌膚如雪鞋屈。 梳的紋絲不亂的頭發(fā)上范咨,一...
    開(kāi)封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音厂庇,去河邊找鬼渠啊。 笑死,一個(gè)胖子當(dāng)著我的面吹牛权旷,可吹牛的內(nèi)容都是我干的替蛉。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼炼杖,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼灭返!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起坤邪,我...
    開(kāi)封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤熙含,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后艇纺,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體怎静,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年黔衡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蚓聘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡盟劫,死狀恐怖夜牡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情侣签,我是刑警寧澤塘装,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站影所,受9級(jí)特大地震影響蹦肴,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜猴娩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一阴幌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧卷中,春花似錦矛双、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至无埃,卻和暖如春徙瓶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背嫉称。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工侦镇, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人织阅。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓壳繁,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親荔棉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子闹炉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

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