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

Android事件分發機制小結

2020-12-10 12:15:16 移動端開發

文章目錄

  • 基本知識
    • 事件傳遞的三個主體
    • 事件分發機制相關三個經典函式
    • 事件分發機制四個經典事件
  • 事件分發機制場景
    • 不攔截、不消費
    • ViewGroup攔截、無消費
    • ViewGoup消費,不攔截
  • 原始碼分析
    • Activity的dispatchTouchEvent()原始碼
    • ViewGroup的dispatchTouchEvent()原始碼
        • 總體邏輯分析
        • 具體分析一
        • 具體分析二
    • View的dispatchTouchEvent()原始碼
  • 總結


基本知識

事件傳遞的三個主體

Activity、ViewGroup、View
他們三個的嵌套關系一般是這樣:

但是還要明白的是:

  • ViewGroup當然可以嵌套ViewGroup,即ViewGroup也可以是另一個ViewGroup的子View,
  • ViewGroup其實是繼承于View,是View的子類,

事件分發機制相關三個經典函式

  • dispatchTouchEvent():分發函式
  • onInterceptTouchEvent():攔截函式
  • onTouchEvent():消費函式

它們的功能和它們名字一樣,其中攔截函式是ViewGroup獨有的,其它兩個函式在上面說的三個主體都存在,

事件分發機制四個經典事件

  • ACTION_DOWN
  • ACTION_MOVE
  • ACTION_UP
  • ACTION_CANCLE

意如其名

事件分發機制場景

參考文章:https://blog.csdn.net/caifengyao/article/details/65437695?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param

不攔截、不消費

當三個主體的任何函式的 回傳值 都不做任何處理時,即不攔截、不消費:


可見:

  • 對于down事件:我會從外層一層層地分發下去(Activity->ViewGroup->view),看看
  • down不消費,move,up我就不傳遞去了

ViewGroup攔截、無消費

當在ViewGroup使onInterceptTouchEvent()回傳true,即ViewGroup對事件進行攔截時:


可見:

  • 事件被攔截之后就不會往下分發

ViewGoup消費,不攔截

當在ViewGroup使onTouchEvent()回傳true,即ViewGroup對事件進行消費時:


可見:

  • 當down被消費了就不會往上冒
  • move up不會往下發,而是直接分發給消費者,

原始碼分析

具體代碼怎么實作?主要是看分發函式dispatchTouchEvent(),接下來我們看看 三個主體的dispatchTouchEvent() 原始碼分析

Activity的dispatchTouchEvent()原始碼

public boolean dispatchTouchEvent(MotionEvent ev) {
	if (ev.getAction() == MotionEvent.ACTION_DOWN) {
	    onUserInteraction();//這是一個空方法
	}
	//主要看這一句
	//getWindow().superDispatchTouchEvent(ev)
	//這句函式呼叫的時DecorView的superDispatchTouchEvent()
	//而DecorView繼承于ViewGroup
	if (getWindow().superDispatchTouchEvent(ev)) {
	    return true;
	}
	return onTouchEvent(ev);
}

代碼中我寫了注釋,我們可以得出下面的結論:

  • getWindow().superDispatchTouchEvent(ev),實際是呼叫了一個ViewGroup的dispatchTouchEvent()
  • getWindow().superDispatchTouchEvent(ev)回傳true,說明有子View消費該事件(為什么呢?我們要分析完ViewGroup的dispatchTouchEvent()才知道,但現在可以暫時給出這個結論);這個子View可能是某個ViewGroup或者View,
  • 如果有子view消費該事件則回傳true,否則呼叫自身的onTouchEvent(ev),即把事件分發給自己,

ViewGroup的dispatchTouchEvent()原始碼

ViewGroup的dispatchTouchEvent()原始碼很長,我參考了https://blog.csdn.net/wolinxuebin/article/details/53057075
之后得出一些小結,現在貼出一部分,一段一段分析,

總體邏輯分析

我把部分代碼和具體邏輯去掉,看它的空架子

//這是一個單鏈表,我暫時理解為用于存放回應了DOWN的事件
private TouchTarget mFirstTouchTarget;

