תמיכה בבקרים בכל גרסאות Android

אם בחרת לתמוך בבקרי משחקים במשחק שלך, האחריות לכך היא שלך כדי לוודא שהמשחק יגיב לבקרים באופן עקבי בכל המכשירים שפועלת בגרסאות שונות של Android. כך המשחק שלך יוצג לקהל רחב יותר הקהל, והשחקנים יכולים ליהנות מחוויית משחק חלקה את הבקרים שלהם גם כשהם מחליפים או משדרגים את מכשירי Android.

השיעור הזה מדגים איך להשתמש בממשקי API שזמינים ב-Android 4.1 ואילך באופן שתואם לאחור, מה שיאפשר למשחק לתמוך תכונות במכשירים שבהם פועלת מערכת Android 3.1 ואילך:

  • המשחק יכול לזהות אם הוספה, שינוי או הסרה של בקר משחקים חדש.
  • המשחק יכול לשלוח שאילתה לגבי היכולות של בקר המשחקים.
  • המשחק יכול לזהות אירועי תנועה נכנסים מבקר משחק.

הדוגמאות בשיעור הזה מבוססות על יישום קובצי עזר סופק על ידי קובץ ה-ControllerSample.zip לדוגמה שזמין להורדה למעלה. הדוגמה הזו מראה איך להטמיע את המאפיין InputManagerCompat ממשק שיתמוך בגרסאות שונות של Android. כדי להדר את הדוגמה, צריך חייבים להשתמש ב-Android מגרסה 4.1 (רמת API 16) ואילך. לאחר הידור, האפליקציה לדוגמה פועלת בכל מכשיר שבו פועלת גרסת Android 3.1 (רמת API 12) ומעלה בתור ה-build יעד.

הכנה לקראת שימוש מופשט בממשקי API לתמיכה בבקרי משחקים

נניח שרוצים לדעת אם החיבור של בקר משחקים הסטטוס השתנה במכשירים שפועלת בהם מערכת Android 3.1 (רמת API 12). אבל, לפעמים ממשקי ה-API זמינים רק ב-Android 4.1 (API ברמה 16) ואילך, כך צריכים לספק יישום שתומך ב-Android 4.1 ואילך, מתן מנגנון חלופי שתומך ב-Android 3.1 עד Android 4.0.

כדי לעזור לך לקבוע לאילו תכונות נדרש מנגנון חלופי כזה בגרסאות ישנות יותר, טבלה 1 מפרטת את ההבדלים בתמיכה בבקר המשחקים בין Android 3.1 (רמת API 12) לבין Android 4.1 (רמת API) 16).

טבלה 1. ממשקי API לתמיכה בבקר משחקים בכל גרסאות Android שונות.

פרטי הבקר API לבקרים רמת API 12 רמת API 16
זיהוי מכשיר getInputDeviceIds()  
getInputDevice()  
getVibrator()  
SOURCE_JOYSTICK
SOURCE_GAMEPAD
סטטוס החיבור onInputDeviceAdded()  
onInputDeviceChanged()  
onInputDeviceRemoved()  
זיהוי אירועי קלט לחצני החיצים (D-pad) ( KEYCODE_DPAD_UP, KEYCODE_DPAD_DOWN, KEYCODE_DPAD_LEFT, KEYCODE_DPAD_RIGHT, KEYCODE_DPAD_CENTER)
לחיצה על לחצן בקר המשחקים ( BUTTON_A, BUTTON_B, BUTTON_THUMBL, BUTTON_THUMBR, BUTTON_SELECT, BUTTON_START, BUTTON_R1, BUTTON_L1, BUTTON_R2, BUTTON_L2)
תנועת החלפת ג'ויסטיק וכובע ( AXIS_X, AXIS_Y, AXIS_Z, AXIS_RZ, AXIS_HAT_X, AXIS_HAT_Y)
לחיצה על טריגר אנלוגי ( AXIS_LTRIGGER, AXIS_RTRIGGER)

אפשר להשתמש בהפשטה כדי ליצור תמיכה בבקר משחקים עם בקרת גרסאות, פועלת בפלטפורמות שונות. בשיטה הזו צריך לבצע את השלבים הבאים:

  1. להגדיר ממשק Java מתווך שמפשט את היישום של את התכונות של בקר המשחק שנדרשות על ידי המשחק.
  2. יצירת הטמעת שרת proxy של הממשק המשתמש בממשקי API ב-Android 4.1 ואילך.
  3. ליצור הטמעה מותאמת אישית של הממשק שמשתמש בממשקי API זמינים בין Android 3.1 עד Android 4.0.
  4. יוצרים את הלוגיקה למעבר בין ההטמעות האלה בזמן הריצה, ולהתחיל להשתמש בממשק במשחק שלכם.

