扫码 项目总结-V1.3.0

好吧,其实V1.3.5 已经在上个月下旬做完了,现在才来写总结,但凡勤快点都不会拖到现在,害呀。这里标题是V1.3.0,但其实这个版本做出来没有上线,直接上的V1.3.5。所以就一并总结了。
V1.3相对于V1.2主要增加的功能就是创建二维码收藏,但其实关于创建二维码的代码很简单,本篇文章的主要内容也不是单纯的创建。下边是本文的主要内容:

V1.3.0总结

可以看到总结的点比较散,随便一个单独拿出来都可以水一篇文章了,文章虽然水,但都是干货哈

一 . 单选框的定制和使用RadioGroup和RadioButton

我们知道谷歌提供了一个单选控件RadioButton,这个通常是需要和RadioGroup一起使用的。
多个RadioButton置于一个RadioGroup,然后通过RadioGroup去控制其中的RadioButton。
RadioGroup是继承于LinearLayout的,所以用起来也很顺手。

1.布局

这里我需要的是三个横向排布的单选框,所以布局如下:

            <RadioGroup
                android:id="@+id/wifi_encryption_radio"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginHorizontal="30dp"
                android:layout_marginVertical="10dp"
                android:orientation="horizontal"
                android:visibility="gone">

                <RadioButton
                    android:id="@+id/radio1"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:background="@drawable/bg_radio"
                    android:button="@null"
                    android:gravity="center"
                    android:padding="8dp"
                    android:text="@string/WPA_WPA2"
                    android:textColor="@drawable/bg_radio_text"
                    android:textSize="15sp" />

                <RadioButton
                    android:id="@+id/radio2"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_marginHorizontal="10dp"
                    android:layout_weight="1"
                    android:background="@drawable/bg_radio"
                    android:button="@null"
                    android:gravity="center"
                    android:padding="8dp"
                    android:text="@string/WPE"
                    android:textColor="@drawable/bg_radio_text"
                    android:textSize="15sp" />

                <RadioButton
                    android:id="@+id/radio3"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:background="@drawable/bg_radio"
                    android:button="@null"
                    android:gravity="center"
                    android:padding="8dp"
                    android:text="@string/no_encryption"
                    android:textColor="@drawable/bg_radio_text"
                    android:textSize="15sp" />
            </RadioGroup>

主要目的就是让用户选择wifi加密方式
其实就是一个ViewGroup设置方向为横向后,排布几个radiobutton,就这么简单,当然还有其他属性上的设置,等会慢慢讲,现在给每个控件加上id就可以了

2.改变样式

我们知道,原本的radio button是非常丑的,就像这个:


radio button

要是直接这么上去,产品和ui都要骂娘了,所以要设计radio button的样式,就像这样:

效果图

接下来请参考上边的布局代码一起阅读。
第一步
去掉radio button右边的小圆点,这个非常简单:只需要一个属性
android:button="@null"
当然也可以通过这个属性设置这个button的样式,我们不需要就直接去掉。

第二步
设置选择选中和未选中的边框颜色
一个个来,边框实际上就是背景,背景一般采用drawable来绘制,所以新建一个命名为bg_radio的selector drawable文件,
不懂drawable的同学移步:https://www.jianshu.com/p/f01b1af15a88
内容如下:
android:background="@drawable/bg_radio"

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:state_checked="false"
        android:drawable="@drawable/bg_edit_text_normal" />
    <item
        android:state_checked="true"
        android:drawable="@drawable/bg_edit_text_focused" />
</selector>

具体的shape就不给了,其实这里的bg_edit_text_normal和bg_edit_text_focused,就是一个圆角,1dp宽边框的不同颜色的样式。关键点其实的这里item对应的state_checked属性,指定了选中和未选中应该用什么样式。
然后在radio button 中设置属性:
android:background="@drawable/bg_radio"
第三步
设置选择选中和未选中的字体颜色
同样的步骤,新建一个命名为bg_radio_text的selector drawable文件,
内容如下:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_checked="true" android:color="@color/theme_color" />
    <item android:state_checked="false" android:color="@color/gray_text" />
