ThemeSkinning——Android换肤库源码学习

详细的使用方式请参考 ThemeSkinning,本文的核心是学习换肤实现原理,不会详细介绍这个库如何使用,该库中关于夜间模式的内容和改变字体相关内容也暂时不去关心。

应用的assets目录下的字体文件和皮肤包如下图所示。

asset.png

开始分析

首先定义用于观察者接口,需要换肤的观察者可以实现这个接口

public interface ISkinUpdate {
    void onThemeUpdate();
}

默认情况下支持的换肤属性在AttrFactory中定义了。

public class AttrFactory {

    private static HashMap<String, SkinAttr> sSupportAttr = new HashMap<>();

    static {
        sSupportAttr.put("background", new BackgroundAttr());
        sSupportAttr.put("textColor", new TextColorAttr());
        sSupportAttr.put("src", new ImageViewSrcAttr());
    }
//...

}


首先应用的Application要继承SkinBaseApplication,在启动的时候加载皮肤文件。

public class SkinBaseApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        SkinManager.getInstance().init(this);
    }
}

SkinManager的init方法

 public void init(Context ctx) {
        context = ctx.getApplicationContext();
        //设置字体
        TypefaceUtils.CURRENT_TYPEFACE = TypefaceUtils.getTypeface(context);
        //注释1处
        setUpSkinFile(context);
        //...
        String skin = SkinConfig.getCustomSkinPath(context);
        if (SkinConfig.isDefaultSkin(context)) {//使用默认皮肤,则不需要加载皮肤文件
            return;
        }
        //注释2处,加载皮肤文件
        loadSkin(skin, null);
    }
}

在注释1处,调用setUpSkinFile方法,将assets/skin目录下的皮肤复制到指定目录

