主頁 > 移動端開發 > Android10 InputManagerService事件輸入輸出

Android10 InputManagerService事件輸入輸出

2021-10-22 08:17:43 移動端開發

InputManagerService管理著螢屏點擊以及硬體按鈕事件的輸入輸出,InputManagerService的實作是在native代碼中,想要對事件進行處理,那就一定要通過InputManagerService進行注冊或是監聽,如果A應用要想獲取到輸入事件,那要怎么和InputManagerService連接起來呢?答案是Socket,其中InputChannel就是對其進行封裝,InputChannel的實作同樣是native代碼,實作的類是NativeInputChannel,類路徑:frameworks/base/core/jni/android_view_InputChannel.cpp,這里就直接看下InputChannel的openInputChannelPair():

    /**
     * 創建一個新的輸入通道對, 一個通道提供給輸入調度程式,另一個通道提供給應用程式的輸入列,  
     * @param name通道對的描述性(非唯一)名稱,  
     * @return 一對輸入通道, 第一個通道被指定為服務器通道,應該用于發布輸入事件, 第二個通道被指                
     * 定為客戶端通道,用于使用輸入事件,  
     */
    public static InputChannel[] openInputChannelPair(String name) {
        if (name == null) {
            throw new IllegalArgumentException("name must not be null");
        }

        if (DEBUG) {
            Slog.d(TAG, "Opening input channel pair '" + name + "'");
        }
        return nativeOpenInputChannelPair(name);
    }

    private static native InputChannel[] nativeOpenInputChannelPair(String name);

openInputChannelPair()會創建一對輸入通道,一端用于服務器通道,也就是InputManagerService;一端用于客戶端,也就是應用這一端,對InputChannel有個初步的了解后,接下來就看主角com.android.server.input.InputManagerService:

