BottomNavigationView

Android Support Library 25.0.0 版本中挥唠,新增加了一個(gè)API –> BottomNavigationView – 底部導(dǎo)航視圖。

先來看看這個(gè)控件的實(shí)現(xiàn)效果。


1.gif
2.gif

基本使用

使用起來也很簡單
首先在xml中引入該控件

<android.support.design.widget.BottomNavigationView
        android:id="@+id/bottom_navi_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        app:itemBackground="@android:color/white"
        app:menu="@menu/menu_bottom_navi" />

該控件的基本屬性有:


3.png
app:itemIconTint : 設(shè)置菜單圖標(biāo)著色
app:itemTextColor : 設(shè)置菜單文本顏色
app:menu : 設(shè)置菜單
app:itemBackground : 設(shè)置導(dǎo)航欄的背景色

@menu/menu_buttom_navi

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/menu_recent"
        android:icon="@drawable/ic_history_black_24dp"
        android:title="@string/menu_recents" />
    <item
        android:id="@+id/menu_favorites"
        android:icon="@drawable/ic_favorite_black_24dp"
        android:title="@string/menu_favorites" />
    <item
        android:id="@+id/menu_nearby"
        android:icon="@drawable/ic_place_black_24dp"
        android:title="@string/menu_nearby" />
    <item
        android:id="@+id/menu_navi"
        android:icon="@drawable/ic_navigation_black_24dp"
        android:title="@string/menu_navigation" />
</menu>

與定義普通menu布局一樣。

接下來是Java代碼

bottomNaviView = (BottomNavigationView) findViewById(R.id.bottom_navi_view);
        bottomNaviView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                switch (item.getItemId()) {
                    case R.id.menu_recent:
                        break;
                    case R.id.menu_favorites:
                        break;
                    case R.id.menu_nearby:
                        break;
                    case R.id.menu_navi:
                        break;
                }
                return true;
            }
        });

對BottomNavigationView設(shè)置選擇監(jiān)聽器就可以做一些item切換事件了。

注意事項(xiàng)

  • 底部導(dǎo)航欄默認(rèn)高度是56dp
  • 菜單只能是3-5個(gè)

源碼分析

BottomNavigationView 有幾個(gè)先關(guān)的重要類

  • BottomNavigationView
  • BottomNavigationMenu
  • BottomNavigationMenuView
  • BottomNavigationPresenter

它的設(shè)計(jì)有點(diǎn)類似于開發(fā)中的 MVP模式隙袁。

先來看 BottomNavigationView 的構(gòu)造函數(shù)

public BottomNavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        ThemeUtils.checkAppCompatTheme(context); //檢測當(dāng)前主題
        // Create the menu
        mMenu = new BottomNavigationMenu(context);
        mMenuView = new BottomNavigationMenuView(context);
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        params.gravity = Gravity.CENTER;
        mMenuView.setLayoutParams(params);
        mPresenter.setBottomNavigationMenuView(mMenuView);
        mMenuView.setPresenter(mPresenter);
        mMenu.addMenuPresenter(mPresenter);
        // Custom attributes

        // ...省略若干代碼

        if (a.hasValue(R.styleable.BottomNavigationView_menu)) {
            inflateMenu(a.getResourceId(R.styleable.BottomNavigationView_menu, 0)); // 加載menu
        }
        a.recycle();
        addView(mMenuView, params); //
        mMenu.setCallback(new MenuBuilder.Callback() { // 設(shè)置監(jiān)聽器
            @Override
            public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
                return mListener != null && mListener.onNavigationItemSelected(item);
            }
            @Override
            public void onMenuModeChange(MenuBuilder menu) {}
        });
    }

ThemeUtils.checkAppCompatTheme(context) 是用來檢測當(dāng)前主題的痰娱。代碼很簡單弃榨,就是判斷是否有 colorPrimary 屬性。

class ThemeUtils {
    private static final int[] APPCOMPAT_CHECK_ATTRS = {
            android.support.v7.appcompat.R.attr.colorPrimary
    };
    static void checkAppCompatTheme(Context context) {
        TypedArray a = context.obtainStyledAttributes(APPCOMPAT_CHECK_ATTRS);
        final boolean failed = !a.hasValue(0);
        if (a != null) {
            a.recycle();
        }
        if (failed) {
            throw new IllegalArgumentException("You need to use a Theme.AppCompat theme "
                    + "(or descendant) with the design library.");
        }
    }
}

