Android開發(fā)藝術(shù)探索-第三章-View的事件體系


layout: post
date: 2016-01-08
title: Android開發(fā)藝術(shù)探索-第三章-View的事件體系
categories: blog
tags: [Activity,Android,View,MotionEvent,TouchSlop]
category: Android
description:


本文首發(fā)于個人博客KuTear,轉(zhuǎn)載引用請注明原出處.謝謝!
另外,更多文章分享請查看博客KuTear

3.1 View的基礎(chǔ)知識

  • 位置參數(shù)

    top、left、right、bottom,在3.0之后增加了x、y、translationX、translationY.這里的所有參數(shù)都是相對其父布局來說的.
    下面是具體的含義表示

    View參數(shù)

    其中參數(shù)的關(guān)系為

           x = left + translationX
           y = top + translationY
    
  • MontionEvent和TouchSlop

    MontionEvent代表著觸摸事件封裝的數(shù)據(jù),包括常用的Action和位置參數(shù)等.如上面圖示,注意函數(shù)getRaw*()是相對與屏幕的.
    TouchSlop表示滑動的最小常量.是常量(int),不是具體的類.獲取方式為:

           ViewConfiguration.get(getContext()).getScaledTouchSlop()
    
  • VelocityTracker,GestureDetector和Scroller

    VelocityTracker用于追蹤手指在滑動過程中的速度,包括水平和垂直方向上的速度。
    速度計算公式:

           速度 = (終點位置 - 起點位置) / 時間段
    

    速度可能為負值,例如當手指從屏幕右邊往左邊滑動的時候。此外,速度是單位時間內(nèi)移動的像素數(shù),單位時間不一定是1秒鐘,可以使用方法
    computeCurrentVelocity(xxx)指定單位時間是多少,單位是ms。例如通過computeCurrentVelocity(1000)來獲取速度,手指在1s中
    滑動了100個像素,那么速度是100,即100(像素/1000ms)。如果computeCurrentVelocity(100)來獲取速度,在100ms內(nèi)手指只是滑動了
    10個像素,那么速度是10,即10(像素/100ms)。
    VelocityTracker的使用方式:

           //初始化
           VelocityTracker mVelocityTracker = VelocityTracker.obtain();
           //在onTouchEvent方法中
           mVelocityTracker.addMovement(event);
           //獲取速度
           mVelocityTracker.computeCurrentVelocity(1000);
           float xVelocity = mVelocityTracker.getXVelocity();
           //重置和回收
           mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的時候調(diào)用
           mVelocityTracker.recycle(); //一般在onDetachedFromWindow中調(diào)用
    

    GestureDetector用于輔助檢測用戶的單擊、滑動、長按、雙擊等行為。GestureDetector的使用比較簡單,主要也是輔助檢測常見的觸屏事件。
    作者建議:如果只是監(jiān)聽滑動相關(guān)的事件在onTouchEvent中實現(xiàn);如果要監(jiān)聽雙擊這種行為的話,那么就使用GestureDetector。

    GestureDetector
    DoubleTabListener
    GestureListener
           //自定義的View,實現(xiàn)相關(guān)接口(onGestureListener,onDoubleTabListener)
           GestureDetector mGestureDetector = 
                   new GestureDetector(this/*context*/,listener/*onGestureListener*/);
           
           //function onTouchEvent(...)或onTouchListener的onTouch(...)中,直接返回
           return mGestureDetector.onTouchEvent(event)
    

    更多使用參見[參考2]

