Android开发(29)——Fragment和Navigation

本节内容

1.简述

2.Fragment的创建

3.添加动画

4.Fragment数据传递

5.显示HomeFragment

6.添加toolBar关联

7.设置ButtonNavigationView

8.抽屉布局drawLayout

9.页面切换和数据传递

一、简述
1.对于MVVM来说,View指可交互的视图。ViewModel一般是管理数据的,通常是MutableLiveData。Model就是数据模块,通常需要一个Repository来联系Model和ViewModel。
2.Activity:管理一个界面的生命周期。
3.Fragment :可以理解为一个小型的Activity。一个手机app,用户在使用的时候,手机屏幕上只会显示一个页面,但是会有很多不同的页面进行切换。Fragment就相当于每一个小的页面,而管理这些页面的就是ViewPager。当用户滑动页面时,ViewPager就会把当前这个Fragment移出去,然后换下一个新的Fragment进来。
二、Fragment的创建
1.Fragment 表示应用界面中可重复使用的一部分。Fragment 定义和管理自己的布局,具有自己的生命周期,并且可以处理自己的输入事件。Fragment 不能独立存在,而是必须由 Activity 或另一个 Fragment 托管。Fragment 的视图层次结构会成为宿主的视图层次结构的一部分,或附加到宿主的视图层次结构。
2.FragmentActivity之间的关系:
  • Fragment依赖于Activity
  • Fragment 就是Activity中不同功能的分离,它有自己的生命周期。
3.Fragment的生命周期。
  • onAttach():当Fragment被添加到activity中时调用
  • onCreate():创建Fragment时调用
  • onCreateView():Fragment具体显示的内容 -xml
  • onActivityCreated():当Activity创建好了之后,才会调用这个方法。相对Fragment进行操作的时候,就在这个方法里面操作。
Fragment生命周期
4.静态使用Fragment:
  • (1)在Activity对应的xml中添加Fragment组件
 <Fragment
        android:id="@+id/mContainer"
        android:name="com.example.fragment.LoginFragment"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
  • (2)自定义一个类继承于fragment。比如我们创建一个类,名为LoginFragment,继承于Fragment,然后重写对应的生命周期方法。onCreateView方法必须实现。
class LoginFragment :Fragment() {
     override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragmentlogin,container,false)
    }
}
  • (3)创建xml文件 进行页面布局。创建一个fragmentlogin.xml文件,自己随便布局一下。
  • (4)使用LayoutInflater解析布局文件.
return inflater.inflate(R.layout.fragmentlogin,container,false)
  • (5)在xml中设置fragment对应的name属性。在activity_xml的<Fragment>里面设置。
android:name="com.example.fragment.LoginFragment"
5.在activity.xml中添加几个按钮。最上方是fragment。
xml布局
5.动态使用Fragment。
  • (1)在Activity对应的xml中添加FrameLayout组件.
<FrameLayout
        android:id="@+id/mContainer"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
  • (2)自定义一个类继承于fragment(3)重写对应的生命周期方法 onCreateView(4)创建xml文件 进行页面布局(5)用layoutInflater解析布局文件
  • (6)使用FragmentManager管理fragment的切换, MainActivity中使用supportFragmentManager 获取FragmentManager,获取FragmentTransaction对象。
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.add(R.id.mContainer,LoginFragment())
fragmentTransaction.commit()
  • mContainer是activtiy里面FrameLayout容器的id。
运行程序,得到以下结果。
运行结果
6.新建一个Fragment,然后布局一下对应的xml文件。随便布局一下。
7.然后在MainActivity里面实现一下mReplace的监听事件,当我们点击REPLACE按钮的时候,它就会直接替换。
 mReplace.setOnClickListener { 
supportFragmentManager
.beginTransaction()
.replace(R.id.mContainer,RegistFragment("jack"))
.commit()
        }
