開源項目Plaid學習(七)FourThreeImageView&BadgedFourThreeImageView

前言

上一期介紹了ForegroundImageView,這一期介紹其子類FourThreeImageView和子類的子類BadgedFourThreeImageView。

FourThreeImageView

源碼

/**
 * A extension of ForegroundImageView that is always 4:3 aspect ratio.
 */
public class FourThreeImageView extends ForegroundImageView {

    public FourThreeImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        int fourThreeHeight = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthSpec) * 3 / 4,
                MeasureSpec.EXACTLY);
        super.onMeasure(widthSpec, fourThreeHeight);
    }
}

非常短的代碼走芋,目的也非常簡單:強制4:3尺寸。具體來說就是把高度設定為寬度的3/4.實現(xiàn)就是改寫onMeasure方法屿笼。

BadgedFourThreeImageView

源碼

/**
 * A view group that draws a badge drawable on top of it's contents.
 */
public class BadgedFourThreeImageView extends FourThreeImageView {

    private Drawable badge;
    private boolean drawBadge;
    private boolean badgeBoundsSet = false;
    private int badgeGravity;
    private int badgePadding;

    public BadgedFourThreeImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        badge = new GifBadge(context);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BadgedImageView, 0, 0);
        badgeGravity = a.getInt(R.styleable.BadgedImageView_badgeGravity, Gravity.END | Gravity
                .BOTTOM);
        badgePadding = a.getDimensionPixelSize(R.styleable.BadgedImageView_badgePadding, 0);
        a.recycle();

    }

    public void showBadge(boolean show) {
        drawBadge = show;
    }

    public void setBadgeColor(@ColorInt int color) {
        badge.setColorFilter(color, PorterDuff.Mode.SRC_IN);
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        if (drawBadge) {
            if (!badgeBoundsSet) {
                layoutBadge();
            }
            badge.draw(canvas);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        layoutBadge();
    }

    private void layoutBadge() {
        Rect badgeBounds = badge.getBounds();
        Gravity.apply(badgeGravity,
                badge.getIntrinsicWidth(),
                badge.getIntrinsicHeight(),
                new Rect(0, 0, getWidth(), getHeight()),
                badgePadding,
                badgePadding,
                badgeBounds);
        badge.setBounds(badgeBounds);
        badgeBoundsSet = true;
    }

    /**
     * A drawable for indicating that an image is animated
     */
    private static class GifBadge extends Drawable {

        private static final String GIF = "GIF";
        private static final int TEXT_SIZE = 12;    // sp
        private static final int PADDING = 4;       // dp
        private static final int CORNER_RADIUS = 2; // dp
        private static final int BACKGROUND_COLOR = Color.WHITE;
        private static final String TYPEFACE = "sans-serif-black";
        private static final int TYPEFACE_STYLE = Typeface.NORMAL;
        private static Bitmap bitmap;
        private static int width;
        private static int height;
        private final Paint paint;

        GifBadge(Context context) {
            if (bitmap == null) {
                final DisplayMetrics dm = context.getResources().getDisplayMetrics();
                final float density = dm.density;
                final float scaledDensity = dm.scaledDensity;
                final TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint
                        .SUBPIXEL_TEXT_FLAG);
                textPaint.setTypeface(Typeface.create(TYPEFACE, TYPEFACE_STYLE));
                textPaint.setTextSize(TEXT_SIZE * scaledDensity);

                final float padding = PADDING * density;
                final float cornerRadius = CORNER_RADIUS * density;
                final Rect textBounds = new Rect();
                textPaint.getTextBounds(GIF, 0, GIF.length(), textBounds);
                height = (int) (padding + textBounds.height() + padding);
                width = (int) (padding + textBounds.width() + padding);
                bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
                bitmap.setHasAlpha(true);
                final Canvas canvas = new Canvas(bitmap);
                final Paint backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
                backgroundPaint.setColor(BACKGROUND_COLOR);
                canvas.drawRoundRect(0, 0, width, height, cornerRadius, cornerRadius,
                        backgroundPaint);
                // punch out the word 'GIF', leaving transparency
                textPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
                canvas.drawText(GIF, padding, height - padding, textPaint);
            }
            paint = new Paint();
        }

        @Override
        public int getIntrinsicWidth() {
            return width;
        }

        @Override
        public int getIntrinsicHeight() {
            return height;
        }

        @Override
        public void draw(Canvas canvas) {
            canvas.drawBitmap(bitmap, getBounds().left, getBounds().top, paint);
        }

        @Override
        public void setAlpha(int alpha) {
            // ignored
        }

        @Override
        public void setColorFilter(ColorFilter cf) {
            paint.setColorFilter(cf);
        }

        @Override
        public int getOpacity() {
            return 0;
        }
    }
}