</selector>

最后设置radio button属性:
android:textColor="@drawable/bg_radio_text"

现在样式就改完了,感觉还行,接下来实现功能。

3.获取选择的内容

我们要点击使用时,需要获取一下当前选了什么
非常简单,就使用radio group的一条属性就可以了:

                when (wifi_encryption_radio.checkedRadioButtonId) {
                    R.id.radio1 -> {
                        encryption = "WPA"
                    }
                    R.id.radio2 -> {
                        encryption = "WPE"
                    }
                    R.id.radio3 -> {//没有加密类型
                        password = ""
                        encryption = ""
                    }
                }
4.给定默认值

本来radio group默认是什么都不选的,但是如果需要默认选择一个,可以这么设置:

 //默认选择第一个
wifi_encryption_radio.check(R.id.radio1)
5.选择监听

有的小伙伴又有需求了,说之前获取选择内容只是用户最终选择的结果,但是我想要用户每次选择我都知道选了哪个,怎么办
这个时候设置给radio group设置一下选择监听就可以了:

        wifi_encryption_radio.setOnCheckedChangeListener(object:RadioGroup.OnCheckedChangeListener{
            override fun onCheckedChanged(group: RadioGroup?, checkedId: Int) {
                when (checkedId) {
                    R.id.radio1 -> {
                        
                    }
                    R.id.radio2 -> {
                        
                    }
                    R.id.radio3 -> {
                       
                    }
                }
            }
        })

二 . RecycleView统一修改所有item的布局

就像这个样子:

效果图

在点击删除后,原本的收藏按钮消失,取而代之的是删除按钮,这该怎么实现呢?
非常简单:在adapter中添加一个全局变量isShowDeleteBtn一开始置为false
然后在onBindViewHolder方法中加入以下代码:

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
......省略代码

        if (isShowDeleteBtn) {
            holder.deleteBtn.visibility=View.VISIBLE
            holder.collectBtn.visibility=View.GONE
        } else {
            holder.deleteBtn.visibility=View.GONE
           holder.collectBtn.visibility=View.VISIBLE
    }

最后在需要改变布局的时候这样做:

    fun changeDeleteLayout() {
        mAdapter.isShowDeleteBtn=! mAdapter.isShowDeleteBtn
        //item发生改变 重新绘制item布局
        mAdapter.notifyItemRangeChanged(0,mData.size)
    }

这里的notifyItemRangeChanged非常关键,它表示一定范围内item内容修改,需要重新绘制。如果把范围扩大到全部,则会重新绘制所有item。
源码在这里:感兴趣可以看看注释说明

        /**
         * Notify any registered observers that the <code>itemCount</code> items starting at
         * position <code>positionStart</code> have changed.
         * Equivalent to calling <code>notifyItemRangeChanged(position, itemCount, null);</code>.
         *
         * <p>This is an item change event, not a structural change event. It indicates that
         * any reflection of the data in the given position range is out of date and should
         * be updated. The items in the given range retain the same identity.</p>
         *
         * @param positionStart Position of the first item that has changed
         * @param itemCount Number of items that have changed
         *
         * @see #notifyItemChanged(int)
         */
        public final void notifyItemRangeChanged(int positionStart, int itemCount) {
            mObservable.notifyItemRangeChanged(positionStart, itemCount);
        }

在附赠一个删除item的效果,当你从mData中移除了某一项后,
请调用notifyItemRemoved而不是notifyDataSetChanged

    fun removeItem(position: Int){
        mData.remove(mData[position])
        mAdapter.notifyItemRemoved(position)
    }

mAdapter.notifyItemRemoved(position)表示某个位置的内容被移除了,这时会有动画效果体现出来(这个动画也可以自定义)

三 . 软键盘

在我们程序中,难免会遇到使用输入框,这个时候就会弹出软键盘,那么关于软键盘和输入框的一些注意点我就写在这了。

1.软键盘的弹出方式