点击replace按钮之后
三、添加动画
1.前面点击按钮完了之后,文本切换了,但是点击下方的返回按钮时并不会回到原来的页面,即刚刚那个fragment并不在栈里面。如果想要点击返回时回到之前的界面,那么可以添加一个.addToBackStack(null),这样就可以让它在栈里面了。
我们通过实现add按钮的点击事件来展示一下是如何使用的。
 mAdd.setOnClickListener {
 supportFragmentManager
.beginTransaction()
.add(R.id.mContainer,RegistFragment("rose"))
.addToBackStack(null)
.commit()
        }
  • 这个函数的意思就是把它添加到回退栈里面。
2.给这个页面切换添加动画效果,首先添加一个anim包,然后添加动画的资源文件。
目录
  • slide_in.xml
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500">
    <translate android:fromXDelta="100%" android:toXDelta="0%"/>
</set>
  • slide_out.xml
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500">
    <translate android:fromXDelta="0%" android:toXDelta="-100%"/>
</set>
  • 在MainActivity里面用上这个动画。
.setCustomAnimations(R.anim.slide_in,R.anim.pop_out)
3.重新布局一下fragmentlogin页面,如下图所示。
fragmentlogin页面
4.重新布局一下activity_main.xml,把那几个按钮都删掉,只留下一个FrameLayout,然后把它的高设为0dp,宽度设为match_parent。
5.实现上面LOGIN按钮的点击事件,要在LoginFragment类里面实现。重写一下这个类里面的onActivityCreated方法,然后在里面先获取子控件,再实现其点击事件。
   override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        val btn= view?.findViewById<Button>(R.id.mLogin)
        btn?.setOnClickListener {
            fragmentManager?.let {
                    it.beginTransaction()
                    .setCustomAnimations(R.anim.slide_in,R.anim.slide_out)
                    .replace(R.id.mContainer,RegistFragment("jack"))
                    .addToBackStack(null)
                    .commit()
            }
        }
    }
6.在MainActivity里面使用一下supportFragmentManager
supportFragmentManager
            .beginTransaction()
            .replace(R.id.mContainer,LoginFragment())
            .commit()
7.如果这样的话,点击返回按钮并没有动画。想要点击返回按钮也有动画的话,那么就要再在anim包里面添加两个动画,然后把它们添加到.setCustomAnimations里面
.setCustomAnimations(R.anim.slide_in,R.anim.slide_out,R.anim.pop_in,R.anim.pop_out)
四、Fragment数据传递
1.给ResgisterFragment添加一个构造方法
class RegistFragment ( val name:String):Fragment()
2.给fragmentregister.xml添加一个TextView,然后在ResgisterFragment类里面实现onActivityCreated方法
override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        view?.findViewById<TextView>(R.id.mTextView)?.text  =name
    }
3.在LoginFragmentonActivityCreated方法里面先把EditText里的内容解析出来。
val nameTv = view?.findViewById<EditText>(R.id.mNameTv)
  • 然后在replace方法里面把解析到的内容传进去。
.replace(R.id.mContainer,RegistFragment(nameTv?.text.toString()))
  • 这样数据就能传递过去了。但是一旦把手机旋转一下,就会报错。因为旋转屏幕之后,Activity重新走了一遍生命周期,所以Fragment也要重新创建一下。但是它重新创建的时候需要我们给它的构造函数添加一个参数,可是我们给不了。如果手机不支持屏幕旋转功能的话,那么就可以这样写。但是如果手机屏幕可以旋转,这样就不行。
4.想要让屏幕旋转过来后数据也不会变的话,可以像下面一样操作。使用View Model,但是一般情况下都不会这么用。
  • 先创建一个MyViewModel类继承自ViewModel,然后在里面添加一个liveData类型的数据。
