動(dòng)手試試Android Studio插件開發(fā)

由于業(yè)務(wù)關(guān)系,經(jīng)常需要寫一些表單頁面樟凄,基本也就是簡(jiǎn)單的增刪改查然后上傳瘩燥,做過幾個(gè)頁面之后就有點(diǎn)想偷懶了,這么低水平重復(fù)性的體力勞動(dòng)不同,能不能用什么辦法自動(dòng)生成呢,查閱相關(guān)資料,發(fā)現(xiàn)android studio插件正好可以滿足需求二拐,在Github上搜了一下服鹅,找到BorePlugin這個(gè)幫助自動(dòng)生成布局代碼的插件挺不錯(cuò)的,在此基礎(chǔ)上修改為符合自己需求的插件百新,整體效果還不錯(cuò)企软。
發(fā)現(xiàn)了android studio插件的魅力,自己也總結(jié)一下饭望,也給小伙伴們提供一點(diǎn)參考仗哨,今天就以實(shí)現(xiàn)自動(dòng)生成findviewbyid代碼插件的方式來個(gè)簡(jiǎn)單的總結(jié)。這里就不寫行文思路了铅辞,一切從0開始厌漂,一步一步搭建起這個(gè)插件項(xiàng)目吧。效果如下:

效果圖

一斟珊、搭建環(huán)境

由于android studio是基于Intellij IDEA開發(fā)的苇倡,但Android Studio自身不具備開發(fā)插件的功能,所以插件開發(fā)需要在IntelliJ IDEA上開發(fā)囤踩。
好了旨椒,說了這么多,開始去官網(wǎng)下載吧堵漱,下載地址:https://www.jetbrains.com/idea/
安裝運(yùn)行后我們就可以開始開發(fā)了综慎。
創(chuàng)建項(xiàng)目

創(chuàng)建項(xiàng)目

創(chuàng)建成功之后的文件夾是這個(gè)樣子的:

創(chuàng)建項(xiàng)目

我們重點(diǎn)關(guān)注plugin.xml和src,plugin.xml是我們這個(gè)插件項(xiàng)目的配置說明勤庐,類似于android開發(fā)中的AndroidManifest.xml文件示惊,用于配置信息的注冊(cè)和聲明。

<idea-plugin version="2">
  <id>com.your.company.unique.plugin.id</id>
  <name>Plugin display name here</name>
  <version>1.0</version>
  <vendor email="support@yourcompany.com" url="http://www.yourcompany.com">YourCompany</vendor>

  <description><![CDATA[
      Enter short description for your plugin here.<br>
      <em>most HTML tags may be used</em>
    ]]></description>

  <change-notes><![CDATA[
      Add change notes here.<br>
      <em>most HTML tags may be used</em>
    ]]>
  </change-notes>

  <!-- please see http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/build_number_ranges.html for description -->
  <idea-version since-build="141.0"/>

  <!-- please see http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/plugin_compatibility.html
       on how to target different products -->
  <!-- uncomment to enable plugin in all products
  <depends>com.intellij.modules.lang</depends>
  -->

  <extensions defaultExtensionNs="com.intellij">
    <!-- Add your extensions here -->
  </extensions>

  <actions>
    <!-- Add your actions here -->
  </actions>

</idea-plugin>

來簡(jiǎn)單介紹下這個(gè)XML配置文件:
id:插件的ID埃元,保證插件的唯一性涝涤,如果上傳倉庫的話。
name:插件名稱岛杀。
version:版本號(hào)阔拳。
description:插件的簡(jiǎn)介。
change-notes:版本更新信息类嗤。
extensions:擴(kuò)展組件注冊(cè) 糊肠。
actions:Action注冊(cè),比如在某個(gè)菜單下增加一個(gè)按鈕就要在這注冊(cè)遗锣。

二货裹、開始編碼

1、編寫菜單選項(xiàng)精偿,用于觸發(fā)我們的插件弧圆。

類似于這樣的菜單選項(xiàng)

好了赋兵,現(xiàn)在我們要用到很關(guān)鍵的一個(gè)類:AnAction,選擇new->Action就可以創(chuàng)建:

