從一次真實經(jīng)歷中說說使用嵌套滑動過程中常見的坑

??本來認(rèn)為自己對嵌套滑動的理解和應(yīng)用還是不錯的舱沧,但是最近做了一個跟手動畫的需求组题,使用嵌套滑動發(fā)現(xiàn)了這里有了很多的坑芯急,本文來根據(jù)自身的踩坑經(jīng)歷和經(jīng)驗來總結(jié)使用嵌套滑動的注意項谬以。

??本文不會介紹嵌套滑動的基本使用,不了解的同學(xué)可以參考我的文章:Android 源碼分析 - 嵌套滑動機制的實現(xiàn)原理雹有。同時,本文嵌套滑動皆以RecyclerView為例臼寄。

1. 不要在onInterceptTouchEvent方法里面攔截事件

??如果你有一個ViewGroup作為RecyclerView的父布局霸奕,這個ViewGroup主要來處理一些嵌套滑動的邏輯,比如說使用系統(tǒng)的SwipeRefreshLayout來做下拉刷新吉拳。如果這個ViewGroup不可能有父布局處理嵌套滑動质帅,那么是否重寫onInterceptTouchEvent可以自身需求來定,比如說SwipeRefreshLayout就重寫了留攒。

??但是如果你的業(yè)務(wù)場景可能還會有ViewGroup來處理嵌套滑動煤惩,作為關(guān)系鏈中間的View千萬不要重寫onInterceptTouchEvent

??可能有對此有疑惑炼邀,現(xiàn)在我以一個具體的場景來解釋具體的原因魄揉,假設(shè)有如下一個場景:

image

??整個事件傳遞的流程是:首先由RecyclerView產(chǎn)生嵌套滑動的事件,然后提交給SwipeRefreshLayout嘗試著處理汤善, SwipeRefreshLayout收到事件之后什猖,發(fā)現(xiàn)還有父View可能會處理,然后在提交給ViewGroup红淡,ViewGroup根據(jù)自身條件選擇消費一定的距離不狮,然后又返回給SwipeRefreshLayoutSwipeRefreshLayout在根據(jù)自身條件選擇消費在旱,最后RecyclerView在消費摇零。整個事件傳遞和消費的流程如下:
image

??這里存在一種特殊情況,如果中間的SwipeRefreshLayout重寫了onInterceptTouchEvent方法桶蝎,導(dǎo)致事件不能傳遞到RecyclerView驻仅,從而導(dǎo)致了嵌套滑動的機制不能觸發(fā)。有人可能有人疑問: SwipeRefreshLayout自己想攔截事件登渣,并且處理事件噪服,這難道有問題嗎?

??針對這個問題胜茧,我想說的是粘优,正常情況下是沒有問題的仇味,但是如果ViewGroup必須跟手變化,只有ViewGroup跟手變化到最終態(tài)才能讓 SwipeRefreshLayout下拉或者RecyclerView滑動雹顺,這種情況下丹墨,不走嵌套滑動的邏輯根本沒法實現(xiàn)。

??可能有人會提出相應(yīng)的解決方法:我重寫ViewGrouponInterceptTouchEvent方法來攔截事件嬉愧,然后消費事件不行嗎贩挣?針對于這種解決方法,我想問的是没酣,如果一次滑動產(chǎn)生10px的有效距離王财,而ViewGroup只能消費其中的5px,剩下的5px怎么辦呢四康?根據(jù)情況傳遞到子View中去或者不消費搪搏?首先不消費是肯定不行的,否則就會顯得滑動不靈敏闪金,其次如果傳遞到子View中去疯溺,這也太麻煩了嘛。

