RecyclerView.smoothScrollToPosition了解一下

小狗狗.jpg

前言

最近開發(fā)中遇到了一個需求,需要RecyclerView滾動到指定位置后置頂顯示,當時遇到這個問題的時候,心里第一反應是直接使用RecyclerView的smoothScrollToPosition()方法,實現(xiàn)對應位置的平滑滾動。但是在實際使用中發(fā)現(xiàn)并沒有到底自己想要的效果。本想著偷懶直接從網(wǎng)上Copy下,但是發(fā)現(xiàn)效果并不是很好。于是就自己去研究源碼。

該系列文章分為兩篇文章。

什么是可見范圍?

在了解RecyclerView的smoothScrollToPosition方法之前,有個知識點,我覺得有必要給大家說一下,因為使用smoothScrollToPosition中遇到的問題都與可見范圍有關。

可見范圍.png

這里所說的可見范圍是,RecyclerView第一個可見item的位置與最后一個可見item的位置之間的范圍。

一、實際使用中遇見的問題

如果當前滾動位置在可見范圍內,是不會發(fā)生滾動的

不會滾動.gif

當前RecyclerView的可見范圍為0到9,當我們想要滾動到1位置時,發(fā)現(xiàn)當前RecyclerView并沒有發(fā)生滾動。

二、如果當前滾動位置在可見范圍之后,會滾動到底部

滾動到底部.gif

當前RecyclerView的可見范圍為0到9,當我們想要滾動到10位置時,發(fā)現(xiàn)RecyclerView滾動了,且當前位置對應的視圖在RecyclreView的底部。

三、如果當前滾動位置在可見范圍之前,會滾動到頂部

滾動到頂部.gif

這里我們滾動RecyclerView,使其可見范圍為10到19,當我們分別滾動到1、3位置時,RecyclerView滾動了。且當前位置對應的視圖在RecyclerView的頂部。

二、RecyclerView smoothScrollToPosition源碼解析

到了這里我們發(fā)現(xiàn)對于不同情況,RecyclerView內部處理是不一樣的,所以為了解決實際問題,看源碼是必不可少的,接下來我們就一起跟著源碼走一遍。來看看RecyclerView具體的滾動實現(xiàn)。(這里需要提醒大家的是這里我采用的是LinearLayoutManager,本文章都是基于LinearLayoutManager進行分析的)

  public void smoothScrollToPosition(int position) {
        if (mLayoutFrozen) {
            return;
        }
        if (mLayout == null) {
            Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. "
                    + "Call setLayoutManager with a non-null argument.");
            return;
        }
        mLayout.smoothScrollToPosition(this, mState, position);
    }

mRecycler.smoothScrollToPosition()方法時,內部調用了LayoutManager的smoothScrollToPosition方法,LayoutManager中smoothScrollToPosition沒有實現(xiàn),具體實現(xiàn)在其子類中,這里我們使用的是LinearLayoutManager,所以我們來看看內部是怎么實現(xiàn)的。

   @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
            int position) {
        LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext());
        scroller.setTargetPosition(position);//設定目標位置
        startSmoothScroll(scroller);
    }

這里我們可以看到,這里導致RecyclerView滑動的是LinearSmoothScroller,而LinearSmoothScroller的父類是RecyclerView.SmoothScroller,看到這里我相信大家都會感到一絲熟悉,因為我們在對控件內內容進行移動的時候,我們都會使用到一個類,那就是Scroller。這里RecyclerView也自定了一個滑動Scroller??隙ㄊ桥c滑動其內部視圖相關的。

 public void startSmoothScroll(SmoothScroller smoothScroller) {
            if (mSmoothScroller != null && smoothScroller != mSmoothScroller
                    && mSmoothScroller.isRunning()) {
                mSmoothScroller.stop();
            }
            mSmoothScroller = smoothScroller;
            mSmoothScroller.start(mRecyclerView, this);
        }

繼續(xù)走startSmoothScroll,方法內部判斷了如果正在計算坐標值就停止,然后調用start()方法重新開始計算坐標值。接著開始看start()方法。

 void start(RecyclerView recyclerView, LayoutManager layoutManager) {
            mRecyclerView = recyclerView;
            mLayoutManager = layoutManager;
            if (mTargetPosition == RecyclerView.NO_POSITION) {
                throw new IllegalArgumentException("Invalid target position");
            }
            mRecyclerView.mState.mTargetPosition = mTargetPosition;
            mRunning = true;//設置當前scroller已經(jīng)開始執(zhí)行
            mPendingInitialRun = true;
            mTargetView = findViewByPosition(getTargetPosition());//根據(jù)目標位置查找相應View,
            onStart();
            mRecyclerView.mViewFlinger.postOnAnimation();
        }

