Scroller實(shí)現(xiàn)滾動(dòng)的原理

以前買了一本《Android 開(kāi)發(fā)藝術(shù)探索》,當(dāng)時(shí)看完也是感覺(jué)受益匪淺锡移,書(shū)上面也是留下了努力學(xué)習(xí)的筆記呕童,哈哈,結(jié)果不知道怎么搞丟了淆珊,也是艱難夺饲,最近又新買了一本,看起來(lái)還是感覺(jué)受益匪淺,哈哈往声。

先看一個(gè)簡(jiǎn)單的使用Scroller的例子

1562401862002342.gif

從上面的圖片中也可以看出來(lái)擂找,這里的滾動(dòng)是指View內(nèi)容的滾動(dòng)而非View本身位置的改變。

先來(lái)一張流程圖烁挟。

Scroller實(shí)現(xiàn)滾動(dòng)的原理.jpg

上面例子中使用到的自定義的TestSmoothScrollView婴洼,代碼如下

class TestSmoothScrollView @JvmOverloads constructor(
        context: Context,
        attrs: AttributeSet? = null,
        defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private var scroller: Scroller = Scroller(context)
    private val paint = Paint()
    private var color: Int = 0

    init {
        color = context.resources.getColor(R.color.colorAccent)
        paint.color = color
        paint.style = Paint.Style.FILL
    }

    override fun onDraw(canvas: Canvas) {
        canvas.drawColor(color)
        paint.color = context.resources.getColor(R.color.colorPrimary)
        canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
    }

    /**
     * 使用 scroller滾動(dòng)
     *
     * @param destX 在水平方滾動(dòng)到的目的地
     * @param destY 豎直方向上滾動(dòng)的目的地
     */
    fun smoothScrollTo(destX: Int, destY: Int) {
        //要滾動(dòng)的距離
        val deltaX = destX - scrollX
        val deltaY = destY - scrollY

        scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000)
        invalidate()
    }

    override fun computeScroll() {
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.currX, scroller.currY)
            invalidate()
        }
    }
}

調(diào)用TestSmoothScrollView的smoothScrollTo方法即可實(shí)現(xiàn)滾動(dòng)。

btnStartScroll.setOnClickListener {
   //向右下方向滾動(dòng)100像素
   smoothScrollView.smoothScrollTo(-100, -100)
}

我們先來(lái)看一下TestSmoothScrollView的smoothScrollTo方法

/**
  * 使用 scroller滾動(dòng)
  *
  * @param destX 在水平方滾動(dòng)到的目的地
  * @param destY 豎直方向上滾動(dòng)的目的地
  */
fun smoothScrollTo(destX: Int, destY: Int) {

     //計(jì)算出要滾動(dòng)的距離
     val deltaX = destX - scrollX
     val deltaY = destY - scrollY
     //注釋1處
     scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000)
     //注釋2處
     invalidate()
}

首先我們根據(jù)View當(dāng)前的scrollX撼嗓,scrollY 和傳入的參數(shù)計(jì)算出水平和豎直方向上要滾動(dòng)的距離柬采。然后在注釋1處調(diào)用了Scroller的startScroll方法。

Scroller的startScroll方法

/**
 * 通過(guò)提供一個(gè)起點(diǎn)且警,滾動(dòng)距離和滾動(dòng)時(shí)間開(kāi)始滾動(dòng)粉捻。
 * 
 * @param startX 水平方向上的滾動(dòng)起點(diǎn),單位是像素斑芜。
 * @param startY 豎直方向上的滾動(dòng)起點(diǎn)肩刃,單位是像素。
 * @param dx 水平滾動(dòng)距離杏头,單位是像素盈包。正值會(huì)使View的內(nèi)容向左滾動(dòng)。
 * @param dy 豎直方向上的滾動(dòng)距離醇王,單位是像素呢燥。正值會(huì)使View的內(nèi)容向上滾動(dòng)。
 * @param 滾動(dòng)時(shí)間寓娩,單位是毫秒
 */
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
   //為mMode賦值為SCROLL_MODE
    mMode = SCROLL_MODE;
    mFinished = false;
    mDuration = duration;
    //開(kāi)始滾動(dòng)時(shí)間
    mStartTime = AnimationUtils.currentAnimationTimeMillis();
    mStartX = startX;
    mStartY = startY;
    mFinalX = startX + dx;
    mFinalY = startY + dy;
    mDeltaX = dx;
    mDeltaY = dy;
    //滾動(dòng)時(shí)間的倒數(shù)
    mDurationReciprocal = 1.0f / (float) mDuration;
}