??像這種情況哎垦,我們最好的解決方法就是所有的滑動走嵌套滑動的邏輯囱嫩,因為嵌套滑動本身自己支持消費部分距離的功能,而不用我們?nèi)ヌ厥馓幚怼?br> ??解釋了在什么情況下不要重寫onInterceptTouchEvent方法之后漏设,我們現(xiàn)在來解釋一下系統(tǒng)的SwipeRefreshLayout為什么要重寫onInterceptTouchEvent墨闲。

  1. Google爸爸默認(rèn)為SwipeRefreshLayout已經(jīng)嵌套滑動關(guān)系鏈上最后一個View了,SwipeRefreshLayout不可能再有父View處理嵌套滑動郑口。
  2. 重寫onInterceptTouchEvent可以為SwipeRefreshLayout增加一個新特性--就是不用依賴子View就可以實現(xiàn)下拉刷新鸳碧。也是說,我們在xml布局中直接添加一個SwipeRefreshLayout犬性,不用給它添加子View就能下拉刷新瞻离。這也是嵌套滑動的弊端,必須得有一個View來產(chǎn)生嵌套滑動乒裆。

??針對于上面兩個原因套利,還是不能說服我堅持的觀點--在嵌套滑動鏈上的View不用重寫onInterceptTouchEvent方法。為什么呢鹤耍?上面的第二個問題肉迫,我們還是可以避免:既然是鏈上最底端的View,可以完全自己產(chǎn)生嵌套滑動事件稿黄,然后嘗試著傳遞到父View喊衫,然后自己在消費,而不用去攔截事件杆怕。這樣的話族购,整個關(guān)系鏈都不會破壞鼻听。所以我對系統(tǒng)的SwipeRefreshLayout的設(shè)計抱有遲疑態(tài)度。

2. 不要私自在dispatchTouchEvent的ACTION_CANCEL時機或者ACTION_UP時機調(diào)用stopNestedScroll方法

??在解釋具體原因联四,我們來看一下NestedScrollingChildHelperstartNestedScroll方法和stopNestedScroll方法。
??stopNestedScroll方法比較簡單撑教,我們先來看看

    public void stopNestedScroll(@NestedScrollType int type) {
        ViewParent parent = getNestedScrollingParentForType(type);
        if (parent != null) {
            ViewParentCompat.onStopNestedScroll(parent, mView, type);
            setNestedScrollingParentForType(type, null);
        }
    }

??stopNestedScroll表示的意思朝墩,當(dāng)前type的嵌套滑動結(jié)束了,這里主要做的是將對應(yīng)的ViewParent跟重置為null伟姐。這里為什么需要強調(diào)type呢收苏?通常來說,在正常的滑動中愤兵,stopNestedScroll只會被調(diào)用一次鹿霸,但是別忘了還有fling滑動,所以type分為兩種:

  1. TYPE_TOUCH,表示正掣讶椋滑動懦鼠,然后手指松開。
  2. TYPE_NON_TOUCH,表示手指松開之后還在滑動屹堰。

??所以在RecyclerView中,一次帶fling操作的滑動stopNestedScroll方法會被調(diào)用兩次肛冶,一次是ACTION_UPACTION_CANCEL調(diào)用一次,此時type為TYPE_TOUCH,一次是fling完畢扯键,此時type為TYPE_NON_TOUCH睦袖。
??那么將ViewParent跟重置為null有什么意義呢?這個就得從startNestedScroll方法得到答案荣刑。

    public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
        if (hasNestedScrollingParent(type)) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                    setNestedScrollingParentForType(type, p);
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

??startNestedScroll先從緩存判斷是否有View可以處理馅笙,然而就是因為這個緩存會導(dǎo)致一個問題。以上面的場景厉亏,SwipeRefreshLayout私自在ACTION_UPACTION_CANCEL調(diào)用了stopNestedScroll方法董习,切斷了它與父View的關(guān)系鏈,但是沒有切斷它與RecyclerView的關(guān)系鏈叶堆,導(dǎo)致后面再有事件來的話阱飘,只能傳遞到SwipeRefreshLayout中去,而再也不能傳遞到SwipeRefreshLayout的父View上去虱颗。

??有人說沥匈,這沒事啊,RecyclerView也會在ACTION_UPACTION_CANCEL切斷關(guān)系啊忘渔。但是有沒有考慮到一種情況--就是ACTION_UPACTION_CANCEL事件不能傳遞到RecylcerView當(dāng)中高帖。有很多場景都存在這種情況,比如說我們長按RecyclerView的ItemView然后彈出一個Dialog或者浮層畦粮,然后松開散址,這些都有可能導(dǎo)致事件不能傳遞到RecyclerView中去乖阵。

