다양한 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)
    }
}

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()를 미러링합니다. 각각 다른 입력 기기의 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
    ...
}

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

다음 객체를 사용하여 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)
    …
}

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

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

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() 메서드를 재정의합니다. 시스템에서 모션 이벤트를 보고할 때 이 이벤트가 이미 추적된 기기 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()
}

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

리스너 알림은 Handler 객체를 사용하여 DeviceEvent Runnable 객체를 메시지 큐로 전송하는 방식으로 구현됩니다. DeviceEvent에는 InputManagerCompat.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
            }
        }
    }

}

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의 두 가지 구현이 있습니다. 하나는 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()
            }
}

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 객체를 인스턴스화하고 기본 ViewInputManagerCompat.InputDeviceListener를 등록하기만 하면 됩니다. 설정한 버전 전환 로직으로 인해 게임은 기기가 실행 중인 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);
        ...
    }
}

그런 다음 게임 컨트롤러에서 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)
}

Java

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

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

이 호환성 코드의 전체 구현은 위에서 다운로드 가능한 샘플 ControllerSample.zip에 제공된 GameView 클래스에서 확인할 수 있습니다.