class MyViewModel:ViewModel() {
    var fragment:MutableLiveData<Fragment>?=MutableLiveData()
}
  • LoginFragment类里面的onActivityCreated方法里面,用rf来记录数据。
  override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        val btn= view?.findViewById<Button>(R.id.mLogin)
        val nameTv = view?.findViewById<EditText>(R.id.mNameTv)
        btn?.setOnClickListener {
            val rf = RegistFragment()
            val bundle = Bundle()
            bundle.putString("name",nameTv?.text.toString())
            rf.arguments = bundle

            val act = activity as MainActivity
            act.viewModel.fragment?.value = rf

            fragmentManager?.let {
                   it.beginTransaction()
                    .setCustomAnimations(R.anim.slide_in,R.anim.slide_out,R.anim.pop_in,R.anim.pop_out)
                       .replace(R.id.mContainer,rf)
                    .addToBackStack(null)
                    .commit()
            }
        }
  • 然后在MainActivity先懒加载一个变量,在onCreate方法把它解析出来。然后判断它是不是空,如果是空的就把Fragment传递过去,否则就把它作为livedata进行监听。
class MainActivity : AppCompatActivity() {
lateinit var viewModel:MyViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

         viewModel = ViewModelProvider(this,ViewModelProvider.NewInstanceFactory()).get(MyViewModel::class.java)
        if (viewModel.fragment?.value==null){
            viewModel.fragment?.value = LoginFragment()
        }
       viewModel.fragment?.observe(this, Observer {
           supportFragmentManager
               .beginTransaction()
               .replace(R.id.mContainer,it)
               .commit()
       })
   }
}
  • 最后屏幕转过来数据也没有改变。
屏幕旋转后
5.传数据用arguments来传,但这还不是最好的写法。
五、显示homefragment
1.前情提要
  • NavController:管理界面切换。
  • NavGraph:导航图。定义好了界面之间的跳转。
2.因为我们要实现在目的地之间传递数据,所以在gradel的dependencies里面添加以下代码。
def nav_version = "2.3.1"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
3.我们要生成适用于 Kotlin 独有的模块的 Kotlin 代码,那么就在第二个gradle里面添加以下代码。
apply plugin: "androidx.navigation.safeargs.kotlin"
4.先创建一个名为HomeFragment的类,继承自Fragment()。可以直接通过构造方法把页面id 传过来,这样就不需要在onCreateView里面再解析了。
class HomeFragment :Fragment(R.layout.fragment_home){
}
5.创建一个和上面这个类匹配的xml文件,取名fragment_home。随意布局一下这个页面,我的如下图所示。给按钮添加一个id。
fragment_home.xml布局
6.创建一个navGraph来管理页面之间的跳转。新建一个navigation的包,然后再创建一个navigation的xml文件。
文件目录
  • 在fragment_home.xml中关联一下HomeFragment类。
tools:context="HomeFragment"
  • 在nav_graph.xml中把fragment_home.xml文件添加进来
nav_graph界面
7.完成这个新的demo我们需要完成以下工作:
  • Navigation Graph 导航图,管理fragment之间的逻辑关系
  • NavigationFragment 容器
  • NavController 导航控制器,控制页面之间的切换。
8.我们已经完成了第一步,现在我们来完成第二步,添加容器。在activity_xml里面,拖动containers的NavHostFragment过来,然后选择nav_graph。
拖动一个containers
9.当它运行起来,显示的就是我们刚刚布局的那个页面。
运行结果
六、添加toolBar关联navVontroller
1.在activity.xml中添加一个toolBar,并把fragment的顶部约束到toolBar的下方。
<androidx.appcompat.widget.Toolbar
        android:id="@+id/toolBar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/colorPurple"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="MissingConstraints" />
2.然后我们在AndroidManifest.xml中把theme改为NoActionBar。这样就不会显示系统自带的Bar了。
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
3.在MainActivity里面添加一个导航控制器。
 private lateinit var navController:NavController
4.显示toolBar里面的内容。
  • 在onCreate里面获取navController
     //获取NavHostFragment对象
       val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
       //通过这个对象获取对应的navController
        navController = navHostFragment.findNavController()
  • 用自己的toolbar作为默认ActionBar
