主頁 > 移動端開發 > Android事件分發機制

Android事件分發機制

2022-01-04 10:07:24 移動端開發

Android事件分發機制

一.初識

1.1 用戶對螢屏的操作的事件可以劃分為3種最基礎的事件:
1.ACTION_DOWN:手指剛接觸螢屏,按下去的那一瞬間產生該事件
2.ACTION_MOVE:手指在螢屏上移動時候產生該事件
3.ACTION_UP:手指從螢屏上松開的瞬間產生該事件
1.2 用戶對螢屏的操作最終可以劃分為這三種事件,用戶的ACTION_DOWN到ACTION_UP的操作可以稱為一個事件序列
一個事件序列主要有以下兩種組成:
一: ACTION_DOWN->ACTION_UP

二 :ACTION_DOWN->許多個ACTION_MOVE>ACTION_UP
圖片來自網路

1.3 Android 的事件分發機制大體可以分為三部分 事件生產 事件分發 事件消費 事件的生產是由用戶點擊螢屏產生,這篇文章著重分析事件的分發和消費,因為事件分發和處理聯系的過于緊密,這篇文章將把事件的分發和消費放在一起分析

在Activity上的事件分發和Activity PhoneView DecorView ViewGroup view 密不可分, 其中 Activity PhoneView DecorView ViewGroup view的關系可以用如下圖來描述:

在這里插入圖片描述
若干GroupView和若干View組成的控制元件樹可以用下午來概括:
在這里插入圖片描述

1.4 事件分發的大概流程可以這樣來描述:Activity -> PhoneWindow ->DecorView(DecorView其實就是一種ViewGroup) ->View

1.5 事件分發需要的三個重要方法來共同完成
public boolean dispatchTouchEvent(event):用于進行點擊事件的分發
public boolean onInterceptTouchEvent(event):用于進行點擊事件的攔截
public boolean onTouchEvent(event):用于處理點擊事件
三個函式的引數均為even,即上面所說的3種型別的輸入事件,回傳值均為boolean 型別
上面的三種方法的呼叫關系大致可以用下面的偽代碼來描述

    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false;//事件是否被消費
        if (onInterceptTouchEvent(ev)){//呼叫onInterceptTouchEvent判斷是否攔截事件
            consume = onTouchEvent(ev);//如果攔截則呼叫自身的onTouchEvent方法
        }else{
            consume = child.dispatchTouchEvent(ev);//不攔截呼叫子View的dispatchTouchEvent方法
        }
        return consume;//回傳值表示事件是否被消費,true事件終止,false呼叫父View的onTouchEvent方法
    }

