10分鐘帶你入門NestedScrolling機制

1、從一個簡單的DEMO看什麼是嵌套滾動

咱們先來看一下DEMO的效果,直觀的感覺一下什麼是嵌套滾動:java

scrolling

在解釋上圖涉及到哪些嵌套滑動操做以前,我先貼一下嵌套佈局的xml結構:android

<com.wzy.nesteddetail.view.NestedWebViewRecyclerViewGroup>

    <com.wzy.nesteddetail.view.NestedScrollWebView/>
    
    <TextView />
    
    <android.support.v7.widget.RecyclerView />

</com.wzy.nesteddetail.view.NestedWebViewRecyclerViewGroup>

其中:git

  1. NestedWebViewRecyclerViewGroup爲最外層滑動容器;
  2. com.wzy.nesteddetail.view.NestedScrollWebView爲佈局頂部可嵌套滑動的View;
  3. TextView爲佈局中部不可滑動的View;
  4. android.support.v7.widget.RecyclerView爲佈局底部可滑動的View;

如今咱們來講明一下,簡單的DEMO效果中包含了哪些嵌套滑動操做:github

  1. 向上滑動頂部WebView時,首先滑動WebView的內容,WebView的內容滑動到底後再滑動外層容器。外層容器滑動到RecyclerView徹底露出後,再將滑動距離或者剩餘速度傳遞給RecyclerView繼續滑動.
  2. 滑動底部RecyclerView時,首先滑動RecyclerView的內容,RecyclerView的內容滑動到頂後再滑動外層容器。外層容器也滑動到頂後,再將滑動距離或者剩餘速度傳遞給WebView繼續滑動.
  3. 觸摸自己不可滑動的TextView時,滑動事件被外層容器攔截。外層容器根據滑動方向和是否滑動到相應閾值,再將相應的滑動距離或者速度傳遞給WebView或者RecyclerView.

再不知道NestedScrolling機制以前,我相信大部分人想實現上面的滑動效果都是比較頭大的,特別是滑動距離和速度要從WebView->外層容器->RecyclerView而且還要支持反向傳遞
有了Google提供的牛逼嵌套滑動機制,再加上這篇文章粗淺的科普,我相信大部分人都可以實現這種滑動效果。這種效果最多見的應用場景就是各類新聞客戶端的詳情頁。web

2、NestedScrolling接口簡介

Android在support.v4包中提供了用於View支持嵌套滑動的兩個接口:ide

  • NestedScrollingParent
  • NestedScrollingChild

我先用比較白話的語言介紹一下NestedScrolling的工做原理:佈局

  1. Google從邏輯上區分了滑動的兩個角色:NestedScrollingParent簡稱ns parent,NestedScrollingChild簡稱ns child。對應了滑動佈局中的外層滑動容器和內部滑動容器。
  2. ns child在收到DOWN事件時,找到離本身最近的ns parent,與它進行綁定並關閉它的事件攔截機制。
  3. ns child會在接下來的MOVE事件中斷定出用戶觸發了滑動手勢,並把事件攔截下來給本身消費。
  4. 消費MOVE事件流時,對於每個MOVE事件增長的滑動距離:
    4.1. ns child並非直接本身消費,而是先將它交給ns parent,讓ns parent能夠在ns child滑動前進行消費。
    4.2. 若是ns parent沒有消費或者滑動沒消費完,ns child再消費剩下的滑動。
    4.3. 若是ns child消費後滑動仍是有剩餘,會把剩下的滑動距離再交給ns parent消費。
    4.4. 最後若是ns parent消費滑動後還有剩餘,ns child能夠作最終處理。
  5. ns child在收到UP事件時,能夠計算出須要滾動的速度,ns child對於速度的消費流程是:
    5.1 ns child在進行flying操做前,先詢問ns parent是否須要消費該速度。若是ns parent消費該速度,後續就由ns parent帶飛,本身就不消費該速度了。若是ns parent不消費,則ns child進行本身的flying操做。
    5.2 ns child在flying過程當中,若是已經滾動到閾值速度仍沒有消費完,會再次將速度分發給ns parent,將ns parent進行消費。