使用了自定義屬性attrs_badge_image_view.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="BadgedImageView">
        <attr name="badgeGravity">
            <!-- Push object to the top of its container, not changing its size. -->
            <flag name="top" value="0x30" />
            <!-- Push object to the bottom of its container, not changing its size. -->
            <flag name="bottom" value="0x50" />
            <!-- Push object to the left of its container, not changing its size. -->
            <flag name="left" value="0x03" />
            <!-- Push object to the right of its container, not changing its size. -->
            <flag name="right" value="0x05" />
            <!-- Push object to the beginning of its container, not changing its size. -->
            <flag name="start" value="0x00800003" />
            <!-- Push object to the end of its container, not changing its size. -->
            <flag name="end" value="0x00800005" />
        </attr>
        <attr name="badgePadding" format="dimension|reference"/>
    </declare-styleable>
</resources>

這里的自定義屬性和以往的有一點不同眷柔,即多了flag項目。觀察:0x30=0b_0011_0000公你,0x50=0b_0101_0000,之后的都類似踊淳。很多時候我們都會用到Gravity的或運算,例如Gravity.left|Gravity.bottom省店,其實也就是其value的或嚣崭,因此將某一對矛盾的屬性設置到特定的比特位上,這樣相互之間就不會干擾懦傍。當然雹舀,這里這么設置值自然有其道理,具體還得參考Gravity的源碼粗俱。
自定義屬性就兩個说榆,一個Gravity,另一個padding寸认,兩者一起來規(guī)定前景圖片的具體位置和尺寸签财。
此外,這個類里面還定義了一個GifBadge偏塞,就是把“Gif”字樣轉為Drawable類型用于前景唱蒸,表面該圖片實際上是Gif的靜態(tài)圖。具體實現(xiàn)也牽扯了很多細節(jié)灸叼,簡單來說就是在bitmap畫個背景和Textview組成Drawable神汹。根據(jù)這個類,可以很方便地舉一反三得到通用可以設置字符串內容古今,字體屁魏,字的顏色,字的大小捉腥,背景的一個類氓拼,估計也就用在需要前景字體的圖片上了。
還是回到源碼抵碟。前面沒太多好說的桃漾。PorterDuff.Mode有非常多模式,這里有比較詳細的解釋立磁。簡單而言就是圖像重疊的處理呈队。

圖解
圖解

