Android 多主题切换 (theme + style) 及 selector/drawable 无法引用 ?attr 属性的问题

需求:

最近需要实现应用内多主题的需求: 要求应用内预置 10 个左右的主题配色方案, 用户可按需切换.
刚一拿到需求, 觉得这简单, 用 Android 的 theme + style 就可以搞定了. 没过多久就遇到了 attr 无法被 selector, drawable 等 xml 资源引用的大坑.
主题色切换的方案中文网络上一搜一大堆, 但没有哪位博主好心的提起这里还有这么深一个坑的...
这里先把解决方案简要叙述一下.

Android 预置多主题解决方案:

首先定义主题配色相关属性, 我将之单独写在 values/style_themes_attrs.xml 里.

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <attr name="color_bkg_main" format="color" />

    <attr name="color_action_bar" format="color" />
    <attr name="color_action_bar_text" format="color" />

    <attr name="color_primary" format="color" />
    <attr name="color_primary_pressed" format="color" />
    <attr name="color_primary_disabled" format="color" />

    <attr name="color_text" format="color" />
    <attr name="color_text_pressed" format="color" />
    <attr name="color_text_disabled" format="color" />
    <attr name="color_text_sub" format="color" />
    <attr name="color_text_hint" format="color" />

    <attr name="color_divider" format="color" />

</resources>

这些属性是应用全局的, 为便于引用, 不应该写在 <declare-styleable> 标签里.

然后定义各个主题配色的具体颜色值 (即给以上属性赋值), 我将之单独写在 values/style_themes.xml 里.

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <style name="theme_default" parent="BaseAppTheme">
        <item name="color_bkg_main">#f1f1f1</item>
        <item name="color_action_bar">#ee9c18</item>
        <item name="color_action_bar_text">#fff</item>
        <item name="color_primary">#ee9c18</item>
        <item name="color_primary_pressed">#80ee9c18</item>
        <item name="color_primary_disabled">#666</item>
        <item name="color_text">#202020</item>
        <item name="color_text_pressed">#80202020</item>
        <item name="color_text_disabled">#666</item>
        <item name="color_text_sub">#717171</item>
        <item name="color_text_hint">#b6b6b6</item>
        <item name="color_divider">#e2e2e2</item>
    </style>

    <style name="theme_sky" parent="theme_default">
        <item name="color_action_bar">#02a8f3</item>
        <item name="color_primary">#02a8f3</item>
        <item name="color_primary_pressed">#8002a8f3</item>
    </style>

    <style name="theme_grass" parent="theme_default">
        <item name="color_action_bar">#63d64a</item>
        <item name="color_primary">#63d64a</item>
        <item name="color_primary_pressed">#8063d64a</item>
    </style>

</resources>

theme_default 继承的 BaseAppTheme 是接到此需求前就已经在 AndroidManifest.xml 中赋值给 App 的 theme. 这个不重要, 你也可以继承系统自带的一些主题, 也可以不继承任何主题, 与实现需求关系不大, 怎么方便怎么来就成.
下面两个主题 theme_sky 和 theme_grass 继承自 theme_default , 这样做是为了不至于重复给颜色属性赋值, 比如文字颜色在这三个主题中是一样的, 继承了就可以避免再写一遍.
注意: 要想在 App 全局使用主题属性, 就必须保证每个 style 内的各个属性都是全的. 比如 theme_grass 里如果没有 color_primary 这个属性, 那么有代码引用这个属性时将发生异常. 注意是运行时异常哦~, 编译时不会报错的. 因此稳妥的做法还是写一个 base style , 然后其他 style 继承之, 这样起码不会崩溃.

最后将主题颜色属性用于各个View.

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="?attr/color_bkg_main"
    android:orientation="vertical" >

        ......
</LinearLayout>

或者省略 attr/ 也是可以的:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textSize="16sp"
    android:textColor="?color_text"
    android:text="Text Normal" />

最最后, 就是动态切换主题的代码了.

package lx.af.demo.activity.test;

import android.app.Activity;
import android.content.res.Resources;
import android.os.Bundle;
import android.support.annotation.AttrRes;
import android.support.annotation.ColorInt;
import android.util.TypedValue;
import android.view.View;

import lx.af.demo.R;

/**
 * author: lx
 * date: 17-2-24
 */
public class ThemeChangeActivity extends Activity implements View.OnClickListener {

    private static int sCurrentTheme = R.style.theme_default;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 这句必须放在 setContentView() 之前, 否则不生效.
        // 一般的做法是把这句话放在你的 BaseActivity 里面.
        setTheme(sCurrentTheme);

        setContentView(R.layout.activity_change_theme);
        findViewById(R.id.btn_switch_theme_default).setOnClickListener(this);
        findViewById(R.id.btn_switch_theme_sky).setOnClickListener(this);
        findViewById(R.id.btn_switch_theme_grass).setOnClickListener(this);