NestedScrollingParent和NestedScrollingChild的源碼定義也是爲了配合滑動實現定義出來的:this

NestedScrollingChildspa

void setNestedScrollingEnabled(boolean enabled);    // 設置是否開啓嵌套滑動
boolean isNestedScrollingEnabled();                 // 得到設置開啓了嵌套滑動
boolean startNestedScroll(@ScrollAxis int axes);    // 沿給定的軸線開始嵌套滾動
void stopNestedScroll();                            // 中止當前嵌套滾動
boolean hasNestedScrollingParent();                 // 若是有ns parent,返回true
boolean dispatchNestedPreScroll(int dx
    , int dy
    , @Nullable int[] consumed
    , @Nullable int[] offsetInWindow);              // 消費滑動時間前,先讓ns parent消費
boolean dispatchNestedScroll(int dxConsumed
    , int dyConsumed
    , int dxUnconsumed
    , int dyUnconsumed
    , @Nullable int[] offsetInWindow);              // ns parent消費ns child剩餘滾動後是否還有剩餘。return true表明還有剩餘
boolean dispatchNestedPreFling(float velocityX
    , float velocityY);                            // 消費fly速度前,先讓ns parent消費
boolean dispatchNestedFling(float velocityX
    , float velocityY
    , boolean consumed);                           // ns parent消費ns child消費後的速度以後是否還有剩餘。return true表明還有剩餘

NestedScrollingParent.net

boolean onStartNestedScroll(@NonNull View var1
    , @NonNull View var2
    , int var3);                                  // 決定是否接收子View的滾動事件
void onNestedScrollAccepted(@NonNull View var1
    , @NonNull View var2
    , int var3);                                 // 響應子View的滾動
void onStopNestedScroll(@NonNull View var1);     // 滾動結束的回調
void onNestedPreScroll(@NonNull View var1
    , int var2
    , int var3
    , @NonNull int[] var4);                      // ns child滾動前回調
void onNestedScroll(@NonNull View var1
    , int var2
    , int var3
    , int var4
    , int var5);                                // ns child滾動後回調
boolean onNestedPreFling(@NonNull View var1
    , float var2
    , float var3);                             // ns child flying前回調
boolean onNestedFling(@NonNull View var1
    , float var2
    , float var3
    , boolean var4);                           // ns child flying後回調
int getNestedScrollAxes();                     // 返回當前佈局嵌套滾動的座標軸

Google爲了讓開發者更加方便的實現這兩個接口,提供了NestedScrollingParentHelper和NestedScrollingChildHelper這兩個輔助。因此實現NestedScrolling這兩個接口的經常使用寫法是:

ns child:

public class NestedScrollingWebView extends WebView implements NestedScrollingChild {
    private NestedScrollingChildHelper mChildHelper;

    private NestedScrollingChildHelper getNestedScrollingHelper() {
        if (mChildHelper == null) {
            mChildHelper = new NestedScrollingChildHelper(this);
        }
        return mChildHelper;
    }

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        getNestedScrollingHelper().setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return getNestedScrollingHelper().isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return getNestedScrollingHelper().startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        getNestedScrollingHelper().stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return getNestedScrollingHelper().hasNestedScrollingParent();
    }

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

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
        return getNestedScrollingHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

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

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

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

    @Override
    public void stopNestedScroll(int type) {
        getNestedScrollingHelper().stopNestedScroll(type);
    }

    @Override
    public boolean hasNestedScrollingParent(int type) {
        return getNestedScrollingHelper().hasNestedScrollingParent(type);
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type) {
        return getNestedScrollingHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type);
    }

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

ns parent:

public class NestedScrollingDetailContainer extends ViewGroup implements NestedScrollingParent {
    private NestedScrollingParentHelper mParentHelper;    
    
    private NestedScrollingParentHelper getNestedScrollingHelper() {
        if (mParentHelper == null) {
            mParentHelper = new NestedScrollingParentHelper(this);
        }
        return mParentHelper;
    }    

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public int getNestedScrollAxes() {
        return getNestedScrollingHelper().getNestedScrollAxes();
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int axes) {
        getNestedScrollingHelper().onNestedScrollAccepted(child, target, axes);
    }