事件的分發到處理的程序大致可以用一個U形圖來描述:
在這里插入圖片描述

  • 首先是要說明的是,上圖的分析僅僅只限于對ACTION_DOWN的分析
  • U形圖從上到下可以分為三層,分別是Activity層,ViewGroup層,和View層
  • 事件的開始分發是從右上角的大紅色的箭頭開始傳輸的
  • 線上的false/ture/super 分別表示的是箭頭起點函式的回傳值:return false/return ture/return supperxxx(呼叫父類的回傳)
  • 箭頭指向消費,表示該事件就到此為止,不會再往下傳或往上一級傳
  • 如果事件在分發的程序中一直不被消費,那么整個分發的程序看起來就是一個U形的結構
  • 圖中只是說明了activity中只有一個ViewGroup的情況,實際操作中可能會有View ViewGroup多層嵌套的情況,原理也是這個原理,大同小異,
    總結一下同種分發的幾條規律:
  • 對于activity來說dispatchTouchEvent中無論是回傳ture/false都會將事件消費,即不會再往下面傳播,只有return super的時候才會傳到 ViewGroup()
  • 對于dispatchTouchEvent和onTouchEvent來說return false會把該事件交給父容器來處理
  • 對于onInterceptTouchEvent來說回傳false,顧名思義,該viewGroup將不會攔截該事件,這個事件就可以繼續向下傳播了,如果onInterceptTouchEvent回傳ture就表明該ViewGroup將會攔截該事件,事件將會交給該組件的onTouchEvent來處理,事件也就不會再往下面的組件分發了
  • 對于onTouchEvent來說對于傳入他的事件,如果回傳ture就代表該事件已經被消費,則流程就終止,如果回傳false,那么就代表該事件將不會由他處理,將會將給父容器的onTouchEvent來處理,
  • 每個ViewGroup每次在做分發的時候,問一問攔截器要不要攔截(也就是問問自己這個事件要不要自己來處理)如果要自己處理那就在onInterceptTouchEvent方法中 return true就會交給自己的onTouchEvent的處理,如果不攔截就是繼續往子控制元件往下傳,默認是不會去攔截的,因為子View也需要這個事件,所以onInterceptTouchEvent攔截器return super.onInterceptTouchEvent()和return false是一樣的,是不會攔截的,事件會繼續往子View的dispatchTouchEvent傳遞,
  • 看下ViewGroup 的dispatchTouchEvent,之前說的return true是終結傳遞,return false 是回溯到父View的onTouchEvent,然后ViewGroup怎樣通過dispatchTouchEvent方法能把事件分發到自己的onTouchEvent處理呢,return true和false 都不行,那么只能通過Interceptor把事件攔截下來給自己的onTouchEvent,所以ViewGroup dispatchTouchEvent方法的super默認實作就是去呼叫onInterceptTouchEvent
  • 那么對于View的dispatchTouchEvent return super.dispatchTouchEvent()的時候呢事件會傳到哪里呢,很遺憾View沒有攔截器,但是同樣的道理return true是終結,return false 是回溯會父類的onTouchEvent,怎樣把事件分發給自己的onTouchEvent處理呢,那只能return
    super.dispatchTouchEvent,View類的dispatchTouchEvent()方法默認實作就是能幫你呼叫View自己的onTouchEvent方法的,

1.6 對ACTION_MOVE 和ACTION_UP事件的處理

  • 對這兩種事件的處理可以總結為下面這些內容ACTION_DOWN事件在哪個控制元件消費了(return true), 那么ACTION_MOVE和ACTION_UP就會從上往下(通過dispatchTouchEvent)做事件分發往下傳,就只會傳到這個控制元件,不會繼續往下傳,如果ACTION_DOWN事件是在dispatchTouchEvent消費,那么事件到此為止停止傳遞,如果ACTION_DOWN事件是在onTouchEvent消費的,那么會把ACTION_MOVE或ACTION_UP事件傳給該控制元件的onTouchEvent處理并結束傳遞,(下面的三張圖均來自圖解 Android 事件分發機制 紅色的箭頭代表ACTION_DOWN 事件的流向藍色的箭頭代表ACTION_MOVE 和 ACTION_UP 事件的流向)
  • 在這里插入圖片描述
    在這里插入圖片描述

在這里插入圖片描述

二.原始碼分析

1. 從Activity到ViewGroup

Activity.java
  public boolean dispatchTouchEvent(MotionEvent ev) {
	......
    // ->>分析1
    if (getWindow().superDispatchTouchEvent(ev)) {  //getWindow回傳的是一個PhoneWindow			  		  
    												//物件 ,即這里呼叫的是PhoneWindow		  
    												// 的superDispatchTouchEvent,也就
    												//說在這里Activity把時間傳給了
    												//PhoneWindow

        return true;
        // 若getWindow().superDispatchTouchEvent(ev)的回傳true
        // 則Activity.dispatchTouchEvent()就回傳true,則方法結束,即 :該點擊事件停止往下傳遞 & 事件傳遞程序結束
        // 否則:繼續呼叫Activity.onTouchEvent

    }
    return onTouchEvent(ev);
  }
  
	// :當一個點擊事件未被Activity下任何一個View接收/處理時,就會呼叫該方法
   public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) { 
            finish();
            return true;
        }

        return false;
    }
PhoneWindow.java
  @Override
  public boolean superDispatchTouchEvent(MotionEvent event) {
      return mDecor.superDispatchTouchEvent(event);
      // mDecor的型別是DecorView,DecorView是PhoneWindow的內部類,繼承自FrameLayout,而FrameLayout是ViewGroup的子類,
      // 所以mDecor是一個頂級的ViewGroup,在這里就實作了事件從PhoneWindow到頂級
      //ViewGroup的傳遞
  }

