Android自定义控件(一)_2015-03-22

自定义控件教程:

1,http://blog.csdn.net/aigestudio/article/details/41212583
2,http://blog.csdn.net/aigestudio/article/details/41316141
3,http://blog.csdn.net/aigestudio/article/details/41447349
4,http://blog.csdn.net/aigestudio/article/details/41960507
5,http://blog.csdn.net/aigestudio/article/details/42677973
6,http://blog.csdn.net/aigestudio/article/details/42989325

1,Paint类

    private void init() {
        paint = new Paint();
        paint.setAntiAlias(true);//反锯齿
        paint.setColor(Color.RED);
        /*
         * 设置画笔样式为描边,圆环嘛……当然不能填充不然就么意思了
         *
         * 画笔样式分三种:
         * 1.Paint.Style.STROKE:描边
         * 2.Paint.Style.FILL_AND_STROKE:描边并填充
         * 3.Paint.Style.FILL:填充
         */
        paint.setStyle(Paint.Style.FILL);
        /*
         * 设置描边的粗细,单位:像素px
         * 注意:当setStrokeWidth(0)的时候描边宽度并不为0而是只占一个像素
         */
        paint.setStrokeWidth(paintStrokeWidth);
    }

Paint的其他方法:

  • setStrokeCap(Paint.Cap cap)
    方法,该方法用来设置我们画笔的笔触风格,上面的例子中我使用的是ROUND,表示是圆角的笔触,那么什么叫笔触呢,其实很简单,就像我们现实世界中的笔,如果你用圆珠笔在纸上戳一点,那么这个点一定是个圆,即便很小,它代表了笔的笔触形状,如果我们把一支铅笔笔尖削成方形的,那么画出来的线条会是一条弯曲的“矩形”,这就是笔触的意思。除了ROUND,Paint.Cap还提供了另外两种类型:SQUARE和BUTT

  • setStrokeJoin(Paint.Join join)
    这个方法用于设置结合处的形态,就像上面的代码中我们虽说是花了一条心电线,但是这条线其实是由无数条小线拼接成的,拼接处的形状就由该方法指定。

  • setShadowLayer(float radius, float dx, float dy, int shadowColor)
    该方法为我们绘制的图形添加一个阴影层效果:


TextPaint专门用户绘制文字的画笔

/* 
 * 初始化文字画笔 
 */
textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.SUBPIXEL_TEXT_FLAG);
textPaint.setColor(Color.WHITE);
textPaint.setTextSize(30);
textPaint.setTextAlign(Paint.Align.CENTER);

与画笔颜色相关的类

详情见博客:2,http://blog.csdn.net/aigestudio/article/details/41316141

PorterDuffXfermode类,图形混合模式的意思

2,View类的invalidate()方法,和postInvalidate()方法

在Android中提供了一个叫invalidate()的方法来让我们重绘我们的View。poistInvalidate()方法和invaliadate方法相同,只是它可以在子线程中运行.
nvalidate()方法只能在主线程中调用,而postInvalidate()方法可以在子线程中调用。

3,Shader类(着色器)

Paste_Image.png

Shader类呢也是个灰常灰常简单的类,它有五个子类,像PathEffect一样每个子类都实现了一种Shader,Shader在三维软件中我们称之为着色器,其作用嘛就像它的名字一样是来给图像着色的或者更通俗的说法是上色!这么说该懂了吧!再不懂去厕所哭去!这五个Shader里最异类的是BitmapShader,因为只有它是允许我们载入一张图片来给图像着色,那我们还是先来看看这个怪胎吧BitmapShader只有一个含参的构造方法BitmapShader (Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)

BitmapShader (Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)的第一个参数是位图这个很显然,而后两个参数则分别表示XY方向上的着色模式。
Shader.TileMode里有三种模式:CLAMP、MIRROR和REPETA。MIRROR镜像效果,REPETA重复,CLAMP的意思就是边缘拉伸的意思

4,Canvas 画布

  • canvas.save();锁定画布
  • canvas.restore(); 释放画布,即将画布释放到之前锁定的位置。先调用sava()方法,才能调用restore()方法进行释放。
    在Android中我们可以使用Canvas的saveXXX和restoreXXX方法来模拟图层的类似效果