弹出方式分别是:键盘覆盖页面,键盘挤占页面布局,键盘顶起整个页面(不覆盖,不挤占),自定义方式(监听根布局Layout 的Size改变,获得软键盘高度,动态修改页面),等等
参考:https://www.cnblogs.com/jerehedu/p/4194125.html
处理方式:项目的AndroidManifest.xml文件中界面对应的<activity>里修改属性
例子:这会使屏幕整体上移

android:windowSoftInputMode="stateVisible|adjustResize" 

关于windowSoftInputMode的一些知识点:
activity主窗口与软键盘的交互模式,可以用来避免输入法面板遮挡问题。
它的设置必须是下面列表中的一个值,或一个”state…”值加一个”adjust…”值的组合:(值之间采用 | 分开)
列表:
各值的含义:

【A】stateUnspecified:软键盘的状态并没有指定,系统将选择一个合适的状态或依赖于主题的设置

【B】stateUnchanged:当这个activity出现时,软键盘将一直保持在上一个activity里的状态,无论是隐藏还是显示

【C】stateHidden:用户选择activity时,软键盘总是被隐藏

【D】stateAlwaysHidden:当该Activity主窗口获取焦点时,软键盘也总是被隐藏的

【E】stateVisible:软键盘通常是可见的

【F】stateAlwaysVisible:用户选择activity时,软键盘总是显示的状态

【G】adjustUnspecified:默认设置,通常由系统自行决定是隐藏还是显示

【H】adjustResize:该Activity总是调整屏幕的大小以便留出软键盘的空间

【I】adjustPan:当前窗口的内容将自动移动以便当前焦点从不被键盘覆盖和用户能总是看到输入内容的部分

2.软键盘弹起收回的监听

Android系统并没有直接提供监听键盘弹起收回的方法,只能通过一些特殊的方式来监听。比如下边这种,通过监听Layout高度的改变,来确认键盘是否弹起收回。有一个工具类如下:


import android.app.Activity
import android.graphics.Rect
import android.view.View
import android.view.ViewTreeObserver


/**
 * Created by liujinhua on 15/10/25.
 */
class SoftKeyBoardListener(activity: Activity) {
    private val rootView: View//activity的根视图
    var rootViewVisibleHeight: Int //纪录根视图的显示高度

    private var onSoftKeyBoardChangeListener: OnSoftKeyBoardChangeListener? = null
    private fun setOnSoftKeyBoardChangeListener(onSoftKeyBoardChangeListener: OnSoftKeyBoardChangeListener) {
        this.onSoftKeyBoardChangeListener = onSoftKeyBoardChangeListener
    }

    interface OnSoftKeyBoardChangeListener {
        fun keyBoardShow(height: Int)
        fun keyBoardHide(height: Int)
    }

    companion object {
        fun setListener(
            activity: Activity?,
            onSoftKeyBoardChangeListener: OnSoftKeyBoardChangeListener?
        ) {
            val softKeyBoardListener = activity?.let { SoftKeyBoardListener(it) }
            if (onSoftKeyBoardChangeListener != null) {
                softKeyBoardListener!!.setOnSoftKeyBoardChangeListener(onSoftKeyBoardChangeListener)
            }
        }
    }

    init {
        //获取activity的根视图
        rootView = activity.getWindow().getDecorView()
        val r = Rect()
        rootView.getWindowVisibleDisplayFrame(r)
        rootViewVisibleHeight = r.height()
        //监听视图树中全局布局发生改变或者视图树中的某个视图的可视状态发生改变
        rootView.getViewTreeObserver().addOnGlobalLayoutListener(object :
            ViewTreeObserver.OnGlobalLayoutListener {
            override fun onGlobalLayout() {
                //获取当前根视图在屏幕上显示的大小
                val r = Rect()
                rootView.getWindowVisibleDisplayFrame(r)
                val visibleHeight: Int = r.height()
                println("" + visibleHeight)
                if (rootViewVisibleHeight == 0) {
                    rootViewVisibleHeight = visibleHeight
                    return
                }

                //根视图显示高度没有变化,可以看作软键盘显示/隐藏状态没有改变
                if (rootViewVisibleHeight == visibleHeight) {
                    return
                }

                //根视图显示高度变小超过200,可以看作软键盘显示了
                if (rootViewVisibleHeight - visibleHeight > 200) {
                    if (onSoftKeyBoardChangeListener != null) {
                        onSoftKeyBoardChangeListener!!.keyBoardShow(rootViewVisibleHeight - visibleHeight)
                    }
                    rootViewVisibleHeight = visibleHeight
                    return
                }

                //根视图显示高度变大超过200,可以看作软键盘隐藏了
                if (visibleHeight - rootViewVisibleHeight > 200) {
                    if (onSoftKeyBoardChangeListener != null) {
                        onSoftKeyBoardChangeListener!!.keyBoardHide(visibleHeight - rootViewVisibleHeight)
                    }
                    rootViewVisibleHeight = visibleHeight
                    return
                }
            }
        })
    }
}