Action
配置Action

ActionID:代表該Action的唯一的ID
ClassName:類名
Name:插件在菜單上的名稱
Description:對(duì)這個(gè)Action的描述信息
Groups:定義這個(gè)菜單選項(xiàng)出現(xiàn)的位置,比如圖中設(shè)置當(dāng)點(diǎn)擊菜單欄Edit時(shí)搔预,第一項(xiàng)會(huì)出現(xiàn)GenerateCode的選項(xiàng)霹期,右邊的Anchor是選擇該選項(xiàng)出現(xiàn)的位置,默認(rèn)First即最頂部拯田。
之后會(huì)出現(xiàn)我們創(chuàng)建的GenerateCodeAction類:

public class GenerateCodeAction extends AnAction {

    
@Override
   
 public void actionPerformed(AnActionEvent e) {
       
 // TODO: insert action logic here
    
  }

}

plugin.xml中也多了一段代碼:

<action id="HelloWorld.TestGenerateCodeAction" class="com.example.helloworld.GenerateCodeAction" text="GenerateCode"
description="generate findviewbyid code ">
<add-to-group group-id="CodeMenu" anchor="first"/>
<keyboard-shortcut keymap="$default" first-keystroke="meta I"/>
</action>

這樣历造,一個(gè)菜單選項(xiàng)就完成了,接下來就該實(shí)現(xiàn)當(dāng)用戶點(diǎn)擊GenerateCode菜單或者按快捷鍵Command+ M后的功能代碼了船庇。

2吭产、實(shí)現(xiàn)功能邏輯代碼

在實(shí)現(xiàn)功能邏輯之前,我們要先理清需求鸭轮,首先我們是想在選中布局文件的時(shí)候臣淤,自動(dòng)解析布局文件并生成findviewbyid代碼。那我們主要關(guān)注三個(gè)點(diǎn)就可以了张弛。

1荒典、如何獲取布局文件
2、如何解析布局文件
3吞鸭、如何根據(jù)將代碼寫入文件

1寺董、如何獲取布局文件
為簡(jiǎn)單起見,我們這里通過讓用戶自己輸入布局文件的方式通過FilenameIndex.getFilesByName方法來查找布局文件刻剥。
查找文件我們要用到PsiFile類遮咖,官方文檔給我們的提供了幾種方式:

From an action: 
    e.getData(LangDataKeys.PSI_FILE).
From a VirtualFile: 
    PsiManager.getInstance(project).findFile()
From a Document:     
    PsiDocumentManager.getInstance(project).getPsiFile()
From an element inside the file:
     psiElement.getContainingFile()
To find files with a specific name anywhere in the project, use :
    FilenameIndex.getFilesByName(project, name, scope)

這里使用最后一種方式來獲取圖片,獲取用戶選中的布局文件造虏,如果用戶沒有選中內(nèi)容御吞,通過在狀態(tài)欄彈窗提示:

 public static void showNotification(Project project, MessageType type, String text) {
        StatusBar statusBar = WindowManager.getInstance().getStatusBar(project);

        JBPopupFactory.getInstance()
                .createHtmlTextBalloonBuilder(text, type, null)
                .setFadeoutTime(7500)
                .createBalloon()
                .show(RelativePoint.getCenterOf(statusBar.getComponent()), Balloon.Position.atRight);
    }

獲取用戶選中內(nèi)容:

@Override
    public void actionPerformed(AnActionEvent e) {

        Project project = e.getProject();
        Editor editor = e.getData(PlatformDataKeys.EDITOR);
        if (null == editor) {
            return;
        }

        SelectionModel model = editor.getSelectionModel();
        //獲取選中內(nèi)容
        final String selectedText = model.getSelectedText();
        if (TextUtils.isEmpty(selectedText)) {
            Utils.showNotification(project,MessageType.ERROR,"請(qǐng)選中生成內(nèi)容");
            return;
        }
    }
彈窗效果

獲取XML文件:

PsiFile[] mPsiFiles = FilenameIndex.getFilesByName(project, selectedText+".xml", GlobalSearchScope.allScope(project));
        if (mPsiFiles.length<=0){
            Utils.showNotification(project,MessageType.INFO,"所輸入的布局文件沒有找到!");
            return;
        }
        XmlFile xmlFile =  (XmlFile) mPsiFiles[0];

至此,布局文件獲取到了漓藕,我們開始下一步陶珠,解析布局文件啦。
2享钞、如何解析布局文件
關(guān)于文件操作揍诽,官方文檔是這樣寫的:

Most interesting modification operations are performed on the level of individual PSI elements, not files as a whole.
To iterate over the elements in a file, use
psiFile.accept(new PsiRecursiveElementWalkingVisitor()...);

我們這里通過file.accept(new XmlRecursiveElementVisitor())方法對(duì)XML文件進(jìn)行解析:

public static ArrayList<Element> getIDsFromLayout(final PsiFile file, final ArrayList<Element> elements) {
        file.accept(new XmlRecursiveElementVisitor() {

            @Override
            public void visitElement(final PsiElement element) {
                super.visitElement(element);
                //解析XML標(biāo)簽
                if (element instanceof XmlTag) {
                    XmlTag tag = (XmlTag) element;
                  //解析include標(biāo)簽
                    if (tag.getName().equalsIgnoreCase("include")) {
                        XmlAttribute layout = tag.getAttribute("layout", null);

                        if (layout != null) {
                            Project project = file.getProject();
//                            PsiFile include = findLayoutResource(file, project, getLayoutName(layout.getValue()));
                            PsiFile include = null;
                            PsiFile[] mPsiFiles = FilenameIndex.getFilesByName(project, getLayoutName(layout.getValue())+".xml", GlobalSearchScope.allScope(project));
                            if (mPsiFiles.length>0){
                                include = mPsiFiles[0];
                            }

                            if (include != null) {
                                getIDsFromLayout(include, elements);

                                return;
                            }
                        }
                    }

                    // get element ID
                    XmlAttribute id = tag.getAttribute("android:id", null);
                    if (id == null) {
                        return; // missing android:id attribute
                    }
                    String value = id.getValue();
                    if (value == null) {
                        return; // empty value
                    }

                    // check if there is defined custom class
                    String name = tag.getName();
                    XmlAttribute clazz = tag.getAttribute("class", null);
                    if (clazz != null) {
                        name = clazz.getValue();
                    }

                    try {
                        Element e = new Element(name, value, tag);
                        elements.add(e);
                    } catch (IllegalArgumentException e) {
                        // TODO log
                    }
                }
            }
        });


        return elements;
    }

    public static String getLayoutName(String layout) {
        if (layout == null || !layout.startsWith("@") || !layout.contains("/")) {
            return null; // it's not layout identifier
        }

        String[] parts = layout.split("/");
        if (parts.length != 2) {
            return null; // not enough parts
        }

        return parts[1];
    }

以及實(shí)體類Element:

package com.example.helloworld.entity;

import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlTag;