public boolean dispatchTouchEvent(MotionEvent ev) {
	...
	//判斷是否是模糊視窗,如果是視窗,則表示不希望處理改事件,(如dialog后的視窗)
	if (onFilterTouchEventForSecurity(ev)) {
	    // 清空之前的狀態
	    if (actionMasked == MotionEvent.ACTION_DOWN) {}
	
	    //檢查是否需要攔截
	    final boolean intercepted;
	    if (actionMasked == MotionEvent.ACTION_DOWN
	            || mFirstTouchTarget != null) {} else {}
	            
	    //檢查是否要取消,即標記了PFLAG_CANCEL_NEXT_UP_EVENT 或者 當前是一個Cancel事件
	    final boolean canceled = resetCancelNextUpFlag(this)
	            || actionMasked == MotionEvent.ACTION_CANCEL;
	
	    //不用取消,無需攔截,則進行事件分發        
	    if (!canceled && !intercepted) {
	       //分發DOWN事件
	        if (actionMasked == MotionEvent.ACTION_DOWN
	                || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
	                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
	           //分發DOWN給child
	            if (newTouchTarget == null && childrenCount != 0) { 
	                
	            }
	            
	            //如果沒有child相應該事件,則將此事件交給最近加入的target?
	            //這里不是很懂,如果沒有child回應,那么mFirstTouchTarget也是null呀
	            if (newTouchTarget == null && mFirstTouchTarget != null) {
	               
	            }
	        }
	    }
	
	    //mFirstTouchTarget為空表明沒有child回應這個事件,則分發給自己
	    if (mFirstTouchTarget == null) {}
	    //按照mFirstTouchTarget分發
	    else {}
   
    }
	...
	//如果自身或者child消費了事件則回傳true,否則回傳false
	return handled;    
}

關于決議看代碼中的注釋,接下來看看其中幾段邏輯具體怎么實作的,

具體分析一

看一下分發給DOWN給子View的具體邏輯:

//分發DOWN給child
if (newTouchTarget == null && childrenCount != 0) { 
	
	// 對子Views進行排序,有兩種方式:1、按照Z軸,2、按照draw
	final ArrayList<View> preorderedList = buildTouchDispatchChildList();
	final boolean customOrder = preorderedList == null
	        && isChildrenDrawingOrderEnabled();
	final View[] children = mChildren;

	//遍歷子View
	for (int i = childrenCount - 1; i >= 0; i--) {
		//這里兩行代碼,簡單的理解根據不同的排列選項(1、view添加到 2、view的draw順序 3、viewZ 軸順序)
	    final int childIndex = getAndVerifyPreorderedIndex(
	            childrenCount, i, customOrder);
	    final View child = getAndVerifyPreorderedView(
	            preorderedList, children, childIndex);
	            
		//canViewReceivePointerEvents 判斷child是否為visiable 或者 是否有影片
        //isTransformedTouchPointInView 判斷x, y是否在view的區域內(如果是執行了補間影片 則x,y會通過獲取的matrix變換值
        // 換算當相應的區域,這也是為什么補間影片的觸發區域不隨著影片而改變)
	    if (!child.canReceivePointerEvents()
	            || !isTransformedTouchPointInView(x, y, child, null)) {
	        continue;
	    }
	    
		//如果chile已經在mFirstTouchTarget單鏈表里面,結束回圈
	    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.
	        newTouchTarget.pointerIdBits |= idBitsToAssign;
	        break;
	    }
	
		//判斷child的dispatchTouchEvent()是否會回傳true,如果是true,將child加入單鏈表,然后結束回圈
		//dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)會呼叫child的dispatchTouchEvent()
	    resetCancelNextUpFlag(child);
	    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
	        // Child wants to receive touch within its bounds.
	        mLastTouchDownTime = ev.getDownTime();
	        if (preorderedList != null) {
	            // 找到childIndex所代表的child的最原始的index
	            for (int j = 0; j < childrenCount; j++) {
	                if (children[childIndex] == mChildren[j]) {
	                    mLastTouchDownIndex = j;
	                    break;
	                }
	            }
	        } else {
	            mLastTouchDownIndex = childIndex;
	        }
			
			//將相應該事件的child包裝成一個Target,添加到mFirstTouchTarget鏈表中
	        mLastTouchDownX = ev.getX();
	        mLastTouchDownY = ev.getY();
	        newTouchTarget = addTouchTarget(child, idBitsToAssign);
	        alreadyDispatchedToNewTouchTarget = true;
	        break;
	    }
	}
}

這里我只貼出部分的代碼,決議都在注釋,可以看到,mFirstTouchTarget鏈表只存在一個值,就是回應了事件的那個child,

具體分析二

