1. 程式人生 > >從 ViewRoot 來分析 TouchEvent 觸控事件

從 ViewRoot 來分析 TouchEvent 觸控事件

在 ViewRoot 中:

  • 有這幾個資料成員:

    InputChannel        mInputChannel;
    InputQueue.Callback mInputQueueCallback;
    InputQueue          mInputQueue;
    
    private final InputHandler mInputHandler = new InputHandler() {
        public void handleKey(KeyEvent event, Runnable finishedCallback) {
            startInputEvent(finishedCallback);
            dispatchKey(event, true);
        }
    
        public void handleMotion(MotionEvent event, Runnable finishedCallback) {
            startInputEvent(finishedCallback);
            dispatchMotion(event, true);
        }
    };
    
  • 這個 mInputHandler 是在 setView 中註冊的:

    /**
     * We have one child
     */
    public void setView(View view, WindowManager.LayoutParams attrs,
            View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
                mWindowAttributes.copyFrom(attrs);
                attrs = mWindowAttributes;
                if (view instanceof RootViewSurfaceTaker) {
                    mSurfaceHolderCallback =
                            ((RootViewSurfaceTaker)view).willYouTakeTheSurface();
                    if (mSurfaceHolderCallback != null) {
                        mSurfaceHolder = new TakenSurfaceHolder();
                        mSurfaceHolder.setFormat(PixelFormat.UNKNOWN);
                    }
                }
                Resources resources = mView.getContext().getResources();
                CompatibilityInfo compatibilityInfo = resources.getCompatibilityInfo();
                mTranslator = compatibilityInfo.getTranslator();
    
                if (mTranslator != null || !compatibilityInfo.supportsScreen()) {
                    mSurface.setCompatibleDisplayMetrics(resources.getDisplayMetrics(),
                            mTranslator);
                }
    
                boolean restore = false;
                if (mTranslator != null) {
                    restore = true;
                    attrs.backup();
                    mTranslator.translateWindowLayout(attrs);
                }
                if (DEBUG_LAYOUT) Log.d(TAG, "WindowLayout in setView:" + attrs);
    
                if (!compatibilityInfo.supportsScreen()) {
                    attrs.flags |= WindowManager.LayoutParams.FLAG_COMPATIBLE_WINDOW;
                }
    
                mSoftInputMode = attrs.softInputMode;
                mWindowAttributesChanged = true;
                mAttachInfo.mRootView = view;
                mAttachInfo.mScalingRequired = mTranslator != null;
                mAttachInfo.mApplicationScale =
                        mTranslator == null ? 1.0f : mTranslator.applicationScale;
                if (panelParentView != null) {
                    mAttachInfo.mPanelParentWindowToken
                            = panelParentView.getApplicationWindowToken();
                }
                mAdded = true;
                int res; /* = WindowManagerImpl.ADD_OKAY; */
    
                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                requestLayout();
                mInputChannel = new InputChannel();
                try {
                    res = sWindowSession.add(mWindow, mWindowAttributes,
                            getHostVisibility(), mAttachInfo.mContentInsets,
                            mInputChannel);
                } catch (RemoteException e) {
                    mAdded = false;
                    mView = null;
                    mAttachInfo.mRootView = null;
                    mInputChannel = null;
                    unscheduleTraversals();
                    throw new RuntimeException("Adding window failed", e);
                } finally {
                    if (restore) {
                        attrs.restore();
                    }
                }
    
                if (mTranslator != null) {
                    mTranslator.translateRectInScreenToAppWindow(mAttachInfo.mContentInsets);
                }
                mPendingContentInsets.set(mAttachInfo.mContentInsets);
                mPendingVisibleInsets.set(0, 0, 0, 0);
                if (Config.LOGV) Log.v(TAG, "Added window " + mWindow);
                if (res < WindowManagerImpl.ADD_OKAY) {
                    mView = null;
                    mAttachInfo.mRootView = null;
                    mAdded = false;
                    unscheduleTraversals();
                    switch (res) {
                        case WindowManagerImpl.ADD_BAD_APP_TOKEN:
                        case WindowManagerImpl.ADD_BAD_SUBWINDOW_TOKEN:
                            throw new WindowManagerImpl.BadTokenException(
                                "Unable to add window -- token " + attrs.token
                                + " is not valid; is your activity running?");
                        case WindowManagerImpl.ADD_NOT_APP_TOKEN:
                            throw new WindowManagerImpl.BadTokenException(
                                "Unable to add window -- token " + attrs.token
                                + " is not for an application");
                        case WindowManagerImpl.ADD_APP_EXITING:
                            throw new WindowManagerImpl.BadTokenException(
                                "Unable to add window -- app for token " + attrs.token
                                + " is exiting");
                        case WindowManagerImpl.ADD_DUPLICATE_ADD:
                            throw new WindowManagerImpl.BadTokenException(
                                "Unable to add window -- window " + mWindow
                                + " has already been added");
                        case WindowManagerImpl.ADD_STARTING_NOT_NEEDED:
                            // Silently ignore -- we would have just removed it
                            // right away, anyway.
                            return;
                        case WindowManagerImpl.ADD_MULTIPLE_SINGLETON:
                            throw new WindowManagerImpl.BadTokenException(
                                "Unable to add window " + mWindow +
                                " -- another window of this type already exists");
                        case WindowManagerImpl.ADD_PERMISSION_DENIED:
                            throw new WindowManagerImpl.BadTokenException(
                                "Unable to add window " + mWindow +
                                " -- permission denied for this window type");
                    }
                    throw new RuntimeException(
                        "Unable to add window -- unknown error code " + res);
                }
    
                if (view instanceof RootViewSurfaceTaker) {
                    mInputQueueCallback =
                        ((RootViewSurfaceTaker)view).willYouTakeTheInputQueue();
                }
                if (mInputQueueCallback != null) {
                    mInputQueue = new InputQueue(mInputChannel);
                    mInputQueueCallback.onInputQueueCreated(mInputQueue);
                } else {
                    InputQueue.registerInputChannel(mInputChannel, 
                                                    mInputHandler,   //這個地方注意一下.
                                                    Looper.myQueue());
                }
    
                view.assignParent(this);
                mAddedTouchMode = (res&WindowManagerImpl.ADD_FLAG_IN_TOUCH_MODE) != 0;
                mAppVisible = (res&WindowManagerImpl.ADD_FLAG_APP_VISIBLE) != 0;
            }
        }
    }
    
  • 這裡最主要的是 mInputHandler 中的 handleMotion 中呼叫到了 dispatchMotion 方法:

    public void handleMotion(MotionEvent event, Runnable finishedCallback) {
        startInputEvent(finishedCallback);
        dispatchMotion(event, true);
    }
    

    見 ViewRoot 中的 dispatchMotion 方法:

        private void dispatchMotion(MotionEvent event, boolean sendDone) {
            int source = event.getSource();
            if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
                dispatchPointer(event, sendDone);  //這個地方!
            } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
                dispatchTrackball(event, sendDone);
            } else {
                // TODO
                Log.v(TAG, "Dropping unsupported motion event (unimplemented): " + event);
                if (sendDone) {
                    finishInputEvent();
                }
            }
        }
    
        private void dispatchPointer(MotionEvent event, boolean sendDone) {
            Message msg = obtainMessage(DISPATCH_POINTER);  //發出這樣的訊息.
            msg.obj = event;
            msg.arg1 = sendDone ? 1 : 0;
            sendMessageAtTime(msg, event.getEventTime());
        }        
    
  • 處理這個訊息:

    case DISPATCH_POINTER: {//觸控事件訊息的處理
        MotionEvent event = (MotionEvent) msg.obj;
        try {
            deliverPointerEvent(event);//走入到這裡.----這個地方
        } finally {
            event.recycle(); //處理完這個事件後, 把這個event回收掉.
            if (msg.arg1 != 0) {
                finishInputEvent();//向發出訊息模組發一個回執, 以便進行下一次的訊息派發.
            }
            if (LOCAL_LOGV || WATCH_POINTER) Log.i(TAG, "Done dispatching!");
        }
    } break;
    
    private void deliverPointerEvent(MotionEvent event) {
        if (mTranslator != null) {
            mTranslator.translateEventInScreenToAppWindow(event);//物理座標向邏輯座標的轉換.
        }
    
        boolean handled;
        if (mView != null && mAdded) {
    
            // enter touch mode on the down
            boolean isDown = event.getAction() == MotionEvent.ACTION_DOWN;
            if (isDown) {
                ensureTouchMode(true);//進入觸控模式.----這個方法見下面
            }
            if(Config.LOGV) {
                captureMotionLog("captureDispatchPointer", event);
            }
            if (mCurScrollY != 0) {
                event.offsetLocation(0, mCurScrollY);
            }
            if (MEASURE_LATENCY) {
                lt.sample("A Dispatching TouchEvents", System.nanoTime() - event.getEventTimeNano());
            }
    
            //進行事件的派發, 對view和activity系統產生影響. 見 DecorView 和 ViewGroup中的方法.
            handled = mView.dispatchTouchEvent(event); //--------------------------這句話是最重要的.
    
            if (MEASURE_LATENCY) {
                lt.sample("B Dispatched TouchEvents ", System.nanoTime() - event.getEventTimeNano());
            }
            if (!handled && isDown) {//對於上面沒有處理的事件, 進行螢幕邊界偏移.螢幕偏移用(edge slop)進行表示. 
                //它的作用是當用戶正好觸控到螢幕邊界時,系統自動對原始訊息進行一定的偏移,
                //然後在新的偏移後的位置上尋找是否有匹配的檢視,
                //為什麼要有"螢幕偏移"呢? 因為對於觸控式螢幕而言, 尤其是電容觸控式螢幕, 人類手指尖有一定的大小, 
                //當觸控到邊界時, 力量會被自動吸附到螢幕邊界, 
                //所以, 此處根據上下左右不同的邊界物件訊息原始位置進行一定的偏移.
                int edgeSlop = mViewConfiguration.getScaledEdgeSlop();
    
                final int edgeFlags = event.getEdgeFlags();
                int direction = View.FOCUS_UP;
                int x = (int)event.getX();
                int y = (int)event.getY();
                final int[] deltas = new int[2];
    
                if ((edgeFlags & MotionEvent.EDGE_TOP) != 0) {
                    direction = View.FOCUS_DOWN;
                    if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
                        deltas[0] = edgeSlop;
                        x += edgeSlop;
                    } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
                        deltas[0] = -edgeSlop;
                        x -= edgeSlop;
                    }
                } else if ((edgeFlags & MotionEvent.EDGE_BOTTOM) != 0) {
                    direction = View.FOCUS_UP;
                    if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
                        deltas[0] = edgeSlop;
                        x += edgeSlop;
                    } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
                        deltas[0] = -edgeSlop;
                        x -= edgeSlop;
                    }
                } else if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
                    direction = View.FOCUS_RIGHT;
                } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
                    direction = View.FOCUS_LEFT;
                }
    
                if (edgeFlags != 0 && mView instanceof ViewGroup) {
                    View nearest = FocusFinder.getInstance().findNearestTouchable(
                            ((ViewGroup) mView), x, y, direction, deltas);
                    if (nearest != null) {
                        event.offsetLocation(deltas[0], deltas[1]);
                        event.setEdgeFlags(0);
                        mView.dispatchTouchEvent(event);
                    }
                }
            }
        }
    }
    
    其中 ensureTouchMode 如下所示:
    boolean ensureTouchMode(boolean inTouchMode) {//進否進入觸控模式.---即 非觸控模式 與 觸控模式 之間的切換.
        if (DBG) Log.d("touchmode", "ensureTouchMode(" + inTouchMode + "), current "
                + "touch mode is " + mAttachInfo.mInTouchMode);
        //如果當前觸控 與 原來的觸控模式 相同, 則沒有改變, 所以返回false.
        if (mAttachInfo.mInTouchMode == inTouchMode) return false;
    
        // tell the window manager----即通知window----因為 wms在佈局視窗時, 會根據不同的touch模式進行不同的處理
        try {
            //通知視窗, WmS在進行客戶窗口布局時, 需要根據客戶視窗的Touch模式進行不同的處理.
            sWindowSession.setInTouchMode(inTouchMode);
        } catch (RemoteException e) {
            throw new RuntimeException(e);
        }
    
        // handle the change ----view自身的改變.----如清除焦點, 或者requestFocus 之類的 可能涉及 介面更新的操作.
        return ensureTouchModeLocally(inTouchMode);//---點進去去看下. 這個方法 其實就在這下面.
    }
    
  • 如果這個 mView是 DecorView 而言, 執行這個:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        //注意, activity實現了 Window.CallBack介面, 這裡獲得的cb, 就是這個activity.
        final Callback cb = getCallback(); 
        return cb != null && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super
                .dispatchTouchEvent(ev);
    }
    
    • 如果 cb為空, 則直接執行 ViewGroup中的 dispatchTouchEvent方法.—-下面會講到.
    • 如果 cb不為空, 則執行 activity中的 dispatchTouchEvent方法:

      public boolean dispatchTouchEvent(MotionEvent ev) {
          if (ev.getAction() == MotionEvent.ACTION_DOWN) { 
              //如果是down事件的話, activity有機會在事件響應之前做點事情.
              onUserInteraction();
          }
          if (getWindow().superDispatchTouchEvent(ev)) {  //調的是PhoneWindow中的superDispatchTouchEvent
                                                          //--->DecorView中的superDispatchTouchEvent
                                                          //--->ViewGroup中的dispatchTouchEvent方法.
              return true;
          }
          return onTouchEvent(ev); //如果view系統不處理, 則呼叫 activity中的 onTouchEvent.
      }
      
  • 如果這個 mView直接就是 ViewGroup的話, 那直接調到 ViewGroup中的 dispatchTouchEvent.

  • 反正先 處理 viewgroup的 dispatchTouchEvent, 如果沒有消化掉, 才去處理 activity中的 onTouchEvent方法.

  • 至於 ViewGroup中的 dispatchTouchEvent(event)方法:

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {//touch到時, 會從 ViewRoot那裡調到 ViewGroup的這個方法.
        if (!onFilterTouchEventForSecurity(ev)) {
            return false;
        }
    
        final int action = ev.getAction();
        //當前ViewGroup佈局座標系的座標.  當前ViewGroup檢視座標原點在佈局座標系中的位置為(-mScrollX, -mScrollY)
        final float xf = ev.getX();
        final float yf = ev.getY();
        //座標系 轉換成 當前ViewGroup檢視座標系的座標. 這個混算要整明白.----不要誤以為是child什麼的.
        //因為當前這個viewgroup可能會在scroll的, 
        //所以要算上(mScrollX, mScrollY)來得到這個觸控點相對於當前這個Viewgroup檢視座標原點的座標.
        final float scrolledXFloat = xf + mScrollX;
        final float scrolledYFloat = yf + mScrollY;
        final Rect frame = mTempRect;
    
        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//true表示不允許攔截
    
        if (action == MotionEvent.ACTION_DOWN) { //先處理 action_down 情況
            if (mMotionTarget != null) {//這個mMotionTarget是指這個viewgroup中的捕獲事件的child.
                // this is weird, we got a pen down, but we thought it was
                // already down!
                // XXX: We should probably send an ACTION_UP to the current
                // target.
                //當action_down時, 通常情況下, 這個mMotionTarget當然應為null. 不為空則可能是出錯的.
                mMotionTarget = null;
            }
            // If we're disallowing intercept or if we're allowing and we didn't
            // intercept
            if (disallowIntercept || !onInterceptTouchEvent(ev)) {//不允許攔截 或者 沒有攔截
                // reset this event's action (just to protect ourselves)
                ev.setAction(MotionEvent.ACTION_DOWN);//重置
                // We know we want to dispatch the event down, find a child
                // who can handle it, start with the front-most child.
                final int scrolledXInt = (int) scrolledXFloat;//檢視座標
                final int scrolledYInt = (int) scrolledYFloat;
                final View[] children = mChildren;//要對孩子進行遍歷, 這個孩子可能是相鄰, 也可能是相互前後疊加.
                final int count = mChildrenCount;
                //被觸控的點處, 可能會疊加多個孩子. 
                //讓序號最後面的child先拿事件試試, 如果不要的話, 再讓序號前面的孩子拿事件.
                for (int i = count - 1; i >= 0; i--) {//遍歷孩子, 確定孩子要不要這個down事件.
                    final View child = children[i];
                    //只有當child是可見或者動畫時, 才可以響應這個down.
                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                            || child.getAnimation() != null) {
                        child.getHitRect(frame);//獲得該child的佈局區域----父view檢視座標系中的.
                        if (frame.contains(scrolledXInt, scrolledYInt)) {//判斷點選的位置是否在這個child上.
                            // offset the event to the view's coordinate system
                            //座標系切到孩子的佈局座標系統上. 這個要理解好.
                            final float xc = scrolledXFloat - child.mLeft;
                            final float yc = scrolledYFloat - child.mTop;
                            //點選事件在child上, 則現在座標轉換到以child的原點為基準,---但非child顯示區域座標啊.
                            ev.setLocation(xc, yc);
                            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                            if (child.dispatchTouchEvent(ev))  {//交給child來分發了.
                                // Event handled, we have a target now.
                                //如果孩子消費了這個down事件, 則這個mMotionTarget就記錄這個孩子, 然後返回.
                                mMotionTarget = child;
                                return true;
                            }
                            // The event didn't get handled, try the next view.
                            // Don't reset the event's location, it's not
                            // necessary here.
                        }
                    }
                }
            }
        }
    
        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||//指是不是up或cancel事件, true表示是.
                (action == MotionEvent.ACTION_CANCEL);
    
        if (isUpOrCancel) {
            //如果現在的事件是up或者cancel掉了, 那麼應當允許攔截. 因為在按下還沒有釋放時, 要攔截訊息的.
            // Note, we've already copied the previous state to our local
            // variable, so this takes effect on the next event
            //現在允許攔截.----因為 這一系列的(down/move/up/cacel)事件 已經結束了!
            //----所以沒有是否允許攔截的意義了.
            //----即 設一個不允許攔截, 其有效期僅這麼一套down/move/up/cancel週期而已.
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
    
        // The event wasn't an ACTION_DOWN, dispatch it to our target if
        // we have one.
        final View target = mMotionTarget;
        if (target == null) {  
            //如果child沒有消耗這個down事件的話, 說明move和up也不會響應. 所以, 應由這個viewgroup自己響應.
            // We don't have a target, this means we're handling the
            // event as a regular view.
            ev.setLocation(xf, yf);//座標移回viewgroup自己的座標體系, 即佈局座標.
            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {//這個CANCEL_NEXT_UP_EVENT通常是不存在的
                ev.setAction(MotionEvent.ACTION_CANCEL);
                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            }
            return super.dispatchTouchEvent(ev);//呼叫viewgroup的view部分的dispatchTouchEvent方法.
        }
        //如果有孩子響應了down事件, 那麼往下走.----上面剛處理的是 沒有孩子響應的事情, 現在處理有孩子響應的情況.
        // if have a target, see if we're allowed to and want to intercept its
        // events
        if (!disallowIntercept && onInterceptTouchEvent(ev)) {
            //如果允許viewgroup截獲, 並且確實被viewgroup截獲了, 
            //那麼child應當放棄down,move,up事件, 所以下面用cancel來取消child.
            //這個target是指捕獲down事件的child
            //即, 將獲得點選位置---即以child的佈局座標系統來算的.
            final float xc = scrolledXFloat - (float) target.mLeft; 
            final float yc = scrolledYFloat - (float) target.mTop;  
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            ev.setAction(MotionEvent.ACTION_CANCEL);//事件改為action_cancel事件.
            ev.setLocation(xc, yc);//座標換到child的佈局座標來.
            if (!target.dispatchTouchEvent(ev)) {
                //用於讓child處理cancel事件
                //----因為原來的事件被父viewgroup給攔截了,所以用cancel來逐個逐級通知child處理cancel.
                // target didn't handle ACTION_CANCEL. not much we can do
                //這個cancel事件可以通知child去取消之前對事件的追蹤, 如長按, 特定手勢之類.
                // but they should have.
            }
            // clear the target
            mMotionTarget = null;//把其置為null
            // Don't dispatch this event to our own view, because we already
            // saw it when intercepting; we just want to give the following
            // event to the normal onTouchEvent().
            return true;//返回true, 表示事件被消耗了.----這裡是被viewgroup消耗了, 而不是child.
        }
        //
        if (isUpOrCancel) {//true表示當前事件是 up或cancel事件, 
            //表示 事件處理 處於 尾聲了. 
            //mMotionTarget置回空, 不過target仍在, 以便下面呼叫 target.dispatchTouchEvent.
            //對於 move事件, 因為事件 後面還會有, 所以 mMotionTarget不能為空的.
            mMotionTarget = null;
        }                        
        //下面這些, 都是指 由child來響應 move, up事件!
        // finally offset the event to the target's coordinate system and
        // dispatch the event.
        final float xc = scrolledXFloat - (float) target.mLeft;//轉到child的佈局座標方式.
        final float yc = scrolledYFloat - (float) target.mTop;
        ev.setLocation(xc, yc);//更改座標系統為child的佈局座標方式.
    
        if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
            //通常不走進來.這個CANCEL_NEXT_UP_EVENT表示 取消 隨後的up事件.
            ev.setAction(MotionEvent.ACTION_CANCEL); //走進來的話, 表示要取消隨後的up事件, 所以事件改為cancel事件.
            target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            mMotionTarget = null;  //然後 也把 這個置為null
        }
    
        return target.dispatchTouchEvent(ev);//由child來處理這個move和up事件.----以及可能的cancel事件.
    }
    
  • View 中的 dispatchTouchEvent 方法:

    //這裡注意的是: 這裡先處理 外界設定的 OnTouchEventListener
    //    如果返回 true, 說明外界要 搶佔 這個事件, 所以不執行 控制元件自身的 onTouchEvent.
    //    如果返回 false, 說明外界 認為可以 把這個事件 分發給 控制元件自身的 onTouchEvent處理.
    //主要是這點:
    //(1) 提供了一個介面給外界設定, 即通過 setOnTouchEventListener 設定一個監聽器.
    //(2) 自身處理的方法: onTouchEvent ----在自定義一個view時寫的.
    //優先執行 外界的要求(即監聽器中的方法), 如果返回 false, 才去執行 控制元件自身的onTouchEvent.
    public boolean dispatchTouchEvent(MotionEvent event) {           
        if (!onFilterTouchEventForSecurity(event)) {
            //處理當視窗處於模糊狀態下的事件.---返回true表示, 事件應當處理; 為false時, 表示事件不處理.
            return false;
        }
    
        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
                mOnTouchListener.onTouch(this, event)) {
            //如果 ENABLE, 並且該view註冊了OnTouchListener監聽器, 則執行這個監聽器的onTouch, 處理完直接返回true
            return true;
        }
        return onTouchEvent(event); //如果沒有設定監聽器, 則執行 onTouchEvent方法.
    }
    
  • View 中的 onTouchEvent 方法:
    這個呢, 在其它的筆記中已做了說明, 這裡就不列出來了.