לסקירה כללית של האופן שבו ניתן להשתמש בהפשטה כדי להבטיח יכולות לפעול באופן של תאימות לאחור בגרסאות שונות של Android, יצירת ממשקי משתמש שתואמים לאחור.

הוספת ממשק לתאימות לאחור

כדי לספק תאימות לאחור, אפשר ליצור ממשק מותאם אישית להוסיף הטמעות ספציפיות לגרסה. אחד היתרונות של הגישה הזו הוא מאפשרת לשקף את הממשקים הציבוריים ב-Android 4.1 (רמת API 16) לתמוך בבקרי משחקים.

// The InputManagerCompat interface is a reference example.
// The full code is provided in the ControllerSample.zip sample.
interface InputManagerCompat {
    val inputDeviceIds: IntArray
    fun getInputDevice(id: Int): InputDevice

    fun registerInputDeviceListener(
            listener: InputManager.InputDeviceListener,
            handler: Handler?
    )

    fun unregisterInputDeviceListener(listener:InputManager.InputDeviceListener)

    fun onGenericMotionEvent(event: MotionEvent)

    fun onPause()
    fun onResume()

    interface InputDeviceListener {
        fun onInputDeviceAdded(deviceId: Int)
        fun onInputDeviceChanged(deviceId: Int)
        fun onInputDeviceRemoved(deviceId: Int)
    }
}
// The InputManagerCompat interface is a reference example.
// The full code is provided in the ControllerSample.zip sample.
public interface InputManagerCompat {
    ...
    public InputDevice getInputDevice(int id);
    public int[] getInputDeviceIds();

    public void registerInputDeviceListener(
            InputManagerCompat.InputDeviceListener listener,
            Handler handler);
    public void unregisterInputDeviceListener(
            InputManagerCompat.InputDeviceListener listener);

    public void onGenericMotionEvent(MotionEvent event);

    public void onPause();
    public void onResume();

    public interface InputDeviceListener {
        void onInputDeviceAdded(int deviceId);
        void onInputDeviceChanged(int deviceId);
        void onInputDeviceRemoved(int deviceId);
    }
    ...
}

בממשק InputManagerCompat מוצגות השיטות הבאות:

getInputDevice()
שיקוף של getInputDevice(). מקבל את InputDevice שמייצג את היכולות של בקר משחקים.
getInputDeviceIds()
שיקוף של getInputDeviceIds(). מחזירה מערך של מספרים שלמים, כל אחד שהוא מזהה של מכשיר קלט אחר. האפשרות הזאת שימושית משחק שתומך בכמה שחקנים ואתם רוצים לזהות כמה שחקנים הבקרים מחוברים.
registerInputDeviceListener()
שיקוף של registerInputDeviceListener(). מאפשר לכם להירשם כדי לקבל הודעה על המכשיר נוסף, השתנה או הוסר.
unregisterInputDeviceListener()
שיקוף של unregisterInputDeviceListener(). ביטול הרישום של אוזן המכשיר לקליטת נתונים.
onGenericMotionEvent()
שיקוף של onGenericMotionEvent(). המשחק שלך יכול ליירט ומטפל בך MotionEvent אובייקטים וערכי צירים שמייצגים אירועים למשל, תנועות בג'ויסטיק ולחיצות הדק אנלוגיות.
onPause()
מפסיק את הסקרים באירועים של בקר המשחקים כאשר הפעילות הראשית מושהית, או כשהמשחק כבר לא מתמקד.
onResume()
מתחיל סקרים לאירועים של בקר המשחקים כאשר הפעילות הראשית מתחילה, או כשהמשחק מתחיל ופועל חזית.
InputDeviceListener
שיקוף של InputManager.InputDeviceListener גרפי. מציין את המשחק כשמוסיפים או משנים בקר משחקים, או אם הוסר.