3.2 View的滑動

  • layout

           public void layout (int l, int t, int r, int b)
    

    參數(shù)都是相對與父布局.

           @Override
           public boolean onTouchEvent(MotionEvent event) {
               int rawX = (int) (event.getRawX()); //相對與屏幕的坐標
               int rawY = (int) (event.getRawY());
               switch (event.getAction()) {
                   case MotionEvent.ACTION_DOWN:
                       // 記錄觸摸點坐標
                       lastX = rawX;
                       lastY = rawY;
                       break;
                   case MotionEvent.ACTION_MOVE:
                       // 計算偏移量
                       int offsetX = rawX - lastX;
                       int offsetY = rawY - lastY;
                       // 在當前l(fā)eft、top、right、bottom的基礎(chǔ)上加上偏移量
                       layout(getLeft() + offsetX,
                               getTop() + offsetY,
                               getRight() + offsetX,
                               getBottom() + offsetY);
                       // 重新設(shè)置初始坐標
                       lastX = rawX;
                       lastY = rawY;
                       break;
               }
               return true;
           }
    
  • offsetLeftAndRight和offsetTopAndBottom

    使用方法同上幾乎一致

           //直接在onTouchEvent中調(diào)用,替換上面的layout(...)部分
           offsetLeftAndRight(offestX);
           offsetTopAndBottom(offestY);
    
  • LayoutParams

    這個方式在平時開發(fā)中應(yīng)該使用的比較多.使用也是很簡單,就是修改params的某些參數(shù)

           //ViewGroup.MarginLayoutParams layoutParams = 
           //               (ViewGroup.MarginLayoutParams) getLayoutParams();
           //LinearLayout.LayoutParams extends ViewGroup.MarginLayoutParams,
           //幾乎所有的LayoutParms都是繼承至
           //ViewGroup.MarginLayoutParams,
           //所以ViewGroup.MarginLayoutParams是通用的...
           LinearLayout.LayoutParams layoutParams = 
                                 (LinearLayout.LayoutParams) getLayoutParams();
           layoutParams.leftMargin = getLeft() + offsetX;
           layoutParams.topMargin = getTop() + offsetY;
           setLayoutParams(layoutParams);
           //requestLayout();效果和上面這一句一樣
    
  • 動畫

    動畫部分在Android群英傳-第七章 Android動畫機制與使用技巧中已經(jīng)有比較詳細的說明,在這里就不做說明.

  • ViewDragHelper

    ViewDragHelper的使用過程其實也是比較簡單的,主要用戶控制部分都在Callback中.CallBack中的函數(shù)比較多

    CallBack

    下面是一個簡單的栗子:

           //初始化
           mDragHelper = ViewDragHelper.create(this/*要處理的ViewGroup*/, 
                          1.0f/*敏感度*/, new DragHelperCallback()/*前面說的Callback*/);
           
           //復寫一些函數(shù),代碼幾乎固定
           @Override
           public boolean onInterceptTouchEvent(MotionEvent ev) {
             final int action = MotionEventCompat.getActionMasked(ev);
             if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
                 mDragHelper.cancel();
                 return false;
             }
             return mDragHelper.shouldInterceptTouchEvent(ev);
           }
           @Override
           public boolean onTouchEvent(MotionEvent ev) {
             mDragHelper.processTouchEvent(ev);
             return true;
           }
    

    這里沒有詳細寫出CallBack的代碼,可以在這里查看.

  • ScrollTo和ScrollBy

    根據(jù)函數(shù)名稱就知道這兩個函數(shù)的區(qū)別,To是到具體的點,by只是與當前的偏移.
    這兩個函數(shù)不是針對view本身,而是針對其內(nèi)容,具體來說就是ViewGroup調(diào)用這兩函數(shù),是其內(nèi)部的view在移動,view調(diào)用是其內(nèi)容在動(TextView-->文本,ImageView-->圖像)
    另一方面就是他的參數(shù)不同與其他,正數(shù)X往左,正數(shù)Y往上.原因查看這里,
    如果想要移動View,就需要在她的parent上調(diào)用這函數(shù),下面是個栗子

         //替換上文onTouchEvent中的layout(...)
         ((ViewGroup) getParent()).scrollBy(-offsetX, -offsetY);
    
  • Scroller

    在以前都不知道有這個類,哎,基礎(chǔ)不夠誒.下面一個栗子說明

         //初始化,還可以使用插值器
         Scroller mScroller = new Scroller(mContext,interpolator/*插值器,可以不用*/);
         
         //View的computescroll()
         @Override
         public void computeScroll() {
             super.computeScroll();
             // 判斷Scroller是否執(zhí)行完畢
             if (mScroller.computeScrollOffset()) {
                 ((View) getParent()).scrollTo( mScroller.getCurrX(), mScroller.getCurrY());
                 // 通過重繪來不斷調(diào)用computeScroll
                 invalidate();//很重要
             }
         }
         
         //啟動
         mScroller.startScroll(startX,startY,dX,dY,duration);
    

    本質(zhì)上Scroller不能移動View,在我看來她同屬性動畫中的ValueAnimator是一樣的,因為他們都只是按照某種插值器產(chǎn)生數(shù)值,需要自己把數(shù)值同移動
    相聯(lián)系.