在這里插入圖片描述

DecorViewpublic boolean superDispatchTouchEvent(MotionEvent event) {

      return super.dispatchTouchEvent(event);
      // 在這里它呼叫了父類的dispatchTouchEvent,而DecorView的父類是FrameLayout,而
      // FrameLayout的父類是ViewGroup,到這里就是我們熟悉的ViewGroup 的dispatchTouchEvent

  }

總結
當一個點擊事件發生時,從Activity的事件分發開始(Activity.dispatchTouchEvent()),流程總結如下:
在這里插入圖片描述
1. ViewGroup中的分發
1.1

class ViewGroup:
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        // Handle an initial down.
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            //清除FLAG_DISALLOW_INTERCEPT設定并且mFirstTouchTarget 設定為null
            resetTouchState();
        }
        // Check for interception.
        final boolean intercepted;//是否攔截事件
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {   // 注意這里 -> 分析一
            //FLAG_DISALLOW_INTERCEPT是子View通過
            //requestDisallowInterceptTouchEvent方法進行設定的
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;                                      // 分析二
            if (!disallowIntercept) {
                //呼叫onInterceptTouchEvent方法判斷是否需要攔截
                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;
        }
        ...
    }

分析一

這里首先判斷事件是否為DOWN事件,如果是,則進行初始化,resetTouchState方法中會把 mFirstTouchTarget的值置為null,這里為什么要進行初始化呢?原因就是一個完整的事件序列是以 DOWN開始,以UP結束的,所以如果是DOWN事件,那么說明這是一個新的事件序列,故而需要初 始化之前的狀態,接著往下看,上面代碼注釋1處的條件如果滿足,則執行下面的句子,mFirstTouchTarget 的意義是:當 前ViewGroup 是否攔截了事件,如果攔截了, mFirstTouchTarget=null;如果沒有攔截并交由子View來處 理,mFirstTouchTarget!=null 從上面代碼我們可以看出,ViewGroup在如下兩種情況下會判斷是否要攔截當前事件:事件型別為ACTION DOWN或者mFirstTouchTarget != null,ACTION_ DOWN事件好理解,那么mFirstTouchTarget!=null是什么意思呢?這個從后面的代碼邏輯可以看出來,當事件由ViewGroup的子元素成功處理時,mFirstTouchTarget 會被賦值并指向子
元素,換種方式來說,當ViewGroup 不攔截事件并將事件交由子元素處理時
mFirstTouchTarget != null, 反過來,一旦事件由當前ViewGroup 攔截時,mFirstTouchTarget != null就不成立,那么當ACTION_ MOVE和ACTION UP事件到來
時,由于(actionMasked == MotionEvent. ACTION
DOWN II mFirstTouchTarget != null)這
個條件為false, 將導致ViewGroup的onInterceptTouchEvent 不會再被呼叫,并且同一序列中的其他事件都會默認交給它處理,

分析二

當然,這里有一種特殊情況,那就是FLAG_ DISALLOW_ INTERCEPT 標記位,這個
標記位是通過requestDisallowInterceptTouchEvent 方法來設定的,一般用于子 View中, FLAG_ DISALLOW_ INTERCEPT 一旦設定后,ViewGroup 將無法攔截除了 ACTION_ DOWN以外的其他點擊事件,為什么說是除了ACTION_ DOWN以外的其他事 件呢?這是因為ViewGroup在分發事件時,如果是ACTION DOWN就會重置 FLAG_ DISALLOW_ INTERCEPT這個標記位,將導致子View中設定的這個標記位無效, 因此,當面對ACTION_DOWN事件時,ViewGroup總是會呼叫自己的onInterceptTouchEvent方法來詢問自己是否要攔截事件,這一點從原始碼中也可以看出來,

分析三

如果ViewGroup在這里對事件進行了ACTION_DOWN攔截,則會呼叫 super.dispatchTouchEvent(event)對事 件進行處理,因為ViewGroup的父類為View,在View的dispatchTouchEvent()中會呼叫onTouchEvent(),即ViewGroup在選擇對事件進行攔截后會交給ViewGroup的onTouchEvent去處理,