//這一段的要么分發down給自己要么按照單鏈表分發move、up
//偽代碼
// 
if (mFirstTouchTarget == null) {
    //沒有child回應事件,則分發給自己,handled將作為回傳值回傳,
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
} else {
    // 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) {
        final TouchTarget next = target.next;
        //在前面Down事件處理中,已經將這個事件交給newTouchTarget處理過了,就不重復處理了
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            handled = true;
        } else {
        	//這里其實是分發move和up事件,因為down事件在前面已經處理完了,不會進入這里
        	//再次判定是否需要cancel,因為有可能在move程序事件被攔截
            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                    || intercepted;
            if (dispatchTransformedTouchEvent(ev, cancelChild,
                    target.child, target.pointerIdBits)) {
                handled = true;
            }
            //如果cancel,回收鏈表節點空間,最后使mFirstTouchTarget置null
            if (cancelChild) {
                if (predecessor == null) {
                    mFirstTouchTarget = next;
                } else {
                    predecessor.next = next;
                }
                target.recycle();
                target = next;
                continue;
            }
        }
        predecessor = target;
        target = next;
    }
}

View的dispatchTouchEvent()原始碼

同樣的,我省略了部分代碼,決議看注釋

public boolean dispatchTouchEvent(MotionEvent event) {
	boolean result = false;
	
	if (onFilterTouchEventForSecurity(event)) {
	    //如果設定了onTouchListener,會先呼叫onTouch()
	    ListenerInfo li = mListenerInfo;
	    if (li != null && li.mOnTouchListener != null
	            && (mViewFlags & ENABLED_MASK) == ENABLED
	            && li.mOnTouchListener.onTouch(this, event)) {
	        result = true;
	    }
	
		//呼叫onTouchEvent()
	    if (!result && onTouchEvent(event)) {
	        result = true;
	    }
	}
	return result;
}

可以看到:

  • View的dispatchTouchEvent()的回傳值取決于onTouch()或onTouchEvent(),也就是有沒有消費該事件,
  • 如果onTouch()回傳true的話就不會呼叫onTouchEvent(),這就是為什么有些博客寫onTouch()優先于onTouchEvent(),

總結

分析原始碼就可以知道前面說的三種場景是怎么回事了,

有點像皇帝派任務,皇帝說現在有個好活兒,但是不知道誰要接這個活兒,于是派了一個小太監去探測一下;
小太監先去找宰相,宰相又讓他去找知府,知府讓他去找衙門小兵,

這里面皇帝就像Acticity、各級官員就像ViewGroup、小兵就像View、而小太監就像DOWN事件,活兒就是跟著DOWN后面的MOVE和UP事件,

  • 不攔截、不消費:小太監一層層找到小兵后,沒有一個小兵想接這個活兒(一層層分發DOWN事件),于是小兵沿路回傳報告給知府、知府也不想做就報告給宰相,宰相不想做就回去報告給皇帝,皇帝說沒人做那我看看自己能不能做吧(DOWN事件回到Activity派給自己,MOVE、UP也不再分發而是直接派給自己)
  • ViewGroup攔截、不消費:宰相讓小太監找到知府的時候, 這個知府有點霸道直接把小太監攔下了,小太監就就不再繼續通知下級人員了(攔截事件),但是呢這個官員只是單純攔下了小太監但他并不想接這個活兒,于是小兵還是沿路回去報告給說下面沒人接活,最侄訓是傳回給皇帝說沒人做那我看看自己能不能做(MOVE、UP不再分發直接派給自己)
  • ViewGroup消費、不攔截:同樣,無人攔截的話,小太監一層層找到小兵,發現小兵沒人想做,就回去報告知府,這時候知府說小兵不做我來做(DOWM在這里被消費了),然后知府寫信報告宰相說這活兒我接了(回傳true),宰相又報告皇帝說下面有人接受任務了,然會皇帝下次就直接派發任務給宰相,宰相找到那個愿意接受任務知府,把任務派給他,(派發MOVE、UP)

分發函式分發DOWN時其實有點類似于遞回的方式,只不過不是自己呼叫自己,而是一層層地呼叫child的同名函式,分發MOVE、UP則不再一一詢問,而是根據DOWN是否被消費進行分發,

原始碼很長現在頭都有點亂,那么從原始碼可以學習到什么呢,大家幫忙補充吧

  • 如有有這種嵌套式的應答需求,可以學習ViewGroup的分發函式,用類似于遞回的方式提問和接收應答,
  • 對于較大量的資訊,命令傳送,可以先派一個小兵嗅探一下,記錄可行的路線,后續資訊按路線分發,

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

標籤:其他

上一篇:一文看懂h5+app拍照上傳圖片

下一篇:mac安裝homebrew,xcode報錯

標籤雲
其他(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