構(gòu)造函數(shù)中接下來調(diào)用了 inflateMenu()

public void inflateMenu(int resId) {
        mPresenter.setUpdateSuspended(true);
        getMenuInflater().inflate(resId, mMenu);
        mPresenter.initForMenu(getContext(), mMenu);
        mPresenter.setUpdateSuspended(false);
        mPresenter.updateMenuView(true);
    }

可以看出都是調(diào)用的BottomNavigationPresenter的函數(shù)梨睁。

setUpdateSuspended(true) – 暫停修改menu

setUpdateSuspended(false) — 可以修改menu

在initForMenu()一前一后鲸睛,設(shè)置一個(gè)標(biāo)志來表示當(dāng)前正在操作menu。

重點(diǎn)來看 BottomNavigationPresenter.initForMenu()

@Override
    public void initForMenu(Context context, MenuBuilder menu) {
        mMenuView.initialize(mMenu);
        mMenu = menu;
    }

函數(shù)內(nèi)部又是調(diào)用的 BottomNavigationMenuView.initialize()

@Override
    public void initialize(MenuBuilder menu) {
        mMenu = menu;
        if (mMenu == null) return;
        if (mMenu.size() > mActiveButton) {
            mMenu.getItem(mActiveButton).setChecked(true);
        }
    }

代碼中就是一些簡單的初始化操作坡贺。

接下來看 BottomNavigationPresenter.updateMenuView(true)

@Override
    public void updateMenuView(boolean cleared) {
        if (mUpdateSuspended) return;
        if (cleared) {
            mMenuView.buildMenuView();
        } else {
            mMenuView.updateMenuView();
        }
    }

第一次創(chuàng)建Menu時(shí)官辈,調(diào)用的是buildMenuView方法。

BottomNavigationMenuView.buildMenuView()

public void buildMenuView() {
        if (mButtons != null) {
            for (BottomNavigationItemView item : mButtons) {
                sItemPool.release(item);
            }
        }
        removeAllViews();
        mButtons = new BottomNavigationItemView[mMenu.size()];

        // 當(dāng)menu item大于3個(gè)的時(shí)候遍坟,會(huì)出現(xiàn)縮放動(dòng)畫
        mShiftingMode = mMenu.size() > 3;

        for (int i = 0; i < mMenu.size(); i++) {
            mPresenter.setUpdateSuspended(true);
            mMenu.getItem(i).setCheckable(true);
            mPresenter.setUpdateSuspended(false);

            BottomNavigationItemView child = getNewItem();

            mButtons[i] = child;
            child.setIconTintList(mItemIconTint);
            child.setTextColor(mItemTextColor);
            child.setItemBackground(mItemBackgroundRes);
            child.setShiftingMode(mShiftingMode);

            // 單個(gè)item -- BottomNavigationItenView 的初始化操作
            child.initialize((MenuItemImpl) mMenu.getItem(i), 0);
            child.setItemPosition(i);

            // 設(shè)置點(diǎn)擊事件
            child.setOnClickListener(mOnClickListener);

            // 添加子視圖
            addView(child);
        }
    }

在 BottomNavigationMenuView類中定義了一個(gè)Pool對象拳亿,用來緩存BottomNavigationItemView對象。

private static final Pools.Pool<BottomNavigationItemView> sItemPool = new Pools.SynchronizedPool<>(5);

通過for循環(huán)創(chuàng)建了nMenu.size() 個(gè)BottomNavigationItemView 對象愿伴。

BottomNavigationItemView child = getNewItem();
mButtons[i] = child;

getNewItem()

private BottomNavigationItemView getNewItem() {
        BottomNavigationItemView item = sItemPool.acquire();
        if (item == null) {
            item = new BottomNavigationItemView(getContext());
        }
        return item;
    }

getNewItem() 類似于 **Message.obtain() **的機(jī)制肺魁。