private void setUpSkinFile(Context context) {
    try {
        String[] skinFiles = context.getAssets().list(SkinConfig.SKIN_DIR_NAME);
        for (String fileName : skinFiles) {
            File file = new File(SkinFileUtils.getSkinDir(context), fileName);
            if (!file.exists()) {//如果文件不存在,则拷贝
                SkinFileUtils.copySkinAssetsToDir(context, fileName, 
                    SkinFileUtils.getSkinDir(context));
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

注释2处,加载皮肤文件,使用AsyncTask来实现。loadSkin方法部分代码

@Override
protected Resources doInBackground(String... params) {//后台加载
    String skinPkgPath = SkinFileUtils.getSkinDir(context) + File.separator + params[0];
    // skinPackagePath:/storage/emulated/0/Android/data/solid.ren.themeskinning/cache/skin/theme-20180417.skin
    SkinL.i(TAG, "skinPackagePath:" + skinPkgPath);
    File file = new File(skinPkgPath);
    if (!file.exists()) {
        return null;
    }
    /**
     * 把路径在
     * /storage/emulated/0/Android/data/solid.ren.themeskinning/cache/skin/xxx.skin
     * 的文件作为asset的一部分
     */
    PackageManager mPm = context.getPackageManager();
    PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
    //保存皮肤包的包名
    skinPackageName = mInfo.packageName;
    AssetManager assetManager = AssetManager.class.newInstance();
    Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
    addAssetPath.invoke(assetManager, skinPkgPath);

    /**
     * 根据asset来获取Resources
     */
    Resources superRes = context.getResources();
    Resources skinResource = ResourcesCompat.getResources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
    //保存当前的皮肤路径
    SkinConfig.saveSkinPath(context, params[0]);
    isDefaultSkin = false;
    //返回新的Resources
    return skinResource;
}

@Override
protected void onPostExecute(Resources result) {
    mResources = result;
    if (mResources != null) {
        //...
        //注释1处,通知变更皮肤
        notifySkinUpdate();
    } 
}

上面代码主题流程就是把应用cache/skin/目录下的皮肤包作为asset的一部分,然后
从asset中加载皮肤包中的Resources,这个Resources包含了皮肤包中的所有资源信息。然后通知变更皮肤。

我们看一下注释1处,调用了notifySkinUpdate方法。

@Override
public void notifySkinUpdate() {
    if (mSkinObservers != null) {//如果存在需要换肤的观察者,就通知观察者
        for (ISkinUpdate observer : mSkinObservers) {
            observer.onThemeUpdate();
        }
    }
}

这个方法是在ISkinLoader接口中定义的。

//用来添加、删除、通知需要换肤的观察者
public interface ISkinLoader {
    //观察者的类型是ISkinUpdate
    void attach(ISkinUpdate observer);

    void detach(ISkinUpdate observer);

    void notifySkinUpdate();
}

SkinBaseActivity实现了ISkinUpdate接口,需要实现换肤的功能可以直接继承SkinBaseActivity。

SkinBaseActivity部分代码

    //SkinInflaterFactory
    private SkinInflaterFactory mSkinInflaterFactory;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        mSkinInflaterFactory = new SkinInflaterFactory(this);
        //设置setFactory2为mSkinInflaterFactory,使用mSkinInflaterFactory来创建view
        LayoutInflaterCompat.setFactory2(getLayoutInflater(), mSkinInflaterFactory);
        super.onCreate(savedInstanceState);
        changeStatusColor();
    }

    @Override
    public void onThemeUpdate() {
        mSkinInflaterFactory.applySkin();
    }

接下来看SkinInflaterFactory这个类是实现换肤的关键。我们创建View靠的就是这个类。

SkinInflaterFactory的四个参数的onCreateView方法

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        //注释1处,
        boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
        AppCompatDelegate delegate = mAppCompatActivity.getDelegate();
        //注释2处
        View view = delegate.createView(parent, name, context, attrs);
        //...
        //支持换肤
        if (isSkinEnable || SkinConfig.isGlobalSkinApply()) {
            if (view == null) {//注释3处
                view = ViewProducer.createViewFromTag(context, name, attrs);
            }
            if (view == null) {
                return null;
            }
            //注释4处
            parseSkinAttr(context, attrs, view);
        }
        return view;
    }

在注释1处,如果我们需要为布局中的某些view提供换肤功能的时候,可以这样


single_change_skin.png

这样,只有tv_text_color会实现换肤操作。

注释2处和注释3处先来创建view。我们不去研究其中的细节。
注释4处应用皮肤属性。

    private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
        //保存所有的换肤属性
        List<SkinAttr> viewAttrs = new ArrayList<>();
        for (int i = 0; i < attrs.getAttributeCount(); i++) {
            //注释1处如果给view设置了style
            if ("style".equals(attrName)) {
                //获取 textColor ,background 和 src属性值
                int[] skinAttrs = new int[]{android.R.attr.textColor, android.R.attr.background,android.R.attr.src};
                TypedArray a = context.getTheme().obtainStyledAttributes(attrs, skinAttrs, 0, 0);
                int textColorId = a.getResourceId(0, -1);
                int backgroundId = a.getResourceId(1, -1);
                int srcId = a.getResourceId(2, -1);

                if (textColorId != -1) {
                    String entryName = context.getResources().getResourceEntryName(textColorId);
                    String typeName = context.getResources().getResourceTypeName(textColorId);
                    SkinAttr skinAttr = AttrFactory.get("textColor", textColorId, entryName, typeName);
                    //添加color属性
                    if (skinAttr != null) {
                        viewAttrs.add(skinAttr);
                    }
                }
                if (backgroundId != -1) {
                    String entryName = context.getResources().getResourceEntryName(backgroundId);
                    String typeName = context.getResources().getResourceTypeName(backgroundId);
                    SkinAttr skinAttr = AttrFactory.get("background", backgroundId, entryName, typeName);
                   //保存background属性
                    if (skinAttr != null) {
                        viewAttrs.add(skinAttr);
                    }
                }
                
                if (srcId != -1) {
                    String entryName = context.getResources().getResourceEntryName(srcId);
                    String typeName = context.getResources().getResourceTypeName(srcId);
                    SkinAttr skinAttr = AttrFactory.get("src", srcId, entryName, typeName);
                    SkinL.w(TAG, "    srcId in style is supported:" + "\n" +
                            "    resource id:" + backgroundId + "\n" +
                            "    attrName:" + attrName + "\n" +
                            "    attrValue:" + attrValue + "\n" +
                            "    entryName:" + entryName + "\n" +
                            "    typeName:" + typeName);
                    if (skinAttr != null) {
                        viewAttrs.add(skinAttr);
                    }

                }
                a.recycle();
                continue;
            }
            //注释2处,换肤支持并以“@”开头的属性值,例如@color/red
            if (AttrFactory.isSupportedAttr(attrName) && attrValue.startsWith("@")) {
                try {
                    // //获取resource id,去掉开头的"@"
                    int id = Integer.parseInt(attrValue.substring(1));
                    if (id == 0) {
                        continue;
                    }
                    //entryName,eg:text_color_selector
                    String entryName = context.getResources().getResourceEntryName(id);
                    //typeName,eg:color、drawable
                    String typeName = context.getResources().getResourceTypeName(id);
                    SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
                    //添加换肤属性
                    if (mSkinAttr != null) {
                        viewAttrs.add(mSkinAttr);
                    }
                } catch (NumberFormatException e) {
                    SkinL.e(TAG, e.toString());
                }
            }
        }
        //注释3处,如果换肤属性不为null,构建SkinItem
        if (!SkinListUtils.isEmpty(viewAttrs)) {
            SkinItem skinItem = new SkinItem();
            //注释4处,注意,我们这里把view保存了起来,
            skinItem.view = view;
            skinItem.attrs = viewAttrs;
            mSkinItemMap.put(skinItem.view, skinItem);
            if (SkinManager.getInstance().isExternalSkin() ||
                    SkinManager.getInstance().isNightMode()) {
                //注释5处,换肤
                skinItem.apply();
            }
        }
    }

注释1处,获取style中的 textColor , background ,src属性值

      <item name="android:textColor">@color/item_tv_title_color</item>
       <item name="android:background">@drawable/ic_homepage_car</item>
       <item name="android:src">@drawable/ic_homepage_car</item>

//注释2处,获取换肤支持并以“@”开头的属性值

android:textColor="@color/text_color_black"
android:background="@color/colorPrimary"
android:src="@drawable/ic_homepage_car"

注释3处,如果换肤属性不为null,构建SkinItem。
注释4处,注意注意,我们这里把view保存到了mSkinItemMap中,用于后来的动态换肤。并且我们要注意在合适的时机清除这些view避免内存泄漏。

注释5处,换肤。

public class SkinItem {

    public View view;

    public List<SkinAttr> attrs;

    public SkinItem() {
        attrs = new ArrayList<>();
    }

    public void apply() {
        if (SkinListUtils.isEmpty(attrs)) {
            return;
        }
        for (SkinAttr at : attrs) {
            at.apply(view);
        }
    }
//...
}

SkinAttr是一个抽象类,默认的实现类如下

BackgroundAttr

public class BackgroundAttr extends SkinAttr {

    @Override
    protected void applySkin(View view) {
        if (isColor()) {
            int color = SkinResourcesUtils.getColor(attrValueRefId);
            view.setBackgroundColor(color);
        } else if (isDrawable()) {
            Drawable bg = SkinResourcesUtils.getDrawable(attrValueRefId);
            view.setBackgroundDrawable(bg);
        }
    }
    //...
}

ImageViewSrcAttr

public class ImageViewSrcAttr extends SkinAttr {
    @Override
    protected void applySkin(View view) {
        if (view instanceof ImageView) {
            ImageView iv = (ImageView) view;
            if (isDrawable()) {
                iv.setImageDrawable(SkinResourcesUtils.getDrawable(attrValueRefId));
            } else if (isColor()) {
                iv.setImageDrawable(new ColorDrawable(SkinResourcesUtils.getColor(attrValueRefId)));
            }
        }
    }
}

TextColorAttr

public class TextColorAttr extends SkinAttr {

    @Override
    protected void applySkin(View view) {
        if (view instanceof TextView) {
            TextView tv = (TextView) view;
            if (isColor()) {
                tv.setTextColor(SkinResourcesUtils.getColorStateList(attrValueRefId));
            }
        }
    }
    //...
}

这个三个属性内部都是使用SkinResourcesUtils来获取属性值。
我们看一下SkinResourcesUtils的getColor方法。

public static int getColor(int resId) {
        //调用SkinManager的getColor方法。
        return SkinManager.getInstance().getColor(resId);
    }

SkinManager的getColor方法。

public int getColor(int resId) {
        //获取原始color
        int originColor = ContextCompat.getColor(context, resId);
        if (mResources == null || isDefaultSkin) {//如果不需要换肤,就直接返回
            return originColor;
        }
        //下面这些操作获取皮肤包里的color
        String resName = context.getResources().getResourceEntryName(resId);
        int trueResId = mResources.getIdentifier(resName, "color", skinPackageName);
        Log.d(TAG, "\ngetColor: resId=" + resId + "\nresName=" + resName + "\ntrueResId=" + trueResId + "\nskinPackageName=" + skinPackageName);
        int trueColor;
        if (trueResId == 0) {
            trueColor = originColor;
        } else {
            trueColor = mResources.getColor(trueResId);
        }
        return trueColor;
    }

SkinInflaterFactory的applySkin方法

public void applySkin() {
        //如果mSkinItemMap为空则返回。
        if (mSkinItemMap.isEmpty()) {
            return;
        }
        for (View view : mSkinItemMap.keySet()) {
            if (view == null) {
                continue;
            }
            mSkinItemMap.get(view).apply();
        }
    }

自定义换肤属性

该库默认支持的换肤属性只有 background,textColor,src,如果需要其他的换肤属性,就需要自己定义了。
那么如何自定义呢?举个例子
TabLayout大家应该都用过吧。它下面会有一个指示器,当我们换肤的时候也希望这个指示器的颜色也跟着更改。

  1. 新建TabLayoutIndicatorAttr继承SkinAttr
public class TabLayoutIndicatorAttr extends SkinAttr {

    @Override
    protected void applySkin(View view) {
        if (view instanceof TabLayout) {
            TabLayout tl = (TabLayout) view;
            if (isColor()) {//表示属性值类型是color类型
                //获取颜色
                int color = SkinResourcesUtils.getColor(attrValueRefId);
                //设置指示器的颜色
                tl.setSelectedTabIndicatorColor(color);
            }
        }
    }
}
  1. 然后将TabLayoutIndicatorAttr添加到AttrFactory中
SkinConfig.addSupportAttr("tabIndicatorColor", new TabLayoutIndicatorAttr());
AttrFactory.addSupportAttr(attrName, skinAttr);
public static void addSupportAttr(String attrName, SkinAttr skinAttr) {
        sSupportAttr.put(attrName, skinAttr);
    }

这个时候SkinInflaterFactory的parseSkinAttr方法中

 if (AttrFactory.isSupportedAttr(attrName) && attrValue.startsWith("@")) {
        //...
}

这时候就会处理app:tabIndicatorColor="@color/colorPrimaryDark"属性,并添加到viewAttrs中。这样就可以了。

动态添加view支持换肤

先看定义的接口,需要动态添加view并支持换肤可以实现这个接口

public interface IDynamicNewView {
    //添加多个换肤属性
    void dynamicAddView(View view, List<DynamicAttr> pDAttrs);
    //添加一个换肤属性
    void dynamicAddView(View view, String attrName, int attrValueResId);
 
}

SkinBaseFragment和SkinBaseActivity都实现了这个接口,而SkinBaseFragment内部还是通过它的宿主Activity来添加View的。

public class SkinBaseFragment extends Fragment implements IDynamicNewView {

    private IDynamicNewView mIDynamicNewView;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        try {
            //为mIDynamicNewView赋值
            mIDynamicNewView = (IDynamicNewView) context;
        } catch (ClassCastException e) {
            mIDynamicNewView = null;
        }
    }

    @Override
    public final void dynamicAddView(View view, List<DynamicAttr> pDAttrs) {
        if (mIDynamicNewView == null) {
            throw new RuntimeException("IDynamicNewView should be implements !");
        } else {
            mIDynamicNewView.dynamicAddView(view, pDAttrs);
        }
    }

    @Override
    public final void dynamicAddView(View view, String attrName, int attrValueResId) {
        mIDynamicNewView.dynamicAddView(view, attrName, attrValueResId);
    }

    //...
}