3.3 View的事件分發(fā)機制

  1. 事件分發(fā)過程的三個重要方法

    • dispatchTouchEvent

      函數(shù)原型

         public boolean dispatchTouchEvent(MotionEvent ev)
      

      主要的功能是負責事件的分發(fā).
      返回值:
      true: 表示向下分發(fā)中斷
      false: 表示繼續(xù)向下分發(fā)

    • onInterceptTouchEvent

      函數(shù)原型

         public boolean onInterceptTouchEvent(MotionEvent event)
      

      主要功能是負責事件的攔截
      返回值:
      true:攔截,事件交由自己(View/ViewGroup)的onTouchEvent(...)處理
      false:不攔截,事件繼續(xù)向下分發(fā).

    • onTouchEvent

      函數(shù)原型

         public boolean onTouchEvent(MotionEvent event)
      

      主要功能是處理觸摸事件
      返回值:
      true:表示消費了這個事件.
      false:表示沒有消費該事件,返回到上級處理.如果一直得不到處理,最終反饋到Activity的onTouchEvent(...)

  2. 函數(shù)之間的邏輯關(guān)系

    • 以上三個函數(shù)的偽代碼

      類似于遞歸調(diào)用的方式

        public boolean dispatchTouchEvent(MotionEvent ev) {
            boolean consume = false;
            if (onInterceptTouchEvent(ev)) {
                consume = onTouchEvent(ev);
            } else {
                consume = child.dispatchTouchEvent(ev);
            }
            return consume;
        }
      
    • 函數(shù)與監(jiān)聽接口

      在通常情況下,我們?yōu)锽utton等組件設(shè)置了onClickListener接口,有時也會設(shè)置onTouchListener接口,但在什么時候接口中的方法才會執(zhí)行呢?如果設(shè)置了onTouchListener接口監(jiān)聽,會對View(ViewGroup)的onTouchEvent有一定的影響.如果設(shè)置了onTouchListener,她的onTouch的返回值會影響view中onTouchEvent的調(diào)用與否,onTouch返回值的含義與onTouchEvent一樣,表示是否消費了該事件.onTouch會先于onTouchEvent執(zhí)行.偽代碼為

         //true表示消費掉
         if(!listener.onTouch(ev)){
             onTouchEvent(ev);
         }
      

      對于onClickListener接口,他內(nèi)部方法onCLick的調(diào)用是在onTouchEvent中(根據(jù)上面就知道如果在onTouchListener的onTouch中返回true,onclick就不會再執(zhí)行了),其內(nèi)部部分代碼如下.

         //View#onTouchEvent(...)
         if (mPerformClick == null) {
            mPerformClick = new PerformClick();
         }
         if (!post(mPerformClick)) {
            performClick();
         }
         
         //點擊事件的處理者 
         private final class PerformClick implements Runnable {
            @Override
            public void run() {
                performClick();
            }
        }
        
        //點擊調(diào)用onClick函數(shù)
        public boolean performClick() {
            //ListenerInfo封裝了各種監(jiān)聽
            final ListenerInfo li = mListenerInfo;
            if (...) {
                //調(diào)用部分
                li.mOnClickListener.onClick(this);
                result = true;
            }
            ...
            return result;
        }
      

      根據(jù)上面的描述,知道調(diào)用順序為onTouchListener#onTouch,返回值決定是否繼續(xù)執(zhí)行view的onTouchEvent,最后在onTouchEvent中執(zhí)行onClickListener的onClick方法.

  3. 分發(fā)過程

    • Activity分發(fā)

      觸摸事件最先到達Activity,所以首先會在Activity中分發(fā)

             //Activity#dispatchTouchEvent()
             public boolean dispatchTouchEvent(MotionEvent ev) {
                 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                     onUserInteraction();
                 }
                 //分發(fā)到Window.
                 if (getWindow().superDispatchTouchEvent(ev)) {
                     //true表示不再向下分發(fā)
                     return true;
                 }
                 return onTouchEvent(ev);
             }
      

      在getWindow()中返回mWindow,最終在函數(shù)attach(...)中發(fā)現(xiàn)

             mWindow = new PhoneWindow(this);
      

      PhoneWindow不在SDK中,在在線源碼(Android源碼)網(wǎng)站上可以找到相關(guān)的代碼

             public boolean superDispatchTouchEvent(MotionEvent event ) {
                 //DecorView extends FrameLayout 
                 //       DecorView#superDispatchTouchEvent(ev)
                 //       public boolean superDispatchTouchEvent(MotionEvent event) {
                 //               //來到了ViewGroup
                 //               return super.dispatchTouchEvent(event);
                 //       }
                 return mDecorView.superDispatchTouchEvent(event);
             }
      

      由此就把事件分發(fā)到了ViewGroup,接下來就是在VieGroup中分發(fā).

  • View分發(fā)

    函數(shù)dispatchTouchEvent(...)中的部分代碼

           ...
           if (onFilterTouchEventForSecurity(event)) {
               //noinspection SimplifiableIfStatement
               ListenerInfo li = mListenerInfo;
               if (li != null && li.mOnTouchListener != null
                       && (mViewFlags & ENABLED_MASK) == ENABLED
                       && li.mOnTouchListener.onTouch(this, event)) {
                   result = true;
               }
               // result==true,函數(shù)onTouchEvent(...)就執(zhí)行不到了,而影想result的主要就是
               //li.mOnTouchListener.onTouch(this, // event)的返回值,返回true,
               //表示事件被處理了,自然不需要在調(diào)用onTouchEvent(...)來重新處理
               // 前面說過onClick(...)是在onTouchEvent(...)中調(diào)用的.即優(yōu)先級小于onTouch()
               if (!result && onTouchEvent(event)) {
                   result = true;
               }
           }
           ... 
    

    函數(shù)onTouchEvent(...)主要就是處理事件,前面已經(jīng)說過onClick的執(zhí)行過程了.這里就不說了.

  • ViewGroup分發(fā)

    函數(shù)dispatchTouchEvent(...)中的部分代碼

           // Check for interception.
           final boolean intercepted;
           // 事件為ACTION_DOWN或者mFirstTouchTarget不為null
           //(即已經(jīng)找到能夠接收touch事件的目標組件)時if成立
           if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
               //判斷disallowIntercept(禁止攔截)標志位
               //因為在其他地方可能調(diào)用了
               //requestDisallowInterceptTouchEvent(boolean disallowIntercept)
               //從而禁止執(zhí)行是否需要攔截的判斷
               //(有點拗口~其實看requestDisallowInterceptTouchEvent()方法名就可明白)
               final boolean disallowIntercept = 
                                  (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
               //補充:根據(jù)下面的代碼可以發(fā)現(xiàn), disallowIntercept 的值等于函數(shù)
               //requestDisallowInterceptTouchEvent的參數(shù).                 
               if (!disallowIntercept) {
                   intercepted = onInterceptTouchEvent(ev);
                   ev.setAction(action); // restore action in case it was changed
               } else {
                   intercepted = false;
               }
           } else {
               // There are no touch targets and this action is not an initial down
               // so this view group continues to intercept touches.
               intercepted = true;
           }
    

    注意上文代碼中的注釋部分,這里看一下部分requesrDisallowInterceptTouchEvent(...)的部分源碼

         public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
                   //更具這里可以看出,當disallowIntercept=true時,
                   //(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 成立,
                   //這就意味著上面一段代碼中的disallowIntercept=true;
                   if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
                       // We're already in this state, assume our ancestors are too
                       return;
                   }
                   ...
          }
    

    由此可見VIewGroup只會在ACTION=ACTION_DOWN或者mFirstTouchTarget != null時才判斷是否攔截事件,因為一個事件序列(DOWN->MOVE->...->UP)只能有一個View處理.但是mFirstTouchTarget != null表示什么呢?

    當事件被ViewGroup的子元素成功處理了(子View的onTouchEvent/onTouch返回了true??),mFirstTouchTarget被賦值指向子元素(即!=null)

    函數(shù)dispatchTouchEvent(...)的部分實現(xiàn).

       final View[] children = mChildren;
       for (int i = childrenCount - 1; i >= 0; i--) {
           final int childIndex = customOrder
                   ? getChildDrawingOrder(childrenCount, i) : i;
           final View child = (preorderedList == null)
                   ? children[childIndex] : preorderedList.get(childIndex);
           // If there is a view that has accessibility focus we want it
           // to get the event first and if not handled we will perform a
           // normal dispatch. We may do a double iteration but this is
           // safer given the timeframe.
           if (childWithAccessibilityFocus != null) {
               if (childWithAccessibilityFocus != child) {
                   continue;
               }
               childWithAccessibilityFocus = null;
               i = childrenCount - 1;
           }
    
           if (!canViewReceivePointerEvents(child)
                   || !isTransformedTouchPointInView(x, y, child, null)) {
               ev.setTargetAccessibilityFocus(false);
               continue;
           }
           newTouchTarget = getTouchTarget(child);
           if (newTouchTarget != null) {
               // Child is already receiving touch within its bounds.
               // Give it the new pointer in addition to the ones it is handling.
               // 找到接收Touch事件的子View!!!!!!!即為newTouchTarget.
               newTouchTarget.pointerIdBits |= idBitsToAssign;
               break;
           }
    
           resetCancelNextUpFlag(child);
           //注意這個方法,再后面再看看..根據(jù)源碼,
           //可以知道它返回的是子View(child)的dispatchTouchEvent(...)
           //當child==null,返回super.dispatchTouchEvent(...),
           //即View的dispatchTouchEvent(...)
           if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
               // Child wants to receive touch within its bounds.
               mLastTouchDownTime = ev.getDownTime();
               if (preorderedList != null) {
                   // childIndex points into presorted list, find original index
                   for (int j = 0; j < childrenCount; j++) {
                       if (children[childIndex] == mChildren[j]) {
                           mLastTouchDownIndex = j;
                           break;
                       }
                   }
               } else {
                   mLastTouchDownIndex = childIndex;
               }
               mLastTouchDownX = ev.getX();
               mLastTouchDownY = ev.getY();
               //找到了事件的處理者,終止循環(huán)
               newTouchTarget = addTouchTarget(child, idBitsToAssign);
               alreadyDispatchedToNewTouchTarget = true;
               break;
           }
    
           // The accessibility focus didn't handle the event, so clear
           // the flag and do a normal dispatch to all children.
           ev.setTargetAccessibilityFocus(false);
       }
    