在start方法中,會標識當前scroller的執(zhí)行狀態(tài),同時會根據(jù)滾動的位置去尋找對應的目標視圖。這里需要著重提示一下,findViewByPosition()這個方法,該方法會在Recycler的可見范圍內去查詢是否有目標位置對應的視圖,例如,現(xiàn)在RecyclerView的可見范圍為1-9,目標位置為10,那么mTargetView =null,如果可見范圍為9-20,目標位置為1,那么mTargetView =null。

最終調用RecyclerView的內部類 ViewFlinger的postOnAnimation()方法。

   class ViewFlinger implements Runnable {
       ....省略部分代碼
     void postOnAnimation() {
            if (mEatRunOnAnimationRequest) {
                mReSchedulePostAnimationCallback = true;
            } else {
                removeCallbacks(this);
                ViewCompat.postOnAnimation(RecyclerView.this, this);
            }
        }
   }

這里我們發(fā)現(xiàn),ViewFlinger其實一個Runnable,在postOnAnimation()內部又將該Runnable發(fā)送出去了。那下面我們只用關心ViewFlinger的run()方法就行了。

   @Override
        public void run() {
                   ...省略部分代碼
            final OverScroller scroller = mScroller;
            //獲得layoutManger中的SmoothScroller
            final SmoothScroller smoothScroller = mLayout.mSmoothScroller;
            if (scroller.computeScrollOffset()) {//如果是第一次走,會返回false
                   ...省略部分代碼
             }
            if (smoothScroller != null) {
                if (smoothScroller.isPendingInitialRun()) {
                    smoothScroller.onAnimation(0, 0);
                }
                if (!mReSchedulePostAnimationCallback) {
                    smoothScroller.stop(); //stop if it does not trigger any scroll
                }
            }
              ...省略部分代碼
        }

ViewFlinger的run()方法內部實現(xiàn)比較復雜, 在該方法第一次執(zhí)行的時候,會執(zhí)行,if (scroller.computeScrollOffset()) ,其中scroller是ViewFlinger中的屬性mScroller的引用,其中mScroller會在ViewFlinger創(chuàng)建對象的時候,就默認初始化了。那么第一次判斷時候,因為還沒有開始計算,所以不會進這個if語句塊,那么接下來就會直接走下面的語句:

     if (smoothScroller != null) {
                if (smoothScroller.isPendingInitialRun()) {
                    smoothScroller.onAnimation(0, 0);
                }
                if (!mReSchedulePostAnimationCallback) {
                    smoothScroller.stop(); //stop if it does not trigger any scroll
                }
            }

最后發(fā)現(xiàn),只是走了一個onAnimation(0,0),繼續(xù)走該方法。

 private void onAnimation(int dx, int dy) {
            final RecyclerView recyclerView = mRecyclerView;
            if (!mRunning || mTargetPosition == RecyclerView.NO_POSITION || recyclerView == null) {
                stop();
            }
            mPendingInitialRun = false;
            if (mTargetView != null) {//判斷目標視圖是否存在,如果存在則計算移動到位置需要移動的距離
                if (getChildPosition(mTargetView) == mTargetPosition) {
                    onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction);
                    mRecyclingAction.runIfNecessary(recyclerView);
                    stop();
                } else {
                    Log.e(TAG, "Passed over target position while smooth scrolling.");
                    mTargetView = null;
                }
            }
            if (mRunning) {//如果不存在,繼續(xù)去找
                onSeekTargetStep(dx, dy, recyclerView.mState, mRecyclingAction);
                boolean hadJumpTarget = mRecyclingAction.hasJumpTarget();
                mRecyclingAction.runIfNecessary(recyclerView);
                if (hadJumpTarget) {
                    // It is not stopped so needs to be restarted
                    if (mRunning) {
                        mPendingInitialRun = true;
                        recyclerView.mViewFlinger.postOnAnimation();
                    } else {
                        stop(); // done
                    }
                }
            }
        }

在onAnimation方法中,判斷了目標視圖是否為空,大家應該還記得上文中,我們對目標視圖的查找。如果當前位置不在可見范圍之內,那么mTargetView =null,就不回走對應的判斷語句。繼續(xù)查看onSeekTargetStep()。

    protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
        if (getChildCount() == 0) {
            stop();
            return;
        }
        //noinspection PointlessBooleanExpression
        if (DEBUG && mTargetVector != null
                && ((mTargetVector.x * dx < 0 || mTargetVector.y * dy < 0))) {
            throw new IllegalStateException("Scroll happened in the opposite direction"
                    + " of the target. Some calculations are wrong");
        }
        mInterimTargetDx = clampApplyScroll(mInterimTargetDx, dx);
        mInterimTargetDy = clampApplyScroll(mInterimTargetDy, dy);

        if (mInterimTargetDx == 0 && mInterimTargetDy == 0) {
            updateActionForInterimTarget(action);
        } // everything is valid, keep going

    }