分析四

ViewGroup消費了ACTION_DOWN后,對后續事件的處理
分析過ViewGroup的dispatchTouchEvent()發現,當ViewGroup對ACTION_DOWN事件攔截后, mFirstTouchTarget 的值應該還是空的,這就使得ViewGroup的(dispatchTouchEventactionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)不成立 走else,使得intercepted = false;這樣的話 ,ViewGroup在這里對事件進行了ACTION_DOWN攔截之后的,對后續的ACTION_MOVE 和ACTION_UP都進行攔截,流程與ViewGroup對事件ACTION_DOWN處理的流程一致,

1.2

class ViewGroup:
    public boolean dispatchTouchEvent(MotionEvent ev) {
    
	....
	
        final View[] children = mChildren;
        //對子View進行遍歷
        //我們看到了for回圈,首先遍歷ViewGroup的子元素,判斷子元素是否能夠接收到點
		//擊事件,如果子元素能夠接收到點擊事件,則交由子元素來處理,需要注意這個for回圈				  		 //是倒序遍歷的,即從最上層的子View開始往內層遍歷,
        for (int i = childrenCount - 1; i >= 0; i--) {
            final int childIndex = getAndVerifyPreorderedIndex(
                    childrenCount, i, customOrder);
            final View child = getAndVerifyPreorderedView(
                    preorderedList, children, 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;
            }
			// 注釋1 具體分析在下文
            //判斷1,View可見并且沒有播放影片,2,點擊事件的坐標落在View的范圍內
            //如果上述兩個條件有一項不滿足則continue繼續回圈下一個View
            if (!canViewReceivePointerEvents(child)
                    || !isTransformedTouchPointInView(x, y, child, null)) {
                ev.setTargetAccessibilityFocus(false);
                continue;
            }
			// 注釋 2  具體分析見下文
            newTouchTarget = getTouchTarget(child);
            //如果有子View處理即newTouchTarget 不為null則跳出回圈,
            if (newTouchTarget != null) {
                // Child is already receiving touch within its bounds.
                // Give it the new pointer in addition to the ones it is handling.
                newTouchTarget.pointerIdBits |= idBitsToAssign;
                break;
            }

            resetCancelNextUpFlag(child);
            //dispatchTransformedTouchEvent第三個引數child這里不為null
            //實際呼叫的是child的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();
                //當child處理了點擊事件,那么會設定mFirstTouchTarget 在addTouchTarget被賦值
                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                alreadyDispatchedToNewTouchTarget = true;
                //子View處理了事件,然后就跳出了for回圈
                break;
            }
        }
    }

分析1

在注釋1處,有這樣的判斷 if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null))
判斷1,View可見并且沒有播放影片,2,點擊事件的坐標落在View的范圍內
若該判斷不生效即 不可見 沒播放影片, 或者 坐標點不在該View里面,則執行continue跳過這次回圈,

    /**
     * Returns true if a child view can receive pointer events.
     * @hide
     */
     // 判斷有沒有可見并且沒有播放影片的函式
    private static boolean canViewReceivePointerEvents(@NonNull View child) {
        return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                || child.getAnimation() != null;
    }
    /**
     * Returns true if a child view contains the specified point when transformed
     * into its coordinate space.
     * Child must not be null.
     * @hide
     */
     // 判斷坐標是否落在了 該View里面
    protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
        final float[] point = getTempPoint();
        point[0] = x;
        point[1] = y;
        transformPointToViewLocal(point, child);
        //呼叫View的pointInView方法進行判斷坐標點是否在View內
        final boolean isInView = child.pointInView(point[0], point[1]);
        if (isInView && outLocalPoint != null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }

分析2

在1.2代碼里 對view可見性 坐標是否落在該View等條件判斷完后,會有這樣的步驟
newTouchTarget = getTouchTarget(child),此時由于ACTION_DOWN事件還未被該ViewGroup的任何一個子View處理,結合上面的分析此時mFirstTouchTarget為null,所以此時getTouchTarget的回傳值為null

    private TouchTarget getTouchTarget(@NonNull View child) {
        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            if (target.child == child) {
                return target;
            }
        }
        return null;
    }