??我們一旦在ACTION_UPACTION_CANCEL時切斷SwipeRefreshLayout與父View的關(guān)系,但是沒有切斷RecyclerViewSwipeRefreshLayout的關(guān)系预麸,整個關(guān)系鏈就變成這樣了:

image

??事件傳遞就變成了這樣:
image

??從而會導(dǎo)致一種bug瞪浸,在Dialog或者浮層View消失之后第一次滑動中,ViewGroup不能收到事件吏祸,第二次滑動能正常收到对蒲。這是為什么呢?因為第一次滑動之后,RecyclerView會調(diào)用stopNestedScroll方法贡翘;而第二次滑動會重新建立關(guān)系蹈矮,本次關(guān)系鏈就是正常的。

??所以鸣驱,我們千萬不要在ACTION_CANCEL或者ACTION_UP時調(diào)用stopNestedScroll方法泛鸟。研究過RecyclerView源碼的同學(xué)應(yīng)該都知道,RecyclerView卻調(diào)用了踊东,這是為什么呢北滥?

這是因為,在整個嵌套滑動關(guān)系鏈中递胧,RecyclerView只可能是最底層的View碑韵,也就是只能產(chǎn)生嵌套滑動,不可能作為關(guān)系中間的一員缎脾。這一點祝闻,我們可以從RecyclerView繼承的接口加以證明,RecyclerView只實現(xiàn)了NestedScrollingChild接口遗菠,而沒有實現(xiàn)NestedScrollingParent接口联喘。

??所以,我們得出一個結(jié)論辙纬,如下:

一旦一個View實現(xiàn)了NestedScrollingParent接口豁遭,不能在ACTION_CANCEL或者ACTION_UP時調(diào)用stopNestedScroll方法。說到底就是贺拣,誰是startNestedScroll的源頭蓖谢,誰才有資格調(diào)用stopNestedScroll

??同時,有人可能會問譬涡,如果我們的工程已經(jīng)這么干了闪幽,并且不能修改,或者修改的成本比較大怎么辦呢涡匀?也是有解決方法的盯腌,在這個關(guān)系鏈中,凡是實現(xiàn)了NestedScrollingParent接口的View必須在ACTION_CANCEL或者ACTION_UP時調(diào)用stopNestedScroll方法陨瘩。這種方法會強制RecyclerView在調(diào)用startNestedScroll方法時腕够,不走緩存级乍,而是重新建立關(guān)系鏈。有一個小小的弊端帚湘,就是fling開始的時候調(diào)用startNestedScroll方法時本可以使用緩存的玫荣,但是使用此方法之后,會重新建立關(guān)系鏈大诸,性能有所損耗(當(dāng)然這個性能微乎其微崇决,幾乎可以不計??)。

??但是這種方法還有一個比較嚴(yán)重的缺點底挫,就是從此以后fling事件,不能傳遞到ViewGroup脸侥。這是為什么呢?我們從源碼找一下答案:

??首先建邓,RecyclerView是在fling之后切斷Type為TYPE_TOUCH的鏈:

    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;
        if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
           setScrollState(SCROLL_STATE_IDLE);
        }
        resetTouch();
        }
        break;
//----------------------------------------------------------------------------
    private void resetTouch() {
        if (mVelocityTracker != null) {
            mVelocityTracker.clear();
        }
        stopNestedScroll(TYPE_TOUCH);
        releaseGlows();
    }

??其次通過在fling方法里面,我們都是通過TYPE_TOUCH的傳遞鏈傳遞事件的:

    public boolean fling(int velocityX, int velocityY) {
        // ·······
        if (!dispatchNestedPreFling(velocityX, velocityY)) {
            final boolean canScroll = canScrollHorizontal || canScrollVertical;
            dispatchNestedFling(velocityX, velocityY, canScroll);
            // ······
        }
        return false;
    }