接著看buildMenuView() ,方法最后面隔节,調(diào)用了 **BottomNavigationItemView.initialize() **鹅经,然后調(diào)用 **addView(child) ** 來添加子item視圖。

@Override
    public void initialize(MenuItemImpl itemData, int menuType) {
        mItemData = itemData;
        setCheckable(itemData.isCheckable());
        setChecked(itemData.isChecked());
        setEnabled(itemData.isEnabled());
        setIcon(itemData.getIcon());
        setTitle(itemData.getTitle());
        setId(itemData.getItemId());
    }

看一下 BottomNavigationItemView的構(gòu)造函數(shù)

public BottomNavigationItemView(@NonNull Context context) {
        this(context, null);
    }
    public BottomNavigationItemView(@NonNull Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public BottomNavigationItemView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        final Resources res = getResources();
        int inactiveLabelSize =
                res.getDimensionPixelSize(R.dimen.design_bottom_navigation_text_size);
        int activeLabelSize = res.getDimensionPixelSize(
                R.dimen.design_bottom_navigation_active_text_size);
        mDefaultMargin = res.getDimensionPixelSize(R.dimen.design_bottom_navigation_margin);
        mShiftAmount = inactiveLabelSize - activeLabelSize;
        mScaleUpFactor = 1f * activeLabelSize / inactiveLabelSize;
        mScaleDownFactor = 1f * inactiveLabelSize / activeLabelSize;
        // 注意下面的代碼
        LayoutInflater.from(context).inflate(R.layout.design_bottom_navigation_item, this, true);
        setBackgroundResource(R.drawable.design_bottom_navigation_item_background);
        mIcon = (ImageView) findViewById(R.id.icon);
        mSmallLabel = (TextView) findViewById(R.id.smallLabel);
        mLargeLabel = (TextView) findViewById(R.id.largeLabel);
    }

代碼中映射了一個(gè)布局文件

design_bottom_navigation_item.xml

<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <ImageView
        android:id="@+id/icon"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="@dimen/design_bottom_navigation_margin"
        android:layout_marginBottom="@dimen/design_bottom_navigation_margin"
        android:duplicateParentState="true" />
    <android.support.design.internal.BaselineLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="@dimen/design_bottom_navigation_margin"
        android:layout_gravity="bottom|center_horizontal"
        android:duplicateParentState="true">
        <TextView
            android:id="@+id/smallLabel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="@dimen/design_bottom_navigation_text_size"
            android:duplicateParentState="true" />
        <TextView
            android:id="@+id/largeLabel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="invisible"
            android:textSize="@dimen/design_bottom_navigation_active_text_size"
            android:duplicateParentState="true" />
    </android.support.design.internal.BaselineLayout>
</merge>

該布局文件由系統(tǒng)提供怎诫,包括一個(gè)ImageView和兩個(gè)TextView瘾晃。
當(dāng)菜單項(xiàng)大于3個(gè),切換item時(shí)幻妓,被選中的item會(huì)將largeLabel顯示蹦误,將smallLabel隱藏。然后改變ImageView和TextView的margin值達(dá)到動(dòng)畫效果肉津。
在BottomNavigationMenuView的構(gòu)造函數(shù)中對mAnimationHelper進(jìn)行了實(shí)例化

private final BottomNavigationAnimationHelperBase mAnimationHelper;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            mAnimationHelper = new BottomNavigationAnimationHelperIcs();
        } else {
            mAnimationHelper = new BottomNavigationAnimationHelperBase();
        }

當(dāng)版本小于14(Android 4.0)時(shí)胖缤,是沒有動(dòng)畫效果的。

class BottomNavigationAnimationHelperBase {
    void beginDelayedTransition(ViewGroup view) {
        // Do nothing.
    }
}

Android 4.0及之上的版本

class BottomNavigationAnimationHelperIcs extends BottomNavigationAnimationHelperBase {
    private static final long ACTIVE_ANIMATION_DURATION_MS = 115L;
    private final TransitionSet mSet;
    BottomNavigationAnimationHelperIcs() {
        mSet = new AutoTransition();
        mSet.setOrdering(TransitionSet.ORDERING_TOGETHER);
        mSet.setDuration(ACTIVE_ANIMATION_DURATION_MS);
        mSet.setInterpolator(new FastOutSlowInInterpolator());
        TextScale textScale = new TextScale();
        mSet.addTransition(textScale);
    }
    void beginDelayedTransition(ViewGroup view) {
        TransitionManager.beginDelayedTransition(view, mSet);
    }
}