Canvas我们一直称其为画布,其实更准确地说Canvas是一个容器,如果把Canvas理解成画板,那么我们的“层”就像张张夹在画板上的透明的纸,而这些纸对应到Android则是一个个封装在Canvas中的Bitmap。
除了save()方法Canvas还给我们提供了一系列的saveLayerXXX方法给我们保存画布,与save()方法不同的是,saveLayerXXX方法会将所有的操作存到一个新的Bitmap中而不影响当前Canvas的Bitmap,而save()方法则是在当前的Bitmap中进行操作,并且只能针对Bitmap的形变和裁剪进行操作,saveLayerXXX方法则无所不能,当然两者还有很多的不同.
save和saveLayerXXX方法有着本质的区别,saveLayerXXX方法会将所有操作在一个新的Bitmap中进行,而save则是依靠stack栈来进行
详情:http://blog.csdn.net/aigestudio/article/details/42677973

  • canvas.translate(x, y);平移画布,就相当于将画布的原点移到x,y的位置 ,即相当于坐标系移动了。
Paste_Image.png
  • canvas.rotate(degrees);旋转画布,将画布按照一定角度进行旋转,即相当于坐标系进行了旋转。
Paste_Image.png

注意:画布的平移旋转同样也会影响画布的自身坐标

5,Canvas类(画布)


Canvas所提供的各种方法根据功能来看大致可以分为几类,第一是以drawXXX为主的绘制方法,第二是以clipXXX为主的裁剪方法,第三是以scale、skew、translate和rotate组成的Canvas变换方法,最后一类则是以saveXXX和restoreXXX构成的画布锁定和还原。

  • drawBitmapMesh (Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, int vertOffset, int[] colors, int colorOffset, Paint paint)
    很吊毛的方法,但是计算复杂度很高,所有被沉默了。(一般不推荐使用,因为计算难度很大)
    详细原理http://blog.csdn.net/aigestudio/article/details/41960507

drawBitmapMesh是个很屌毛的方法,为什么这样说呢?因为它可以对Bitmap做几乎任何改变,是的,你没听错,是任何,几乎无所不能,这个屌毛方法我曾一度怀疑谷歌那些逗比为何将它屈尊在Canvas下,因为它对Bitmap的处理实在在强大了。上一节我们在讲到Matrix的时候说过Matrix可以对我们的图像做多种变换,实际上drawBitmapMesh也可以,只不过需要一点计算,比如我们可以使用drawBitmapMesh来模拟错切skew的效果。

  • Canvas(Bitmap bitmap);含参数的构造。
    可以实现在一张Bitmap的基础上进行绘制。- canvas.setBitmap(Bitmap bitmap); 作用同含参数的构造.
    例如:
ivMain = (ImageView) findViewById(R.id.main_iv);
Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.RED);
ivMain.setImageBitmap(bitmap); 
  • canvas.translate()和rotate()方法:
    此类方法是对画布进行平移旋转等操作。其实可以理解为对坐标轴的旋转平移等操作。
    -canvas.clipXXX()方法:对画布的一种剪切方式,剪切后只能在剪切的部分显示需要绘制的内容。当前画布被“裁剪”了,只有剪裁的部分能够出现我们绘制的内容,如果我们所绘制的东西在该区域外部,即便绘制了你也看不到
clipPath(Path path, Region.Op op)
clipRect(Rect rect, Region.Op op)
clipRect(RectF rect, Region.Op op)
clipRect(float left, float top, float right, float bottom, Region.Op op)
clipRegion(Region region, Region.Op op) 

Canvas中有关裁剪的方法,你会发现有一大堆带有Region.Op参数的重载方法.要明白这些方法的Region.Op参数那么首先要了解Region为何物。Region的意思是“区域”,在Android里呢它同样表示的是一块封闭的区域。
详情请看 一下8中有关Region的简介
-canvas.drawPath(Path path, Paint paint) 绘制路径。
-canvas.drawTextOnPath (String text, Path path, float hOffset, float vOffset, Paint paint) ; 绘制文字在Path路径上。

