Android嵌套滑动下篇

嵌套滑动版本1.gif

上篇文章 Android嵌套滑动上篇 中实现的滑动效果并不是很流畅,如上图所示。不流畅的原因是因为对于fling,NestedScrolling要么交给child处理,要么交给parent处理。

而通过NestedScrolling2可以实现fling类型的滚动先由外层控件处理一部分,剩余的再交给内层控件处理,这样使滑动效果比较流畅。实现的效果如下图所示:

嵌套滑动版本2.gif

GitHub源码

这里先说一下NestedScrolling2能让内层控件和外层控件在惯性滑动的时候更流畅的关键的逻辑。

  1. NestedScrolling2分发滚动事件的时候区分了滚动事件的类型:是正常的触摸滚动还是惯性滑动。

  2. 内层控件先调用dispatchNestedPreFling来处理惯性滑动。如果外层控件处理了惯性滑动,即外层控件的onNestedPreFling方法返回了true。那么NestedScrolling2和NestedScrolling的惯性滑动效果没有什么差异。

  3. 如果外层控件没有处理惯性滑动,也就是外层控件的onNestedPreFling方法返回了false。那么就会调用dispatchNestedFling方法并且内层控件自身开始惯性滑动mViewFlinger.fling(velocityX, velocityY),但是在惯性滑动的每一帧,通过Scroller计算出来的滚动距离通过dispatchNestedPreScroll先分发给外层控件。外层控件可以通过onNestedPreScroll先消耗部分滚动距离,然后内层控件再自身滚动。

流程图

NestedScrolling2流程.jpg

接下来先看一下NestedScrolling2相关的几个类。

NestedScrollingChild2继承了NestedScrollingChild接口并新增了几个方法。

public interface NestedScrollingChild2 extends NestedScrollingChild {

    boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type);

    void stopNestedScroll(@NestedScrollType int type);

    boolean hasNestedScrollingParent(@NestedScrollType int type);

    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
            @NestedScrollType int type);

    boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type);

}

新增的几个方法都有一个@NestedScrollType int type参数,是用来区分滚动类型的,有两个取值:

//触摸屏幕类型
public static final int TYPE_TOUCH = 0;

//惯性滑动类型   
public static final int TYPE_NON_TOUCH = 1;

对应的NestedScrollingParent2继承了NestedScrollingParent接口也新增了几个方法。

public interface NestedScrollingParent2 extends NestedScrollingParent {

    boolean onStartNestedScroll(View child, View target, @ScrollAxis int axes,
            @NestedScrollType int type);
    
    void onNestedScrollAccepted(View child, View target, @ScrollAxis int axes,
                @NestedScrollType int type);

    void onStopNestedScroll(View target, @NestedScrollType int type);

    void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);
    
    void onNestedPreScroll(View target, int dx, int dy, @NonNull int[] consumed,
                @NestedScrollType int type);

}

最新的嵌套滑动已经都出NestedScrolling3了,这个更新也是日新月异啊,哈哈。

public interface NestedScrollingChild3 extends NestedScrollingChild2 {

    void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
            @Nullable int[] offsetInWindow, @ViewCompat.NestedScrollType int type,
            @NonNull int[] consumed);
}
public interface NestedScrollingParent3 extends NestedScrollingParent2 {
    void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
            int dyUnconsumed, @ViewCompat.NestedScrollType int type, @NonNull int[] consumed);
}

NestedScrolling3相对于NestedScrolling2的改变与本篇文章内容无关,我们暂时忽略。

下面我们以RecyclerView为例开始分析,androidx1.1.0的源码。

public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 {
    //...
}

RecyclerView的onTouchEvent方法