??因為我們在dispathcTouchEvent方法里面就把傳遞鏈給中斷了睁枕,這個中斷肯定在fling之前執(zhí)行官边,進(jìn)而導(dǎo)致fling事件只能傳遞到SwipeRefreshLayou,而不能傳遞到ViewGroup(Ps:我們假設(shè)·SwipeRefreshLayoudispathcTouchEvent方法里面就把傳遞鏈給中斷)。這就是fling事件傳遞不過來的根的原因外遇。所以注簿,為了避免各種錯誤,我們千萬不要在私自的調(diào)用stopNestedScroll方法跳仿。

3. 慎重重寫onStartNestedScroll方法

??我們都知道onStartNestedScroll方法是用來標(biāo)識當(dāng)前ViewGroup是消費嵌套滑動的事件诡渴,但是你們不知道這里面也有坑。這里我以一個例子來解釋其中奧妙菲语,同時還會介紹RecyclerView的一個巨坑妄辩。

??我相信大家都做過RecyclerView加載更多的功能,如圖:


??大家可能直接看這張圖有點懵逼山上,我來解釋一下:很多時候眼耀,我們使用RecyclerView來實現(xiàn)加載更多的功能,當(dāng)加載完成之后佩憾,就讓RecyclerView停在那里不再動哮伟,可是一旦我們給RecyclerView套上了一個ViewGroup之后,用來處理嵌套滑動妄帘,就會出現(xiàn)這種情況:

??我來解釋一下上圖中的情況:我們還在加載完成之后楞黄,RecyclerView還在繼續(xù)fling。這種情況是不能容忍的寄摆,怎么來解決呢谅辣?這就需要正確的重寫onStartNestedScroll方法,最簡單和正確的方法是我們在重寫onStartNestedScroll方法時婶恼,必須對type進(jìn)行判斷桑阶,代碼如下:

  @Override
  public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ViewCompat.ScrollAxis int axes, @ViewCompat.NestedScrollType int type) {
    return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 && type == ViewCompat.TYPE_TOUCH;
  }

??我們在onStartNestedScroll方法對type進(jìn)行了判斷柏副,這也是我們重寫onStartNestedScroll方法時非常容易忽視的點。

??問題倒是解決了蚣录,可是大家肯定好奇為什么會出現(xiàn)這種情況割择,同時為什么加了type的判斷就能解決呢?
??首先萎河,我先來解釋一下為什么會這種情況荔泳,其實答案是非常的簡單,在加載完成過程中虐杯,ViewFlinger還在繼續(xù)fling玛歌,當(dāng)數(shù)據(jù)回來時,此時fling事件還未完成擎椰,新數(shù)據(jù)加載到RecyclerView中去支子,ViewFlinger發(fā)現(xiàn)此時已經(jīng)有空間可以滑動了,那么就會繼續(xù)滑動达舒。我自己覺得這是RecyclerView挖的一個坑值朋。

??其次,我們來看一下巩搏,為什么加上type判斷就能解決問題呢昨登?我們從RecyclerView的fling方法尋找答案:

    public boolean fling(int velocityX, int velocityY) {
        // ······
        // 1. 分發(fā)fling
        if (!dispatchNestedPreFling(velocityX, velocityY)) {
            final boolean canScroll = canScrollHorizontal || canScrollVertical;
            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;
                }
                // 2. 建立type為TYPE_NON_TOUCH的傳遞鏈
                startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);

                velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
                velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
                mViewFlinger.fling(velocityX, velocityY);
                return true;
            }
        }
        return false;
    }

??在fling方法里面,做了比較重要兩件事:

  1. 分發(fā)fling事件贯底。如果我們在處理嵌套滑動丰辣,很少會自己處理fling事件,所以dispatchNestedPreFling方法通常返回為false禽捆,從而進(jìn)入了if的判斷語句中糯俗。
  2. 通過startNestedScroll方法建立type為TYPE_NON_TOUCH的嵌套滑動傳遞鏈。由于睦擂,我們在上層View中沒有對type進(jìn)行判斷得湘,所以最終的傳遞鏈中會有我們的ViewGroup

