이미지 키보드 지원

사용자는 이모티콘, 스티커 및 다른 종류의 리치 콘텐츠를 사용하여 소통하고 싶어 하는 경우가 많습니다. 이전 버전의 Android에서는 소프트 키보드(입력 방식 편집기 또는 IME라고도 함)가 유니코드 이모티콘만 앱으로 전송할 수 있었습니다. 리치 콘텐츠의 경우 앱은 다른 앱에서 사용할 수 없는 앱별 API를 빌드했거나 간단한 공유 작업 또는 클립보드를 통한 이미지 전송과 같은 해결 방법을 사용했습니다.

이미지 검색을 지원하는 키보드를 보여주는 이미지
그림 1. 이미지 키보드 지원의 예

Android 7.1 (API 수준 25)부터 Android SDK에는 Commit Content API가 포함됩니다. 이 API는 IME가 이미지 및 기타 리치 콘텐츠를 앱의 텍스트 편집기로 직접 전송할 수 있는 보편적인 방법을 제공합니다. 버전 25.0.0부터 v13 지원 라이브러리에서도 이 API를 사용할 수 있습니다. 구현을 간소화하는 도우미 메서드가 포함되어 있으므로 지원 라이브러리를 사용하는 것이 좋습니다.

이 API를 사용하면 모든 키보드에서 리치 콘텐츠를 허용하는 메시지 앱뿐만 아니라 모든 앱에 리치 콘텐츠를 보낼 수 있는 키보드에서 메시지 앱을 빌드할 수 있습니다. Google 키보드Google 메시지와 같은 앱은 그림 1과 같이 Android 7.1에서 Commit Content API를 지원합니다.

이 문서에서는 IME 및 앱 모두에서 Commit Content API를 구현하는 방법을 보여줍니다.

작동 방식

키보드 이미지 삽입에는 IME 및 앱이 참여해야 합니다. 다음 시퀀스에서는 이미지 삽입 프로세스의 각 단계를 설명합니다.

  1. 사용자가 EditText를 탭하면 편집기는 EditorInfo.contentMimeTypes에서 허용하는 MIME 콘텐츠 유형 목록을 전송합니다.

  2. IME는 지원되는 유형 목록을 읽고 편집기가 허용할 수 있는 소프트 키보드의 콘텐츠를 표시합니다.

  3. 사용자가 이미지를 선택하면 IME는 commitContent()를 호출하고 InputContentInfo를 편집기에 전송합니다. commitContent() 호출은 commitText() 호출과 비슷하지만 리치 콘텐츠를 위한 것입니다. InputContentInfo에는 콘텐츠 제공자의 콘텐츠를 식별하는 URI가 포함됩니다.

이 프로세스는 그림 2에 묘사되어 있습니다.

애플리케이션에서 IME로, 그리고 애플리케이션으로 돌아오는 시퀀스를 보여주는 이미지
그림 2. IME에서 애플리케이션 흐름으로의 애플리케이션 흐름

앱에 이미지 지원 추가

IME의 리치 콘텐츠를 허용하려면 앱에서 허용되는 콘텐츠 유형을 IME에 알리고 콘텐츠가 수신될 때 실행되는 콜백 메서드를 지정해야 합니다. 다음 예는 PNG 이미지를 허용하는 EditText를 만드는 방법을 보여줍니다.

Kotlin

var editText: EditText = object : EditText(this) {
    override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {
        var ic = super.onCreateInputConnection(outAttrs)
        EditorInfoCompat.setContentMimeTypes(outAttrs, arrayOf("image/png"))
        val mimeTypes = ViewCompat.getOnReceiveContentMimeTypes(this)
        if (mimeTypes != null) {
            EditorInfoCompat.setContentMimeTypes(outAttrs, mimeTypes)
            ic = InputConnectionCompat.createWrapper(this, ic, outAttrs)
        }
        return ic
    }
}

Java

EditText editText = new EditText(this) {
    @Override
    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        InputConnection ic = super.onCreateInputConnection(outAttrs);
        EditorInfoCompat.setContentMimeTypes(outAttrs, new String[]{"image/png"});
        String[] mimeTypes = ViewCompat.getOnReceiveContentMimeTypes(this);
        if (mimeTypes != null) {
            EditorInfoCompat.setContentMimeTypes(outAttrs, mimeTypes);
            ic = InputConnectionCompat.createWrapper(this, ic, outAttrs);
        }
        return ic;
    }
};

자세한 설명은 다음과 같습니다.

다음은 권장사항입니다.

  • 리치 콘텐츠를 지원하지 않는 편집기는 setContentMimeTypes()를 호출하지 않고 EditorInfo.contentMimeTypesnull로 설정합니다.

  • 편집기는 InputContentInfo에 지정된 MIME 유형이 허용되는 유형과 일치하지 않으면 콘텐츠를 무시합니다.

  • 리치 콘텐츠는 텍스트 커서의 위치에 영향을 주지 않으며 영향을 받지 않습니다. 편집기는 콘텐츠를 사용할 때 커서 위치를 무시할 수 있습니다.

  • 편집기의 OnCommitContentListener.onCommitContent() 메서드에서 콘텐츠를 로드하기 전이라도 true를 비동기식으로 반환할 수 있습니다.

  • 커밋되기 전에 IME에서 수정할 수 있는 텍스트와 달리 리치 콘텐츠는 즉시 커밋됩니다. 사용자가 콘텐츠를 수정하거나 삭제할 수 있도록 하려면 로직을 직접 구현합니다.