在Scroller的startScroll方法中叛氨,代碼很簡(jiǎn)單,只是保存了某些值棘伴。有幾個(gè)比較重要的點(diǎn)寞埠。

  • 將mMode賦值為SCROLL_MODE
  • 為開(kāi)始滾動(dòng)時(shí)間mStartTime賦值
  • 計(jì)算滾動(dòng)時(shí)間的倒數(shù)mDurationReciprocal

但是我們發(fā)現(xiàn)這里并沒(méi)有讓View滾動(dòng)起來(lái)。那么View是怎么滾動(dòng)起來(lái)的呢焊夸,答案就是TestSmoothScrollView的smoothScrollTo方法的注釋2處仁连,調(diào)用了invalidate方法。

調(diào)用invalidate以后淳地,會(huì)導(dǎo)致View重繪怖糊,View在重繪過(guò)程中又會(huì)調(diào)用computeScroll方法,而computeScroll又會(huì)從Scroller中獲取當(dāng)前的scrollX和scrollY然后通過(guò)scrollTo方法實(shí)現(xiàn)滾動(dòng)颇象。接著又調(diào)用invalidate方法進(jìn)行第二次重繪伍伤,如此反復(fù)直到滑動(dòng)到最終的位置。

調(diào)用invalidate以后遣钳,會(huì)導(dǎo)致TestSmoothScrollView重繪扰魂,TestSmoothScrollView的父View的drawChild()方法會(huì)調(diào)用下面的方法來(lái)讓TestSmoothScrollView繪制自己。

/**
 * ViewGroup的drawChild()方法會(huì)調(diào)用該方法來(lái)讓每個(gè)子View來(lái)繪制自己。
 *
 * 在這里View會(huì)根據(jù) layer type 來(lái)指定 渲染行為和硬件加速劝评。
 */
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    //...
    if (drawingWithRenderNode) {
        //注釋1處
        renderNode = updateDisplayListIfDirty();
        if (!renderNode.isValid()) {
            renderNode = null;
            drawingWithRenderNode = false;
        }
    }
    //...

}

注釋1處姐直,調(diào)用updateDisplayListIfDirty方法。

public RenderNode updateDisplayListIfDirty() {

    try {
        if (layerType == LAYER_TYPE_SOFTWARE) {
            //...
        } else {
           //注釋1處
           computeScroll();
           //注釋2處
           canvas.translate(-mScrollX, -mScrollY);
           //注釋3處
           draw(canvas);    
        }
    } 
    //...
}

注釋1處蒋畜,調(diào)用computeScroll方法声畏,我們重寫(xiě)了該方法,方法內(nèi)部會(huì)計(jì)算出當(dāng)前的mScrollXmScrollY姻成。

注釋2處插龄,畫(huà)布偏移mScrollXmScrollY這才是實(shí)現(xiàn)內(nèi)容滾動(dòng)的根本原因?普埂>巍!

注釋3處才睹,在偏移了的畫(huà)布上繪制內(nèi)容徘跪,表現(xiàn)出來(lái)的結(jié)果就是我們的內(nèi)容偏移了。畫(huà)布每一幀都偏移一點(diǎn)琅攘,從而產(chǎn)生了滾動(dòng)的效果垮庐。

接下來(lái),我們看一看我們重寫(xiě)的computeScroll方法坞琴。

override fun computeScroll() {
    //注釋1處突硝,
    if (scroller.computeScrollOffset()) {
        //注釋2處,調(diào)用scrollTo方法
        scrollTo(scroller.currX, scroller.currY)
       //繼續(xù)調(diào)用invalidate方法請(qǐng)求重繪置济。
        invalidate()
    }
}

我們看下注釋1處Scroller的computeScrollOffset方法