??然后顿仇,我們再來看看ViewFlingerrun方法的一段代碼:

                if (!dispatchNestedScroll(hresult, vresult, overscrollX, overscrollY, null,
                        TYPE_NON_TOUCH)
                        && (overscrollX != 0 || overscrollY != 0)) {
                    final int vel = (int) scroller.getCurrVelocity();

                    int velX = 0;
                    if (overscrollX != x) {
                        velX = overscrollX < 0 ? -vel : overscrollX > 0 ? vel : 0;
                    }

                    int velY = 0;
                    if (overscrollY != y) {
                        velY = overscrollY < 0 ? -vel : overscrollY > 0 ? vel : 0;
                    }

                    if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
                        absorbGlows(velX, velY);
                    }
                    if ((velX != 0 || overscrollX == x || scroller.getFinalX() == 0)
                            && (velY != 0 || overscrollY == y || scroller.getFinalY() == 0)) {
                        scroller.abortAnimation();
                    }
                }

??這段代碼中的作用就是淘正,當(dāng)fling的速度為0時或者滑動的距離為0時,會通過abortAnimation來中斷后面的fling臼闻。因為我們在startNestedScroll成功的建立傳遞鏈鸿吆,所以在這里dispatchNestedScroll肯定為true,所以永遠(yuǎn)走不到這段邏輯述呐,最終就會導(dǎo)致上面出現(xiàn)的那個問題惩淳。

??而我們在我們ViewGroup的onStartNestedScroll方法對type加上了判斷,在建立的傳遞鏈中不會有我們得ViewGroup,所以dispatchNestedScroll方法就會返回為false,在滑不動時思犁,自然就會中斷未完成的fling代虾。最終我們證實了上面的解決方法為什么是正確的,而不是通過一種hack方式來實現(xiàn)激蹲。

到此棉磨,我就對此坑的分析就結(jié)束了。綜上所述学辱,我們在重寫onStartNestedScroll方法一定要小心乘瓤,一定要考慮到type為TYPE_NON_TOUCH的情況。

4. 總結(jié)

??最后策泣,我在此說幾句衙傀,嵌套滑動是爸爸給我們的好東西,但是我也們不能亂用萨咕,否則出了問題真的是太難找的根本原因了差油,血的教訓(xùn)啊H味础!发侵!????

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末交掏,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子刃鳄,更是在濱河造成了極大的恐慌盅弛,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件叔锐,死亡現(xiàn)場離奇詭異挪鹏,居然都是意外死亡,警方通過查閱死者的電腦和手機愉烙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進(jìn)店門讨盒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人步责,你說我怎么就攤上這事返顺。” “怎么了蔓肯?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵遂鹊,是天一觀的道長。 經(jīng)常有香客問我蔗包,道長秉扑,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任调限,我火速辦了婚禮舟陆,結(jié)果婚禮上误澳,老公的妹妹穿的比我還像新娘。我一直安慰自己吨娜,他們只是感情好脓匿,可當(dāng)我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著宦赠,像睡著了一般陪毡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上勾扭,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天毡琉,我揣著相機與錄音,去河邊找鬼妙色。 笑死桅滋,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的身辨。 我是一名探鬼主播丐谋,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼煌珊!你這毒婦竟也來了号俐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤定庵,失蹤者是張志新(化名)和其女友劉穎吏饿,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蔬浙,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡猪落,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了畴博。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片笨忌。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖俱病,靈堂內(nèi)的尸體忽然破棺而出蜜唾,到底是詐尸還是另有隱情,我是刑警寧澤庶艾,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布袁余,位于F島的核電站,受9級特大地震影響咱揍,放射性物質(zhì)發(fā)生泄漏颖榜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望掩完。 院中可真熱鬧噪漾,春花似錦、人聲如沸且蓬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽恶阴。三九已至诈胜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間冯事,已是汗流浹背焦匈。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留昵仅,地道東北人缓熟。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像摔笤,于是被迫代替她去往敵國和親够滑。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,828評論 2 345

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