6 Rect和RectF类

Rect和RectF是类似的,只不过RectF中涉及计算的时候数值类型均为float型,两者均表示一块规则矩形。重要的方法:

  • intersect(RectF rectF) / intersect (float left, float top, float right, float bottom) :此方法表示两个矩形相交的部分。取两个区域的相交区域作为最终区域。intersect方法的计算方式是相当有趣的,它不是单纯地计算相交而是去计算相交区域最近的左上端点和最近的右下端点
public class CanvasView extends View {  
    private Rect mRect;  
  
    public CanvasView(Context context, AttributeSet attrs) {  
        super(context, attrs);  
        mRect = new Rect(0, 0, 500, 500);  
  
        mRect.intersect(250, 250, 750, 750);  
    }  
  
    @Override  
    protected void onDraw(Canvas canvas) {  
        canvas.drawColor(Color.BLUE);  
  
        canvas.clipRect(mRect);  
  
        canvas.drawColor(Color.RED);  
    }  

图示.png

注意:黄色线框为后期加上的辅助线非程序生成

  • union(RectF rect) / union(float left, float top, float right, float bottom) 方法:union方法与intersect相反,取的是相交区域最远的左上端点作为新区域的左上端点,而取最远的右下端点作为新区域的右下端点
  • mRect.union(250, 250, 750, 750);

运行后我们会看到如下结果:

Paste_Image.png

是不是觉得不是我们想象中的那样单纯地两个区域相加。此方法是指的左上角的点和右下角的点的最远距离。

7 Path 类

此类表示一个路径

  • lineTo(float x, float y): 将路径连接至某个点坐标。(不适用moveTo方法时默认起点坐标为原点)。我们可以考虑多次调用lineTo方法来绘制更复杂的图形.
  • moveTo(float x, float y):将Path的起始点移动到某个特定的点。(Path默认起始点为原点)
  • close() 此方法含义师闭合曲线。
// 实例化路径  
mPath = new Path();    
// 移动点至[300,300]  
mPath.moveTo(100, 100);    
// 连接路径到点  
mPath.lineTo(300, 100);  
mPath.lineTo(400, 200);  
mPath.lineTo(200, 200);    
// 闭合曲线  
mPath.close(); 
Paste_Image.png
  • XXXTo() 贝塞尔曲线相关方法这些方法帮助我们绘制各类直线、曲线
    -- quadTo(float x1, float y1, float x2, float y2) 绘制二阶贝塞尔曲线。 quadTo的前两个参数为控制点的坐标,后两个参数为终点坐标, 当然起点坐标就是path的起点坐标
    -- cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) 绘制三阶贝塞尔曲线。与quadTo类似,前四个参数表示两个控制点,最后两个参数表示终点:
// 实例化路径  
mPath = new Path();    
// 移动点至[100,100]  
mPath.moveTo(100, 100);    
// 连接路径到点  
mPath.cubicTo(200, 200, 300, 0, 400, 100);  
三阶贝塞尔曲线.png
  • arcTo (RectF oval, float startAngle, float sweepAngle) 用来生成弧线的方法。说白了就是从圆或者椭圆上截取一部分而已。
// 实例化路径  
mPath = new Path();    
// 移动点至[100,100]  
mPath.moveTo(100, 100);    
// 连接路径到点  
RectF oval = new RectF(100, 100, 200, 200);  
mPath.arcTo(oval, 0, 90); 

运行后.png

运行后效果跟我们想想的有点不一样,这是因为使用Path生成的路径必定都是连贯的,虽然我们使用arcTo绘制的时一段狐,但是其最终都会与我们Path的起点链接起来。如果你不想要链接,那么path也提供了一种方法,而已设置是否强制链接起点。

  • arcTo (RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) 该方法只是多了一个布尔值,值为true时将会把弧的起点作为Path的起点

  • Path中除了上面介绍的几个XXXTo方法外还有一套rXXXTo方法:

rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3)  
rLineTo(float dx, float dy)  
rMoveTo(float dx, float dy)  
rQuadTo(float dx1, float dy1, float dx2, float dy2)  