public class InputManagerService extends IInputManager.Stub
        implements Watchdog.Monitor {

    // Pointer to native input manager service object.
    private final long mPtr;
    //所有輸入事件在分發前,會優先派發到這個回呼中處理
    private WindowManagerCallbacks mWindowManagerCallbacks;
    //內部會初始化兩個執行緒,一個用于讀取底層的輸入事件,一個用于將事件派發到應用層
    private static native long nativeInit(InputManagerService service,
            Context context, MessageQueue messageQueue);
    //前面說InputChannel時會創建一對輸入通過對,這里就是將其中的一個注冊到底層,也就是所說的服務端
    //InputChannel只會收到注冊頁面的輸入事件
    private static native void nativeRegisterInputChannel(long ptr, InputChannel inputChannel,
            int displayId);
    //解注冊頁面注冊的輸入事件
    private static native void nativeUnregisterInputChannel(long ptr, InputChannel inputChannel);
    //這個方法是添加監聽,InputChannel會收到所有的輸入事件,
    //其中isGestureMonitor表示收到的事件是否是手勢,系統的全域手勢就是通過這個方法注冊的
    private static native void nativeRegisterInputMonitor(long ptr, InputChannel inputChannel,
            int displayId, boolean isGestureMonitor);
    //注入模擬螢屏的點擊事件,比如實作全域的回傳按鈕
    private static native int nativeInjectInputEvent(long ptr, InputEvent event,
            int injectorPid, int injectorUid, int syncMode, int timeoutMillis,
            int policyFlags);

    public InputManagerService(Context context) {
        ... ...
        mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());
        ... ...
    }

    //這個回呼是在SystemServer中設定的,其實作是InputManagerCallback,最侄訓呼叫到PhoneWindowManager
    public void setWindowManagerCallbacks(WindowManagerCallbacks callbacks) {
        mWindowManagerCallbacks = callbacks;
    }

    /**
     * Creates an input channel that will receive all input from the input dispatcher.
     * @param inputChannelName The input channel name.
     * @param displayId Target display id.
     * @return The input channel.
     */
    public InputChannel monitorInput(String inputChannelName, int displayId) {
        if (inputChannelName == null) {
            throw new IllegalArgumentException("inputChannelName must not be null.");
        }

        if (displayId < Display.DEFAULT_DISPLAY) {
            throw new IllegalArgumentException("displayId must >= 0.");
        }

        InputChannel[] inputChannels = InputChannel.openInputChannelPair(inputChannelName);
        // Give the output channel a token just for identity purposes.
        inputChannels[0].setToken(new Binder());
        nativeRegisterInputMonitor(mPtr, inputChannels[0], displayId, false /*isGestureMonitor*/);
        inputChannels[0].dispose(); // don't need to retain the Java object reference
        return inputChannels[1];
    }

    /**
     * Creates an input monitor that will receive pointer events for the purposes of system-wide
     * gesture interpretation.
     *
     * @param inputChannelName The input channel name.
     * @param displayId Target display id.
     * @return The input channel.
     */
    @Override // Binder call
    public InputMonitor monitorGestureInput(String inputChannelName, int displayId) {
        if (!checkCallingPermission(android.Manifest.permission.MONITOR_INPUT,
                "monitorInputRegion()")) {
            throw new SecurityException("Requires MONITOR_INPUT permission");
        }

        Objects.requireNonNull(inputChannelName, "inputChannelName must not be null.");

        if (displayId < Display.DEFAULT_DISPLAY) {
            throw new IllegalArgumentException("displayId must >= 0.");
        }


        final long ident = Binder.clearCallingIdentity();
        try {
            InputChannel[] inputChannels = InputChannel.openInputChannelPair(inputChannelName);
            InputMonitorHost host = new InputMonitorHost(inputChannels[0]);
            inputChannels[0].setToken(host.asBinder());
            nativeRegisterInputMonitor(mPtr, inputChannels[0], displayId,
                    true /*isGestureMonitor*/);
            return new InputMonitor(inputChannelName, inputChannels[1], host);
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }

    /**
     * Registers an input channel so that it can be used as an input event target.
     * @param inputChannel The input channel to register.
     * @param inputWindowHandle The handle of the input window associated with the
     * input channel, or null if none.
     */
    public void registerInputChannel(InputChannel inputChannel, IBinder token) {
        if (inputChannel == null) {
            throw new IllegalArgumentException("inputChannel must not be null.");
        }

        if (token == null) {
            token = new Binder();
        }
        inputChannel.setToken(token);

        nativeRegisterInputChannel(mPtr, inputChannel, Display.INVALID_DISPLAY);
    }

    /**
     * Unregisters an input channel.
     * @param inputChannel The input channel to unregister.
     */
    public void unregisterInputChannel(InputChannel inputChannel) {
        if (inputChannel == null) {
            throw new IllegalArgumentException("inputChannel must not be null.");
        }

        nativeUnregisterInputChannel(mPtr, inputChannel);
    }

    @Override // Binder call
    public boolean injectInputEvent(InputEvent event, int mode) {
        return injectInputEventInternal(event, mode);
    }

    private boolean injectInputEventInternal(InputEvent event, int mode) {
        ... ...
        final int pid = Binder.getCallingPid();
        final int uid = Binder.getCallingUid();
        final long ident = Binder.clearCallingIdentity();
        final int result;
        try {
            result = nativeInjectInputEvent(mPtr, event, pid, uid, mode,
                    INJECTION_TIMEOUT_MILLIS, WindowManagerPolicy.FLAG_DISABLE_KEY_REPEAT);
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
        ... ...
    }

    // Native callback.
    // 有輸入事件時,這里會最先呼叫到
    private int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
        return mWindowManagerCallbacks.interceptKeyBeforeQueueing(event, policyFlags);
    }

    // Native callback.
    // 如果interceptKeyBeforeQueueing沒有處理,在事件分發給應用端前會呼叫到
    private long interceptKeyBeforeDispatching(IBinder focus, KeyEvent event, int policyFlags) {
        return mWindowManagerCallbacks.interceptKeyBeforeDispatching(focus, event, policyFlags);
    }

}

這里先把InputManagerService的底層實作看成一個沙盒,下一篇文章再聊,這里先來聊聊上面列出的這些方法,類中有一個型別是WindowManagerCallbacks的mWindowManagerCallbacks成員變數,其實作類是InputManagerCallback,但最終呼叫到的是PhoneWindowManager,事件在分發給應用前,會分別呼叫interceptKeyBeforeQueueing()和interceptKeyBeforeDispatching(),這也就是說PhoneWindowManager是最先處理輸入事件的地方,

這里先說個小插曲,interceptKeyBeforeQueueing()這個方法為什么是這樣命名的,最開始看到這個名字還是挺疑惑的,后面看了底層代碼才明白,InputManagerService的底層實作開啟了兩個執行緒,一個用于讀輸入事件(讀執行緒),一個用于將讀取的到事件分發(分發執行緒),讀執行緒如何將事件傳遞到分發執行緒呢,這里就會涉及到佇列(Queue),所以現在看到這個interceptKeyBeforeQueueing()是不是就很明白了,

對于上面其他的方法,先來看下方法的命名,monitorXXX()、registerXXX()、unregisterXXX()、injectXXX(),可以分為三類:

1、帶有monitorXXX()的方法,表示只要有輸入事件就會接收到;

2、injectXXX()就是模擬按鍵發送事件了;

