Android中的各种Drawable类详解

Drawable简介

图形图像的绘制需要在画布上进行操作和处理,但是绘制需要了解很多细节以及可能要进行一些复杂的处理,这样就会增加学习和使用的成本,因此系统提供了一个被称之为Drawable的类来进行绘制处理。通过这个类可以减少我们的绘制工作和使用成本,同时系统也提供了众多的Drawable的派生类比如单色、图形、位图、裁剪、动画等等来完成一些常见的绘制需求。Drawable是一个抽象的可绘制类。他主要是提供了一个可绘制的区域bound属性以及一个draw成员函数,不同的派生类通过重载draw函数的实现而产生不同的绘制结果。

视图View的背景就是一个Drawable对象,在视图需要进行绘制而调用draw函数时,内部的一部分实现会将调用委托给背景属性mBackground这个Drawable对象的draw函数来完成背景的绘制,而当对视图调用setBackgroundXXXX方法进行背景设置时会根据不同的函数名来创建不同的Drawable派生类。下面是Drawable基类中的一些常用方法介绍:

Drawable类的核心是draw函数的实现,这个函数是一个抽象函数,派生类必须要实现他,函数的入参是一个Canvas画布对象,所有需要绘制的东西都最终绘制到画布上面去:

    //抽象的绘制方法
    public abstract void draw(Canvas canvas);

既然绘制提供了画布上下文,那么就还需要提供一个可绘制的区域,下面方法就是用来指定绘制的区域。Drawable在绘制调用draw函数之前必须要先指定绘制的区域,这个区域也是Canvas中要绘制的区域。一旦用户改变了绘制区域时会激发onBoundsChange方法,派生类可以重载onBoundsChange来实现区域变更的处理。

// 获取和设定可绘制区域。
 public final Rect getBounds()
 public void setBounds(int left, int top, int right, int bottom)
 public void setBounds(Rect bounds) 

你可以用下面的方法来设置显示的级别,以便进行一些绘制时的区间和条件控制,这个属性并不是所有Drawable派生类都能用到,具体用到的派生类我会在下面说明这个属性的意义。如果设置有变化则会调用onLevelChange,派生类可以重载onLevelChange来实现级别变化的更新处理:

//设置显示的级别,从0到10000
public final boolean setLevel(int level)
public final int getLevel()

你可以设置绘制的状态列表,以便实现在不同的状态下绘制不同的内容。这个方法是并不是所有的派生类都有作用。在下面的StateListDrawable类中可以设置某些状态下的可绘制对象,而你通过设置状态时就会显示在特定状态下的可绘制对象:

//设置状态列表
public boolean setState(final int[] stateSet)
public int[] getState()

下面是常用状态的定义:

 R.attr.state_window_focused
 R.attr.state_pressed     
 R.attr.state_selected
 R.attr.state_focused
 R.attr.state_enabled

状态属性并不会对所有Drawable的派生类都有意义。一旦设置有变化会激发onStateChange方法,派生类可以实现这个方法来实现状态变更的处理。

上面的几个属性的设置都会激发onXXXX系列方法来让派生类在属性值发生变化时进行更新处理,而有时候我们希望一些更新逻辑不是在派生类中处理,而是在持有Drawable对象的对象上或者委托给外部进行处理,因此可以调用如下方法:

//设置绘制更新委托对象
 public final void setCallback(Callback cb) 
 //方法表示让自己无效,通知回调Callback进行更新处理。
 public void invalidateSelf() 
 public void scheduleSelf(Runnable what, long when) 

上面的几个方法要求外部实现一个Callback接口以便接收Drawable对象的各种变化。接口的定义如下:

 public static interface Callback {
        //自生需要重绘时的处理逻辑
        public void invalidateDrawable(Drawable who);
       //下面两个主要用于动画处理
        public void scheduleDrawable(Drawable who, Runnable what, long when);
        public void unscheduleDrawable(Drawable who, Runnable what);

    }

你也可以设置绘制的内容是否可见。如果一变化会调用callback的invalidateDrawable方法。

  public boolean setVisible(boolean visible, boolean restart) 
  public final boolean isVisible()

你也可以设置绘制内容的透明度。派生类必须实现这个函数

  public abstract void setAlpha(int alpha);