同樣是dispatchTouchEvent(...)的部分代碼

            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                //這里說明沒有子View處理該事件,只得有View的dispatchTouchEvent(...)來處理.
                //關(guān)于該函數(shù)的部分源碼在后面介紹.
                handled = dispatchTransformedTouchEvent(ev, canceled, null/*child*/,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
             ...   
            }
    
 函數(shù)addTouchTarget(...)的具體實現(xiàn).
            
            private TouchTarget addTouchTarget(View child, int pointerIdBits) {
                TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
                target.next = mFirstTouchTarget;
                mFirstTouchTarget = target;
                return target;
            }
            
  函數(shù)dispatchTransformedTouchEvent(...)的部分實現(xiàn).
  
        ....
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        ....
        return handled.

3.4 View的滑動沖突

  • 常見的滑動沖突的場景:

    1. 外部滑動方向和內(nèi)部滑動方向不一致,例如viewpager中包含listview;
    2. 外部滑動方向和內(nèi)部滑動方向一致,例如viewpager的單頁中存在可以滑動的bannerview;
    3. 上面兩種情況的嵌套,例如viewpager的單個頁面中包含了bannerview和listview。
  • 滑動沖突處理規(guī)則

    可以根據(jù)滑動距離和水平方向形成的夾角;或者根絕水平和豎直方向滑動的距離差;或者兩個方向上的速度差等

  • 解決方式

    1. 外部攔截法:點擊事件都先經(jīng)過父容器的攔截處理,如果父容器需要此事件就攔截,如果不需要就不攔截。該方法需要重寫父容器的onInterceptTouchEvent方法,在內(nèi)部做相應(yīng)的攔截即可,其他均不需要做修改。偽代碼如下:

       public boolean onInterceptTouchEvent(MotionEvent event) {
           boolean intercepted = false;
           int x = (int) event.getX();
           int y = (int) event.getY();
       
           switch (event.getAction()) {
           case MotionEvent.ACTION_DOWN: {
               intercepted = false;
               break;
           }
           case MotionEvent.ACTION_MOVE: {
               int deltaX = x - mLastXIntercept;
               int deltaY = y - mLastYIntercept;
               if (父容器需要攔截當前點擊事件的條件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
                   intercepted = true;
               } else {
                   intercepted = false;
               }
               break;
           }
           case MotionEvent.ACTION_UP: {
               intercepted = false;
               break;
           }
           default:
               break;
           }
       
           mLastXIntercept = x;
           mLastYIntercept = y;
       
           return intercepted;
       }
      
    2. 內(nèi)部攔截法:父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉,否則就交給父容器來處理。這種方法和Android中的事件分發(fā)機制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。

         public boolean dispatchTouchEvent(MotionEvent event) {
             int x = (int) event.getX();
             int y = (int) event.getY();
         
             switch (event.getAction()) {
             case MotionEvent.ACTION_DOWN: {
                 getParent().requestDisallowInterceptTouchEvent(true);
                 break;
             }
             case MotionEvent.ACTION_MOVE: {
                 int deltaX = x - mLastX;
                 int deltaY = y - mLastY;
                 if (當前view需要攔截當前點擊事件的條件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
                     getParent().requestDisallowInterceptTouchEvent(false);
                 }
                 break;
             }
             case MotionEvent.ACTION_UP: {
                 break;
             }
             default:
                 break;
             }
             mLastX = x;
             mLastY = y;
             return super.dispatchTouchEvent(event);
         }
      

      父View的onInterceptTouchEvent(...)偽代碼

         public boolean  onInterceptTouchEvent(MotionEvent ev){
             if(ev.getAction() == MotionEvent.ACTION_DOWN){
                 retuen false;
             }else{
                 retuen true;
             }
         }        
      

      內(nèi)部攔截法過程說明,父類在ACTION_DOWN時不攔截,子類在ACTION_DOWN時攔截,這時mFirstTouchTarget!=null, disallowIntercept = true,這意味著父類的onInterceptTouchEvent(...)不會再被執(zhí)行,并且一個事件序列只有一個View來處理,則所有的后續(xù)ACTION_MOVE都會傳到子View,當在子View中判斷到某個事件應(yīng)該由父View處理,只需重置disallowIntercept=false即可,即調(diào)用函數(shù)requestDisallowInterceptTouchEvent(false),這時事件就到父View的onTouchEvent(...)處理的(因為onInterceptionTouchEvent在非ACTION_DOWN時都返回true).如果父類沒有在設(shè)置requestDisallowInterceptTouchEvent(true)的話,這個事件就會一直都在父View中做處理了.(注:為個人理解,若有不對,望其指出)

參考

  1. Art of Android Development Reading Notes 3

  2. 用戶手勢檢測-GestureDetector使用詳解

  3. ViewDragHelper詳解

  4. Android觸摸屏事件派發(fā)機制詳解與源碼分析一(View篇)

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

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容