分析3

因為getTouchTarget()在ViewGroup的回傳值為null,最有接下來進入到 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) ,把之前的代碼copy下來方便分析,dispatchTransformedTouchEvent這和函式將會呼叫ViewGroup中子View的dispatchTouchEvent,并把結果回傳回來,如果子View的onTouchEven()消費了事件,那么
dispatchTransformedTouchEvent的回傳值為Ture,先回在注釋1處呼叫newTouchTarget = addTouchTarget(child, idBitsToAssign),這一步會把消費掉ACTION_DOWN的子View記錄下來,同時使得mFirstTouchTarget 不再為null,然后在注釋2處break ,結束ViewGroup中對子View的遍歷

class ViewGroup:
    public boolean dispatchTouchEvent(MotionEvent ev) {
    
    .....
    
      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();
            //當child處理了點擊事件,那么會設定mFirstTouchTarget 在addTouchTarget被賦值
            newTouchTarget = addTouchTarget(child, idBitsToAssign);  // 注釋1
            alreadyDispatchedToNewTouchTarget = true;
            //子View處理了事件,然后就跳出了for回圈
            break;   // 注釋2
        }
// 通過這函式可以看出,這里采用了頭插法把target 插入到一個單向鏈表中,
//其中TouchTarget.obtain是產生一個TouchTarget 物件,TouchTarget存有消費事件的View
   private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

TouchTarget.java
// 觀察TouchTarget的欄位可以發現嗎,TouchTarget里面存有消費事件的View,
//pointerIdBits(和多點觸碰有關),還有指向下一個節點的參考
  private static final class TouchTarget {
        private static final int MAX_RECYCLED = 32;
        private static final Object sRecycleLock = new Object[0];
        private static TouchTarget sRecycleBin;
        private static int sRecycledCount;

        public static final int ALL_POINTER_IDS = -1; // all ones

        // The touched child view.
        @UnsupportedAppUsage
        public View child;

        // The combined bit mask of pointer ids for all pointers captured by the target.
        public int pointerIdBits;

        // The next target in the target list.
        public TouchTarget next;

分析4

ViewGroup的子View消費了ACTION_DOWN事件后,ViewGroup對后續事件的處理
這里的處理邏輯在代碼里的注釋了做了詳細的說明,這里就不再贅述了,

class ViewGroup:
    public boolean dispatchTouchEvent(MotionEvent ev) {
    
    .....
 // Dispatch to touch targets.
 			// 由之前的分析可知,當ViewGroup中的一子View消費了事件后,mFirstTouchTarget 
 			//不在為空,故將執行else
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                //  mFirstTouchTarget != null會走到這里
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                //注釋1 在這里會不斷從target鏈表里在取出target物件,(在單點觸摸的時候
                //只有一個物件),
                    final TouchTarget next = target.next;  
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                         // 注釋2 在這里呼叫了dispatchTransformedTouchEvent 引數中
                         // target.child傳了進去,target.child是消費ACTION_DOWN事件
                         // 的View ,dispatchTransformedTouchEvent 將會呼叫該View的
                         // dispatchTouchEvent去處理事件,
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            // 對target做回收,實作復用
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
            
        }
        
        return handled;
    }
    ......

2. View中的分發
2.1
在View的dispatchTouchEvent有以下流程:

class View:
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }
		// //如果視窗沒有被遮蓋  注釋1 具體分析見下文
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

下面的代碼是對上文代碼注釋1處的截取

從下下面代碼的注釋1可以看得出上面代碼我們可以看到View會先判斷是否設定了OnTouchListener,如果設定了OnTouchListener并且onTouch方法回傳了true,那么onTouchEvent不會被呼叫,當沒有設定OnTouchListener或者設定了OnTouchListener但是onTouch方法回傳false則會呼叫View自己的onTouchEvent方法,

        //如果視窗沒有被遮蓋
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            //當前監聽事件
            ListenerInfo li = mListenerInfo;
            //需要特別注意這個判斷當中的li.mOnTouchListener.onTouch(this, event)條件 //注釋1
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //result為false呼叫自己的onTouchEvent方法處理
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