3、registerXXX()和unRegisterXXX()這是成對出現的,這對方法有什么作用呢?一個頁面要想接收到輸入事件,那就必須呼叫registerXXX()進行對接收事件的注冊,頁面銷毀了就呼叫解注冊;

下面就來看下android原始碼中對這些方法的使用:

1、frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java,這是對系統手勢處理的類,來看下它內部的使用:

    private void updateIsEnabled() {
        ... ...
        // Register input event receiver
        mInputMonitor = InputManager.getInstance().monitorGestureInput("edge-swipe", mDisplayId);
        mInputEventReceiver = new InputChannelCompat.InputEventReceiver(mInputMonitor.getInputChannel(), 
Looper.getMainLooper(),Choreographer.getInstance(), this::onInputEvent);
        ... ...
    }

呼叫monitor監聽后,當有輸入事件輸入,就會呼叫到這里的onInputEvent()方法,

2、對于injectInputEvent()方法,在InputManager中有@hide標識,也就是只能在系統中使用,那現在要怎么才能使用呢?在上一篇文章Android10 AppComponentFactory原始碼梳理有提到android.app.Instrumentation這個類,來看下它是怎么使用的:

    //需要傳入的是KeyEvent中Keycode的常量,比如:KeyEvent.KEYCODE_BACK就是執行回傳按鍵的功能
    public void sendKeyDownUpSync(int key) {        
        sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, key));
        sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, key));
    }

    public void sendKeySync(KeyEvent event) {
        validateNotAppThread();

        long downTime = event.getDownTime();
        long eventTime = event.getEventTime();
        int source = event.getSource();
        if (source == InputDevice.SOURCE_UNKNOWN) {
            source = InputDevice.SOURCE_KEYBOARD;
        }
        if (eventTime == 0) {
            eventTime = SystemClock.uptimeMillis();
        }
        if (downTime == 0) {
            downTime = eventTime;
        }
        KeyEvent newEvent = new KeyEvent(event);
        newEvent.setTime(downTime, eventTime);
        newEvent.setSource(source);
        newEvent.setFlags(event.getFlags() | KeyEvent.FLAG_FROM_SYSTEM);
        InputManager.getInstance().injectInputEvent(newEvent,
                InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

所以,要想實作按鍵功能就可以通過上面的方法,

3、下面重點來看下registerXX(),這個對于每個顯示的頁面都會呼叫到,只是在frameworks層呼叫了,對于應用層來說無感而已,應用層的所有的處理流程都是從ViewRootImpl開始的,當界面顯示時,會呼叫ViewRootImpl.setView():

    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        ... ...
        if ((mWindowAttributes.inputFeatures
                        & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
            mInputChannel = new InputChannel();
        }
        ... ...
        res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
                            mTempInsets);
        ... ...
        mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
                            Looper.myLooper());
        ... ...

    }

先是new了一個InputChannel,但是并沒有去創建通道對,也就是說這個InputChannel還沒有初始化,這里的mWindowSession是通過WindowManagerService.openSession()回傳,其實作是Session類,呼叫它的addToDisplay()最終呼叫到的是WindowManagerService.addWindow():

WindowManagerService
    public int addWindow(Session session, IWindow client, int seq,
            LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,
            Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
            DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
            InsetsState outInsetsState) {
        ... ...
        final WindowState win = new WindowState(this, session, client, token, parentWindow,
                    appOp[0], seq, attrs, viewVisibility, session.mUid,
                    session.mCanAddInternalSystemWindow);
        ... ...
        win.openInputChannel(outInputChannel);
        ... ...

    }

WindowState
    void openInputChannel(InputChannel outInputChannel) {
        if (mInputChannel != null) {
            throw new IllegalStateException("Window already has an input channel.");
        }
        String name = getName();
        InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
        mInputChannel = inputChannels[0];
        mClientChannel = inputChannels[1];
        mInputWindowHandle.token = mClient.asBinder();
        if (outInputChannel != null) {
            //初始化應用端的InputChannel,實際就是初始化mPtr,這個變數通過native代碼可以轉化成指標
            mClientChannel.transferTo(outInputChannel);
            mClientChannel.dispose();
            mClientChannel = null;
        } else {
            // If the window died visible, we setup a dummy input channel, so that taps
            // can still detected by input monitor channel, and we can relaunch the app.
            // Create dummy event receiver that simply reports all events as handled.
            mDeadWindowEventReceiver = new DeadWindowEventReceiver(mClientChannel);
        }
        mWmService.mInputManager.registerInputChannel(mInputChannel, mClient.asBinder());
    }