用法:

       //设置键盘的监听
        SoftKeyBoardListener.setListener(activity, object : OnSoftKeyBoardChangeListener {
            override fun keyBoardShow(height: Int) {
                Log.d("键盘监听", "弹起")
   
            }

            override fun keyBoardHide(height: Int) {
                Log.d("键盘监听", "回收")
    
            }
        })
3.指定输入框的输入方式

这个其实是edittext的属性,修改inputType。
例子:editText.inputType = InputType.TYPE_CLASS_NUMBER
这个就表示输入框只想要纯数字,其他的输入类型可以自己研究下。
输入框的错误提示:editText.error = "输入内容不可为空"
聚焦和非聚焦ui样式的改变(通过drawable)
https://blog.csdn.net/tracydragonlxy/article/details/100558915
其他关于输入框的知识点都很简单,需要的时候一搜就可以了。
最后推荐一个还不错的自定义输入框:
https://github.com/wrapp-archive/floatlabelededittext

四 . 一个非常好用的时间选择器

https://github.com/JZXiang/TimePickerDialog

五 .图片保存和分享

1.图片保存

图片保存通常就是将bitmap在手机上保存为jpg,png等格式图片。
这里有几个注意点:
1.文件读写权限
2.判断手机是否有外部存储卡,若没有则只能保存在App内部存储
3.图片保存后并不会直接在相册里显示,而是要发出广播通知系统刷新媒体库
我的一个工具类在这里,比较清晰,可以作为一个参考,后期再加上接口回调,将保存结果成功或失败回调出去。

import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Environment
import android.os.Handler
import android.os.Message
import android.util.Log
import android.widget.Toast
import androidx.browser.customtabs.CustomTabsClient.getPackageName
import androidx.core.content.FileProvider
import com.matrix.framework.utils.DirUtils.getCacheDir
import com.qr.scanlife.R
import com.qr.scanlife.base.App
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import kotlin.concurrent.thread

/**
 * 保存图片工具类 将bitmap对象保存到本地相册
 *
 **/

class SaveImageUnit {

    //读写权限!
    companion object {
        val instance: SaveImageUnit by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { SaveImageUnit() }
    }

    //保存图片的文件夹地址
    var appDir: File? = null
    private val TAG = "图片保存"
    val context = App.context
    var mediaScanIntent: Intent? = null
    val saveSucCode = 2211

    //检查保存的文件夹是否存在 不存在则创建一个
    private fun checkDir() {
        val state = Environment.getExternalStorageState()
        if (Environment.MEDIA_MOUNTED == state) {
            //如果有外部内存卡可进行读写 则建在外部内存卡上
            appDir = File(
                Environment.getExternalStorageDirectory().absolutePath + File.separator + Environment.DIRECTORY_PICTURES + File.separator + context.getText(
                    R.string.app_name
                )
            )
            if (!appDir!!.exists()) {
                appDir!!.mkdir()
            }
        } else {//否则将文件夹建在 APP内部存储上
            appDir =
                File(context.filesDir.absolutePath + File.separator + context.getText(R.string.app_name))
            if (!appDir!!.exists()) {
                appDir!!.mkdir()
            }
        }
        Log.d(TAG, "图片文件夹地址${appDir?.absolutePath}")
    }