新建一个DynamicAddFragment来做测试

public class DynamicAddFragment extends SkinBaseFragment {

    private LinearLayout ll_dynamic_view;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, 
        Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_dynamic_add, container, false);
        ll_dynamic_view = view.findViewById(R.id.ll_dynamic_view);    
        createDynamicView();
        return view;
    }

  private void createDynamicView() {
        ImageView imageView = new ImageView(getContext());
        imageView.setBackgroundResource(R.mipmap.mipmap_img);
        imageView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
        dynamicAddView(imageView, "background", R.mipmap.mipmap_img);
        ll_dynamic_view.addView(imageView);

        List<DynamicAttr> attrList = new ArrayList<>(2);
        attrList.add(new DynamicAttr("textColor", R.color.item_tv_title_color));
        attrList.add(new DynamicAttr("background", R.color.item_tv_title_background));
        for (int i = 0; i < 10; i++) {
            TextView textView1 = new TextView(getContext());
            textView1.setText("我是动态创建的TextView" + i + ",我也可以换肤");
            textView1.setTextColor(getResources().getColor(R.color.item_tv_title_color));
            ViewGroup.MarginLayoutParams params = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            params.setMargins(20, 20, 20, 20);
            textView1.setLayoutParams(params);
            //添加多个属性
            dynamicAddView(textView1, attrList);
            ll_dynamic_view.addView(textView1);
            dynamicAddFontView(textView1);
        }
    }

    //...
}