这一系列rXXXTo方法其实跟上面的那些XXXTo差不多的,唯一的不同是rXXXTo方法的参考坐标是相对的而XXXTo方法的参考坐标始终是参照画布原点坐标。相对的指的是,终点的坐标是相对于起点的坐标的,例如rMoveTo(100,100) ; rLineTo(200,200) ,表示这个线段的长度是200而不是100.

  • addXXX()方法,Path的这一系列的add方法允许我们直接往Path中添加一些曲线。
    比如:addArc(RectF oval, float startAngle, float sweepAngle) 方法允许我们将一段弧形添加至Path,注意这里我用到了“添加”这个词汇,也就是说,通过addXXX方法添加到Path中的曲线是不会和上一次的曲线进行连接的。
    -- 除了addArc(RectF oval, float startAngle, float sweepAngle)方法之外还有这一系列的add方法。
addCircle(float x, float y, float radius, Path.Direction dir)  
addOval(float left, float top, float right, float bottom, Path.Direction dir)  
addRect(float left, float top, float right, float bottom, Path.Direction dir)  
addRoundRect(float left, float top, float right, float bottom, float rx, float ry, Path.Direction dir)  

这些方法和addArc有很明显的区别,就是多了一个Path.Direction参数Path.Direction只有两个常量值CCW和CW分别表示逆时针方向闭合和顺时针方向闭合
mPath.addOval(oval, Path.Direction.CW);顺时针

Paste_Image.png

mPath.addOval(oval, Path.Direction.CCW); 逆时针
Paste_Image.png

public class PathView extends View {  
    private Path mPath;// 路径对象  
    private Paint mPaint;// 路径画笔对象  
  
    public PathView(Context context, AttributeSet attrs) {  
        super(context, attrs);    
        /* 
         * 实例化画笔并设置属性 
         */  
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);  
        mPaint.setStyle(Paint.Style.STROKE);  
        mPaint.setColor(Color.CYAN);  
        mPaint.setStrokeWidth(5);    
        // 实例化路径  
        mPath = new Path();    
        // 移动点至[100,100]  
        mPath.moveTo(100, 100);    
        // 连接路径到点  
        mPath.lineTo(200, 200);    
        // 添加一条弧线到Path中  
        RectF oval = new RectF(100, 100, 300, 400);  
        mPath.addArc(oval, 0, 90);  
    }    
    @Override  
    protected void onDraw(Canvas canvas) {  
        // 绘制路径  
        canvas.drawPath(mPath, mPaint);  
    }  
} 
Paste_Image.png

8 Region

回顾Canvas中有关裁剪的方法,你会发现有一大堆带有Region.Op参数的重载方法:
clipPath(Path path, Region.Op op) clipRect(Rect rect, Region.Op op) clipRect(RectF rect, Region.Op op) clipRect(float left, float top, float right, float bottom, Region.Op op) clipRegion(Region region, Region.Op op)
要明白这些方法的Region.Op参数那么首先要了解Region为何物。Region的意思是“区域”,在Android里呢它同样表示的是一块封闭的区域,Region中的方法都非常的简单,我们重点来瞧瞧Region.Op,Op是Region的一个枚举类,里面呢有六个枚举常量:

Paste_Image.png

那么Region.Op究竟有什么用呢?其实它就是个组合模式,我们曾学过一个叫图形混合模式的,而在本节开头我们也曾讲过Rect也有类似的组合方法,Region.Op灰常简单,如果你看过图形混合模式的话。这里我就给出一段测试代码,大家可以尝试去改变不同的组合模式看看效果

public class CanvasView extends View {  
    private Region mRegionA, mRegionB;// 区域A和区域B对象  
    private Paint mPaint;// 绘制边框的Paint  
  