import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Element {

    // constants
    private static final Pattern sIdPattern = Pattern.compile("@\\+?(android:)?id/([^$]+)$", Pattern.CASE_INSENSITIVE);
    private static final Pattern sValidityPattern = Pattern.compile("^([a-zA-Z_\\$][\\w\\$]*)$", Pattern.CASE_INSENSITIVE);
    public String id;
    public boolean isAndroidNS = false;
    public String nameFull; // element mClassName with package
    public String name; // element mClassName
    public int fieldNameType = 1; // 1 aa_bb_cc; 2 aaBbCc 3 mAaBbCc
    public boolean isValid = false;
    public boolean used = true;
    public boolean isClickable = false; // Button, view_having_clickable_attr etc.
    public boolean isItemClickable = false; // ListView, GridView etc.
    public boolean isEditText = false; // EditText
    public XmlTag xml;

    //GET SET mClassName
    public String strGetMethodName;
    public String strSetMethodName;

    /**
     * Constructs new element
     *
     * @param name Class mClassName of the view
     * @param id   Value in android:id attribute
     * @throws IllegalArgumentException When the arguments are invalid
     */
    public Element(String name, String id, XmlTag xml) {
        // id
        final Matcher matcher = sIdPattern.matcher(id);
        if (matcher.find() && matcher.groupCount() > 1) {
            this.id = matcher.group(2);

            String androidNS = matcher.group(1);
            this.isAndroidNS = !(androidNS == null || androidNS.length() == 0);
        }

        if (this.id == null) {
            throw new IllegalArgumentException("Invalid format of view id");
        }

        // mClassName
        String[] packages = name.split("\\.");
        if (packages.length > 1) {
            this.nameFull = name;
            this.name = packages[packages.length - 1];
        } else {
            this.nameFull = null;
            this.name = name;
        }

        this.xml = xml;

        // clickable
        XmlAttribute clickable = xml.getAttribute("android:clickable", null);
        boolean hasClickable = clickable != null &&
                clickable.getValue() != null &&
                clickable.getValue().equals("true");
        String xmlName = xml.getName();
        if (xmlName.contains("RadioButton")) {
            // TODO check
        } else {
            if ((xmlName.contains("ListView") || xmlName.contains("GridView")) && hasClickable) {
                isItemClickable = true;
            } else if (xmlName.contains("Button") || hasClickable) {
                isClickable = true;
            }
        }

        // isEditText
        isEditText = xmlName.contains("EditText");
    }

    /**
     * Create full ID for using in layout XML files
     *
     * @return
     */
    public String getFullID() {
        StringBuilder fullID = new StringBuilder();
        String rPrefix;

        if (isAndroidNS) {
            rPrefix = "android.R.id.";
        } else {
            rPrefix = "R.id.";
        }

        fullID.append(rPrefix);
        fullID.append(id);

        return fullID.toString();
    }

    /**
     * Generate field mClassName if it's not done yet
     *
     * @return
     */
    public String getFieldName() {
        String fieldName = id;
        String[] names = id.split("_");
        if (fieldNameType == 2) {
            // aaBbCc
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < names.length; i++) {
                if (i == 0) {
                    sb.append(names[i]);
                } else {
                    sb.append(firstToUpperCase(names[i]));
                }
            }
            fieldName = sb.toString();
        } else if (fieldNameType == 3) {
            // mAaBbCc
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < names.length; i++) {
                if (i == 0) {
                    sb.append("m");
                }
                sb.append(firstToUpperCase(names[i]));
            }
            fieldName = sb.toString();
        }
        return fieldName;
    }

    /**
     * Check validity of field mClassName
     *
     * @return
     */
    public boolean checkValidity() {
        Matcher matcher = sValidityPattern.matcher(getFieldName());
        isValid = matcher.find();

        return isValid;
    }
    public static String firstToUpperCase(String key) {
        return key.substring(0, 1).toUpperCase(Locale.CHINA) + key.substring(1);
    }
}

一些有用的方法
通用方法
FilenameIndex.getFilesByName()通過給定名稱(不包含具體路徑)搜索對(duì)應(yīng)文件
ReferencesSearch.search()類似于IDE中的Find Usages操作
RefactoringFactory.createRename()重命名
FileContentUtil.reparseFiles()通過VirtualFile重建PSI

Java專用方法
ClassInheritorsSearch.search()搜索一個(gè)類的所有子類
JavaPsiFacade.findClass()通過類名查找類
PsiShortNamesCache.getInstance().getClassesByName()通過一個(gè)短名稱(例如LogUtil)查找類
PsiClass.getSuperClass()查找一個(gè)類的直接父類
JavaPsiFacade.getInstance().findPackage()獲取Java類所在的Package
OverridingMethodsSearch.search()查找被特定方法重寫的方法

3、如何根據(jù)將代碼寫入文件
如Android不允許在UI線程中進(jìn)行耗時(shí)操作一樣栗竖,Intellij Platform也不允許在主線程中進(jìn)行實(shí)時(shí)的文件寫入暑脆,而需要通過一個(gè)異步任務(wù)來進(jìn)行。

  new WriteCommandAction(project) {
            @Override
            protected void run(@NotNull Result result) throws Throwable {
                //writing to file
            } 
       }.execute();