需要获取不透明度状态,派生类要实现这个函数。

 public int getOpacity()

函数的返回有三个可选值:
PixelFormat.OPAQUE:不透明
PixelFormat.TRANSPARENT:透明
PixelFormat.TRANSLUCENT:半透明

一般情况下可绘制内容都是简单的显示在界面上的,但有时候我们需要增加一些滤镜效果。这时候我们可以用如下方法来完成:

 public void setColorFilter(int color, PorterDuff.Mode mode) {
 public abstract void setColorFilter(ColorFilter cf);
 public void clearColorFilter()  //清除颜色过滤器。

设置颜色滤镜,作用是把当前的Drawable和指定的颜色进行各种模式的融合。ColorFilter是一个抽象的基类, 其中的一个派生类就是PorterDuffColorFilter,这个类的构造方法接收一个颜色,以及和这个颜色融合的方式PorterDuff.Mode。PorterDuffColorFilter中的颜色作为S ,下面是各种融合的模式算法:

 public enum Mode {
        /** [0, 0] */
        CLEAR       (0),
        /** [Sa, Sc] */
        SRC         (1),
        /** [Da, Dc] */
        DST         (2),
        /** [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] */
        SRC_OVER    (3),
        /** [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc] */
        DST_OVER    (4),
        /** [Sa * Da, Sc * Da] */
        SRC_IN      (5),
        /** [Sa * Da, Sa * Dc] */
        DST_IN      (6),
        /** [Sa * (1 - Da), Sc * (1 - Da)] */
        SRC_OUT     (7),
        /** [Da * (1 - Sa), Dc * (1 - Sa)] */
        DST_OUT     (8),
        /** [Da, Sc * Da + (1 - Sa) * Dc] */
        SRC_ATOP    (9),
        /** [Sa, Sa * Dc + Sc * (1 - Da)] */
        DST_ATOP    (10),
        /** [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc] */
        XOR         (11),
        /** [Sa + Da - Sa*Da,
             Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)] */
        DARKEN      (12),
        /** [Sa + Da - Sa*Da,
             Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)] */
        LIGHTEN     (13),
        /** [Sa * Da, Sc * Dc] */
        MULTIPLY    (14),
        /** [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] */
        SCREEN      (15),
        /** Saturate(S + D) */
        ADD         (16),

        OVERLAY     (17);
};

我们可以通过上面的模式设置中的定义来实现将Drawable中的内容和特定的颜色进行特殊的结合渲染处理。

你可以获取当前的可绘制对象,这个主要是针对有动画的可绘制对象

 public Drawable getCurrent()

你可以获取绘制对象的实际高度,如果没有则为-1,比如位图的实际高宽就可能比绘制的区域要大或者小

public int getIntrinsicHeight()
public int getIntrinsicWidth()

对于有些可绘制对象可以共享其中的资源而不需要有多个副本,因此可以通过如下方法来得到一个内部的保存的常量状态:

 public ConstantState getConstantState()

而常量状态是一个抽象接口ConstantState,其中的2个方法:

 public static abstract class ConstantState {
        public abstract Drawable newDrawable();
        public Drawable newDrawable(Resources res);
}

用于创建新的可绘制对象,新可绘制对象有可能会共享原来的资源,有可能不会,对于位图来说就会共享。如果我们要单独制作一份不共享的可绘制对象,就调用如下方法来返回:

 public Drawable mutate() 

上面是Drawable基类的一些属性和方法的介绍,下面分别介绍系统提供的Drawable的派生类。

ColorDrawable 颜色可绘制类

颜色可绘制类。类构造时指定一个颜色,或者调用setColor指定颜色,setAlpha函数会把设置的透明度和本来的颜色的透明度相乘。这个可绘制类用来实现简单的单颜色的绘制。

BitmapDrawable 位图可绘制类

位图可绘制类。在构造时指定一个Bitmap对象或者一个位图文件。位图显示时需要指定:像素和设备尺寸的映射,显示的位置,显示模式三种属性

因为位图里面的像素是一个抽象的概念他没有具体的物理尺寸,而设备的像素则是物理的,他有大小。因此需要有一个方法来指定位图像素转化为物理像素的映射关系,这样位图的像素才可以真正的显示在设备上。下面就是提供的三种映射设置方法:

//画布上的密度比值
 public void setTargetDensity(Canvas canvas)

//当前屏幕的密度比值
public void setTargetDensity(DisplayMetrics metrics)

//指定密度比值,注意这里的密度是DisplayMetrics中的DENSITY_XXX。
public void setTargetDensity(int density)

系统会根据密度公式:** 位图尺寸 * density / 屏幕的密度 ** 来将位图显示在具体的设备上。

你需要为位图指定绘制到画布上的位置以及缩放到区域的方式:

//这里的android.view.Gravity参考值。比如显示在左上角,比如拉伸显示在整个画布中等等
public void setGravity(int gravity)

图片的默认显示是一张图片进行拉伸,你可以设置平铺的方式。如果设置了这种模式则图像是平铺显示在画布上的:

public final void setTileModeY(Shader.TileMode mode)
public void setTileModeX(Shader.TileMode mode)

为了绘制更加优质以及性能的优化,在绘制位图会使用到画笔Paint类进行一些属性设置。而下面的一些属性的实现其实就是简单的委托给了Paint类:

//设置是否使用抗锯齿功能,会消耗较大资源,绘制图形速度会变慢。  
void setAntiAlias(boolean aa);  
    
//设定是否使用图像抖动处理,会使绘制出来的图片颜色更加平滑和饱满,图像更加清晰  
void setDither(boolean dither);  
     
// 如果该项设置为true,则图像在动画进行中会滤掉对Bitmap图像的优化操作,加快显示速度,本设置项依赖于dither和xfermode的设置  
 void setFilterBitmap(boolean filter);  
   
//设置颜色过滤器,可以在绘制颜色时实现不用颜色的变换效果 
void setColorFilter(ColorFilter colorfilter);  
    

ClipDrawable 裁剪可绘制类

这个是一个Drawable的修饰派生类,构造函数是对某个Drawable对象进行裁剪显示。裁剪的范围设置通过setLevel来设置,0为全部不显示,10000为全部显示,设置了范围之后还需要设置裁剪的方向和从什么地方开始裁剪,参考构造函数:

   public ClipDrawable(Drawable drawable, int gravity, int orientation)
  • gravity指定从什么地方开始裁剪,比如Gravity.CENTER表示从中间向两边;Gravity.LEFT左边开始裁剪。
  • orientation是指裁剪的方向有水平HORIZONTAL和VERTICAL,也可以同时方向。

ClipDrawable主要的应用场景是让某个Drawable逐渐进行展开或者收缩显示,而这都是通过调用setLevel方法来完成的。

AnimationDrawable 帧动画可绘制类

这是一个容器可绘制类,用于定期播放一批指定的Drawable。不可以和ClipDrawable结合起来播放逐渐展开的动画。这个类里面的一个Drawable对象就是一帧。下面的一系列方法分别用来管理这些帧动画:

public void addFrame(Drawable frame, int duration)  //添加要播放的Drawable帧以及播放时长。
public int getDuration(int i)   //每帧时长
public Drawable getFrame(int index) //获取帧
public int getNumberOfFrames()   //得到总帧数
public void setOneShot(boolean oneShot)  //单次还是循环

InsetDrawable缩进可绘制类

这个是一个容器类。实现容器内Drawable四个方向缩进或者某个方向的缩进。如果为负数则是外缩进,这个类也可以实现缩放的功能,注意这里不是裁剪,而是会有缩放效果。

 public InsetDrawable(Drawable drawable, int inset);
 public InsetDrawable(Drawable drawable, int insetLeft, int insetTop,
                         int insetRight, int insetBottom);

ScaleDrawable 缩放可绘制类

这个一个容器类,可以用来缩放另外一个Drawable对象。你可以在构造中指定缩放的比例和缩放的中心点,注意的是所缩放的Drawable对象的level不能为0:

    public ScaleDrawable(Drawable drawable, int gravity, float scaleWidth, float scaleHeight)
  • gravity是指缩放的中心点(也是初始缩小后显示的位置)。
  • scaleWidth,scaleHeight的值是缩放的比例,注意是指缩放了多少,而不是缩放为多少,缩放的取值大于0, 当setLevel为0时将显示为缩放了多少,而设置为10000时就是原图。

RotateDrawable 旋转可绘制类

这个可绘制类不支持代码建立,只支持XML文件构造。可以指定开始角度,结束角度,旋转的中心点。 最后可以通过setLevel来控制从开始到结束的角度中间的过程。下面是一个XML来设置旋转可绘制类的方法:

<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android" 
    android:fromDegrees="30"
    android:toDegrees="90"
    android:drawable="@drawable/b0"
    android:pivotX="50%"
    android:pivotY="50%"
    >
    
</rotate>

ShapeDrawable 形状可绘制类。

用于建立各种形状。对于形状的边线来说可以通过如下方法来获取一个Paint对象并设置画笔和阴影效果:

public Paint getPaint()

您可以在类的构造函数以及特定的方法:

public ShapeDrawable(Shape s) 
public void setShape(Shape s)

中设置需要显示的形状。Shape是一个抽象的形状类,您可以使用如下具体的形状类:

  • ArcShape 弧形。构造指定开始角度和增加的角度,是顺时针方向, 这里0度是在正右边。

  • OvalShape 椭圆形。

  • PathShape 路径。相当于自定义图形。构造函数的后两个参数是缩放因子。也就是x轴缩放: bounds宽度/stdWidth, y轴缩放:bounds高度/stdHeight。也就是会对绘制的位置和长度进行放大缩小。 比如设置某个点的位置为(10,10)如果缩放为2则是(20,20)。也就是说构造函数中的stdWidth, stdHeight是绘制path时设定的高度和宽度。因此在绘制时到具体的高度和宽度则要进行放大或者缩小。

  • RectShape 矩形。

  • RoundRectShape 圆角矩形。构造函数如下:

public RoundRectShape(float[] outerRadii, RectF inset,

                          float[] innerRadii)

这里要指定外圆角和内圆角,以及内外圆角的距离inset。注意内外圆角都是长度为8,分别为每个方位的圆角执行x,y的圆形半径,这里outerRadii, innerRadii一个可以为空,表示无。

PaintDrawable 圆角矩形可绘制类。

这个类是ShapeDrawable的派生类。是圆角矩形RoundRectShape的简化版本,只会设置外圆角,只需要设置一个或者8个即可。一般用这个类来设置圆角按钮背景。

LayerDrawable 图层可绘制类。

用于重叠多个可绘制对象。这是一个容器可绘制类,里面可以添加多个子可绘制对象,每个子可绘制对象就是其中的一层。

public LayerDrawable(Drawable[] layers)  构造函数,后面的显示在上面。

下面是一系列管理图层的方法:

public void setId(int index, int id)  //为某个索引设置标识ID
public int getId(int index)  //获取某个索引下的标识ID
public Drawable findDrawableByLayerId(int id)  //根据ID得到某个可绘制对象
public int getNumberOfLayers()  //获取层次的总的数量

你还可以为里面某个子绘制对象设置缩进的值来实现同时显示多层的效果:

   public void setLayerInset(int index, int l, int t, int r, int b)  //设置某层的缩进。

您也可以动态的替换掉某个标识ID下对应的Drawable对象:

  public boolean setDrawableByLayerId(int id, Drawable drawable)  //替换,某子层的可绘制对象,如果为null则不显示这层了。

TransitionDrawable 淡入淡出可绘制类

这个类是LayerDrawable的子类,可以实现动画效果。这个类只支持2个子可绘制对象,所展示的效果是第0层慢慢变淡消失,第一层慢慢清楚显示。

我们可以用如下方法来开始淡入淡出动画并设置时长:

 public void startTransition(int durationMillis)  //开始切换

如果我们完成了淡入淡出动画,这时候想还原则动画可以调用如下方法来设置动画以及时长:

 public void reverseTransition(int duration)   //上面的逆操作,如果没有切换则跟startTransition是一样的功能。

如果我们不想动画而直接恢复为显示0层则可以调用如下方法:

 public void resetTransition()  //恢复,只显示0层。

下面方法设置在淡入淡出动画时是同时进行还是分别进行,如果设置为true则是同时进行,否则就是分别进行:

public void setCrossFadeEnabled(boolean enabled)

LevelListDrawable 等级显示可绘制类。

这是一个容器可绘制类,用于在不同的情况下显示不同的可绘制对象的场景。您可以调用如下方法来设置某个等级区间值下显示的可绘制对象:

   public void addLevel(int low, int high, Drawable drawable)

这个方法添加某个Drawable对象,并且指定具备显示的最大和最小的阈值。这样当调用对象的setLevel方法指定一个级别时则只有这个级别所在的区域的Drawable对象才会显示。

这种类的实用场景在哪里呢? 这个类相当是在特定level下只显示某个子可绘制对象。注意这里如果有重叠区域只会取小索引的那个,也就是前面的那个。举例来说,假设我们通过addLevel设置了(1, 100, a); (200, 300, b)2个区域的Drawable。那么当我们调用setLevel(80)时,系统将会显示a。

StateListDrawable 不同状态下显示可绘制类

这是一个容器可绘制类。用于在不同的状态下显示不同的可绘制对象的情景。您可以调用

  public void addState(int[] stateSet, Drawable drawable)

来设定某些状态显示那个可绘制对象,因此这个对象也只是用于视图的绘制。状态stateSet可以使用:**android.R.attr.state_XXX ** 中列出的值。当为某个视图设置了一个StateListDrawable类型的背景时,那么当视图处理某个state时,背景就会根据特定的state不同而显示不同的背景。

NinePatchDrawable .9格式的可绘制类

.9格式的可绘制类,一般用于那些需要特定区域拉伸显示的场景,比如气泡对话框。.9格式的图片一般用png文件来实现。

GradientDrawable 渐变的可绘制类。

渐变可绘制类提供了一种多颜色过渡显示效果的可绘制类。根据颜色过渡的方式系统默认提供的渐变有线性渐变(LINEAR_GRADIENT)放射渐变(RADIAL_GRADIENT)旋转扫描渐变(SWEEP_GRADIENT)三种类型。你可以通过如下方法来设置可绘制对象的渐变类型:

 public void setGradientType(int gradient)  //渐变的类型

既然是渐变那么就应该有一组渐变的颜色列表,因此你可以通过如下方法来设置渐变的颜色列表:

public void setColors(int[] colors)  //设置渐变的颜色列表。

对于线性渐变来说,我们可以设置渐变的方向:

public void setOrientation(Orientation orientation)  

对于放射渐变来说,我们可以设置渐变的半径长度:

 public void setGradientRadius(float gradientRadius) 

对于放射渐变和旋转扫描渐变类说,我们可以设置渐变开始中心点的位置:

public void setGradientCenter(float x, float y) 

我们还可以设置在画布内某个特殊的形状下渐变

 public void setShape(int shape)  

系统可支持如下的形状:

RECTANGLE:    矩形
OVAL:         圆形
LINE :        线形
RING:         圆环形

如果形状是RECTANGLE或者RING则可以设置圆角的半径以及每个圆环的半径:

public void setCornerRadius(float radius)  //设置圆角半径
 public void setCornerRadii(float[] radii)   //设置圆环半径

PictureDrawable 图像可绘制类。

通过类提供的构造方法来设置一个Picture图像对象。并将图像对象中内容绘制到画布中去。Picture类是一个抽象的图像对象,他可以从一个流中构造出来,也可以写到流中。Picture对象要比Bitmap对象要轻,因为他只是记录绘制的操作,而不是记录图像的像素。因此他更适合用来保存一些矢量图信息。我们可以从Picture类中提供的beginRecording方法来构建出一个抽象的画布:

  public Canvas beginRecording(int width, int height) 

这个方法需要指定画布的高度和宽度,并返回一个抽象的画布对象。这样就可以在这个返回的画布中绘制东西了,记得绘制完成后需要调用:

public void endRecording() 

来结束绘制。我们也可以把一个Piture对象的内容绘制到其他画布中去:

public void draw(Canvas canvas)  //绘制到某个画布去

PictureDrawable类的draw实现就是将绘制的工作委托给了Picture对象的draw方法来完成的。

同样我们也可以序列化和反序列化某个Picture对象:

 public static Picture createFromStream(InputStream stream);
 public void writeToStream(OutputStream stream)


最后欢迎大家访问我的github站点,关注欧阳大哥2013

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

推荐阅读更多精彩内容