這里通過InputChannel創建了一對通道對inputChannels[0]和inputChannels[1],然后將inputChannels[1]和應用端的InputChannel關聯起來,將inputChannels[0]和服務端(事件原始分發)關聯起來,這樣就將原始輸入事件和應用界面關聯起來了,再回到ViewRootImpl.setView(),之后還創建了一個WindowInputEventReceiver物件,這一看就是接受輸入事件的了,WindowInputEventReceiver基礎自InputEventReceiver,這里主要來看下InputEventReceiver:

public abstract class InputEventReceiver {

    private long mReceiverPtr;

    // We keep references to the input channel and message queue objects here so that
    // they are not GC'd while the native peer of the receiver is using them.
    private InputChannel mInputChannel;
    private MessageQueue mMessageQueue;

    private final SparseIntArray mSeqMap = new SparseIntArray();

    private static native long nativeInit(WeakReference<InputEventReceiver> receiver,
            InputChannel inputChannel, MessageQueue messageQueue);

    public InputEventReceiver(InputChannel inputChannel, Looper looper) {
        ... ...
        mInputChannel = inputChannel;
        mMessageQueue = looper.getQueue();
        mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),
                inputChannel, mMessageQueue);
    }

    // Called from native code.
    @SuppressWarnings("unused")
    @UnsupportedAppUsage
    private void dispatchInputEvent(int seq, InputEvent event) {
        mSeqMap.put(event.getSequenceNumber(), seq);
        onInputEvent(event);
    }

    @UnsupportedAppUsage
    public void onInputEvent(InputEvent event) {
        finishInputEvent(event, false);
    }

    public final void finishInputEvent(InputEvent event, boolean handled) {
        if (event == null) {
            throw new IllegalArgumentException("event must not be null");
        }
        if (mReceiverPtr == 0) {
            Log.w(TAG, "Attempted to finish an input event but the input event "
                    + "receiver has already been disposed.");
        } else {
            int index = mSeqMap.indexOfKey(event.getSequenceNumber());
            if (index < 0) {
                Log.w(TAG, "Attempted to finish an input event that is not in progress.");
            } else {
                int seq = mSeqMap.valueAt(index);
                mSeqMap.removeAt(index);
                nativeFinishInputEvent(mReceiverPtr, seq, handled);
            }
        }
        event.recycleIfNeededAfterDispatch();
    }
}

主要的實作也是native代碼,這里就不下看了,感興趣的可以自行查看c++的實作類NativeInputEventReceiver,c++層代碼處理完成后會呼叫這里的dispatchInputEvent()方法,轉而呼叫onInputEvent(),在往回看下WindowInputEventReceiver的實作:

    final class WindowInputEventReceiver extends InputEventReceiver {
        public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
            super(inputChannel, looper);
        }

        @Override
        public void onInputEvent(InputEvent event) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "processInputEventForCompatibility");
            List<InputEvent> processedEvents;
            try {
                processedEvents =
                    mInputCompatProcessor.processInputEventForCompatibility(event);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
            if (processedEvents != null) {
                if (processedEvents.isEmpty()) {
                    // InputEvent consumed by mInputCompatProcessor
                    finishInputEvent(event, true);
                } else {
                    for (int i = 0; i < processedEvents.size(); i++) {
                        enqueueInputEvent(
                                processedEvents.get(i), this,
                                QueuedInputEvent.FLAG_MODIFIED_FOR_COMPATIBILITY, true);
                    }
                }
            } else {
                //正常是會執行到這里
                enqueueInputEvent(event, this, 0, true);
            }
        }

    }

這樣一個流程下來,就通過InputChannel把事件傳遞到了應用界面,接下去就是通過enqueueInputEvent()傳遞給view進行處理了,在事件處理完成后,會呼叫到InputEventReceiver.finishInputEvent(),通知服務端事件處理完成,這樣一次事件的分發就算是完成了,這里就不在往下看了,

這里在總結下

InputManagerService可以理解為通往底層輸入事件的一個大門,要想獲得事件的處理,可以通過以下幾種方式:

1、向InputManagerService中設定回呼,在SystemServer中有設定,最終在PhoneWindowManager中實作事件處理;

2、通過InputManagerService發送模擬按鍵事件,比如回傳鍵,可以通過Instrumentation;

3、通過InputManagerService注冊或者添加監聽,注冊一般用于普通應用,每個啟動的頁面都有注冊,添加監聽一般用于系統應用,比如系統手勢,就是在SystemUi中實作的;

下一篇文章再聊聊InputWindowManagerService的底層實作,這篇就到這了,

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

標籤:其他

上一篇:自定義用戶代碼片之(移動端viewport理想視口)

下一篇:2021最新 李宇軒羽毛球課程很值得購買嗎?

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