    @Override
    public void onStopNestedScroll(View child) {
        getNestedScrollingHelper().onStopNestedScroll(child);
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        // 處理預先flying事件
        return false;
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        // 處理後續flying事件
        return false;
    }

    @Override
    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        // 處理後續scroll事件
    }

    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed) {
        // 處理預先滑動scroll事件
    }
}

3、效果實現

只知道原理你們確定是不知足的,結合原理進行實操纔是關鍵。這裏以DEMO的效果爲例,想要實現DEMO的效果,須要自定義兩個嵌套滑動容器:

  1. 自定義一個支持嵌套WebView和RecyclerView滑動的外部容器。
  2. 自定義一個實現NestedScrollingChild接口的WebView。

外部滑動容器

在實現外部滑動的容器的時候,咱們首先須要考慮這個外部滑動容器的滑動閾值是什麼?
答: 外部滑動的滑動閾值=外部容器中全部子View的高度-外部容器的高度。同理相似WebView的滑動閾值=WebView的內容高度-WebView的容器高度。

對應代碼實現:

private int mInnerScrollHeight;    // 可滑動的最大距離
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    int width;
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int measureWidth = MeasureSpec.getSize(widthMeasureSpec);

    int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
    if (widthMode == MeasureSpec.EXACTLY) {
        width = measureWidth;
    } else {
        width = mScreenWidth;
    }

    int left = getPaddingLeft();
    int right = getPaddingRight();
    int top = getPaddingTop();
    int bottom = getPaddingBottom();
    int count = getChildCount();
    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);
        LayoutParams params = child.getLayoutParams();
        int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, left + right, params.width);
        int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, top + bottom, params.height);
        measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec);
    }
    setMeasuredDimension(width, measureHeight);
    findWebView(this);
    findRecyclerView(this);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childTotalHeight = 0;
    mInnerScrollHeight = 0;
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        int childWidth = child.getMeasuredWidth();
        int childHeight = child.getMeasuredHeight();
        child.layout(0, childTotalHeight, childWidth, childHeight + childTotalHeight);
        childTotalHeight += childHeight;
        mInnerScrollHeight += childHeight;
    }
    mInnerScrollHeight -= getMeasuredHeight();
}

其次,須要考慮當WebView傳遞上滑事件和RecyclerView傳遞下滑事件時如何處理:

  1. 向上滑動時,若是WebView內容尚未到底,該事件交給WebView處理;若是WebView內容已經滑動到底,可是滑動距離沒有超過外部容器的最大滑動距離,該事件由滑動容器自身處理;若是WebView內容已經滑動到底,而且滑動距離超過了外部容器的最大滑動距離,這時將滑動事件傳遞給底部的 RecyclerView,讓RecyclerView處理;
  2. 向下滑動時,若是RecyclerView沒有到頂部,該事件交給RecyclerView處理;若是RecyclerView已經到頂部,而且外部容器的滑動距離不爲0,該事件由外部容器處理;若是RecyclerView已經到頂部,而且外部容器的滑動距離已經爲0,則該事件交給WebView處理;

對應的WebView傳遞上滑速度和RecyclerView傳遞下滑速度,處理和Scroll傳遞相似。

對應代碼實現:

@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, int type) {
    boolean isWebViewBottom = !canWebViewScrollDown();
    boolean isCenter = isParentCenter();
    if (dy > 0 && isWebViewBottom && getScrollY() < getInnerScrollHeight()) {
        //爲了WebView滑動到底部,繼續向下滑動父控件
        scrollBy(0, dy);
        if (consumed != null) {
            consumed[1] = dy;
        }
    } else if (dy < 0 && isCenter) {
        //爲了RecyclerView滑動到頂部時,繼續向上滑動父控件
        scrollBy(0, dy);
        if (consumed != null) {
            consumed[1] = dy;
        }
    }
    if (isCenter && !isWebViewBottom) {
        //異常狀況的處理
        scrollToWebViewBottom();
    }
}