代碼中定義了文本縮放動(dòng)畫 – TextScale

然后來看 BottomNavigationItemView的點(diǎn)擊事件阀圾。

在初始化這些子itemView的時(shí)候就設(shè)置了onClickListener

child.initialize((MenuItemImpl) mMenu.getItem(i), 0);
child.setItemPosition(i);
// 設(shè)置點(diǎn)擊事件
child.setOnClickListener(mOnClickListener);
mOnClickListener = new OnClickListener() {
            @Override
            public void onClick(View v) {
                final BottomNavigationItemView itemView = (BottomNavigationItemView) v;
                final int itemPosition = itemView.getItemPosition();
                activateNewButton(itemPosition);
                mMenu.performItemAction(itemView.getItemData(), mPresenter, 0);
            }
        };

activateNewButtom()

private void activateNewButton(int newButton) {
        if (mActiveButton == newButton) return;
        mAnimationHelper.beginDelayedTransition(this); // 產(chǎn)生動(dòng)畫
        mPresenter.setUpdateSuspended(true);
        mButtons[mActiveButton].setChecked(false); // 舊的被激活的按鈕切換成未被激活狀態(tài)
        mButtons[newButton].setChecked(true); // 將新點(diǎn)擊的按鈕切換到被激活狀態(tài)
        mPresenter.setUpdateSuspended(false);
        mActiveButton = newButton; // 記錄當(dāng)前被激活按鈕的位置
    }

先產(chǎn)生動(dòng)畫哪廓,然后切換被選中和之前選中的item的狀態(tài)。

@Override
    public void setChecked(boolean checked) {
        mItemData.setChecked(checked);
        ViewCompat.setPivotX(mLargeLabel, mLargeLabel.getWidth() / 2);
        ViewCompat.setPivotY(mLargeLabel, mLargeLabel.getBaseline());
        ViewCompat.setPivotX(mSmallLabel, mSmallLabel.getWidth() / 2);
        ViewCompat.setPivotY(mSmallLabel, mSmallLabel.getBaseline());
        if (mShiftingMode) {
            if (checked) {
                LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
                iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
                iconParams.topMargin = mDefaultMargin;
                mIcon.setLayoutParams(iconParams);
                mLargeLabel.setVisibility(VISIBLE);
                ViewCompat.setScaleX(mLargeLabel, 1f);
                ViewCompat.setScaleY(mLargeLabel, 1f);
            } else {
                LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
                iconParams.gravity = Gravity.CENTER;
                iconParams.topMargin = mDefaultMargin;
                mIcon.setLayoutParams(iconParams);
                mLargeLabel.setVisibility(INVISIBLE);
                ViewCompat.setScaleX(mLargeLabel, 0.5f);
                ViewCompat.setScaleY(mLargeLabel, 0.5f);
            }
            mSmallLabel.setVisibility(INVISIBLE);
        } else {
            if (checked) {
                LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
                iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
                iconParams.topMargin = mDefaultMargin + mShiftAmount;
                mIcon.setLayoutParams(iconParams);
                mLargeLabel.setVisibility(VISIBLE);
                mSmallLabel.setVisibility(INVISIBLE);
                ViewCompat.setScaleX(mLargeLabel, 1f);
                ViewCompat.setScaleY(mLargeLabel, 1f);
                ViewCompat.setScaleX(mSmallLabel, mScaleUpFactor);
                ViewCompat.setScaleY(mSmallLabel, mScaleUpFactor);
            } else {
                LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
                iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
                iconParams.topMargin = mDefaultMargin;
                mIcon.setLayoutParams(iconParams);
                mLargeLabel.setVisibility(INVISIBLE);
                mSmallLabel.setVisibility(VISIBLE);
                ViewCompat.setScaleX(mLargeLabel, mScaleDownFactor);
                ViewCompat.setScaleY(mLargeLabel, mScaleDownFactor);
                ViewCompat.setScaleX(mSmallLabel, 1f);
                ViewCompat.setScaleY(mSmallLabel, 1f);
            }
        }
        refreshDrawableState();
    }