בשלב הבא צריך ליצור הטמעות של InputManagerCompat שעובדות בגרסאות פלטפורמה שונות. אם המשחק פועל על Android 4.1 או גבוהה יותר וקוראת ל-method InputManagerCompat, הטמעת שרת ה-proxy קוראת ל-method המקבילה ב-InputManager. עם זאת, אם המשחק פועל ב-Android מגרסה 3.1 עד 4.0, מעבדים קריאות ל-methods של InputManagerCompat באמצעות רק ממשקי API שנוספו ל-Android 3.1 ואילך. לא משנה איזה נעשה שימוש בהטמעה ספציפית לגרסה בזמן הריצה, ההטמעה עוברת תוצאות השיחה מתקבלות בצורה שקופה.

איור 1. תרשים מחלקה של ממשק וגרסה ספציפית בפועל.

הטמעת הממשק ב-Android 4.1 ואילך

InputManagerCompatV16 הוא יישום של ממשק InputManagerCompat ששולח שרת proxy ל-method InputManager ו-InputManager.InputDeviceListener בפועל. InputManager התקבל מהמערכת Context.

// The InputManagerCompatV16 class is a reference implementation.
// The full code is provided in the ControllerSample.zip sample.
public class InputManagerV16(
        context: Context,
        private val inputManager: InputManager =
            context.getSystemService(Context.INPUT_SERVICE) as InputManager,
        private val listeners:
            MutableMap<InputManager.InputDeviceListener, V16InputDeviceListener> = mutableMapOf()
) : InputManagerCompat {
    override val inputDeviceIds: IntArray = inputManager.inputDeviceIds

    override fun getInputDevice(id: Int): InputDevice = inputManager.getInputDevice(id)

    override fun registerInputDeviceListener(
            listener: InputManager.InputDeviceListener,
            handler: Handler?
    ) {
        V16InputDeviceListener(listener).also { v16listener ->
            inputManager.registerInputDeviceListener(v16listener, handler)
            listeners += listener to v16listener
        }
    }

    // Do the same for unregistering an input device listener
    ...

    override fun onGenericMotionEvent(event: MotionEvent) {
        // unused in V16
    }

    override fun onPause() {
        // unused in V16
    }

    override fun onResume() {
        // unused in V16
    }

}

class V16InputDeviceListener(
        private val idl: InputManager.InputDeviceListener
) : InputManager.InputDeviceListener {

    override fun onInputDeviceAdded(deviceId: Int) {
        idl.onInputDeviceAdded(deviceId)
    }
    // Do the same for device change and removal
    ...
}
// The InputManagerCompatV16 class is a reference implementation.
// The full code is provided in the ControllerSample.zip sample.
public class InputManagerV16 implements InputManagerCompat {

    private final InputManager inputManager;
    private final Map<InputManagerCompat.InputDeviceListener,
            V16InputDeviceListener> listeners;

    public InputManagerV16(Context context) {
        inputManager = (InputManager)
                context.getSystemService(Context.INPUT_SERVICE);
        listeners = new HashMap<InputManagerCompat.InputDeviceListener,
                V16InputDeviceListener>();
    }

    @Override
    public InputDevice getInputDevice(int id) {
        return inputManager.getInputDevice(id);
    }

    @Override
    public int[] getInputDeviceIds() {
        return inputManager.getInputDeviceIds();
    }

    static class V16InputDeviceListener implements
            InputManager.InputDeviceListener {
        final InputManagerCompat.InputDeviceListener mIDL;

        public V16InputDeviceListener(InputDeviceListener idl) {
            mIDL = idl;
        }

        @Override
        public void onInputDeviceAdded(int deviceId) {
            mIDL.onInputDeviceAdded(deviceId);
        }

        // Do the same for device change and removal
        ...
    }

    @Override
    public void registerInputDeviceListener(InputDeviceListener listener,
            Handler handler) {
        V16InputDeviceListener v16Listener = new
                V16InputDeviceListener(listener);
        inputManager.registerInputDeviceListener(v16Listener, handler);
        listeners.put(listener, v16Listener);
    }

    // Do the same for unregistering an input device listener
    ...

    @Override
    public void onGenericMotionEvent(MotionEvent event) {
        // unused in V16
    }

    @Override
    public void onPause() {
        // unused in V16
    }

    @Override
    public void onResume() {
        // unused in V16
    }

}

הטמעת הממשק ב-Android 3.1 עד Android 4.0