SkinBaseActivity部分代码

 @Override
    public void dynamicAddView(View view, List<DynamicAttr> pDAttrs) {
        mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, pDAttrs);
    }

    @Override
    public void dynamicAddView(View view, String attrName, int attrValueResId) {
        mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, attrName, attrValueResId);
    }

可以看到在SkinBaseActivity内部是通过mSkinInflaterFactory来动态添加支持换肤的属性的view。

我们看一下

public void dynamicAddSkinEnableView(Context context, View view, List<DynamicAttr> attrs) {
        //存放换肤属性列表
        List<SkinAttr> viewAttrs = new ArrayList<>();
        SkinItem skinItem = new SkinItem();
        //保存view
        skinItem.view = view;

        for (DynamicAttr dAttr : attrs) {
            int id = dAttr.refResId;
            String entryName = context.getResources().getResourceEntryName(id);
            String typeName = context.getResources().getResourceTypeName(id);
            SkinAttr mSkinAttr = AttrFactory.get(dAttr.attrName, id, entryName, typeName);
            //换肤属性
            viewAttrs.add(mSkinAttr);
        }

        skinItem.attrs = viewAttrs;
        //应用属性
        skinItem.apply();
        //添加到mSkinItemMap中去。
        addSkinView(skinItem);
    }