setSupportActionBar(toolBar)
  • 这样就可以显示工程的名字了,之前是只有一个toolBar,没有工程名字。
显示工程名
5.给我们自己的toolBar设置一下主题。
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
6.但是这样显示的只是工程名字,并不是当前fragment所对应的label。想要让它显示的是fragment对应的label的话,我们就要让ActionBar 和NavController产生关联。这样的话,当NavController切换的时候 会把对应的Fragment的label显示到actionbar
setupActionBarWithNavController(navController)
当前fragment的label为Home
七、设置BottomNavigationView
1.新建一个SearchFragment类继承自Fragment,再创建一个fragment_sreach的xml文件。然后随便布局一下。
class SearchFragment :Fragment(R.layout.fragment_sreach){
}
2.在nav_graph里面把serach_fragment添加进去。在那之前记得在search_fragment里面把以下代码添加进去。
tools:context=".SearchFragment"
nav_graph.xml
3.在activity_main.xml的底部添加导航条。然后自行约束一下。
布局之后的结果
4.这样添加的导航条什么内容都没有,要想在里面添加内容就需要一个menu。所以我们new一个resource file,类型为menu,然后在里面添加一个xml文件。
menu文件
5.在drawable里面右击一下,new一个vector Asset,这样就可以添加系统为我们提供的图片。search图标如下图所示:
search图标
6.menu里的xml文件代码如下图所示。id是nav_graph里面每个fragment的id。
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/homeFragment"
        android:title="Home"
        android:icon="@drawable/ic_home"/>

    <item android:id="@+id/searchFragment"
        android:title="Search"
        android:icon="@drawable/ic_search"/>
</menu>
7.在activity_main.xml中的BottomNavigationView里面添加以下代码,把menu加进去,这样它就会显示内容了。
app:menu="@menu/bottom_nav_menu"
BottomNavigationView显示的内容
8.关联bottomNavigationView和NavController,在MainActivity里面。这样设置完了之后,点击search按钮就能自动切换页面了。
bottom_nav_view.setupWithNavController(navController)
Home主界面
点击search切换页面
9.配置程序的顶层页面,顶层之间的切换,没有返回箭头。如果不添加以下代码。第二个页面的toolBar就会默认添加一个返回箭头,意思是把第一个页面作为主页面。这个意思就是让这两个页面处于同等地位,没有什么主次之分。
val config:AppBarConfiguration = AppBarConfiguration(
        setOf(R.id.homeFragment,R.id.searchFragment)
    )
setupActionBarWithNavController(navController,config)
八、抽屉布局drawerLayout
1.在activity_main.xml中,先把最外层设置为抽屉布局
androidx.drawerlayout.widget.DrawerLayout
  • 在里面创建一个ConstraintLayout,然后把toolBar,BottomNavigationView,和中间的fragment都copy进去。
2.在drawerLayout里面添加一个NavigationView,并设置id。gravity设为start,表示在左边。
 <com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:gravity="start"/>
  • 这样运行起来就是以下效果。因为没有添加内容,所以是空的。
加了NavigationView
3.要想让它显示内容的话,我们就需要一个menu,直接把我们前面设置的menu加进去即可。
app:menu="@menu/bottom_nav_menu"
4.但是这样并不能点击,所以我们要关联NavigationView 和NavController
nav_view.setupWithNavController(navController)
  • 这样旁边不仅有标签栏,而且还可以点击进行跳转。
左侧菜单栏
5.给这个菜单栏添加一个icon,这样点击图标也可以把它拉出来。只需要在前面的config里面再添加一下drawerLayout的id即可。
val config:AppBarConfiguration = AppBarConfiguration(
        setOf(R.id.homeFragment,R.id.searchFragment),
        drawer_layout
    )
  • 添加之后icon图标也显示出来了