/**
 * 如果你想知道新的位置,請(qǐng)調(diào)用這個(gè)方法锋八。如果該方法返回true浙于,說(shuō)明動(dòng)畫(huà)還沒(méi)結(jié)束。
 */ 
public boolean computeScrollOffset() {
    if (mFinished) {
        return false;
    }
    //計(jì)算流逝的時(shí)間
    int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
    //如果沒(méi)有結(jié)束
    if (timePassed < mDuration) {
        switch (mMode) {//注意挟纱,我們?cè)谏厦鏋閙Mode賦值為SCROLL_MODE
        case SCROLL_MODE:
            //插值器根據(jù)流逝的時(shí)間計(jì)算應(yīng)改變的百分比x
            final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
            mCurrX = mStartX + Math.round(x * mDeltaX);
            mCurrY = mStartY + Math.round(x * mDeltaY);
            break;
        //...
        }
    }
    else {
        //結(jié)束
        mCurrX = mFinalX;
        mCurrY = mFinalY;
        mFinished = true;
    }
    return true;
}

在上面的方法中羞酗,如果已經(jīng)到了滾動(dòng)的結(jié)束時(shí)間,那么滾動(dòng)結(jié)束紊服,該方法返回false檀轨。

如果時(shí)間還沒(méi)有到滾動(dòng)的結(jié)束時(shí)間,步驟如下:

  1. 計(jì)算流逝的時(shí)間
  2. 插值器根據(jù)流逝的時(shí)間計(jì)算應(yīng)改變的百分比x
  3. 計(jì)算當(dāng)前應(yīng)該滾動(dòng)到的位置賦值給mCurrX欺嗤,mCurrY参萄。
  4. 返回true。

如果返回了true煎饼,則View會(huì)調(diào)用scrollTo方法保存當(dāng)前應(yīng)該到達(dá)位置讹挎,然后繼續(xù)調(diào)用invalidate方法請(qǐng)求重繪。

if (scroller.computeScrollOffset()) {
    //調(diào)用scrollTo方法滾動(dòng)到當(dāng)前應(yīng)該到達(dá)位置currX,currY
    scrollTo(scroller.currX, scroller.currY)
   //繼續(xù)調(diào)用invalidate方法請(qǐng)求重繪筒溃。
    invalidate()
}

View的scrollTo方法马篮。

public void scrollTo(int x, int y) {
    if (mScrollX != x || mScrollY != y) {
        int oldX = mScrollX;
        int oldY = mScrollY;
        //為mScrollX和mScrollY賦值
        mScrollX = x;
        mScrollY = y;
        invalidateParentCaches();
        onScrollChanged(mScrollX, mScrollY, oldX, oldY);
        if (!awakenScrollBars()) {
            postInvalidateOnAnimation();
        }
    }
}

Scroller的Fling

/**
 * 依據(jù)一個(gè)fling手勢(shì)開(kāi)始滾動(dòng)。滾動(dòng)的距離依賴fling的初始速度怜奖。
 * 
 * @param startX 滾動(dòng)起點(diǎn)的X坐標(biāo)
 *
 * @param startY 滾動(dòng)起點(diǎn)的Y坐標(biāo)
 *
 * @param velocityX 在X坐標(biāo)軸上的初始速度浑测,測(cè)量單位是像素/秒
 *
 * @param velocityY 在Y坐標(biāo)軸上的初始速度,測(cè)量單位是像素/秒
 *
 * @param minX 最小的X軸上的值歪玲。scroller的滾動(dòng)不會(huì)超過(guò)這個(gè)點(diǎn)
 *       
 * @param maxX 最大的X軸上的值迁央。scroller的滾動(dòng)不會(huì)超過(guò)這個(gè)點(diǎn)。
 *       
 * @param minY 最小的Y軸上的值读慎。scroller的滾動(dòng)不會(huì)超過(guò)這個(gè)點(diǎn)漱贱。
 *      
 * @param maxY 最小的Y軸上的值。scroller的滾動(dòng)不會(huì)超過(guò)這個(gè)點(diǎn)夭委。
 *        
 */