@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
    if (target instanceof NestedScrollingWebView) {
        //WebView滑到底部時,繼續向下滑動父控件和RV
        mCurFlyingType = FLYING_FROM_WEBVIEW_TO_PARENT;
        parentFling(velocityY);
    } else if (target instanceof RecyclerView && velocityY < 0 && getScrollY() >= getInnerScrollHeight()) {
        //RV滑動到頂部時,繼續向上滑動父控件和WebView,這裏用於計算到達父控件的頂部時RV的速度
        mCurFlyingType = FLYING_FROM_RVLIST_TO_PARENT;
        parentFling((int) velocityY);
    } else if (target instanceof RecyclerView && velocityY > 0) {
        mIsRvFlyingDown = true;
    }

    return false;
}

@Override
public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
        int currY = mScroller.getCurrY();
        switch (mCurFlyingType) {
            case FLYING_FROM_WEBVIEW_TO_PARENT://WebView向父控件滑動
                if (mIsRvFlyingDown) {
                    //RecyclerView的區域的fling由本身完成
                    break;
                }
                scrollTo(0, currY);
                invalidate();
                checkRvTop();
                if (getScrollY() == getInnerScrollHeight() && !mIsSetFlying) {
                    //滾動到父控件底部,滾動事件交給RecyclerView
                    mIsSetFlying = true;
                    recyclerViewFling((int) mScroller.getCurrVelocity());
                }
                break;
            case FLYING_FROM_PARENT_TO_WEBVIEW://父控件向WebView滑動
                scrollTo(0, currY);
                invalidate();
                if (currY <= 0 && !mIsSetFlying) {
                    //滾動父控件頂部,滾動事件交給WebView
                    mIsSetFlying = true;
                    webViewFling((int) -mScroller.getCurrVelocity());
                }
                break;
            case FLYING_FROM_RVLIST_TO_PARENT://RecyclerView向父控件滑動,fling事件,單純用於計算速度。RecyclerView的flying事件傳遞最終會轉換成Scroll事件處理.
                if (getScrollY() != 0) {
                    invalidate();
                } else if (!mIsSetFlying) {
                    mIsSetFlying = true;
                    //滑動到頂部時,滾動事件交給WebView
                    webViewFling((int) -mScroller.getCurrVelocity());
                }
                break;
        }
    }
}

最後,咱們須要考慮,若是用戶觸摸的是內部的一個不可滑動View時,這時事件是無法經過NestedScrolling機制傳遞給自身的。因此須要主動攔截這種事件,攔截標準:

  1. MOVE超過的TOUCH SLOP距離。
  2. 當前觸摸的不是支持NestedScrolling機制的View。

相應代碼以下:

private int mLastY;

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mLastY = (int) event.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            if (mLastY == 0) {
                mLastY = (int) event.getY();
                return true;
            }
            int y = (int) event.getY();
            int dy = y - mLastY;
            mLastY = y;
            scrollBy(0, -dy);
            break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            mLastY = 0;
            break;
    }
    return true;
}

private int mLastMotionY;

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();
    switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_MOVE:
            // 攔截落在不可滑動子View的MOVE事件
            final int y = (int) ev.getY();
            final int yDiff = Math.abs(y - mLastMotionY);
            boolean isInNestedChildViewArea = isTouchNestedInnerView((int)ev.getRawX(), (int)ev.getRawY());
            if (yDiff > TOUCH_SLOP && !isInNestedChildViewArea) {
                mIsBeingDragged = true;
                mLastMotionY = y;
                final ViewParent parent = getParent();
                if (parent != null) {
                    parent.requestDisallowInterceptTouchEvent(true);
                }
            }
            break;
        case MotionEvent.ACTION_DOWN:
            mLastMotionY = (int) ev.getY();
            mIsBeingDragged = false;
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            mIsBeingDragged = false;
            break;
    }

    return mIsBeingDragged;
}

