รองรับตัวควบคุมใน Android เวอร์ชันต่างๆ

หากคุณรองรับตัวควบคุมเกมในเกมของคุณ คุณมีหน้าที่ต้อง ตรวจสอบว่าเกมตอบสนองต่อตัวควบคุมอย่างสม่ำเสมอในอุปกรณ์ ที่ใช้ Android เวอร์ชันต่างๆ ซึ่งจะช่วยให้เกมของคุณเข้าถึงผู้ชมได้กว้างขึ้น และผู้เล่นจะได้รับประสบการณ์การเล่นเกมที่ราบรื่นด้วย คอนโทรลเลอร์ แม้ว่าจะเปลี่ยนหรืออัปเกรดอุปกรณ์ Android ก็ตาม

บทเรียนนี้จะแสดงวิธีใช้ API ที่พร้อมให้บริการใน Android 4.1 ขึ้นไปในลักษณะที่เข้ากันได้แบบย้อนหลัง ซึ่งจะช่วยให้เกมรองรับฟีเจอร์ต่อไปนี้ในอุปกรณ์ที่ใช้ Android 3.1 ขึ้นไป

  • เกมจะตรวจหาได้ว่ามีการเพิ่ม เปลี่ยนแปลง หรือนำเกมคอนโทรลเลอร์ใหม่ออกหรือไม่
  • เกมสามารถค้นหาความสามารถของเกมคอนโทรลเลอร์ได้
  • เกมจะจดจำเหตุการณ์การเคลื่อนไหวที่เข้ามาจากเกมคอนโทรลเลอร์ได้

เตรียมที่จะแยก 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) กับ 4.1 (API ระดับ 16) เพื่อช่วยคุณพิจารณาว่าฟีเจอร์ใดที่ต้องมีกลไกการทำงานสำรองสำหรับเวอร์ชันที่ต่ำกว่า

ตารางที่ 1 API สำหรับการรองรับเกมคอนโทรลเลอร์ใน Android เวอร์ชันต่างๆ

ข้อมูลผู้ควบคุมข้อมูล Controller 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. สร้างการใช้งานพร็อกซีของอินเทอร์เฟซที่ใช้ API ใน Android 4.1 ขึ้นไป
  3. สร้างการติดตั้งใช้งานอินเทอร์เฟซที่กำหนดเองซึ่งใช้ API ที่พร้อมใช้งานระหว่าง Android 3.1 ถึง Android 4.0
  4. สร้างตรรกะสำหรับการสลับระหว่างการติดตั้งใช้งานเหล่านี้ที่รันไทม์ และ เริ่มใช้อินเทอร์เฟซในเกม

ดูภาพรวมของวิธีใช้การแยกส่วนเพื่อยืนยันว่าแอปพลิเคชันสามารถทำงานในลักษณะที่เข้ากันได้แบบย้อนหลังใน Android เวอร์ชันต่างๆ ได้ที่การสร้าง UI ที่ใช้งานย้อนหลังได้

เพิ่มอินเทอร์เฟซเพื่อความเข้ากันได้แบบย้อนหลัง

หากต้องการให้เข้ากันได้แบบย้อนหลัง คุณสามารถสร้างอินเทอร์เฟซที่กำหนดเอง แล้วเพิ่มการติดตั้งใช้งานเฉพาะเวอร์ชัน ข้อดีอย่างหนึ่งของแนวทางนี้คือช่วยให้คุณทำมิเรอร์อินเทอร์เฟซสาธารณะใน Android 4.1 (API ระดับ 16) ที่รองรับเกมคอนโทรลเลอร์ได้

Kotlin

// 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)
    }
}

Java

// 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() ยกเลิกการลงทะเบียน Listener ของอุปกรณ์อินพุต
onGenericMotionEvent()
กระจก onGenericMotionEvent() ช่วยให้เกมของคุณสกัดกั้นและจัดการMotionEventออบเจ็กต์และค่าแกนที่แสดงถึงเหตุการณ์ต่างๆ เช่น การเคลื่อนไหวของจอยสติ๊กและการกดทริกเกอร์แบบอนาล็อก
onPause()
หยุดการสำรวจเหตุการณ์ของเกมคอนโทรลเลอร์เมื่อ กิจกรรมหลักหยุดชั่วคราว หรือเมื่อเกมไม่ได้โฟกัสอีกต่อไป
onResume()
เริ่มสำรวจเหตุการณ์ของเกมคอนโทรลเลอร์เมื่อ กิจกรรมหลักกลับมาทำงานต่อ หรือเมื่อเริ่มเกมและทำงานใน เบื้องหน้า
InputDeviceListener
แสดงInputManager.InputDeviceListener อินเทอร์เฟซ ช่วยให้เกมทราบเมื่อมีการเพิ่ม เปลี่ยนแปลง หรือ นำอุปกรณ์ควบคุมเกมออก

จากนั้นสร้างการติดตั้งใช้งานสำหรับ InputManagerCompat ที่ใช้ได้ ในแพลตฟอร์มเวอร์ชันต่างๆ หากเกมทำงานบน Android 4.1 ขึ้นไปและเรียกใช้เมธอด InputManagerCompat การติดตั้งใช้งานพร็อกซีจะเรียกใช้เมธอดที่เทียบเท่าใน InputManager อย่างไรก็ตาม หากเกมของคุณทำงานบน Android 3.1 ถึง Android 4.0 การติดตั้งใช้งานที่กำหนดเอง จะประมวลผลการเรียกไปยังเมธอด InputManagerCompat โดยใช้ เฉพาะ API ที่เปิดตัวไม่เกิน Android 3.1 ไม่ว่าจะใช้การติดตั้งใช้งานเฉพาะเวอร์ชันใดในรันไทม์ การติดตั้งใช้งานจะส่งผลการเรียกกลับไปยังเกมอย่างโปร่งใส