注意要在适当的时机移除动态添加的view

public class SkinBaseFragment extends Fragment implements IDynamicNewView {

 @Override
    public void onDestroyView() {
        removeAllView(getView());
        super.onDestroyView();
    }

    protected void removeAllView(View v) {
        if (v instanceof ViewGroup) {
            ViewGroup viewGroup = (ViewGroup) v;
            for (int i = 0; i < viewGroup.getChildCount(); i++) {
                removeAllView(viewGroup.getChildAt(i));
            }
            removeViewInSkinInflaterFactory(v);
        } else {
            removeViewInSkinInflaterFactory(v);
        }
    }

    private void removeViewInSkinInflaterFactory(View v) {
        if (getSkinInflaterFactory() != null) {
            //此方法用于Activity中Fragment销毁的时候,移除Fragment中的View
            getSkinInflaterFactory().removeSkinView(v);
        }
    }

}

SkinInflaterFactory的removeSkinView方法

public void removeSkinView(View view) {
        SkinL.i(TAG, "removeSkinView:" + view);
        SkinItem skinItem = mSkinItemMap.remove(view);
        if (skinItem != null) {
            SkinL.w(TAG, "removeSkinView from mSkinItemMap:" + skinItem.view);
        }
    }

总结一下换肤的主要流程

  1. 创建一个Android phone&Tablet Module类型的皮肤包。这个包里面只有颜色和图片等资源文件,没有类文件。这些资源文件的名字要和app的资源文件名字一样。
  2. 将这个皮肤包打包成apk,然后重命名为.skin结尾的文件(例如theme-20171126.skin)放入app的assets文件夹下。
  3. 在应用启动的时候,将skin文件复制到app的缓存目录下面(例如/storage/emulated/0/Android/data/solid.ren.themeskinning/cache/skin/xxx.skin)。
  4. 创建一个全新的AssetManager,然后将解压后的文件路径添加到
    AssetManager。
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
    addAssetPath.invoke(assetManager, skinPkgPath);
  1. 使用新创建的AssetManager来创建一个全新的Resources。这个Resources里面只有和换肤相关的资源文件。

  2. 在换肤资源的时候,从这个全新的Resources里面取资源。例如获取一个color

public int getColor(int resId) {
    //获取原始color
    int originColor = ContextCompat.getColor(context, resId);
    if(mResources == null || isDefaultSkin) { //如果不需要换肤,就直接返回
        return originColor;
    }
    //下面这些操作获取皮肤包里的color
    String resName = context.getResources().getResourceEntryName(resId);
    int trueResId = mResources.getIdentifier(resName, "color", skinPackageName);
    Log.d(TAG, "\ngetColor: resId=" + resId + "\nresName=" + resName + "\ntrueResId=" + trueResId +
        "\nskinPackageName=" + skinPackageName);
    int trueColor;
    if(trueResId == 0) {
        trueColor = originColor;
    } else {
        trueColor = mResources.getColor(trueResId);
    }
    return trueColor;
}
  1. 自定义SkinInflaterFactory用来创建view。在创建view的过程中动态保存可以换肤的属性集合,并把换肤属性集合和view实例保存在一个map中。
  2. 当用户调用换肤方法的时候,则遍历换肤属性集合,将属性值设置给view。
  3. 在适当的时机比如Activity finish或者fragment ondestroyView的时候清除map里面保存的换肤属性集合和view实例。

参考链接

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

推荐阅读更多精彩内容