다양한 Android 버전에서 컨트롤러 지원

게임 컨트롤러를 지원하는 게임은 다양한 버전의 Android에서 실행되는 기기의 컨트롤러에 일관되게 응답해야 합니다. 이렇게 하면 게임이 더 넓은 고객층을 갖게 되고 플레이어가 Android 기기를 전환하거나 업그레이드할 때도 컨트롤러를 이용하여 원활한 게임플레이 환경을 즐길 수 있습니다.

이 과정에서는 Android 4.1 이상에서 이전 버전과 호환되는 API를 사용하여 게임이 Android 3.1 이상을 실행하는 기기에서 다음 기능을 지원하는 방법을 보여줍니다.

  • 게임 컨트롤러가 추가, 변경 또는 삭제되었는지 감지하는 기능
  • 게임 컨트롤러의 기능을 쿼리하는 기능
  • 게임 컨트롤러에서 들어오는 모션 이벤트를 인식하는 기능

이 과정의 예는 위에서 다운로드 가능한 샘플 ControllerSample.zip에서 제공하는 참조 구현을 기반으로 합니다. 이 샘플은 다양한 버전의 Android를 지원하도록 InputManagerCompat 인터페이스를 구현하는 방법을 보여줍니다. 샘플을 컴파일하려면 Android 4.1(API 레벨 16) 이상을 사용해야 합니다. 컴파일된 샘플 앱은 빌드 타겟으로 Android 3.1(API 레벨 12) 이상을 실행하는 모든 기기에서 실행됩니다.

게임 컨트롤러 지원 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. 다양한 버전의 Android에서 게임 컨트롤러를 지원하는 API

컨트롤러 정보 Controller API API 레벨 12 API 수준 16
기기 식별 getInputDeviceIds()  
getInputDevice()  
getVibrator()  
SOURCE_JOYSTICK
SOURCE_GAMEPAD
연결 상태 onInputDeviceAdded()  
onInputDeviceChanged()  
onInputDeviceRemoved()  
입력 이벤트 식별 D패드 누르기(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. 게임에 필요한 게임 컨트롤러 기능을 추상화하여 구현한 중개 자바 인터페이스를 정의합니다.
  2. Android 4.1 이상에서 API를 사용하는 인터페이스의 프록시 구현을 만듭니다.
  3. Android 3.1에서 Android 4.0까지 사용 가능한 API를 사용하는 인터페이스의 맞춤 구현을 만듭니다.
  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)
        }
    }
    

자바

    // 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()를 미러링합니다. 각각 다른 입력 기기의 ID를 정수 배열로 반환합니다. 이는 여러 플레이어를 지원하는 게임을 빌드하고 연결된 컨트롤러 수를 감지하려는 경우에 유용합니다.
registerInputDeviceListener()
registerInputDeviceListener()를 미러링합니다. 기기를 추가, 변경 또는 삭제할 때 알림을 받도록 등록할 수 있습니다.
unregisterInputDeviceListener()
unregisterInputDeviceListener()를 미러링합니다. 입력 기기 리스너를 등록 해제합니다.
onGenericMotionEvent()
onGenericMotionEvent()를 미러링합니다. 게임이 조이스틱 이동 및 아날로그 트리거를 누르는 것 같은 이벤트를 나타내는 MotionEvent 객체 및 축 값을 가로채서 처리합니다.
onPause()
기본 활동이 일시중지되거나 게임에 더 이상 포커스가 없을 때 게임 컨트롤러 이벤트의 폴링을 중지합니다.
onResume()
기본 활동이 계속되거나 포그라운드에서 게임이 시작 및 실행될 때 게임 컨트롤러 이벤트의 폴링을 시작합니다.
InputDeviceListener
InputManager.InputDeviceListener 인터페이스를 미러링합니다. 게임 컨트롤러가 추가, 변경 또는 삭제된 때를 게임이 알 수 있습니다.