直接通過代碼,發(fā)現(xiàn)并不理解改函數(shù)要做什么樣的工作,這里我們只知道第一次發(fā)生滾動時,mInterimTargetDx=0與mInterimTargetDy =0,那么會走updateActionForInterimTarget()方法。

    protected void updateActionForInterimTarget(Action action) {
        // find an interim target position
        PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
        ...省略部分代碼
        normalize(scrollVector);
        mTargetVector = scrollVector;

        mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x);
        mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y);
        
        //計算需要滾動的時間,  默認滾動距離,TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;
        final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX);
          
        //為了避免在滾動的時候出現(xiàn)停頓,我們會跟蹤onSeekTargetStep中的回調距離,實際上不會滾動超出實際的距離
        action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO),
                (int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO),
                //這里存入的時間要比實際花費的時間大一點。
                (int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
    }

根據(jù)官方文檔進行翻譯:當目標滾動位置對應視圖不在RecyclerView的可見范圍內,該方法計算朝向該視圖的方向向量并觸發(fā)平滑滾動。默認滾動的距離為12000(單位:px),(也就是說了為了滾動到目標位置,會讓Recycler至多滾動12000個像素)。

既然該方法計算了時間,那么我們就看看calculateTimeForScrolling()方法,通過方法名我們就應該了解了該方法是計算給定距離在默認速度下需要滾動的時間。

    protected int calculateTimeForScrolling(int dx) {
        //這里對時間進行了四舍五入操作。 
        return (int) Math.ceil(Math.abs(dx) * MILLISECONDS_PER_PX);
    }

其中MILLISECONDS_PER_PX 會在LinearSmoothScroller初始化的時候創(chuàng)建。

  public LinearSmoothScroller(Context context) {
      MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
    }
  

查看calculateSpeedPerPixel()方法

    private static final float MILLISECONDS_PER_INCH = 25f;// 默認為移動一英寸需要花費25ms
    protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
        return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
    }

也就是說,當前滾動的速度是與屏幕的像素密度相關, 通過獲取當前手機屏幕每英寸的像素密度,與每英寸移動所需要花費的時間,用每英寸移動所需要花費的時間除以像素密度就能計算出移動一個像素密度需要花費的時間。OK,既然我們已經(jīng)算出了移動一個像素密度需要花費的時間,那么直接乘以像素,就能算出移動該像素所需要花費的時間了。

既然現(xiàn)在我們算出了時間,我們現(xiàn)在只用關心Action的update()方法到底是干什么的就好了,

        //保存關于SmoothScroller滑動距離信息
        public static class Action {
               ...省略代碼
             public void update(int dx, int dy, int duration, Interpolator interpolator) {
                mDx = dx;
                mDy = dy;
                mDuration = duration;
                mInterpolator = interpolator;
                mChanged = true;
            }
         }

這里我們發(fā)現(xiàn)Action,只是存儲關于SmoothScroller滑動信息的一個類,那么初始時保存了橫向與豎直滑動的距離(12000px)、滑動時間,插值器。同時記錄當前數(shù)據(jù)改變的狀態(tài)。

現(xiàn)在我們已經(jīng)把Action的onSeekTargetStep方法走完了,那接下來,我們繼續(xù)看Action的runIfNecessary()方法。

   void runIfNecessary(RecyclerView recyclerView) {
                 ....省略代碼
                if (mChanged) {
                    validate();
                    if (mInterpolator == null) {
                        if (mDuration == UNDEFINED_DURATION) {
                            recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy);
                        } else {
                            //這里傳入的mDx,mDy,mDuration.是Action之前update()方法。保存的信息
                            recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy, mDuration);
                        }
                    } else {
                        recyclerView.mViewFlinger.smoothScrollBy(
                                mDx, mDy, mDuration, mInterpolator);
                    }
              mChanged = false;
                ....省略代碼
            }