앱을 테스트하려면 기기나 에뮬레이터에 리치 콘텐츠를 전송할 수 있는 키보드가 있는지 확인하세요. Android 7.1 이상에서 Google 키보드를 사용할 수 있습니다.

IME에 이미지 지원 추가

앱에 리치 콘텐츠를 전송하려는 IME는 다음 예와 같이 Commit Content API를 구현해야 합니다.

  • onStartInput() 또는 onStartInputView()를 재정의하고 타겟 편집기에서 지원되는 콘텐츠 유형 목록을 읽습니다. 다음 코드 스니펫은 타겟 편집기에서 GIF 이미지를 허용하는지 확인하는 방법을 보여줍니다.

Kotlin

override fun onStartInputView(editorInfo: EditorInfo, restarting: Boolean) {
    val mimeTypes: Array<String> = EditorInfoCompat.getContentMimeTypes(editorInfo)

    val gifSupported: Boolean = mimeTypes.any {
        ClipDescription.compareMimeTypes(it, "image/gif")
    }

    if (gifSupported) {
        // The target editor supports GIFs. Enable the corresponding content.
    } else {
        // The target editor doesn't support GIFs. Disable the corresponding
        // content.
    }
}

Java

@Override
public void onStartInputView(EditorInfo info, boolean restarting) {
    String[] mimeTypes = EditorInfoCompat.getContentMimeTypes(editorInfo);

    boolean gifSupported = false;
    for (String mimeType : mimeTypes) {
        if (ClipDescription.compareMimeTypes(mimeType, "image/gif")) {
            gifSupported = true;
        }
    }

    if (gifSupported) {
        // The target editor supports GIFs. Enable the corresponding content.
    } else {
        // The target editor doesn't support GIFs. Disable the corresponding
        // content.
    }
}

  • 사용자가 이미지를 선택하면 앱에 콘텐츠를 커밋합니다. 작성 중인 텍스트가 있을 때는 commitContent()를 호출하지 마세요. 편집기에서 포커스를 잃을 수 있습니다. 다음 코드 스니펫은 GIF 이미지를 커밋하는 방법을 보여줍니다.

Kotlin

// Commits a GIF image.

// @param contentUri = Content URI of the GIF image to be sent.
// @param imageDescription = Description of the GIF image to be sent.

fun commitGifImage(contentUri: Uri, imageDescription: String) {
    val inputContentInfo = InputContentInfoCompat(
            contentUri,
            ClipDescription(imageDescription, arrayOf("image/gif")),
            null
    )
    val inputConnection = currentInputConnection
    val editorInfo = currentInputEditorInfo
    var flags = 0
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
        flags = flags or InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION
    }
    InputConnectionCompat.commitContent(inputConnection, editorInfo, inputContentInfo, flags, null)
}

Java

// Commits a GIF image.

// @param contentUri = Content URI of the GIF image to be sent.
// @param imageDescription = Description of the GIF image to be sent.

public static void commitGifImage(Uri contentUri, String imageDescription) {
    InputContentInfoCompat inputContentInfo = new InputContentInfoCompat(
            contentUri,
            new ClipDescription(imageDescription, new String[]{"image/gif"}),
            null
    );
    InputConnection inputConnection = getCurrentInputConnection();
    EditorInfo editorInfo = getCurrentInputEditorInfo();
    Int flags = 0;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
        flags |= InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION;
    }
    InputConnectionCompat.commitContent(
            inputConnection, editorInfo, inputContentInfo, flags, null);
}

IME 작성자는 콘텐츠 URI 요청에 응답하기 위해 자체 콘텐츠 제공자를 구현해야 할 가능성이 높습니다. IME가 MediaStore와 같은 기존 콘텐츠 제공업체의 콘텐츠를 지원하는 경우는 예외입니다. 콘텐츠 제공자 빌드에 관한 자세한 내용은 콘텐츠 제공자파일 제공자 문서를 참고하세요.

자체 콘텐츠 제공업체를 빌드 중인 경우 android:exportedfalse로 설정하여 내보내지 않는 것이 좋습니다. 대신 android:grantUriPermissiontrue로 설정하여 제공자에서 권한 부여를 사용 설정하세요. 그러면 콘텐츠가 커밋될 때 IME에서 콘텐츠 URI 액세스 권한을 부여할 수 있습니다. 다음과 같은 두 가지 방법이 있습니다.

  • Android 7.1 (API 수준 25) 이상에서 commitContent()를 호출할 때 플래그 매개변수를 INPUT_CONTENT_GRANT_READ_URI_PERMISSION로 설정합니다. 그러면 앱이 수신하는 InputContentInfo 객체가 requestPermission()releasePermission()를 호출하여 임시 읽기 권한을 요청하고 해제할 수 있습니다.

  • Android 7.0 (API 수준 24) 이하에서는 INPUT_CONTENT_GRANT_READ_URI_PERMISSION가 무시되므로 수동으로 콘텐츠에 권한을 부여합니다. 이렇게 하는 한 가지 방법은 grantUriPermission()를 사용하는 것이지만 자체 요구사항을 충족하는 자체 메커니즘을 구현해도 됩니다.

IME를 테스트하려면 기기나 에뮬레이터에 리치 콘텐츠를 수신할 수 있는 앱이 있는지 확인합니다. Android 7.1 이상에서 Google Messenger 앱을 사용할 수 있습니다.