    public CanvasView(Context context, AttributeSet attrs) {  
        super(context, attrs);    
        // 实例化画笔并设置属性  
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);  
        mPaint.setStyle(Paint.Style.STROKE);  
        mPaint.setColor(Color.WHITE);  
        mPaint.setStrokeWidth(2);    
        // 实例化区域A和区域B  
        mRegionA = new Region(100, 100, 300, 300);  
        mRegionB = new Region(200, 200, 400, 400);  
    }    
    @Override  
    protected void onDraw(Canvas canvas) {  
        // 填充颜色  
        canvas.drawColor(Color.BLUE);    
        canvas.save();    
        // 裁剪区域A  
        canvas.clipRegion(mRegionA);    
        // 再通过组合方式裁剪区域B  
        canvas.clipRegion(mRegionB, Region.Op.DIFFERENCE);    
        // 填充颜色  
        canvas.drawColor(Color.RED);    
        canvas.restore();    
        // 绘制框框帮助我们观察  
        canvas.drawRect(100, 100, 300, 300, mPaint);  
        canvas.drawRect(200, 200, 400, 400, mPaint);  
    }  
}  

以下是各种组合模式的效果
DIFFERENCE


最终区域为第一个区域与第二个区域不同的区域。
INTERSECT

最终区域为第一个区域与第二个区域相交的区域。
REPLACE

最终区域为第二个区域。
REVERSE_DIFFERENCE

最终区域为第二个区域与第一个区域不同的区域。
UNION

最终区域为第一个区域加第二个区域。
XOR

最终区域为第一个区域加第二个区域并减去两者相交的区域。
Region.Op就是这样,它和我们之前讲到的图形混合模式几乎一模一样换汤不换药……我在做示例的时候仅仅是使用了一个Region,实际上Rect、Cricle、Ovel等封闭的曲线都可以使用Region.Op,介于篇幅,而且也不难以理解就不多说了。
有些童鞋会问那么Region和Rect有什么区别呢?
首先最重要的一点,Region表示的是一个区域,而Rect表示的是一个矩形,这是最根本的区别之一,其次,Region有个很特别的地方是它不受Canvas的变换影响,Canvas的local不会直接影响到Region自身,
什么意思呢?我们来看一个simple你就会明白:

public class CanvasView extends View {  
    private Region mRegion;// 区域对象  
    private Rect mRect;// 矩形对象  
    private Paint mPaint;// 绘制边框的Paint  
  
    public CanvasView(Context context, AttributeSet attrs) {  
        super(context, attrs);  
  
        // 实例化画笔并设置属性  
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);  
        mPaint.setStyle(Paint.Style.STROKE);  
        mPaint.setColor(Color.DKGRAY);  
        mPaint.setStrokeWidth(2);  
  
        // 实例化矩形对象  
        mRect = new Rect(0, 0, 200, 200);  
  
        // 实例化区域对象  
        mRegion = new Region(200, 200, 400, 400);  
    }  
  
    @Override  
    protected void onDraw(Canvas canvas) {  
        canvas.save();  
  
        // 裁剪矩形  
        canvas.clipRect(mRect);  
        canvas.drawColor(Color.RED);  
  
        canvas.restore();  
  
        canvas.save();  
  
        // 裁剪区域  
        canvas.clipRegion(mRegion);  
        canvas.drawColor(Color.RED);  
  
        canvas.restore();  
  
        // 为画布绘制一个边框便于观察  
        canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), mPaint);  
    }  
}  

大家看到,我在[0, 0, 200, 200]和[200, 200, 400, 400]的位置分别绘制了Rect和Region,它们两个所占大小是一样的:



画布因为和屏幕一样大,so~~我们看不出描边的效果,这时,我们将Canvas缩放至75%大小,看看会发生什么:

@Override  
protected void onDraw(Canvas canvas) {  
    // 缩放画布  
    canvas.scale(0.75F, 0.75F);  
  
    canvas.save();  
  
    // 裁剪矩形  
    canvas.clipRect(mRect);  
    canvas.drawColor(Color.RED);  
  
    canvas.restore();  
  
    canvas.save();  
  
    // 裁剪区域  
    canvas.clipRegion(mRegion);  
    canvas.drawColor(Color.RED);  
  
    canvas.restore();  
  
    // 为画布绘制一个边框便于观察  
    canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), mPaint);  
}  

这时我们会看到,Rect随着Canvas的缩放一起缩放了,但是Region依旧泰山不动地淡定:


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

推荐阅读更多精彩内容