這里使用SRC_IN,來實現(xiàn)背景色變唱歧。GIF則是鏤空的宪摧,因為Clear模式會造成透明背景粒竖,在GifBadge里面textPaint已經(jīng)設置。
layoutBadge里面几于,自定義的Gravity如何與源碼的Gravity互動比較值得探究:

    /** Constant indicating that no gravity has been set **/
    public static final int NO_GRAVITY = 0x0000;
    
    /** Raw bit indicating the gravity for an axis has been specified. */
    public static final int AXIS_SPECIFIED = 0x0001;

    /** Raw bit controlling how the left/top edge is placed. */
    public static final int AXIS_PULL_BEFORE = 0x0002;
    /** Raw bit controlling how the right/bottom edge is placed. */
    public static final int AXIS_PULL_AFTER = 0x0004;
    /** Raw bit controlling whether the right/bottom edge is clipped to its
     * container, based on the gravity direction being applied. */
    public static final int AXIS_CLIP = 0x0008;

    /** Bits defining the horizontal axis. */
    public static final int AXIS_X_SHIFT = 0;
    /** Bits defining the vertical axis. */
    public static final int AXIS_Y_SHIFT = 4;

    /** Push object to the top of its container, not changing its size. */
    public static final int TOP = (AXIS_PULL_BEFORE|AXIS_SPECIFIED)<<AXIS_Y_SHIFT;
    /** Push object to the bottom of its container, not changing its size. */
    public static final int BOTTOM = (AXIS_PULL_AFTER|AXIS_SPECIFIED)<<AXIS_Y_SHIFT;
    /** Push object to the left of its container, not changing its size. */
    public static final int LEFT = (AXIS_PULL_BEFORE|AXIS_SPECIFIED)<<AXIS_X_SHIFT;
    /** Push object to the right of its container, not changing its size. */
    public static final int RIGHT = (AXIS_PULL_AFTER|AXIS_SPECIFIED)<<AXIS_X_SHIFT;

    /** Place object in the vertical center of its container, not changing its
     *  size. */
    public static final int CENTER_VERTICAL = AXIS_SPECIFIED<<AXIS_Y_SHIFT;
    /** Grow the vertical size of the object if needed so it completely fills
     *  its container. */
    public static final int FILL_VERTICAL = TOP|BOTTOM;

    /** Place object in the horizontal center of its container, not changing its
     *  size. */
    public static final int CENTER_HORIZONTAL = AXIS_SPECIFIED<<AXIS_X_SHIFT;
    /** Grow the horizontal size of the object if needed so it completely fills
     *  its container. */
    public static final int FILL_HORIZONTAL = LEFT|RIGHT;

    /** Place the object in the center of its container in both the vertical
     *  and horizontal axis, not changing its size. */
    public static final int CENTER = CENTER_VERTICAL|CENTER_HORIZONTAL;

    /** Grow the horizontal and vertical size of the object if needed so it
     *  completely fills its container. */
    public static final int FILL = FILL_VERTICAL|FILL_HORIZONTAL;

    /** Flag to clip the edges of the object to its container along the
     *  vertical axis. */
    public static final int CLIP_VERTICAL = AXIS_CLIP<<AXIS_Y_SHIFT;
    
    /** Flag to clip the edges of the object to its container along the
     *  horizontal axis. */
    public static final int CLIP_HORIZONTAL = AXIS_CLIP<<AXIS_X_SHIFT;

    /** Raw bit controlling whether the layout direction is relative or not (START/END instead of
     * absolute LEFT/RIGHT).
     */
    public static final int RELATIVE_LAYOUT_DIRECTION = 0x00800000;

    /**
     * Binary mask to get the absolute horizontal gravity of a gravity.
     */
    public static final int HORIZONTAL_GRAVITY_MASK = (AXIS_SPECIFIED |
            AXIS_PULL_BEFORE | AXIS_PULL_AFTER) << AXIS_X_SHIFT;
    /**
     * Binary mask to get the vertical gravity of a gravity.
     */
    public static final int VERTICAL_GRAVITY_MASK = (AXIS_SPECIFIED |
            AXIS_PULL_BEFORE | AXIS_PULL_AFTER) << AXIS_Y_SHIFT;

    /** Special constant to enable clipping to an overall display along the
     *  vertical dimension.  This is not applied by default by
     *  {@link #apply(int, int, int, Rect, int, int, Rect)}; you must do so
     *  yourself by calling {@link #applyDisplay}.
     */
    public static final int DISPLAY_CLIP_VERTICAL = 0x10000000;
    
    /** Special constant to enable clipping to an overall display along the
     *  horizontal dimension.  This is not applied by default by
     *  {@link #apply(int, int, int, Rect, int, int, Rect)}; you must do so
     *  yourself by calling {@link #applyDisplay}.
     */
    public static final int DISPLAY_CLIP_HORIZONTAL = 0x01000000;
    
    /** Push object to x-axis position at the start of its container, not changing its size. */
    public static final int START = RELATIVE_LAYOUT_DIRECTION | LEFT;

    /** Push object to x-axis position at the end of its container, not changing its size. */
    public static final int END = RELATIVE_LAYOUT_DIRECTION | RIGHT;

    /**
     * Binary mask for the horizontal gravity and script specific direction bit.
     */
    public static final int RELATIVE_HORIZONTAL_GRAVITY_MASK = START | END;

    /**
     * Apply a gravity constant to an object.
     * 
     * @param gravity The desired placement of the object, as defined by the
     *                constants in this class.
     * @param w The horizontal size of the object.
     * @param h The vertical size of the object.
     * @param container The frame of the containing space, in which the object
     *                  will be placed.  Should be large enough to contain the
     *                  width and height of the object.
     * @param xAdj Offset to apply to the X axis.  If gravity is LEFT this
     *             pushes it to the right; if gravity is RIGHT it pushes it to
     *             the left; if gravity is CENTER_HORIZONTAL it pushes it to the
     *             right or left; otherwise it is ignored.
     * @param yAdj Offset to apply to the Y axis.  If gravity is TOP this pushes
     *             it down; if gravity is BOTTOM it pushes it up; if gravity is
     *             CENTER_VERTICAL it pushes it down or up; otherwise it is
     *             ignored.
     * @param outRect Receives the computed frame of the object in its
     *                container.
     */
    public static void apply(int gravity, int w, int h, Rect container,
            int xAdj, int yAdj, Rect outRect) {
        switch (gravity&((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_X_SHIFT)) {
            case 0:
                outRect.left = container.left
                        + ((container.right - container.left - w)/2) + xAdj;
                outRect.right = outRect.left + w;
                if ((gravity&(AXIS_CLIP<<AXIS_X_SHIFT))
                        == (AXIS_CLIP<<AXIS_X_SHIFT)) {
                    if (outRect.left < container.left) {
                        outRect.left = container.left;
                    }
                    if (outRect.right > container.right) {
                        outRect.right = container.right;
                    }
                }
                break;
            case AXIS_PULL_BEFORE<<AXIS_X_SHIFT:
                outRect.left = container.left + xAdj;
                outRect.right = outRect.left + w;
                if ((gravity&(AXIS_CLIP<<AXIS_X_SHIFT))
                        == (AXIS_CLIP<<AXIS_X_SHIFT)) {
                    if (outRect.right > container.right) {
                        outRect.right = container.right;
                    }
                }
                break;
            case AXIS_PULL_AFTER<<AXIS_X_SHIFT:
                outRect.right = container.right - xAdj;
                outRect.left = outRect.right - w;
                if ((gravity&(AXIS_CLIP<<AXIS_X_SHIFT))
                        == (AXIS_CLIP<<AXIS_X_SHIFT)) {
                    if (outRect.left < container.left) {
                        outRect.left = container.left;
                    }
                }
                break;
            default:
                outRect.left = container.left + xAdj;
                outRect.right = container.right + xAdj;
                break;
        }
        
        switch (gravity&((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_Y_SHIFT)) {
            case 0:
                outRect.top = container.top
                        + ((container.bottom - container.top - h)/2) + yAdj;
                outRect.bottom = outRect.top + h;
                if ((gravity&(AXIS_CLIP<<AXIS_Y_SHIFT))
                        == (AXIS_CLIP<<AXIS_Y_SHIFT)) {
                    if (outRect.top < container.top) {
                        outRect.top = container.top;
                    }
                    if (outRect.bottom > container.bottom) {
                        outRect.bottom = container.bottom;
                    }
                }
                break;
            case AXIS_PULL_BEFORE<<AXIS_Y_SHIFT:
                outRect.top = container.top + yAdj;
                outRect.bottom = outRect.top + h;
                if ((gravity&(AXIS_CLIP<<AXIS_Y_SHIFT))
                        == (AXIS_CLIP<<AXIS_Y_SHIFT)) {
                    if (outRect.bottom > container.bottom) {
                        outRect.bottom = container.bottom;
                    }
                }
                break;
            case AXIS_PULL_AFTER<<AXIS_Y_SHIFT:
                outRect.bottom = container.bottom - yAdj;
                outRect.top = outRect.bottom - h;
                if ((gravity&(AXIS_CLIP<<AXIS_Y_SHIFT))
                        == (AXIS_CLIP<<AXIS_Y_SHIFT)) {
                    if (outRect.top < container.top) {
                        outRect.top = container.top;
                    }
                }
                break;
            default:
                outRect.top = container.top + yAdj;
                outRect.bottom = container.bottom + yAdj;
                break;
        }
    }

以默認的right|bottom為例蕊苗,數(shù)值為0x55.然后到apply函數(shù)里面,因為AXIS_PULL_BEFORE|AXIS_PULL_AFTER這個值就是0x6沿彭,就算左移4位也還是0x60朽砰,那么和參數(shù)gravity與,結果第一個switch是0x4喉刘,第二個結果0x40瞧柔,都和AXIS_PULL_AFTER一致。觀察代碼睦裳,也確實是計算了padding造锅。為什么呢?怎么辦到的廉邑?
從apply的源碼分析哥蔚,其實Gravity這個屬性主要是分成兩個16進制位來看,低位決定X軸也就是左右蛛蒙,高位決定Y軸也就是上下糙箍,因此自定義的值,關系到左右的只在低位有值牵祟,關系到上下的也只在高位有值深夯。當然可以一起有值,不過代碼還是分開處理诺苹。
其實兩個位上的操作很像塌西,都是和對應的6與。6有是0b110筝尾,其實就是AXIS_PULL_BEFORE|AXIS_PULL_AFTER,按照注釋办桨,AXIS_PULL_BEFORE代表top/left筹淫,AXIS_PULL_AFTER代表bottom/right。那center呢呢撞?其實center是AXIS_SPECIFIED管的损姜。具體到switch的case,0的意思就是居中殊霞,AXIS_PULL_BEFORE就是之前解釋的top/left摧阅,AXIS_PULL_AFTER就是bottom/right。當然還有優(yōu)先級更高的一層clip判斷绷蹲。
開始我還以為是作者自己搞了一套Gravity的值棒卷,結果原來還是和源碼里的值一致顾孽。只是從源碼看不出來,START/END是怎么體現(xiàn)的比规?NONE和AXIS_SPECIFIED又如何區(qū)分若厚?
雖然還有疑問,不過也算是搞明白怎么自定義Gravity了蜒什。

實際效果

Plaid里面使用該View的實際設置是這樣的:

<io.plaidapp.ui.widget.BadgedFourThreeImageView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/shot"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:elevation="@dimen/z_card"
    android:stateListAnimator="@animator/raise"
    android:foreground="@drawable/mid_grey_ripple"
    app:badgeGravity="end|bottom"
    app:badgePadding="@dimen/padding_normal" />

也就是說测秸,我前面提到過ForegroundImageView的前景圖片會擴充全圖,其實是故意的灾常,因為這里設置的前景其實是一個ripple Drawable霎冯,使得圖片對點擊產(chǎn)生ripple效果。而這里則是加一個GIF的badge而已钞瀑。
換種想法沈撞,利用FrameLayout結合foreground屬性也可以實現(xiàn)這個效果。Anyway仔戈,學到就是賺到关串。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市监徘,隨后出現(xiàn)的幾起案子晋修,更是在濱河造成了極大的恐慌,老刑警劉巖凰盔,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件墓卦,死亡現(xiàn)場離奇詭異,居然都是意外死亡户敬,警方通過查閱死者的電腦和手機落剪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來尿庐,“玉大人忠怖,你說我怎么就攤上這事〕” “怎么了凡泣?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長皮假。 經(jīng)常有香客問我鞋拟,道長,這世上最難降的妖魔是什么惹资? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任贺纲,我火速辦了婚禮,結果婚禮上褪测,老公的妹妹穿的比我還像新娘猴誊。我一直安慰自己潦刃,他們只是感情好,可當我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布稠肘。 她就那樣靜靜地躺著福铅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪项阴。 梳的紋絲不亂的頭發(fā)上滑黔,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天,我揣著相機與錄音环揽,去河邊找鬼略荡。 笑死,一個胖子當著我的面吹牛歉胶,可吹牛的內容都是我干的汛兜。 我是一名探鬼主播,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼通今,長吁一口氣:“原來是場噩夢啊……” “哼粥谬!你這毒婦竟也來了?” 一聲冷哼從身側響起辫塌,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤漏策,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后臼氨,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體掺喻,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年储矩,在試婚紗的時候發(fā)現(xiàn)自己被綠了感耙。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡持隧,死狀恐怖即硼,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情屡拨,我是刑警寧澤谦絮,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站洁仗,受9級特大地震影響,放射性物質發(fā)生泄漏性锭。R本人自食惡果不足惜赠潦,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望草冈。 院中可真熱鬧她奥,春花似錦瓮增、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至凡资,卻和暖如春砸捏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背隙赁。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工垦藏, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人伞访。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓掂骏,卻偏偏與公主長得像,于是被迫代替她去往敵國和親厚掷。 傳聞我的和親對象是個殘疾皇子弟灼,可洞房花燭夜當晚...
    茶點故事閱讀 43,697評論 2 351

推薦閱讀更多精彩內容