כדי ליצור יישום של InputManagerCompat שתומך ב-Android 3.1 עד גרסה 4.0, אפשר להשתמש ב- האובייקטים הבאים:

  • SparseArray של מזהי מכשירים לצורך מעקב בקרי משחקים שמחוברים למכשיר.
  • Handler לעיבוד אירועים במכשיר. כשמפעילים אפליקציה או המשיך, בHandler תתקבל הודעה להתחלת הסקר לניתוק של בקר משחקים. המכשיר Handler יתחיל כדי לבדוק כל בקר משחקים מחובר ידוע ולראות אם מזהה המכשיר הוחזרו. הערך המוחזר של null מציין שבקר המשחקים מנותק. ההצבעה בHandler מפסיקה כשהאפליקציה מושהה.
  • Map מתוך InputManagerCompat.InputDeviceListener אובייקטים. ניתן להשתמש ב-listens כדי לעדכן את סטטוס החיבור של המכשירים שבמעקב בקרי משחקים.
// The InputManagerCompatV9 class is a reference implementation.
// The full code is provided in the ControllerSample.zip sample.
class InputManagerV9(
        val devices: SparseArray<Array<Long>> = SparseArray(),
        private val listeners:
        MutableMap<InputManager.InputDeviceListener, Handler> = mutableMapOf()
) : InputManagerCompat {
    private val defaultHandler: Handler = PollingMessageHandler(this)
    
}
// The InputManagerCompatV9 class is a reference implementation.
// The full code is provided in the ControllerSample.zip sample.
public class InputManagerV9 implements InputManagerCompat {
    private final SparseArray<long[]> devices;
    private final Map<InputDeviceListener, Handler> listeners;
    private final Handler defaultHandler;
    

    public InputManagerV9() {
        devices = new SparseArray<long[]>();
        listeners = new HashMap<InputDeviceListener, Handler>();
        defaultHandler = new PollingMessageHandler(this);
    }
}

הטמעה של אובייקט PollingMessageHandler שמתרחב Handler, ומשנים את handleMessage() . השיטה הזו בודקת אם שלט משחקים מצורף מנותק ומודיע למאזינים רשומים.

private class PollingMessageHandler(
        inputManager: InputManagerV9,
        private val mInputManager: WeakReference<InputManagerV9> = WeakReference(inputManager)
) : Handler() {

    override fun handleMessage(msg: Message) {
        super.handleMessage(msg)
        when (msg.what) {
            MESSAGE_TEST_FOR_DISCONNECT -> {
                mInputManager.get()?.also { imv ->
                    val time = SystemClock.elapsedRealtime()
                    val size = imv.devices.size()
                    for (i in 0 until size) {
                        imv.devices.valueAt(i)?.also { lastContact ->
                            if (time - lastContact[0] > CHECK_ELAPSED_TIME) {
                                // check to see if the device has been
                                // disconnected
                                val id = imv.devices.keyAt(i)
                                if (null == InputDevice.getDevice(id)) {
                                    // Notify the registered listeners
                                    // that the game controller is disconnected
                                    imv.devices.remove(id)
                                } else {
                                    lastContact[0] = time
                                }
                            }
                        }
                    }
                    sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT, CHECK_ELAPSED_TIME)
                }
            }
        }
    }
}
private static class PollingMessageHandler extends Handler {
    private final WeakReference<InputManagerV9> inputManager;

    PollingMessageHandler(InputManagerV9 im) {
        inputManager = new WeakReference<InputManagerV9>(im);
    }

    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case MESSAGE_TEST_FOR_DISCONNECT:
                InputManagerV9 imv = inputManager.get();
                if (null != imv) {
                    long time = SystemClock.elapsedRealtime();
                    int size = imv.devices.size();
                    for (int i = 0; i < size; i++) {
                        long[] lastContact = imv.devices.valueAt(i);
                        if (null != lastContact) {
                            if (time - lastContact[0] > CHECK_ELAPSED_TIME) {
                                // check to see if the device has been
                                // disconnected
                                int id = imv.devices.keyAt(i);
                                if (null == InputDevice.getDevice(id)) {
                                    // Notify the registered listeners
                                    // that the game controller is disconnected
                                    imv.devices.remove(id);
                                } else {
                                    lastContact[0] = time;
                                }
                            }
                        }
                    }
                    sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT,
                            CHECK_ELAPSED_TIME);
                }
                break;
        }
    }
}

כדי להתחיל ולהפסיק את הדגימה לניתוק של בקר המשחקים, צריך לבטל את השינוי השיטות הבאות:

private const val MESSAGE_TEST_FOR_DISCONNECT = 101
private const val CHECK_ELAPSED_TIME = 3000L