public void fling(int startX, int startY, int velocityX, int velocityY,
        int minX, int maxX, int minY, int maxY) {
    //繼續(xù)一個(gè)未結(jié)束的滾動(dòng)或者fling
    if (mFlywheel && !mFinished) {
        //一個(gè)未結(jié)束的滾動(dòng)或者fling當(dāng)前的速度
        float oldVel = getCurrVelocity();

        float dx = (float) (mFinalX - mStartX);
        float dy = (float) (mFinalY - mStartY);
        //求x和y平方和的二次方根
        float hyp = (float) Math.hypot(dx, dy);

        float ndx = dx / hyp;
        float ndy = dy / hyp;
        //一個(gè)未結(jié)束的滾動(dòng)或者fling在X軸和Y軸上的當(dāng)前的速度
        float oldVelocityX = ndx * oldVel;
        float oldVelocityY = ndy * oldVel;
        //如果一個(gè)未結(jié)束的滾動(dòng)或者fling和這次fling在X軸和Y軸上的速度方向都相同幅狮,則累加在X軸和Y軸上的速度
        if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
                Math.signum(velocityY) == Math.signum(oldVelocityY)) {
            velocityX += oldVelocityX;
            velocityY += oldVelocityY;
        }
    }
   //當(dāng)前模式是fling
    mMode = FLING_MODE;
    mFinished = false;
   //求velocityX和velocityX平方和的二次方根,也就是當(dāng)前速度
    float velocity = (float) Math.hypot(velocityX, velocityY);
    //保存當(dāng)前速度 
    mVelocity = velocity;
    mDuration = getSplineFlingDuration(velocity);
    mStartTime = AnimationUtils.currentAnimationTimeMillis();
    mStartX = startX;
    mStartY = startY;

    float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
    float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;
    
    double totalDistance = getSplineFlingDistance(velocity);
    //計(jì)算總共要滾動(dòng)的距離(帶方向)
    mDistance = (int) (totalDistance * Math.signum(velocity));
        
    mMinX = minX;
    mMaxX = maxX;
    mMinY = minY;
    mMaxY = maxY;
    //最終要到達(dá)的X坐標(biāo)
    mFinalX = startX + (int) Math.round(totalDistance * coeffX);
    // Pin to mMinX <= mFinalX <= mMaxX
    mFinalX = Math.min(mFinalX, mMaxX);
    mFinalX = Math.max(mFinalX, mMinX);
    //最終要到達(dá)的Y坐標(biāo)
    mFinalY = startY + (int) Math.round(totalDistance * coeffY);
    // Pin to mMinY <= mFinalY <= mMaxY
    mFinalY = Math.min(mFinalY, mMaxY);
    mFinalY = Math.max(mFinalY, mMinY);
}

fling方法中主要做了2件事:

  1. 如果有一個(gè)未結(jié)束的滾動(dòng)或者fling和這次fling在X軸和Y軸上的速度方向都相同株灸,則累加在X軸和Y軸上的速度崇摄。

  2. 計(jì)算出最新的速度mVelocity、滾動(dòng)時(shí)間mDuration慌烧、滾動(dòng)距離mDistance逐抑、起始坐標(biāo)mStartX,mStartY、終點(diǎn)坐標(biāo)mFinalX屹蚊、mFinalY厕氨。

下面還是要看computeScroll方法。