TNND,調來調去最后又把Action存儲的信息傳給了ViewFlinger的smoothScrollBy()方法。這里需要注意:一旦調用該方法會將mChanged置為false,下次再次進入該方法時,那么就不會調用ViewFlinger的滑動方法了。

 public void smoothScrollBy(int dx, int dy, int duration, Interpolator interpolator) {
             //判斷是否是同一插值器,如果不是,重新創(chuàng)建mScroller
            if (mInterpolator != interpolator) {
                mInterpolator = interpolator;
                mScroller = new OverScroller(getContext(), interpolator);
            }
            setScrollState(SCROLL_STATE_SETTLING);
            mLastFlingX = mLastFlingY = 0;
            mScroller.startScroll(0, 0, dx, dy, duration);
            if (Build.VERSION.SDK_INT < 23) {
                mScroller.computeScrollOffset();
            }
            postOnAnimation();
        }

這里mScroller接受到Acttion傳入的滑動信息開始滑動后。最后會調用postOnAnimation(),又將ViewFiinger的run()法發(fā)送出去。那么最終我們又回到了ViewFiinger的run()方法。

    public void run() {
         ...省略部分代碼
   if (scroller.computeScrollOffset()) {
                final int[] scrollConsumed = mScrollConsumed;
                final int x = scroller.getCurrX();
                final int y = scroller.getCurrY();
                int dx = x - mLastFlingX;
                int dy = y - mLastFlingY;
                int hresult = 0;
                int vresult = 0;
                mLastFlingX = x;
                mLastFlingY = y;
                int overscrollX = 0, overscrollY = 0;
                ...省略部分代碼
                if (mAdapter != null) {
                    startInterceptRequestLayout();
                    onEnterLayoutOrScroll();
                    TraceCompat.beginSection(TRACE_SCROLL_TAG);
                    fillRemainingScrollValues(mState);
                    if (dx != 0) {//如果橫向方向大于0,開始讓RecyclerView滾動
                        hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
                        overscrollX = dx - hresult;
                    }
                    if (dy != 0) {//如果豎直方向大于0,開始讓RecyclerView滾動,獲得當前滾動的距離
                        vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
                        overscrollY = dy - vresult;
                    }
                    TraceCompat.endSection();
                    repositionShadowingViews();

                    onExitLayoutOrScroll();
                    stopInterceptRequestLayout(false);
                    if (smoothScroller != null && !smoothScroller.isPendingInitialRun()
                            && smoothScroller.isRunning()) {
                        final int adapterSize = mState.getItemCount();
                        if (adapterSize == 0) {
                            smoothScroller.stop();
                        } else if (smoothScroller.getTargetPosition() >= adapterSize) {
                            smoothScroller.setTargetPosition(adapterSize - 1);
                            //傳入當前RecylerView滾動的距離 dx dy
                            smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY);
                        } else {
                            //傳入當前RecylerView滾動的距離 dx dy
                            smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY);
                        }
                    }
                }
                enableRunOnAnimationRequests();
             }

這里scroller(拿到之前Action傳入的滑動距離信息)已經(jīng)開始滑動了,故 if (scroller.computeScrollOffset()) 條件為true, 那么scroller拿到當前豎直方向的值就開始讓RecyclerView滾動了,也就是代碼 mLayout.scrollVerticallyBy(dy, mRecycler, mState);接著又讓smoothScroller執(zhí)行onAnimation()方法。其中傳入的參數(shù)是RecyclerView已經(jīng)滾動的距離。那我們現(xiàn)在繼續(xù)看onAnimation方法。

    private void onAnimation(int dx, int dy) {
            final RecyclerView recyclerView = mRecyclerView;
            if (!mRunning || mTargetPosition == RecyclerView.NO_POSITION || recyclerView == null) {
                stop();
            }
            mPendingInitialRun = false;
            if (mTargetView != null) {
                // verify target position
                if (getChildPosition(mTargetView) == mTargetPosition) {
                    onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction);
                    mRecyclingAction.runIfNecessary(recyclerView);
                    stop();
                } else {
                    Log.e(TAG, "Passed over target position while smooth scrolling.");
                    mTargetView = null;
                }
            }
            if (mRunning) {//獲得當前Recycler需要滾動的距離
                onSeekTargetStep(dx, dy, recyclerView.mState, mRecyclingAction);
                boolean hadJumpTarget = mRecyclingAction.hasJumpTarget();
                mRecyclingAction.runIfNecessary(recyclerView);
                if (hadJumpTarget) {
                    // It is not stopped so needs to be restarted
                    if (mRunning) {
                        mPendingInitialRun = true;
                        recyclerView.mViewFlinger.postOnAnimation();
                    } else {
                        stop(); // done
                    }
                }
            }
        }