class InputManagerV9(
        val devices: SparseArray<Array<Long>> = SparseArray(),
        private val listeners:
        MutableMap<InputManager.InputDeviceListener, Handler> = mutableMapOf()
) : InputManagerCompat {
    ...
    override fun onPause() {
        defaultHandler.removeMessages(MESSAGE_TEST_FOR_DISCONNECT)
    }

    override fun onResume() {
        defaultHandler.sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT, CHECK_ELAPSED_TIME)
    }
    ...
}
private static final int MESSAGE_TEST_FOR_DISCONNECT = 101;
private static final long CHECK_ELAPSED_TIME = 3000L;

@Override
public void onPause() {
    defaultHandler.removeMessages(MESSAGE_TEST_FOR_DISCONNECT);
}

@Override
public void onResume() {
    defaultHandler.sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT,
            CHECK_ELAPSED_TIME);
}

כדי לזהות שהמכשיר נוסף לקליטת נתונים, צריך לשנות את ברירת המחדל אמצעי תשלום אחד (onGenericMotionEvent()). כשהמערכת מדווחת על אירוע תנועה, צריך לבדוק אם האירוע הגיע ממזהה מכשיר שכבר נמצא במעקב, או מזהה המכשיר החדש. אם מזהה המכשיר חדש, צריך ליידע את המאזינים הרשומים.

override fun onGenericMotionEvent(event: MotionEvent) {
    // detect new devices
    val id = event.deviceId
    val timeArray: Array<Long> = mDevices.get(id) ?: run {
        // Notify the registered listeners that a game controller is added
        ...
        arrayOf<Long>().also {
            mDevices.put(id, it)
        }
    }
    timeArray[0] = SystemClock.elapsedRealtime()
}
@Override
public void onGenericMotionEvent(MotionEvent event) {
    // detect new devices
    int id = event.getDeviceId();
    long[] timeArray = mDevices.get(id);
    if (null == timeArray) {
        // Notify the registered listeners that a game controller is added
        ...
        timeArray = new long[1];
        mDevices.put(id, timeArray);
    }
    long time = SystemClock.elapsedRealtime();
    timeArray[0] = time;
}

הודעות על מאזינים מוטמעות באמצעות אובייקט Handler כדי לשלוח DeviceEvent אובייקט Runnable לתור ההודעות. DeviceEvent מכיל הפניה אל InputManagerCompat.InputDeviceListener. מתי הרצת DeviceEvent, שיטת הקריאה החוזרת המתאימה של ה-listen נשלחת כדי לאותת אם שלט המשחק נוסף, שונה או הוסר.

class InputManagerV9(
        val devices: SparseArray<Array<Long>> = SparseArray(),
        private val listeners:
        MutableMap<InputManager.InputDeviceListener, Handler> = mutableMapOf()
) : InputManagerCompat {
    ...
    override fun registerInputDeviceListener(
            listener: InputManager.InputDeviceListener,
            handler: Handler?
    ) {
        listeners[listener] = handler ?: defaultHandler
    }

    override fun unregisterInputDeviceListener(listener: InputManager.InputDeviceListener) {
        listeners.remove(listener)
    }

    private fun notifyListeners(why: Int, deviceId: Int) {
        // the state of some device has changed
        listeners.forEach { listener, handler ->
            DeviceEvent.getDeviceEvent(why, deviceId, listener).also {
                handler?.post(it)
            }
        }
    }
    ...
}

private val sObjectQueue: Queue<DeviceEvent> = ArrayDeque<DeviceEvent>()

private class DeviceEvent(
        private var mMessageType: Int,
        private var mId: Int,
        private var mListener: InputManager.InputDeviceListener
) : Runnable {

    companion object {
        fun getDeviceEvent(messageType: Int, id: Int, listener: InputManager.InputDeviceListener) =
                sObjectQueue.poll()?.apply {
                    mMessageType = messageType
                    mId = id
                    mListener = listener
                } ?: DeviceEvent(messageType, id, listener)

    }

    override fun run() {
        when(mMessageType) {
            ON_DEVICE_ADDED -> mListener.onInputDeviceAdded(mId)
            ON_DEVICE_CHANGED -> mListener.onInputDeviceChanged(mId)
            ON_DEVICE_REMOVED -> mListener.onInputDeviceChanged(mId)
            else -> {
                // Handle unknown message type
            }
        }
    }

}
@Override
public void registerInputDeviceListener(InputDeviceListener listener,
        Handler handler) {
    listeners.remove(listener);
    if (handler == null) {
        handler = defaultHandler;
    }
    listeners.put(listener, handler);
}