2.2
接下來看onTouchEvent方法,

class View:
    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        //1.如果View是設定成不可用的(DISABLED)仍然會消費點擊事件
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
        ...
        //2.CLICKABLE 和LONG_CLICKABLE只要有一個為true就消費這個事件
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    //3.在ACTION_UP方法發生時會觸發performClick()方法
                                    performClick();
                                }
                            }
                        }
                        ...
                    break;
            }
            ...
            return true;
        }
        return false;
    }

2.3
View事件方法執行順序
onTouchListener > onTouchEvent > onLongClickListener > onClickListener
上文對onTouchListener 和onTouchEvent 的處理做了簡單的分析,接下來分析 onLongClickListener 和 onClickListener

onLongClickListener

長按事件可以分解為 按下 和 抬起 兩個步驟,先在ACTION_DOWN事件里找找,具體分析見注釋,

public boolean onTouchEvent(MotionEvent event) {
	...
	case MotionEvent.ACTION_DOWN:
		...
		//mHasPerformedLongPress用于標記是否已經長按
		mHasPerformedLongPress = false;

		if (!clickable) {
           checkForLongClick(
           		// ViewConfiguration.getLongPressTimeout 方法可以獲取到長按觸發所需的時間,默認是500ms,
                ViewConfiguration.getLongPressTimeout(),
                x,
                y,
                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                break;
        }
	...
}

接下來分析checkForLongClick方法
完成了相關的設定后把 mPendingCheckForLongPress延遲一段時間(delay)再發送出去,這里使用了Handel(改日寫一篇handler的文章), postDelayed(mPendingCheckForLongPress, delay)的第一個引數是一定延時后執行的任務,第二個引數是延時,

private void checkForLongClick(long delay, float x, float y, int classification) {
        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
            mHasPerformedLongPress = false;

            if (mPendingCheckForLongPress == null) {
                mPendingCheckForLongPress = new CheckForLongPress();
            }
            mPendingCheckForLongPress.setAnchor(x, y);
            mPendingCheckForLongPress.rememberWindowAttachCount();
            mPendingCheckForLongPress.rememberPressedState();
            mPendingCheckForLongPress.setClassification(classification);
            //1
            postDelayed(mPendingCheckForLongPress, delay);
        }
}

CheckForLongPress 類實作了run方法,那么在訊息發出的500毫秒后將會執行run該run方法

private final class CheckForLongPress implements Runnable {
        ...
        @Override
        public void run() {
            if ((mOriginalPressedState == isPressed()) && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                recordGestureClassification(mClassification);
                //1
                if (performLongClick(mX, mY)) {  //注意這里
                	//2
                    mHasPerformedLongPress = true;
                }
            }
        }
        ...
    }

觀察代碼發現,在onLongClickListener 發送的延時訊息中,在回呼onLongClick()方法之前的一系列呼叫中都沒有進行判斷,也就是說只要這個callback沒有被移除,在指定時間之后肯定要回呼onLongClick()方法,所以說有這種狀況當我么長按了490ms時還未達到500ms,這時候手指抬分發ACTION_UP事件,同時移除hander機制中訊息佇列中onLongClickListener 發送的延時訊息,那么當時間到達500ms時onLongClickListener 并不會得到執行,

public boolean performLongClick(float x, float y) {
        mLongClickX = x;
        mLongClickY = y;
        final boolean handled = performLongClick();
        mLongClickX = Float.NaN;
        mLongClickY = Float.NaN;
        return handled;
}

public boolean performLongClick() {
        return performLongClickInternal(mLongClickX, mLongClickY);
}

 private boolean performLongClickInternal(float x, float y) {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

        boolean handled = false;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLongClickListener != null) {
        	//1
            handled = li.mOnLongClickListener.onLongClick(View.this);
        }
        ...
        return handled;
}