private boolean isTouchNestedInnerView(int x, int y) {
    List<View> innerView = new ArrayList<>();
    if (mChildWebView != null) {
        innerView.add(mChildWebView);
    }
    if (mChildRecyclerView != null) {
        innerView.add(mChildRecyclerView);
    }

    for (View nestedView : innerView) {
        if (nestedView.getVisibility() != View.VISIBLE) {
            continue;
        }
        int[] location = new int[2];
        nestedView.getLocationOnScreen(location);
        int left = location[0];
        int top = location[1];
        int right = left + nestedView.getMeasuredWidth();
        int bottom = top + nestedView.getMeasuredHeight();
        if (y >= top && y <= bottom && x >= left && x <= right) {
            return true;
        }
    }
    return false;
}

實現一個支持嵌套滑動的WebView

自己WebView是不支持嵌套滑動的,想要支持嵌套滑動,咱們須要讓WebView實現NestedScrollingChild接口,而且處理好TouchEvent方法中的事件傳遞。
實現NestedScrollingChild接口比較簡單,上面也介紹過了,可使用Google提供的NestedScrollingChildHelper輔助類。
處理TouchEvent的思路,須要遵循如下步驟:

  1. DOWN事件時通知父佈局,本身要開始嵌套滑動了。
  2. 對於MOVE事件,先交給父佈局消費。父佈局判斷WebView不能向下滑動了,就父佈局消費;還能向下滑動,就給WebView消費。
  3. 對於Flying事件,一樣先諮詢父佈局是否消費。父佈局判斷WebView不能向下滑動了,就父佈局消費;還能向下滑動,就給WebView消費。

WebView最大滑動距離=WebView自身內容的高度-WebView容器的高度

思路比較簡單,咱們看一下對應的核心代碼:

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mWebViewContentHeight = 0;
            mLastY = (int) event.getRawY();
            mFirstY = mLastY;
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
            }
            initOrResetVelocityTracker();
            mIsSelfFling = false;
            mHasFling = false;
            mMaxScrollY = getWebViewContentHeight() - getHeight();
            startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
            if (getParent() != null) {
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            break;
        case MotionEvent.ACTION_MOVE:
            initVelocityTrackerIfNotExists();
            mVelocityTracker.addMovement(event);
            int y = (int) event.getRawY();
            int dy = y - mLastY;
            mLastY = y;
            if (getParent() != null) {
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            if (!dispatchNestedPreScroll(0, -dy, mScrollConsumed, null)) {
                scrollBy(0, -dy);
            }
            if (Math.abs(mFirstY - y) > TOUCHSLOP) {
                //屏蔽WebView自己的滑動,滑動事件本身處理
                event.setAction(MotionEvent.ACTION_CANCEL);
            }
            break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            if (isParentResetScroll() && mVelocityTracker != null) {
                mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int yVelocity = (int) -mVelocityTracker.getYVelocity();
                recycleVelocityTracker();
                mIsSelfFling = true;
                flingScroll(0, yVelocity);
            }
            break;
    }
    super.onTouchEvent(event);
    return true;
}

@Override
public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
        final int currY = mScroller.getCurrY();
        if (!mIsSelfFling) {
            // parent flying
            scrollTo(0, currY);
            invalidate();
            return;
        }

        if (isWebViewCanScroll()) {
            scrollTo(0, currY);
            invalidate();
        }
        if (!mHasFling
                && mScroller.getStartY() < currY
                && !canScrollDown()
                && startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
                && !dispatchNestedPreFling(0, mScroller.getCurrVelocity())) {
            //滑動到底部時,將fling傳遞給父控件和RecyclerView
            mHasFling = true;
            dispatchNestedFling(0, mScroller.getCurrVelocity(), false);
        }
    }
}

總結

NestedScrolling機制看似複雜,但其實就是實現兩個接口的事情,並且Google提供了強大的輔助類Helper來幫助咱們實現接口。這種機制將滑動事件的傳遞封裝起來,經過Helper輔助類實現ns parent和ns child之間的鏈接和交互。經過接口回調,也實現了ns parent和ns child的解耦。

DEMO項目連接:Android-NestedDetail

參考連接

  1. Android NestedScrolling機制徹底解析 帶你玩轉嵌套滑動
  2. NestedScrolling:文章詳情頁的實現
相關文章
相關標籤/搜索