@Override
public boolean onTouchEvent(MotionEvent e) {
    //...
    final MotionEvent vtev = MotionEvent.obtain(e);
    vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);

    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            //...
            int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
            if (canScrollHorizontally) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
            }
            if (canScrollVertically) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
            }
            //注释1处
            startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
        } break;
        case MotionEvent.ACTION_MOVE: {
                
            final int x = (int) (e.getX(index) + 0.5f);
            final int y = (int) (e.getY(index) + 0.5f);
            int dx = mLastTouchX - x;
            int dy = mLastTouchY - y;

            if (mScrollState == SCROLL_STATE_DRAGGING) {
                mReusableIntPair[0] = 0;
                mReusableIntPair[1] = 0;
                //注释2处
                if (dispatchNestedPreScroll(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        mReusableIntPair, mScrollOffset, TYPE_TOUCH
                )) {
                    //注释3处
                    dx -= mReusableIntPair[0];
                    dy -= mReusableIntPair[1];
                    //...
                }

                mLastTouchX = x - mScrollOffset[0];
                mLastTouchY = y - mScrollOffset[1];
                //注释4处
                if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        e)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
            }
        } break;
        case MotionEvent.ACTION_UP: {
            mVelocityTracker.addMovement(vtev);
            eventAddedToVelocityTracker = true;
            //计算速度
            mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
            final float xvel = canScrollHorizontally
                    ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
            final float yvel = canScrollVertically
                    ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
            //注释5处,调用fling方法
            if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                setScrollState(SCROLL_STATE_IDLE);
            }
            //注释6处
            resetScroll();
        } break;
    }
    //...
    return true;
}

注释1处,调用startNestedScroll方法

@Override
public boolean startNestedScroll(int axes, int type) {
    //调用NestedScrollingChildHelper的startNestedScroll方法
    return getScrollingChildHelper().startNestedScroll(axes, type);
}

这里提一下,使用NestedScrollingChildHelper和NestedScrollingParentHelper是为了对Android 5.0 Lollipop (API 21)以前的版本做兼容。

NestedScrollingChildHelper的startNestedScroll方法

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
    //根据嵌套滑动的类型来获取NestedScrollingParent
    if (hasNestedScrollingParent(type)) {
        // 嵌套滑动已经在处理过程中,直接返回true
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = mView.getParent();
        View child = mView;
        //遍历父级控件
        while (p != null) {
            //注释1处
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                //注释2处
                setNestedScrollingParentForType(type, p);
                //注释3处
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

如果开启了嵌套滑动就遍历父控件,询问是否有父控件想要处理。

注释1处,ViewParentCompat的onStartNestedScroll方法

public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
            int nestedScrollAxes, int type) {
    if (parent instanceof NestedScrollingParent2) {
        //注释1处,首先尝试调用NestedScrollingParent2的API
        return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
                nestedScrollAxes, type);
    } else if (type == ViewCompat.TYPE_TOUCH) {//注释2处,NestedScrollingParent只处理正常的触摸类型

        if (Build.VERSION.SDK_INT >= 21) {//大于21版本直接调用ViewParent的方法即可。
            try {
                return parent.onStartNestedScroll(child, target, nestedScrollAxes);
            } catch (AbstractMethodError e) {
                Log.e(TAG, "ViewParent " + parent + " does not implement interface "
                        + "method onStartNestedScroll", e);
            }
        } else if (parent instanceof NestedScrollingParent) {
            //NestedScrollingParent处理
            return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
                    nestedScrollAxes);
        }
    }
    return false;
}

注释1处,首先尝试调用NestedScrollingParent2的onStartNestedScroll(child, target,nestedScrollAxes, type)

注释2处,NestedScrollingParent只处理正常的触摸类型,丢弃掉type参数,onStartNestedScroll(child, target,nestedScrollAxes)。大于21版本直接调用ViewParent的方法即可。否则调用NestedScrollingParent处理。

后面的分析中,我们就只看NestedScrollingParent2相关的内容。

回到NestedScrollingChildHelper的startNestedScroll方法的注释2处。

private void setNestedScrollingParentForType(@NestedScrollType int type, ViewParent p) {
    switch (type) {
        case TYPE_TOUCH:
            //处理触摸类型滑动事件的外层控件
            mNestedScrollingParentTouch = p;
            break;
        case TYPE_NON_TOUCH:
            //处理惯性滑动类型事件的外层控件
            mNestedScrollingParentNonTouch = p;
            break;
    }
}

根据滑动的类型将ViewParent赋值给不同的变量保存。

注释3处

//注释3处
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);

内部会调用NestedScrollingParent2的onNestedScrollAccepted方法。