        // 演示如何用代码获取 attr 定义的主题相关的颜色
        View primaryColorPanel = findViewById(R.id.primary_color_panel);
        primaryColorPanel.setBackgroundColor(getCurrentPrimaryColor());
    }

    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.btn_switch_theme_sky:
                changeTheme(R.style.theme_sky);
                break;
            case R.id.btn_switch_theme_grass:
                changeTheme(R.style.theme_grass);
                break;
            case R.id.btn_switch_theme_default:
                changeTheme(R.style.theme_default);
                break;
        }
    }

    private void changeTheme(int theme) {
        // 改变主题时应该把当前主题设置保存进 SharedPreferences 里去.
        // 比如给这三个主题编号 101, 102, 103, 然后保存该编号, 供下次启动时设置为对应主题.
        // 这里省略了这部分逻辑, 只留主题相关逻辑.
        sCurrentTheme = theme;
        
        // 调用 Activity.recreate() 方法即可从 Activity.onCreate() 开始重新加载界面.
        // 该方法不会启动界面过场动画, 但重启时会有一下闪烁.
        recreate();
    }

    // 直接获取主题的主色颜色值
    public int getCurrentPrimaryColor() {
        return getColorByAttributeId(R.attr.color_primary);
    }

    // 使用代码获取主题属性颜色值的方法
    @ColorInt
    private int getColorByAttributeId(@AttrRes int attrIdForColor){
        TypedValue typedValue = new TypedValue();
        Resources.Theme theme = getTheme();
        theme.resolveAttribute(attrIdForColor, typedValue, true);
        return typedValue.data;
    }
}

这个 Activity 对应的布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="?attr/color_bkg_main"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/btn_switch_theme_sky"
        android:layout_width="match_parent"
        android:layout_height="45dp"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:layout_marginTop="10dp"
        android:background="?attr/color_primary"
        android:gravity="center"
        android:textColor="#fff"
        android:text="change theme sky" />

    <TextView
        android:id="@+id/btn_switch_theme_grass"
        android:layout_width="match_parent"
        android:layout_height="45dp"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:layout_marginTop="10dp"
        android:background="?attr/color_primary"
        android:gravity="center"
        android:textColor="#fff"
        android:text="change theme grass" />

    <TextView
        android:id="@+id/btn_switch_theme_default"
        android:layout_width="match_parent"
        android:layout_height="45dp"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:layout_marginTop="10dp"
        android:background="?attr/color_primary"
        android:gravity="center"
        android:textColor="#fff"
        android:text="change theme default" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:layout_marginTop="10dp"
        android:padding="25dp"
        android:background="?attr/color_primary"
        android:orientation="vertical" >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="16sp"
            android:textColor="?attr/color_text"
            android:text="Text Normal" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="16sp"
            android:textColor="?attr/color_text_pressed"
            android:text="Text pressed" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="16sp"
            android:textColor="?attr/color_text_sub"
            android:text="Text sub" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="16sp"
            android:textColor="?attr/color_text_hint"
            android:text="Text hint" />
    </LinearLayout>

    <FrameLayout
        android:id="@+id/primary_color_panel"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:layout_marginTop="10dp" />

</LinearLayout>

这样就解决了多主题的问题. 新加入一个主题也非常简单, 只要继承 theme_default, 复写一些颜色值就可以了.
下面开始掉坑.

坑!

此坑很凌乱, 各种资源对象在各个版本表现都不一样. 但成坑原因其实基本一致. 大体就是, xml 资源中 (比如 drawable) 如果引用了 attr 定义的颜色, 再引用该 xml 资源时都有可能有问题. 我们暂且称此类资源为 attr-xml 资源.

注意: 因为手头最低版本的设备为 Android 4.1.2 (API level 16), 最高为 Android 7.0 (API level 24), 超出这个范围就没有实测了.

AppCompat

在 Android 6.0 (API level 23) 以下设备上, 如果 Activity 不是继承自 v23.0 以上的 AppCompat 包中的 AppCompatActivity 的话, 使用 attr-xml 资源可能出现各种奇怪问题.
比如 ColorStateList 的 xml 资源在最终显示时会被渲染为红色.
因为解决办法很简单, 所以不对由此产生的各种问题再做具体讨论了.

解决办法:
举例, 比如有以下 ColorStateList 资源 res/color/color_selector_primary.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true" android:color="?attr/color_primary_pressed"/>
    <item android:state_enabled="false" android:color="?attr/color_primary_disabled"/>
    <item android:color="?attr/color_primary"/>
</selector>

被 TextView 的 android:textColor=@color/color_selector_primary 引用就会渲染为红色字体.