icon图标
6.但是此时的icon点击之后是没有效果的。想要点击它之后就弹出菜单的话,我们需要先把config定义在外部。
 private  lateinit var config:AppBarConfiguration
  • 然后在实现一下onSupportNavigateUp方法。
override fun onSupportNavigateUp(): Boolean {
        return navController.navigateUp(config)
    }
  • 这样点击这个图标,菜单栏就会显示出来了。
九、页面切换和数据传递
1.添加一个login的fragment,包括代码和xml布局。代码如下图所示:
class LoginFragment :Fragment(R.layout.fragment_login){
}
  • xml就随便布局一下。然后设置一下context。再把它加进nav_graph。让homeFragment指向loginFragment。这样点击按钮,就会跳转到这个页面来。
指向关系
2.在HomeFragment里面给按钮添加点击事件。这样就可以直接切换。
override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        button_login.setOnClickListener {
            val action:NavDirections= HomeFragmentDirections
                .actionHomeFragmentToLoginFragment()
            findNavController().navigate(action)
        }
    }
3.但是这样跳转很不美观,所以我们可以添加一些动画效果。slide_in如下图所示,其他的都差不多。一共有四个动画。
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500">
    <translate
        android:fromXDelta="100%"
        android:toXDelta="0%"
        />
</set>
4.然后在nav_graph里面把这几个动画添加进去。
添加动画
5.添加一个WelcomeFragment和xml文件,随便布局一下,然后再和WelcomeFragment关联一下。
fragmenr_welcome.xml
6.把它添加进nav_graph,然后和loginFragment关联一下。
关联结果
7.关联完了之后,再把之前的动画添加进去。直接到代码中,把之前的哪那些动画代码copy进去即可。
           app:enterAnim="@anim/slide_in"
            app:exitAnim="@anim/slide_out"
            app:popEnterAnim="@anim/pop_in"
            app:popExitAnim="@anim/pop_out" 
8.然后在LoginFragment里面实现按钮的点击事件。
override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        button_confirm.setOnClickListener {
            val action = LoginFragmentDirections.actionLoginFragmentToWelcomeFragment()
            findNavController().navigate(action)
        }
    }
9.如果想要传递数据怎么办呢,首先在nav_graph里面选中welcomeFragment,然后点击Arguments里面的+号
添加接收的数据
  • 然后在LoginFragment里面把数据传递进去
val action = 
LoginFragmentDirections.actionLoginFragmentToWelcomeFragment(name_edit_view.text.toString())
10.在WelcomeFragment里面获取传递过来的参数值。
class WelcomeFragment :Fragment(R.layout.fragment_welcome){
    val args:WelcomeFragmentArgs by NavArgs()

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        name_text_view.text=args.Username
    }

}
  • 或者直接使用arguments
 name_text_view.text = arguments?.getString("username")
  • 这样点击之后,值就会传递过去。Jack是我在前面一个框内写入的内容。
Jack传过去了
11.如果我们想让输入的内容也变为toolBar的标题的话,那么就在nav_graph中属于welcomeFragment的代码里面修改一下。这样就会显示为标题了。
android:label="{username}"
  • 效果如下图所示:
效果图
12.将welcome界面和home界面关联起来,当点击back按钮的时候会立刻跳转到Home页面。在WelcomeFragment里面,实现按钮的点击事件。
 button_back.setOnClickListener {
            findNavController().navigate(
                WelcomeFragmentDirections.actionWelcomeFragmentToHomeFragment()
            )
        }
13.如果用户成功登录了的话,那么按底下的返回就不会跳到上一个页面,而是跳到主界面。我们就在nav_graph里面,给loginFragment和WelcomeFragment之间那根线,设置一下它的popBehavior
设置
  • 这样在最后一次登录了的时候,点击下方的回退,back按钮,就会直接到home主页面了。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,794评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,050评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,587评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,861评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,901评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,898评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,832评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,617评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,077评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,349评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,483评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,199评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,824评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,442评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,632评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,474评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,393评论 2 352

推荐阅读更多精彩内容