回到RecyclerView的onTouchEvent方法的注释2处

//注释2处
if (dispatchNestedPreScroll(
        canScrollHorizontally ? dx : 0,
        canScrollVertically ? dy : 0,
        mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
    //注释3处
    dx -= mReusableIntPair[0];
    dy -= mReusableIntPair[1];
    //...
}

注释2处,RecyclerView的dispatchNestedPreScroll方法

@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
        int type) {
    return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,
            type);
}

最终会调用NestedScrollingParent2的onNestedPreScroll方法。

注释3处,如果NestedScrollingParent2消耗了一些滑动距离,减去消耗的距离。

RecyclerView的onTouchEvent方法的注释4处,调用scrollByInternal方法

    boolean scrollByInternal(int x, int y, MotionEvent ev) {
        int unconsumedX = 0;
        int unconsumedY = 0;
        int consumedX = 0;
        int consumedY = 0;

        if (mAdapter != null) {
            mReusableIntPair[0] = 0;
            mReusableIntPair[1] = 0;
            //内部自身滑动
            scrollStep(x, y, mReusableIntPair);
            consumedX = mReusableIntPair[0];
            consumedY = mReusableIntPair[1];
            //剩余的滑动距离
            unconsumedX = x - consumedX;
            unconsumedY = y - consumedY;
        }
        if (!mItemDecorations.isEmpty()) {
            invalidate();
        }

        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        //将剩余的滑动距离再次分发给处理嵌套滑动的父View
        dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
                TYPE_TOUCH, mReusableIntPair);
        unconsumedX -= mReusableIntPair[0];
        unconsumedY -= mReusableIntPair[1];
        boolean consumedNestedScroll = mReusableIntPair[0] != 0 || mReusableIntPair[1] != 0;

        //...       
        return consumedNestedScroll || consumedX != 0 || consumedY != 0;
    }

方法内部首先调用scrollStep方法自身滑动,然后计算出剩余的滑动距离。然后将剩余的滑动距离再次分发给处理嵌套滑动的父View。

@Override
public final void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
        int dyUnconsumed, int[] offsetInWindow, int type, @NonNull int[] consumed) {
    getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
            dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed);
}

最终会调用NestedScrollingParent2的onNestedScroll方法。

到目前为止,NestedScrolling2和NestedScrolling并没有差别。

回到RecyclerView的onTouchEvent方法的注释5处,调用fling方法。从这里开始NestedScrolling2和NestedScrolling的处理逻辑产生了差异。

public boolean fling(int velocityX, int velocityY) {
    //...
    //注释1处
    if (!dispatchNestedPreFling(velocityX, velocityY)) {
        final boolean canScroll = canScrollHorizontal || canScrollVertical;
        //注释2处
        dispatchNestedFling(velocityX, velocityY, canScroll);

        //...

        if (canScroll) {
            int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
            if (canScrollHorizontal) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
            }
            if (canScrollVertical) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
            }
            //注释3处
            startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);

            velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
            velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
            //注释4处
            mViewFlinger.fling(velocityX, velocityY);
            return true;
        }
    }
    return false;
}

注释1处,先询问外层控件是否要处理惯性滑动,如果外层控件处理了,fling方法直接返回false,自身不滑动。

@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
    return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
}

方法内部会调用外层控件的onNestedPreFling方法。

如果外层控件处理了惯性滑动,即外层控件的onNestedPreFling方法返回了true,内层控件自身不滑动。

如果!dispatchNestedPreFling(velocityX, velocityY)为true,说明外层控件没有处理惯性滑动。注释2处:

@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
    return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);
}

方法内部会调用外层控件的onNestedFling方法。

注释3处,这里和NestedScrolling有差异。

@Override
public boolean startNestedScroll(int axes, int type) {
    return getScrollingChildHelper().startNestedScroll(axes, type);
}

注释3处,外层控件没有处理惯性滑动,那么内层控件就要开始自身的惯性滑动,在开始惯性滑动之前,会调用startNestedScroll方法,通知外层控件内层控件,这时候的滑动类型是TYPE_NON_TOUCH