方法1: Activity 继承 AppCompatActivity 就可以了 (v7兼容库版本大于 v23.0).
compile 'com.android.support:appcompat-v7:25.2.0'
public class ThemeChangeActivity extends AppCompatActivity { ... }

方法2: 改为使用相关的兼容库控件, 比如 TextView 改为 AppCompatTextView:

    <android.support.v7.widget.AppCompatTextView
        android:id="@+id/btn_switch_theme_grass"
        android:layout_width="match_parent"
        android:layout_height="45dp"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:layout_marginTop="10dp"
        android:gravity="center"
        android:textColor="@color/color_selector_primary"
        android:text="change theme grass" />

很显然, 第一种方法更简单. 除了 dialog 等少数地方不能用这个方法解决, 其他地方用继承 AppCompatActivity 的方法解决最方便. 第二种方法还得逐个改.
下文中的讨论都是在 Activity 已经继承了 AppCompatActivity 的基础上进行的.

background

这个才是真坑...
上面讲为了 android:textColor 属性设置 selector (ColorStateList), 但其实我们更常用的是为 android:background 属性设置 selector. 而 background 只能接受 drawable 类型的 selector.
举例, 有如下 selector 类型的 drawable res/drawable/selector_primary.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape>
            <solid android:color="?attr/color_primary_pressed" />
        </shape>
    </item>

    <item>
        <shape>
            <solid android:color="?attr/color_primary" />
        </shape>
    </item>
</selector>

在 Android 5.0 (API level 21) 以下机器上, drawable xml 资源中引用 attr , 如果在 layout 布局中引用这样的 drawable 资源, 则会引发崩溃. 以下为截取的崩溃 trace 中的片段:

03-14 11:21:30.092  4920  4920 E AndroidRuntime: Caused by: java.lang.UnsupportedOperationException: Can't convert to color: type=0x2
03-14 11:21:30.092  4920  4920 E AndroidRuntime:    at android.content.res.TypedArray.getColor(TypedArray.java:326)
03-14 11:21:30.092  4920  4920 E AndroidRuntime:    at android.graphics.drawable.GradientDrawable.inflate(GradientDrawable.java:951)
03-14 11:21:30.092  4920  4920 E AndroidRuntime:    at android.graphics.drawable.Drawable.createFromXmlInner(Drawable.java:895)
03-14 11:21:30.092  4920  4920 E AndroidRuntime:    at android.graphics.drawable.StateListDrawable.inflate(StateListDrawable.java:183)
03-14 11:21:30.092  4920  4920 E AndroidRuntime:    at android.graphics.drawable.Drawable.createFromXmlInner(Drawable.java:895)
03-14 11:21:30.092  4920  4920 E AndroidRuntime:    at android.graphics.drawable.Drawable.createFromXml(Drawable.java:828)
03-14 11:21:30.092  4920  4920 E AndroidRuntime:    at android.content.res.Resources.loadDrawable(Resources.java:1933)
03-14 11:21:30.092  4920  4920 E AndroidRuntime:    ... 31 more

博主目前暂未找到解决该问题的办法. 如有好的方法, 还请指点.
因此只能尝试绕过, 方式是在代码中手动组装 ColorStateList, Drawable 等可用做 background 背景的资源.

ColorStateList 可以这么组装:

private static ColorStateList createColorStateList(Context context) {
  return new ColorStateList(
      new int[][]{
          new int[]{android.R.attr.state_pressed}, // pressed state.
          StateSet.WILD_CARD,                      // other state.
      },
      new int[]{
          getThemeAttrColor(context, R.attr.color_primary_pressed),  // pressed state.
          getThemeAttrColor(context, R.attr.color_primary),          // other state.
      });
}

drawable 可以这么组装:

public static Drawable createDrawableSelector(Context context) {
    StateListDrawable stateDrawable = new StateListDrawable();
    GradientDrawable normalDrawable = new GradientDrawable();
    GradientDrawable pressedDrawable = new GradientDrawable();
    GradientDrawable disabledDrawable = new GradientDrawable();

    int[][] states = new int[4][];
    states[0] = new int[]{android.R.attr.state_enabled, android.R.attr.state_pressed};
    states[1] = new int[]{android.R.attr.state_enabled, android.R.attr.state_focused};
    states[3] = new int[]{-android.R.attr.state_enabled}; // disabled state
    states[2] = new int[]{android.R.attr.state_enabled};

    // 为各种状态下的 drawable 设置 attr 颜色值
    normalDrawable.setColor(getColorByAttrId(context, R.attr.color_primary));
    pressedDrawable.setColor(getColorByAttrId(context, R.attr.color_primary_pressed));
    disabledDrawable.setColor(getColorByAttrId(context, R.attr.color_primary_disabled));

    // 为各种状态下的 drawable 设置圆角等属性. 仅举一例, 不详述.
    normalDrawable.setCornerRadius(5);
    pressedDrawable.setCornerRadius(5);
    disabledDrawable.setCornerRadius(5);

    stateDrawable.addState(states[0], pressedDrawable);
    stateDrawable.addState(states[1], pressedDrawable);
    stateDrawable.addState(states[3], disabledDrawable);
    stateDrawable.addState(states[2], normalDrawable);

    return stateDrawable;
}