也可以繼承自WriteCommandAction.Simple來執(zhí)行寫操作狐肢。

 @Override
    public void run() throws Throwable {

        generateFields();
        generateFindViewById();
        // reformat class
        JavaCodeStyleManager styleManager = JavaCodeStyleManager.getInstance(mProject);
        styleManager.optimizeImports(mFile);
        styleManager.shortenClassReferences(mClass);
        new ReformatCodeProcessor(mProject, mClass.getContainingFile(), null, false).runWithoutProgress();
    }

主要使用psiclass.add(JavaPsiFacade.getElementFactory(mProject).createMethodFromText(sbInitView.toString(), psiclass))方法為類創(chuàng)建方法添吗;用mFactory.createFieldFromText方法添加字段;用mClass.findMethodsByName方法查找方法份名,用onCreate.getBody().addAfter(mFactory.createStatementFromText("initView();", mClass), setContentViewStatement);方法為方法體添加內(nèi)容碟联。

protected void generateFields() {
        for (Iterator<Element> iterator = mElements.iterator(); iterator.hasNext(); ) {
            Element element = iterator.next();

            if (!element.used) {
                iterator.remove();
                continue;
            }

            // remove duplicate field
            PsiField[] fields = mClass.getFields();
            boolean duplicateField = false;
            for (PsiField field : fields) {
                String name = field.getName();
                if (name != null && name.equals(element.getFieldName())) {
                    duplicateField = true;
                    break;
                }
            }

            if (duplicateField) {
                iterator.remove();
                continue;
            }
            String hint = element.xml.getAttributeValue("android:hint");
            mClass.add(mFactory.createFieldFromText("/** "+hint+" */\nprivate " + element.name + " " + element.getFieldName() + ";", mClass));
        }
    }

    protected void generateFindViewById() {
        PsiClass activityClass = JavaPsiFacade.getInstance(mProject).findClass(
                "android.app.Activity", new EverythingGlobalScope(mProject));
        PsiClass compatActivityClass = JavaPsiFacade.getInstance(mProject).findClass(
                "android.support.v7.app.AppCompatActivity", new EverythingGlobalScope(mProject));

        // Check for Activity class
        if ((activityClass != null && mClass.isInheritor(activityClass, true))
                || (compatActivityClass != null && mClass.isInheritor(compatActivityClass, true))
                || mClass.getName().contains("Activity")) {
            if (mClass.findMethodsByName("onCreate", false).length == 0) {
                // Add an empty stub of onCreate()
                StringBuilder method = new StringBuilder();
                method.append("@Override protected void onCreate(android.os.Bundle savedInstanceState) {\n");
                method.append("super.onCreate(savedInstanceState);\n");
                method.append("\t// TODO: add setContentView(...) and run LayoutCreator again\n");
                method.append("}");

                mClass.add(mFactory.createMethodFromText(method.toString(), mClass));
            } else {
                PsiStatement setContentViewStatement = null;
                boolean hasInitViewStatement = false;

                PsiMethod onCreate = mClass.findMethodsByName("onCreate", false)[0];
                for (PsiStatement statement : onCreate.getBody().getStatements()) {
                    // Search for setContentView()
                    if (statement.getFirstChild() instanceof PsiMethodCallExpression) {
                        PsiReferenceExpression methodExpression = ((PsiMethodCallExpression) statement.getFirstChild()).getMethodExpression();
                        if (methodExpression.getText().equals("setContentView")) {
                            setContentViewStatement = statement;
                        } else if (methodExpression.getText().equals("initView")) {
                            hasInitViewStatement = true;
                        }
                    }
                }

                if(!hasInitViewStatement && setContentViewStatement != null) {
                    // Insert initView() after setContentView()
                    onCreate.getBody().addAfter(mFactory.createStatementFromText("initView();", mClass), setContentViewStatement);
                }
                generatorLayoutCode();
            }
        }
    }
    private void generatorLayoutCode() {
        // generator findViewById code in initView() method
        StringBuilder initView = new StringBuilder();
            initView.append("private void initView() {\n");

        for (Element element : mElements) {
            initView.append(element.getFieldName() + " = (" + element.name + ")findViewById(" + element.getFullID() + ");\n");
        }
        initView.append("}\n");
      mClass.add(mFactory.createMethodFromText(initView.toString(), mClass));

    }