mShiftingMode 表示是否偏移值初烘。當(dāng)菜單個(gè)數(shù)大于3時(shí)涡真,為true分俯。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市哆料,隨后出現(xiàn)的幾起案子缸剪,更是在濱河造成了極大的恐慌,老刑警劉巖东亦,帶你破解...
    沈念sama閱讀 212,222評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件杏节,死亡現(xiàn)場離奇詭異,居然都是意外死亡典阵,警方通過查閱死者的電腦和手機(jī)奋渔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,455評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來壮啊,“玉大人嫉鲸,你說我怎么就攤上這事〈跆洌” “怎么了玄渗?”我有些...
    開封第一講書人閱讀 157,720評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長狸眼。 經(jīng)常有香客問我藤树,道長,這世上最難降的妖魔是什么拓萌? 我笑而不...
    開封第一講書人閱讀 56,568評論 1 284
  • 正文 為了忘掉前任岁钓,我火速辦了婚禮,結(jié)果婚禮上司志,老公的妹妹穿的比我還像新娘甜紫。我一直安慰自己,他們只是感情好骂远,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,696評論 6 386
  • 文/花漫 我一把揭開白布囚霸。 她就那樣靜靜地躺著,像睡著了一般激才。 火紅的嫁衣襯著肌膚如雪拓型。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,879評論 1 290
  • 那天瘸恼,我揣著相機(jī)與錄音劣挫,去河邊找鬼。 笑死东帅,一個(gè)胖子當(dāng)著我的面吹牛压固,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播靠闭,決...
    沈念sama閱讀 39,028評論 3 409
  • 文/蒼蘭香墨 我猛地睜開眼帐我,長吁一口氣:“原來是場噩夢啊……” “哼坎炼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起拦键,我...
    開封第一講書人閱讀 37,773評論 0 268
  • 序言:老撾萬榮一對情侶失蹤谣光,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后芬为,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體萄金,經(jīng)...
    沈念sama閱讀 44,220評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,550評論 2 327
  • 正文 我和宋清朗相戀三年媚朦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了氧敢。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,697評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡莲镣,死狀恐怖福稳,靈堂內(nèi)的尸體忽然破棺而出涎拉,到底是詐尸還是另有隱情瑞侮,我是刑警寧澤,帶...
    沈念sama閱讀 34,360評論 4 332
  • 正文 年R本政府宣布鼓拧,位于F島的核電站半火,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏季俩。R本人自食惡果不足惜钮糖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,002評論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望酌住。 院中可真熱鬧店归,春花似錦、人聲如沸酪我。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,782評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽都哭。三九已至秩伞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間欺矫,已是汗流浹背纱新。 一陣腳步聲響...
    開封第一講書人閱讀 32,010評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留穆趴,地道東北人脸爱。 一個(gè)月前我還...
    沈念sama閱讀 46,433評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像未妹,于是被迫代替她去往敵國和親簿废。 傳聞我的和親對象是個(gè)殘疾皇子勺疼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,587評論 2 350

推薦閱讀更多精彩內(nèi)容

  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程,因...
    小菜c閱讀 6,373評論 0 17
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,813評論 25 707
  • 《裕語言》速成開發(fā)手冊3.0 官方用戶交流:iApp開發(fā)交流(1) 239547050iApp開發(fā)交流(2) 10...
    葉染柒丶閱讀 26,288評論 5 19
  • 旅行,相信大家對這個(gè)詞再熟悉不過了.一個(gè)人的一生都多多少少有或大或小的旅行,而每一次旅行都被構(gòu)建成人生的一...
    凱露小公主閱讀 206評論 0 1
  • 樹枝下的光暈 偷偷存在著 像灰塵 天空上的云朵 慢慢變化著 像繁星 大地上的人們 輕輕呼吸著 不干擾屬于夜的寂靜 ...
    董落憂閱讀 174評論 0 0