@ColorInt
public static int getColorByAttrId(Context context, @AttrRes int attrIdForColor) {
    TypedValue typedValue = new TypedValue();
    Resources.Theme theme = context.getTheme();
    theme.resolveAttribute(attrIdForColor, typedValue, true);
    return typedValue.data;
}

把这种由代码生成的 drawable 通过 View.setBackground(Drawable background) 方法设置, 效果即可生效.
可以据此封装一些常用控件出来, 比如 TextView, 以简化相关工作.
但 workaround 的解决方案, 无论怎么简化, 都是比较恶心的.

文末 大神的文章 中提到, 使用 6.0 的矢量图 (vector drawable, 可通过 AppCompat 包向下兼容) 可以不受此问题影响, 可以解决 drawable 引用 attr 颜色问题. 这个没有实测. 因为即使这种方法可用, 博主也没有时间将所有 drawable xml 全部改为矢量图 (会被 UI 打死的).
小伙伴们有兴趣也可以试下这种方法.

原因:

不关心起因的同学可略过此节.
简单来说, 这个问题是 API < 21 的系统中, Resources.getColor() 方法没有接收 theme 参数导致的.
在 Android 5.0 以下, 我们在代码中获取颜色值, 只能用这个方法:
Resoueces.getColor(@ColorRes int id)
上面那条语句在 5.0 以上的SDK, android studio (lint) 会给我们一条警告, 告诫我们方法已 Deprecated, 建议使用下面的方法:
Resources.getColor(@ColorRes int id, @Nullable Theme theme)
ContextCompat 类中也提供了一个兼容的方法:
ContextCompat.getColor(Context context, @ColorRes int id)

这个方法最终就是在调用类似下面的罗辑 (之所以说 "类似", 是因为各版本的兼容包略有不同):

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  return context.getResources().getColor(id, context.getTheme());
} else {
  return context.getResources().getColor(id);
}

而我们定义的 attr 颜色值, 是直接和主题 theme 相关的. 因此在早于 5.0 的版本上, theme 参数没有被逐层传递下去, 相关控件就肯定取不到对应的颜色值. drawable 的问题是类似的.

又一个坑

这是一个小坑, 或者说, 这是一个我们编程时应避开的错误. 这里一并提出, 以免有小伙伴踩到坑.
本文讨论的 attr 资源, 因为都是和主题直接相关的, 因此一定要注意, 不同的 Context 获取到的资源会有不同!
比如有时候我们为了方便, 在应用全局保存了一个 Application 的实例, 这样就可以用静态方法取到颜色等资源. 比如有以下简单的帮助类:

public final class ResourceUtils {

    private static Application sApp;
    private static Resources sRes;

    private ResourceUtils() {
    }

    public static void init(Application app) {
        sApp = app;
        sRes = app.getResources();
    }

    public static String getString(int resId) {
        return sRes.getString(resId);
    }

    public static int getColor(int resId) {
        return sRes.getColor(resId);
    }
}

上面这个类可以在 Application.onCreate() 里面初始化, 然后就可以愉快的以静态方法获取资源了.
但用这种方法, 是在用 ApplicationContext 获取资源, 其行为和用 Activity 的 Context 获取资源会有不同, 在资源和主题 theme 相关联时, 其取到的资源也会不同. 具体请学习 ContextWrapper 相关知识 (博主还没来得及细研究, 就不展开写了, 以免误导大家...) .
因此 取和主题相关的资源时, 尽量用当前 Activity 的 Context 就是了.


参考:
google code issue
stackoverflow question
Styling Colors & Drawables w/ Theme Attributes

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,048评论 25 707
  • 前言 随着一款APP应用功能的不断完善,用户群体的不断增多,APP的更新也就不仅仅局限于功能需求,如何做好良好的用...
    采蘑菇的里奥马阅读 27,766评论 43 146
  • 样式和主题 样式是指为 View 或窗口指定外观和格式的属性集合。样式可以指定高度、填充、字体颜色、字号、背景色等...
    牧童遥指2000阅读 9,277评论 3 9
  • 我们应该都记得我们初恋是什么时候,跟什么人,有过哪些不愉快又有过哪些小浪漫,之所以我们都记得这些是因为,初恋让我们...
    林沐沐阅读 192评论 0 0
  • 今天2016年12月12日,人家说25岁是个坎,到了便马上觉得离30岁很近很近,就像一个摆锤稍不注意马上就要砸过来...
    小霞儿阅读 343评论 0 0