那么現(xiàn)在代碼就明了了,RecylerView會判斷在滾動的時候,目標視圖是否已經(jīng)出現(xiàn),如果沒有出現(xiàn),會調用onSeekTargetStep保存當前RecylerView滾動距離,然后判斷RecyclerView是否需要滑動,然后又通過postOnAnimation()將ViewFlinger 發(fā)送出去了。那么直到找到目標視圖才會停止。

那什么情況下,目標視圖不為空呢,其實在RecylerView內部滾動的時候。會判斷目標視圖是否存在,如果存在會對mTargetView進行賦值操作。由于篇幅限制,這里就不對目標視圖的查找進行介紹了,有興趣的小伙伴可以自己看一下源碼。

那接下來,我們就假如當前已經(jīng)找到了目標視圖,那么接下來程序會走onTargetFound()方法。

  protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
        //計算讓目標視圖可見的,需要滾動的橫向距離
        final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
       //計算讓目標視圖可見的,需要滾動的橫向距離
        final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
        final int distance = (int) Math.sqrt(dx * dx + dy * dy);
        final int time = calculateTimeForDeceleration(distance);
        if (time > 0) {
            //更新需要滾動的距離。
            action.update(-dx, -dy, time, mDecelerateInterpolator);
        }
    }

當目標視圖被找到以后,會計算讓目標視圖出現(xiàn)在可見范圍內,需要移動的橫向與縱向距離。并計算所需要花費的時間。然后重新讓RecyclerView滾動一段距離。

這里我們著重看calculateDyToMakeVisible。

    public int calculateDyToMakeVisible(View view, int snapPreference) {
        final RecyclerView.LayoutManager layoutManager = getLayoutManager();
        if (layoutManager == null || !layoutManager.canScrollVertically()) {
            return 0;
        }
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                view.getLayoutParams();
        //獲取當前view在其父布局的開始位置
        final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
        //獲取當前View在其父布局結束位置
        final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
        //獲取當前布局的開始位置 
        final int start = layoutManager.getPaddingTop();
        //獲取當前布局的結束位置
        final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
        return calculateDtToFit(top, bottom, start, end, snapPreference);
    }

這里我們會根據(jù)當前view的top、bottom及當前布局的start、end等坐標信息,然后調用了calculateDtToFit()方法?,F(xiàn)在最重要的出現(xiàn)了,也是我們那三個問題出現(xiàn)的原因!!

    public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
            snapPreference) {
        switch (snapPreference) {
            case SNAP_TO_START:
                return boxStart - viewStart;
            case SNAP_TO_END:
                return boxEnd - viewEnd;
            case SNAP_TO_ANY:
                final int dtStart = boxStart - viewStart;
                if (dtStart > 0) {//滾動位置在可見范圍之前
                    return dtStart;
                }
                final int dtEnd = boxEnd - viewEnd;
                if (dtEnd < 0) {//滾動位置在可見范圍之后
                    return dtEnd;
                }
                break;
            default:
                throw new IllegalArgumentException("snap preference should be one of the"
                        + " constants defined in SmoothScroller, starting with SNAP_");
        }
        return 0;//在可見范圍之內,直接返回
    }

我們會根據(jù)snapPreference對應的值來計算相應的距離,同時snapPreference的具體值與getVerticalSnapPreference(這里我們是豎直方向)所以我們看該方法。

 protected int getVerticalSnapPreference() {
        return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY :
                mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START;
    }

其中mTargetVector與layoutManager.computeScrollVectorForPosition有關。

  @Override
    public PointF computeScrollVectorForPosition(int targetPosition) {
        if (getChildCount() == 0) {
            return null;
        }
        final int firstChildPos = getPosition(getChildAt(0));
        final int direction = targetPosition < firstChildPos != mShouldReverseLayout ? -1 : 1;
        if (mOrientation == HORIZONTAL) {
            return new PointF(direction, 0);
        } else {
            return new PointF(0, direction);
        }
    }

也就是說在LinerlayoutManager為豎直的情況下,snapPreference默認為SNAP_ANY,那么我們就可以得到,下面三種情況。

  • 當滾動位置在可見范圍之內時
    boxStart - viewStart<=0
    boxEnd - viewEnd>0
    滾動距離為0,故不會滾動
  • 當滾動位置在可見范圍之前時
    boxStart - viewStart> 0
    那么實際滾動距離為正值,內容向上滾動,故只能滾動到頂部
  • 當滾動位置在可見范圍距離之外時
    boxEnd - viewEnd<0
    那么實際滾動距離為其差值,內容向下滾動,故只能滾動到底部

有可能大家現(xiàn)在看代碼已經(jīng)看暈了,下面我就用一張圖來總結整個流程,結合流程圖再去看代碼,我相信大家能有更好的理解。

基本流程圖.png
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容