至此妓美,我們之前的目標(biāo)已經(jīng)完成了,編碼階段告一段落玄帕。

三部脚、使用插件

我們的插件實(shí)現(xiàn)完了,填寫下plugin.xml文件相關(guān)內(nèi)容裤纹,我們就可以導(dǎo)出需要安裝的jar文件了:

導(dǎo)出安裝jar文件
用于安裝的jar

打開android studio,進(jìn)入setting頁面丧没,安裝插件:

安裝插件

到這里鹰椒,重啟android studio就可以使用我們的插件了。
當(dāng)然呕童,還可以把我們的插件發(fā)布到倉庫漆际,支持在plugin中搜索安裝,可以參考官方給的文檔:
http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/publishing_plugin.html

我們的插件這樣就完成了夺饲,本文很多地方實(shí)現(xiàn)都參考了BorePlugin的實(shí)現(xiàn)奸汇,如果對(duì)實(shí)現(xiàn)細(xì)節(jié)感興趣,可以查看這個(gè)開源項(xiàng)目的源碼往声,再次也對(duì)作者表示感謝擂找。文章簡(jiǎn)化版本的源碼相對(duì)簡(jiǎn)單,方便理解浩销,可以點(diǎn)此下載贯涎。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市慢洋,隨后出現(xiàn)的幾起案子塘雳,更是在濱河造成了極大的恐慌,老刑警劉巖普筹,帶你破解...
    沈念sama閱讀 222,729評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件败明,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡太防,警方通過查閱死者的電腦和手機(jī)妻顶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來杏头,“玉大人盈包,你說我怎么就攤上這事〈纪酰” “怎么了呢燥?”我有些...
    開封第一講書人閱讀 169,461評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)寓娩。 經(jīng)常有香客問我叛氨,道長(zhǎng)呼渣,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,135評(píng)論 1 300
  • 正文 為了忘掉前任寞埠,我火速辦了婚禮屁置,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘仁连。我一直安慰自己蓝角,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評(píng)論 6 398
  • 文/花漫 我一把揭開白布饭冬。 她就那樣靜靜地躺著使鹅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪昌抠。 梳的紋絲不亂的頭發(fā)上患朱,一...
    開封第一講書人閱讀 52,736評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音炊苫,去河邊找鬼裁厅。 笑死,一個(gè)胖子當(dāng)著我的面吹牛侨艾,可吹牛的內(nèi)容都是我干的执虹。 我是一名探鬼主播,決...
    沈念sama閱讀 41,179評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼蒋畜,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼声畏!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起姻成,我...
    開封第一講書人閱讀 40,124評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤插龄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后科展,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體均牢,經(jīng)...
    沈念sama閱讀 46,657評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評(píng)論 3 342
  • 正文 我和宋清朗相戀三年才睹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了徘跪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,872評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡琅攘,死狀恐怖垮庐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情坞琴,我是刑警寧澤哨查,帶...
    沈念sama閱讀 36,533評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站剧辐,受9級(jí)特大地震影響寒亥,放射性物質(zhì)發(fā)生泄漏邮府。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評(píng)論 3 336
  • 文/蒙蒙 一溉奕、第九天 我趴在偏房一處隱蔽的房頂上張望褂傀。 院中可真熱鬧,春花似錦加勤、人聲如沸仙辟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽欺嗤。三九已至,卻和暖如春卫枝,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背讹挎。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評(píng)論 1 274
  • 我被黑心中介騙來泰國打工校赤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人筒溃。 一個(gè)月前我還...
    沈念sama閱讀 49,304評(píng)論 3 379
  • 正文 我出身青樓马篮,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親怜奖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子浑测,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評(píng)論 2 361

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