    //保存bitmap到指定文件夹 并发出广播通知系统刷新媒体库
    fun saveBitmap(bitmap: Bitmap, imageName: String) {
        Toast.makeText(context, App.context.getText(R.string.saving), Toast.LENGTH_SHORT).show()
        checkDir()

        val file = File(appDir, "$imageName.jpg")
        //准备好发出广播 通知系统媒体 刷新相册 在相册中显示出图片
        mediaScanIntent =
            Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))

        Log.d(TAG, "图片地址${file.absolutePath}")
        thread {
            try {
                val fileOutputStream = FileOutputStream(file)
                /**
                 * quality:100
                 * 为不压缩
                 */
                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream)
                fileOutputStream.flush()
                fileOutputStream.close()
                val msg = Message()
                msg.what = saveSucCode
                msg.obj = file.absolutePath
                handler.sendMessage(msg)
            } catch (e: FileNotFoundException) {
                e.printStackTrace()
                Log.d(TAG, "保存失败1${e.message}")
                Toast.makeText(context, "${App.context.getText(R.string.save_failed)}:${e.message}", Toast.LENGTH_SHORT).show()
            } catch (e: IOException) {
                e.printStackTrace()
                Log.d(TAG, "保存失败2${e.message}")
                Toast.makeText(context, "${App.context.getText(R.string.save_failed)}:${e.message}", Toast.LENGTH_SHORT).show()
            }
        }
    }

    @SuppressLint("HandlerLeak")
    private val handler = object : Handler() {
        //接收信息
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            //判断信息识别码 根据不同的识别码进行不同动作
            when (msg.what) {
                saveSucCode -> {
                    context.sendBroadcast(mediaScanIntent)
                    val path: String? = msg.obj as? String
                    Toast.makeText(context, "${App.context.getText(R.string.save_success)} ${App.context.getText(R.string.image_path)}:$path", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    //保存文件到app缓存目录准备分享 耗时操作 请最好在异步线程调用
    fun cacheBitmapForShare(bitmap: Bitmap): Uri? {
        val dir = File(getCacheDir().absolutePath + File.separator + "Share")
        if (!dir.exists()) {
            dir.mkdir()
        }
        val file = File(dir,"Share${System.currentTimeMillis()}.jpg" )
        Log.d(TAG, "图片缓存地址${file.absolutePath}")
        try {
            val fileOutputStream = FileOutputStream(file)
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream)
            fileOutputStream.flush()
            fileOutputStream.close()
            val uri=FileProvider.getUriForFile(context,context.packageName+".fileProvider",file)
            return uri
        } catch (e: FileNotFoundException) {
            e.printStackTrace()
            Log.d(TAG, "缓存失败1${e.message}")
        } catch (e: IOException) {
            e.printStackTrace()
            Log.d(TAG, "缓存失败2${e.message}")
        }
        return null
    }
}
2.图片分享

图片分享之前需要将图片保存,然后将保存的文件uri作为分享内容使用intent分享出去。
这里有两个坑:
首先保存的位置应该是App的缓存文件夹(系统随时回收),不会占用过多的空间,产生垃圾文件。
其次,如果保存在了缓存文件夹,则系统不允许App直接将文件uri暴露出去,而要通过FileProvider
file provider用法:
https://www.jianshu.com/p/f0b2cf0e0353
然后关于分享文件可以看上边的cacheBitmapForShare方法,具体用法:

    private fun shareImg(bitmap:Bitmap) {
        val uri = SaveImageUnit.instance.cacheBitmapForShare(bitmap)
        Log.d("图片分享uri", uri.toString())
        val shareIntent = Intent(Intent.ACTION_SEND)
        shareIntent.putExtra(Intent.EXTRA_STREAM, uri)
        shareIntent.type = "image/*" //设置分享内容的类型:图片
        try {
            startActivity(
                Intent.createChooser(
                    shareIntent,
                    getString("Share")
                )
            )
        } catch (e: Exception) {
            Log.d("图片分享", e.toString())
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341