NestedScrollingChildHelper的startNestedScroll方法

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
    //根据嵌套滑动的类型来获取NestedScrollingParent
    if (hasNestedScrollingParent(type)) {
        // 嵌套滑动已经在处理过程中,直接返回true
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = mView.getParent();
        View child = mView;
        //遍历父级控件
        while (p != null) {
            //注释1处
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                //注释2处
                setNestedScrollingParentForType(type, p);
                //注释3处
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

注释1处,如果外层控件要处理TYPE_NON_TOUCH类型的滚动,外层控件的onStartNestedScroll返回true。

注释3处,外层控件调用onNestedScrollAccepted方法。

我们回到fling方法的注释4处

//注释4处
mViewFlinger.fling(velocityX, velocityY);

ViewFlinger的fling方法。

public void fling(int velocityX, int velocityY) {
    setScrollState(SCROLL_STATE_SETTLING);
    mLastFlingX = mLastFlingY = 0;
    //fling
    mOverScroller.fling(0, 0, velocityX, velocityY,
            Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
    //请求重新绘制
    postOnAnimation();
}

Scroller怎么实现滚动的,这里就不展开了。

ViewFlinger实现了Runnable接口。

@Override
public void run() {
    //...           
    final OverScroller scroller = mOverScroller;
    //如果滚动还没结束
    if (scroller.computeScrollOffset()) {
        final int x = scroller.getCurrX();
        final int y = scroller.getCurrY();
        int unconsumedX = x - mLastFlingX;
        int unconsumedY = y - mLastFlingY;
        mLastFlingX = x;
        mLastFlingY = y;
        int consumedX = 0;
        int consumedY = 0;

        // Nested Pre Scroll
        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        //注释1处,每一帧的计算出来的滚动距离先分发到外层控件
        if (dispatchNestedPreScroll(unconsumedX, unconsumedY, mReusableIntPair, null,
                TYPE_NON_TOUCH)) {
            //注释2处,减去外层控件消耗的距离
            unconsumedX -= mReusableIntPair[0];
            unconsumedY -= mReusableIntPair[1];
        }

        // Local Scroll
        if (mAdapter != null) {
            mReusableIntPair[0] = 0;
            mReusableIntPair[1] = 0;
            //注释3处,自身滚动
            scrollStep(unconsumedX, unconsumedY, mReusableIntPair);
            consumedX = mReusableIntPair[0];
            consumedY = mReusableIntPair[1];
            //减去自身滚动的距离
            unconsumedX -= consumedX;
            unconsumedY -= consumedY;
        }

        // Nested Post Scroll
        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        //注释4处,将剩余的滑动距离分发给外层控件
        dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, null,
                TYPE_NON_TOUCH, mReusableIntPair);
        unconsumedX -= mReusableIntPair[0];
        unconsumedY -= mReusableIntPair[1];

        //...
        //继续请求重绘
        postOnAnimation();
    }
}

注释1处,每一帧的计算出来的滚动距离先分发到外层控件,外层控件调用onNestedPreScroll先滚动。

注释2处,减去外层控件消耗的距离。

注释3处,自身滚动。

注释4处,将剩余的滑动距离分发给外层控件。外层控件调用onNestedScroll来决定是否要进行处理。

这里再总结一下

  1. NestedScrolling2分发滚动事件的时候区分了滚动事件的类型:是正常的触摸滚动还是惯性滑动。

  2. 内层控件先调用dispatchNestedPreFling来处理惯性滑动。如果外层控件处理了惯性滑动,即外层控件的onNestedPreFling方法返回了true。那么NestedScrolling2和NestedScrolling的惯性滑动效果没有什么差异。

  3. 如果外层控件没有处理惯性滑动,也就是外层控件的onNestedPreFling方法返回了false。那么就会调用dispatchNestedFling方法并且内层控件自身开始惯性滑动mViewFlinger.fling(velocityX, velocityY),但是在惯性滑动的每一帧,通过Scroller计算出来的滚动距离通过dispatchNestedPreScroll先分发给外层控件。外层控件可以通过onNestedPreScroll先消耗部分滚动距离,然后内层控件再自身滚动。

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

推荐阅读更多精彩内容