다음으로 다양한 버전의 플랫폼에서 작동하는 InputManagerCompat의 구현을 만듭니다. 게임이 Android 4.1 이상에서 실행 중이고 InputManagerCompat 메서드를 호출하면 프록시 구현은 InputManager에서 동일한 메서드를 호출합니다. 그러나 Android 3.1 이상 Android 4.0 이하의 버전에서 게임을 실행하는 경우 맞춤 구현은 Android 3.1 이전에 도입된 API만 사용하여 InputManagerCompat 메서드 호출을 처리합니다. 런타임에 사용되는 버전별 구현과 관계없이 구현은 호출 결과를 게임에 투명하게 전달합니다.

그림 1. 인터페이스 및 버전별 구현의 클래스 다이어그램입니다.

Android 4.1 이상에서 인터페이스 구현

InputManagerCompatV16은 메서드 호출을 실제 InputManagerInputManager.InputDeviceListener로 프록시하는 InputManagerCompat 인터페이스의 구현입니다. 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
        ...
    }
    

자바

    // 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까지의 인터페이스 구현

다음 객체를 사용하여 Android 3.1에서 Android 4.0까지 지원하는 InputManagerCompat 구현을 만들 수 있습니다.

  • 기기에 연결된 게임 컨트롤러를 추적하는 데 필요한 기기 ID의 SparseArray
  • 기기 이벤트를 처리하는 Handler. 앱이 시작되거나 재개되면 Handler는 게임 컨트롤러의 연결 해제 폴링을 시작하라는 메시지를 받습니다. Handler는 루프를 시작하여 연결된 각 게임 컨트롤러를 확인하고 기기 ID가 반환되는지 알아봅니다. 반환 값이 null이면 게임 컨트롤러의 연결이 해제된 것입니다. 앱이 일시중지되면 Handler는 폴링을 중단합니다.
  • InputManagerCompat.InputDeviceListener 개체의 Map. 리스너를 사용하여 추적된 게임 컨트롤러의 연결 상태를 업데이트합니다.

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

자바

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

Handler를 확장한 PollingMessageHandler 객체를 구현하고 handleMessage() 메서드를 재정의합니다. 이 메서드는 연결된 게임 컨트롤러의 연결이 해제되었는지 확인하고 등록된 리스너에 알립니다.

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

자바

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

자바

    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() 메서드를 재정의합니다. 시스템이 모션 이벤트를 보고하면 이 이벤트가 이미 추적된 기기 ID에서 발생한 것인지 또는 새 기기 ID에서 발생한 것인지 확인합니다. 새로운 기기 ID이면 등록된 리스너에게 알립니다.

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

자바

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

리스너 알림은 DeviceEvent Runnable 객체를 메시지 큐로 보내기 위해 Handler 객체를 사용하여 구현합니다. DeviceEventInputManagerCompat.InputDeviceListener에 관한 참조를 포함합니다. DeviceEvent가 실행될 때 게임 컨트롤러가 추가, 변경 및 삭제되었다면 적절한 리스너의 콜백 메서드가 호출되어 신호를 보냅니다.

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

    }
    

자바

    @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에서 Android 4.0까지의 버전을 실행하는 기기에서 작동합니다.

버전별 구현 사용

버전별 스위칭 로직은 팩토리처럼 동작하는 클래스로 구현합니다.

Kotlin

    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 객체를 간단하게 인스턴스화하고 기본 ViewInputManagerCompat.InputDeviceListener를 등록합니다. 설정한 버전 전환 로직으로 인해 게임은 기기가 실행 중인 버전의 Android에 적합한 구현을 자동으로 사용합니다.

Kotlin

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

그런 다음, 게임 컨트롤러에서 MotionEvent 처리에 설명한 바와 같이 기본 뷰에 onGenericMotionEvent() 메서드를 재정의합니다. 게임이 이제 Android 3.1(API 수준 12) 이상을 실행하는 기기에서 게임 컨트롤러 이벤트를 일관되게 처리할 수 있습니다.

Kotlin

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

이 호환 코드의 전체 구현은 위의 다운로드 가능한 샘플 ControllerSample.zip에서 제공하는 GameView 클래스에서 찾을 수 있습니다.