查看View 的onTouchEvent方法中case : ACTION_UP的情況: 確實有點擊時發送的延時訊息進行了移除,
若沒有進行移除,表明點擊的時間大于了500ms則會先檢查有沒有長按事件的監聽,再執行onLongClick,

if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
    // This is a tap, so remove the longpress check
    removeLongPressCallback(); //移除長按的callback

    if (!focusTaken) {

        if (mPerformClick == null) {
            mPerformClick = new PerformClick();
        }
        if (!post(mPerformClick)) {
            performClick();
        }
    }
}

onClickListener

在View的onTouchEvent中的case ACTION_UP中有對onClickListener的處理,具體分析見原始碼中的注釋

View.java
public boolean onTouchEvent(MotionEvent event) {

	...
	
 	case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {   
                              //在這里可以看到new 出了一個PerformClick,在后面的中又通過post將他post到handler
                              //中,所以想都不用想 PerformClick繼承自Runable 實作了run方法
                                    mPerformClick = new PerformClick();
                                }
                                // 在這里通過無延遲的post的方法將PerformClick psot到主執行緒處理
                                if (!post(mPerformClick)) {
                              
                                    performClick();
                                }
                            }
                        }
                        ...
                    break;
           ...

進入到 PerformClick類中瞧瞧

   private final class PerformClick implements Runnable {
        @Override
        public void run() {
            recordGestureClassification(TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP);
            performClickInternal();  //在run方法里面執行了 performClickInternal
        }
    }
    private boolean performClickInternal() {
        // Must notify autofill manager before performing the click actions to avoid scenarios where
        // the app has a click listener that changes the state of views the autofill service might
        // be interested on.
        notifyAutofillManagerOnClick();
		//performClickInternal中又呼叫了 performClick 
        return performClick();
    }

在performClick中我們可以清晰的看到對OnClickListener 進行了處理

  public boolean performClick() {
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();

        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            // 在這里呼叫了onClick事件
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

回到在View的onTouchEvent中的case ACTION_UP的原始碼(如下):細心的人可能已經發現了,通過上面的 在 注意這里 1 post訊息 中最后還是呼叫 performClick來呼叫onClick的處理,在 注意這里 2 中當 post不成功時,直接呼叫performClick 對消進行處理,那么為什不能在上面直接呼叫performClick進行處理,非要大費周章的使用handler機制來處理呢??哈哈在上面的英文注釋中已經說明了很清楚了:Use a Runnable and post this rather than calling
performClick directly. This lets other visual stateof the view update before click actions start.說白了 就是怕這個performClick處理的 太久影響了view視圖的更新,

View.java
public boolean onTouchEvent(MotionEvent event) {

	...
	
 	case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {   
                              //在這里可以看到new 出了一個PerformClick,在后面的中又通過post將他post到handler
                              //中,所以想都不用想 PerformClick繼承自Runable 實作了run方法
                                    mPerformClick = new PerformClick();
                                }
                                // 在這里通過無延遲的post的方法將PerformClick psot到主執行緒處理
                                if (!post(mPerformClick)) {  注意這里 1
                              
                                    performClick();     注意這里 2 
                                }
                            }
                        }
                        ...
                    break;
           ...

簡單總結一下 onLongClickListener 和 onClickListener的處理程序:當ACTION_DOWN到達一個view后,他會想通過handler post一個500ms訊息(我稱之為長按訊息)給主執行緒,這個訊息里面封裝著LongClickListener的處理任務,如果在500ms之內收到了ACTION_UP事件,則首先會移除掉主執行緒訊息佇列中的長按訊息,然后去執行onClickListener對Click的執行邏輯,因為主執行緒中長按訊息已經被移除,所以500ms后不會執行LongClick的任務,

結語

android的事件分發流程分析到這里就結束了,因為時間和我自己技識訓是很淺薄的原因,這篇文章里難免會有些錯誤的地方,我歡迎大家給我指出來,我們共同進步

同時寫這個博客的時候參考很多優秀的博客和書籍,在這里向各位作者表示感謝,我也只是站在前人的肩膀上,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/402769.html

標籤:其他

上一篇:Android- fragment結合ViewPager實作左右滑動

下一篇:react-native仿京東首頁搜索欄聯動效果

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more