public boolean computeScrollOffset() {
    if (mFinished) {
        return false;
    }

    int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
    //還沒(méi)結(jié)束
    if (timePassed < mDuration) {
        switch (mMode) {
        case SCROLL_MODE:
            final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
            mCurrX = mStartX + Math.round(x * mDeltaX);
            mCurrY = mStartY + Math.round(x * mDeltaY);
            break;
        case FLING_MODE:
            final float t = (float) timePassed / mDuration;
            final int index = (int) (NB_SAMPLES * t);
            float distanceCoef = 1.f;
            float velocityCoef = 0.f;
            if (index < NB_SAMPLES) {//這幾行代碼看不懂汹粤,哈哈
                final float t_inf = (float) index / NB_SAMPLES;
                final float t_sup = (float) (index + 1) / NB_SAMPLES;
                final float d_inf = SPLINE_POSITION[index];
                final float d_sup = SPLINE_POSITION[index + 1];
                velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                distanceCoef = d_inf + (t - t_inf) * velocityCoef;
            }
            //根據(jù)時(shí)間的流逝計(jì)算出當(dāng)前速度
            mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
            //計(jì)算出當(dāng)前X坐標(biāo)   
            mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
            // Pin to mMinX <= mCurrX <= mMaxX
            mCurrX = Math.min(mCurrX, mMaxX);
            mCurrX = Math.max(mCurrX, mMinX);
            //計(jì)算出當(dāng)前Y坐標(biāo)       
            mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
            // Pin to mMinY <= mCurrY <= mMaxY
            mCurrY = Math.min(mCurrY, mMaxY);
            mCurrY = Math.max(mCurrY, mMinY);
            //已經(jīng)結(jié)束
            if (mCurrX == mFinalX && mCurrY == mFinalY) {
                mFinished = true;
            }

            break;
        }
    }
    else {
        mCurrX = mFinalX;
        mCurrY = mFinalY;
        mFinished = true;
    }
    return true;
}

我們發(fā)現(xiàn)fling和滾動(dòng)的區(qū)別就在于當(dāng)前的坐標(biāo)mCurrXmCurrY的計(jì)算方式而已命斧,其他并沒(méi)有什么區(qū)別。真正的滾動(dòng)還是依賴于我們重寫(xiě)方法來(lái)滾動(dòng)到計(jì)算出來(lái)的mCurrXmCurrY嘱兼。

override fun computeScroll() {
    if (scroller.computeScrollOffset()) {
        //滾動(dòng)到當(dāng)前坐標(biāo)`mCurrX`和`mCurrY`
        scrollTo(scroller.currX, scroller.currY)
        invalidate()
    }
}

參考鏈接:

*《Android 開(kāi)發(fā)藝術(shù)探索》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末国葬,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子芹壕,更是在濱河造成了極大的恐慌汇四,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,695評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件踢涌,死亡現(xiàn)場(chǎng)離奇詭異通孽,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)斯嚎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門利虫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)挨厚,“玉大人,你說(shuō)我怎么就攤上這事糠惫∫咛辏” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,130評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵硼讽,是天一觀的道長(zhǎng)巢价。 經(jīng)常有香客問(wèn)我,道長(zhǎng)固阁,這世上最難降的妖魔是什么壤躲? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,648評(píng)論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮备燃,結(jié)果婚禮上碉克,老公的妹妹穿的比我還像新娘。我一直安慰自己并齐,他們只是感情好漏麦,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著况褪,像睡著了一般撕贞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上测垛,一...
    開(kāi)封第一講書(shū)人閱讀 52,268評(píng)論 1 309
  • 那天捏膨,我揣著相機(jī)與錄音,去河邊找鬼食侮。 笑死号涯,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的锯七。 我是一名探鬼主播诚隙,決...
    沈念sama閱讀 40,835評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼起胰!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起巫延,我...
    開(kāi)封第一講書(shū)人閱讀 39,740評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤效五,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后炉峰,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體畏妖,經(jīng)...
    沈念sama閱讀 46,286評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評(píng)論 3 340
  • 正文 我和宋清朗相戀三年疼阔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了戒劫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片半夷。...
    茶點(diǎn)故事閱讀 40,505評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖迅细,靈堂內(nèi)的尸體忽然破棺而出巫橄,到底是詐尸還是另有隱情,我是刑警寧澤茵典,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布湘换,位于F島的核電站,受9級(jí)特大地震影響统阿,放射性物質(zhì)發(fā)生泄漏彩倚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評(píng)論 3 333
  • 文/蒙蒙 一扶平、第九天 我趴在偏房一處隱蔽的房頂上張望帆离。 院中可真熱鬧,春花似錦结澄、人聲如沸哥谷。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,357評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)呼巷。三九已至,卻和暖如春赎瑰,著一層夾襖步出監(jiān)牢的瞬間王悍,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,466評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工餐曼, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留压储,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,921評(píng)論 3 376
  • 正文 我出身青樓源譬,卻偏偏與公主長(zhǎng)得像集惋,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子踩娘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評(píng)論 2 359

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