@Override
public void unregisterInputDeviceListener(InputDeviceListener listener) {
    listeners.remove(listener);
}

private void notifyListeners(int why, int deviceId) {
    // the state of some device has changed
    if (!listeners.isEmpty()) {
        for (InputDeviceListener listener : listeners.keySet()) {
            Handler handler = listeners.get(listener);
            DeviceEvent odc = DeviceEvent.getDeviceEvent(why, deviceId,
                    listener);
            handler.post(odc);
        }
    }
}

private static class DeviceEvent implements Runnable {
    private int mMessageType;
    private int mId;
    private InputDeviceListener mListener;
    private static Queue<DeviceEvent> sObjectQueue =
            new ArrayDeque<DeviceEvent>();
    ...

    static DeviceEvent getDeviceEvent(int messageType, int id,
            InputDeviceListener listener) {
        DeviceEvent curChanged = sObjectQueue.poll();
        if (null == curChanged) {
            curChanged = new DeviceEvent();
        }
        curChanged.mMessageType = messageType;
        curChanged.mId = id;
        curChanged.mListener = listener;
        return curChanged;
    }

    @Override
    public void run() {
        switch (mMessageType) {
            case ON_DEVICE_ADDED:
                mListener.onInputDeviceAdded(mId);
                break;
            case ON_DEVICE_CHANGED:
                mListener.onInputDeviceChanged(mId);
                break;
            case ON_DEVICE_REMOVED:
                mListener.onInputDeviceRemoved(mId);
                break;
            default:
                // Handle unknown message type
                ...
                break;
        }
        // Put this runnable back in the queue
        sObjectQueue.offer(this);
    }
}

עכשיו יש לך שתי הטמעות של InputManagerCompat: אחת פועל במכשירים שבהם פועלת מערכת Android 4.1 ומעלה, ועוד פועל במכשירים עם Android 3.1 עד 4.0.

שימוש בהטמעה הספציפית לגרסה

לוגיקת ההחלפה הספציפית לגרסה מוטמעת במחלקה שפועלת בתור מפעל.

object Factory {
    fun getInputManager(context: Context): InputManagerCompat =
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                InputManagerV16(context)
            } else {
                InputManagerV9()
            }
}
public static class Factory {
    public static InputManagerCompat getInputManager(Context context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            return new InputManagerV16(context);
        } else {
            return new InputManagerV9();
        }
    }
}

עכשיו אפשר פשוט ליצור אובייקט InputManagerCompat לרשום InputManagerCompat.InputDeviceListener View. בגלל לוגיקת החלפת הגרסאות שהגדרת המשחק שלך ישתמש באופן אוטומטי בהטמעה המתאימה גרסת Android שמותקנת במכשיר.

class GameView(context: Context) : View(context), InputManager.InputDeviceListener {
    private val inputManager: InputManagerCompat = Factory.getInputManager(context).apply {
        registerInputDeviceListener(this@GameView, null)
        ...
    }
    ...
}
public class GameView extends View implements InputDeviceListener {
    private InputManagerCompat inputManager;
    ...

    public GameView(Context context, AttributeSet attrs) {
        inputManager =
                InputManagerCompat.Factory.getInputManager(this.getContext());
        inputManager.registerInputDeviceListener(this, null);
        ...
    }
}

לאחר מכן, משנים את שיטת onGenericMotionEvent() בתצוגה הראשית, כפי שמתואר טיפול ב-MotionEvent מתוך משחק בקר משחקים. עכשיו המשחק שלך יכול לעבד אירועים של בקר המשחקים באופן עקבי במכשירים שבהם פועל Android 3.1 (רמת API 12) ומעלה.

override fun onGenericMotionEvent(event: MotionEvent): Boolean {
    inputManager.onGenericMotionEvent(event)

    // Handle analog input from the controller as normal
    ...
    return super.onGenericMotionEvent(event)
}
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
    inputManager.onGenericMotionEvent(event);

    // Handle analog input from the controller as normal
    ...
    return super.onGenericMotionEvent(event);
}

ניתן למצוא הטמעה מלאה של קוד התאימות הזה הכיתה GameView סופקה במדגם ControllerSample.zip לדוגמה זמין להורדה למעלה.