แผนภาพคลาสของอินเทอร์เฟซและการใช้งานเฉพาะเวอร์ชัน
รูปที่ 1: แผนภาพคลาสของอินเทอร์เฟซและการติดตั้งใช้งานเฉพาะเวอร์ชัน

ใช้การเชื่อมต่อใน Android 4.1 ขึ้นไป

InputManagerCompatV16 เป็นการใช้งานอินเทอร์เฟซ InputManagerCompat ที่พร็อกซีการเรียกเมธอดไปยัง InputManager และ InputManager.InputDeviceListener จริง InputManager ได้มาจากContextของระบบ

Kotlin

// 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
    ...
}

Java

// 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 ขึ้นไปจนถึง Android 4.0 คุณสามารถใช้ออบเจ็กต์ต่อไปนี้

  • SparseArray ของรหัสอุปกรณ์เพื่อ ติดตามอุปกรณ์ควบคุมเกมที่เชื่อมต่อกับอุปกรณ์
  • Handler เพื่อประมวลผลเหตุการณ์ในอุปกรณ์ เมื่อแอปเริ่มทำงานหรือกลับมาทำงานอีกครั้ง Handler จะได้รับข้อความให้เริ่ม การสำรวจการเชื่อมต่อของเกมคอนโทรลเลอร์ โดย Handler จะเริ่มวนรอบเพื่อตรวจสอบ เกมคอนโทรลเลอร์ที่เชื่อมต่อที่รู้จักแต่ละตัวและดูว่ามีการส่งคืนรหัสอุปกรณ์หรือไม่ null ค่าที่ส่งคืนจะระบุว่าตัวควบคุมเกมถูกยกเลิกการเชื่อมต่อ Handler จะหยุดการสำรวจเมื่อแอป หยุดชั่วคราว
  • Map ของ InputManagerCompat.InputDeviceListener ออบเจ็กต์ คุณจะใช้ Listener เพื่ออัปเดตสถานะการเชื่อมต่อของตัวควบคุมเกมที่ติดตาม

Kotlin

// 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)
    
}

Java

// 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() วิธีนี้จะตรวจสอบว่าตัวควบคุมเกมที่เชื่อมต่ออยู่ ถูกตัดการเชื่อมต่อแล้วหรือไม่ และจะแจ้งให้ Listener ที่ลงทะเบียนทราบ

Kotlin

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)
                }
            }
        }
    }
}

Java

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;
        }
    }
}

หากต้องการเริ่มและหยุดการสำรวจการเชื่อมต่อตัวควบคุมเกม ให้ลบล้าง เมธอดต่อไปนี้

Kotlin

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)
    }
    ...
}

Java

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() เมื่อระบบรายงานเหตุการณ์การเคลื่อนไหว ให้ตรวจสอบว่าเหตุการณ์นี้มาจากรหัสอุปกรณ์ที่ติดตามอยู่แล้วหรือมาจากรหัสอุปกรณ์ใหม่ หากรหัสอุปกรณ์เป็นรหัสใหม่ ให้แจ้งผู้ฟังที่ลงทะเบียน

Kotlin

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()
}

Java

@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;
}

การแจ้งเตือน Listener จะดำเนินการโดยใช้ออบเจ็กต์ Handler เพื่อส่งออบเจ็กต์ DeviceEvent Runnable ไปยังคิวข้อความ DeviceEvent มีการอ้างอิงถึง InputManagerCompat.InputDeviceListener เมื่อ DeviceEvent ทำงาน ระบบจะเรียกใช้เมธอดการเรียกกลับที่เหมาะสมของ Listener เพื่อส่งสัญญาณว่ามีการเพิ่ม เปลี่ยนแปลง หรือนำเกม คอนโทรลเลอร์ออก

Kotlin

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
            }
        }
    }

}

Java

@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 2 รูปแบบ ได้แก่ รูปแบบที่ใช้ได้ในอุปกรณ์ที่ใช้ Android 4.1 ขึ้นไป และอีกรูปแบบที่ใช้ได้ในอุปกรณ์ที่ใช้ Android 3.1 ถึง Android 4.0

ใช้การติดตั้งใช้งานเฉพาะเวอร์ชัน

เราใช้ตรรกะการสลับเวอร์ชันที่เฉพาะเจาะจงในคลาสที่ทำหน้าที่เป็น Factory

Kotlin

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

Java

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 ที่อุปกรณ์ใช้โดยอัตโนมัติ

Kotlin

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

Java

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 จากเกม คอนโทรลเลอร์ (API ระดับ 12 ขึ้นไป) ตอนนี้เกมของคุณ ควรประมวลผลเหตุการณ์ของเกมคอนโทรลเลอร์ได้อย่างสม่ำเสมอในอุปกรณ์ ที่ใช้ Android 3.1 (API ระดับ)

Kotlin

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

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

Java

@Override
public boolean onGenericMotionEvent(MotionEvent event) {
    inputManager.onGenericMotionEvent(event);

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

คุณดูการใช้งานโค้ดความเข้ากันได้นี้ทั้งหมดได้ในคลาส GameView ที่ระบุไว้ในตัวอย่าง ControllerSample.zip ซึ่งพร้อมให้ ดาวน์โหลด