개발하는 이야기

Android ImageSpan 사용시 주의할 점

ImageSpan이 어떻게 적용되는지를 설명해주는 이미지

ImageSpanDynamicDrawableSpan을 사용하기 편하도록 구현하고 있는 클래스로, 안드로이드에서 텍스트에 이미지를 껴넣을 수 있는 기능을 제공한다. 사용 방법도 매우 간단한데, ImageSpan의 생성자를 통해 텍스트 사이에 넣을 이미지(혹은 이미지 소스)를 주입하고 Spanned.setSpan 메소드를 통해 적용하면 된다. 예를 들면 다음과 같다.

val imageSpan = /** Create ImageSpan **/
val text = "Image Span Text"
// text의 첫번째 공백에 imageSpan을 적용할 경우
SpannableString(text).apply {
    setSpan(imageSpan, 5, 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}

참고로 위의 setSpan 메소드에서 5, 5로 하면 제대로 작동하지 않는다.

보시다시피 매우 간단하게 적용 가능하다. 그런데 위에서 ImageSpan을 생성하는 부분을 생략했는데, 이번 포스팅의 본론이 바로 저 ImageSpan의 생성자에 있기 때문이다. 안드로이드 공식 문서상의 ImageSpan 생성자는 다음과 같다.

참고로 Context 없이 Bitmap을 받는 생성자는 Deprecated 상태

생성자 종류가 굉장히 많다. 이미지 소스를 기준으로 구분하자면 Bitmap, Drawable, Uri, resourceId로 나눌 수 있다. 그 중에 (Deprecated 상태인 생성자를 제외하면) Drawable을 받는 생성자만 Context를 필요로 하지 않는다. 현재 진행중인 프로젝트에서는 데이터바인딩을 적극적으로 사용하고 있고, 뷰에 표기될 데이터를 모델→xml로 직접 전달하고 있기 때문에 ImageSpan의 생성 역시 모델에서 해주고 있다(이게 좋은 생각인지에 대해서는 여전히 고민이 있지만). 

// resourceProvider: resource 제공자 인터페이스를 구현하는 클래스 인스턴스
val imageSpan = ImageSpan(resourceProvider.getDrawable(R.drawable.drawableId))
val text = "Image Span Text"
// text의 첫번째 공백에 imageSpan을 적용할 경우
SpannableString(text).apply {
    setSpan(imageSpan, 5, 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}

그런데 문제는 모델에서 resource를 가져와야 할 경우에 Android 프레임워크와 멀리 떨어뜨리기 위해  Context를 사용하지 않고 별도의 resource 제공자 인터페이스를 제공하고 있기 때문에 ImageSpan 생성자중에 선택할 수 있는건 Drawable을 직접 주입하는 생성자 뿐이라는 것이다. 이게 왜 문제냐고? 왜냐면 위의 ImageSpan 적용 코드를 그대로 사용할 경우 이미지가 나타나지 않기 때문이다.

처음에는 어 이게 왜 안되냐 싶어서 ImageSpan 문서를 들어가보았는데, 별다른 실마리를 찾을 수가 없었다. 그래서 결국 ImageSpan 클래스의 소스를 보았다. 그리고 역시 답은 소스코드에 있었다.

public class ImageSpan extends DynamicDrawableSpan {
    
    //...
    
    @Override
    public Drawable getDrawable() {
        Drawable drawable = null;

        if (mDrawable != null) {
            drawable = mDrawable;
        } else if (mContentUri != null) {
            Bitmap bitmap = null;
            try {
                InputStream is = mContext.getContentResolver().openInputStream(
                        mContentUri);
                bitmap = BitmapFactory.decodeStream(is);
                drawable = new BitmapDrawable(mContext.getResources(), bitmap);
                drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
                        drawable.getIntrinsicHeight());
                is.close();
            } catch (Exception e) {
                Log.e("ImageSpan", "Failed to loaded content " + mContentUri, e);
            }
        } else {
            try {
                drawable = mContext.getDrawable(mResourceId);
                drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
                        drawable.getIntrinsicHeight());
            } catch (Exception e) {
                Log.e("ImageSpan", "Unable to find resource: " + mResourceId);
            }
        }

        return drawable;
    }
    
    //...
    
}

위의 getDrawable 메소드는 DynamicDrawableSpan의 메소드로, 이름에서 알수 있다시피 텍스트 내에 그려질 이미지를 제공하는 메소드다. mDrawable이 null인 경우에 생성자에서 주입받은 소스로부터 drawable을 생성해낸다. 그런데 여기서 보면 drawable을 생성하고 마지막에 setBounds 메소드를 호출한다. 고로 drawable에 setBounds를 통해 영역을 지정해주지 않기 때문에 이미지가 제대로 표기되지 않는 것은 아닐까? 라는 의심이 가능해진다. 그리고 실제로 ImageSpan의 생성자 쪽 코드를 보면 다음과 같이 처리되어있는 것을 볼 수 있다.

public class ImageSpan extends DynamicDrawableSpan {
    // mDrawable을 생성하는 생성자들
    
    public ImageSpan(@NonNull Context context, @NonNull Bitmap bitmap, int verticalAlignment) {
        super(verticalAlignment);
        mContext = context;
        mDrawable = context != null
                ? new BitmapDrawable(context.getResources(), bitmap)
                : new BitmapDrawable(bitmap);
        int width = mDrawable.getIntrinsicWidth();
        int height = mDrawable.getIntrinsicHeight();
        mDrawable.setBounds(0, 0, width > 0 ? width : 0, height > 0 ? height : 0);
    }

    public ImageSpan(@NonNull Drawable drawable, int verticalAlignment) {
        super(verticalAlignment);
        mDrawable = drawable;
    }

    public ImageSpan(@NonNull Drawable drawable, @NonNull String source, int verticalAlignment) {
        super(verticalAlignment);
        mDrawable = drawable;
        mSource = source;
    }

}

여기서 보면 Drawable을 직접 받는 생성자는 setBounds를 따로 호출하지 않는 반면, Context와 Bitmap을 전달하는 생성자에서는 Drawable을 생성한 후 setBounds를 호출하는 것을 볼 수 있다. 바로 이 차이가 Drawable을 직접 주입한 ImageSpan에서 이미지가 노출되지 않도록 하는 것이다. 그렇다면 어떻게 하면 되냐고? 간단하다. drawable을 주입하기 전에 주입할 drawable에 setBounds를 호출하면 된다. 

하단의 결제 영역에는 setBounds를 어떻게 적용했는지가 나와있다. 솔직히 하나도 어려울 것 없는 코드지만 내가 했던 삽질에 커피 한잔의 위안이라도 주고싶으시다면...🙏🙏🙏🙏🙏🙏🙏

이어지는 내용이 궁금하세요? 포스트를 구매하고 이어지는 내용을 감상해보세요.

  • 텍스트 28 공백 제외
  • 코드 11
100P
hell yeah, world
hell yeah, world
구독자 160
멤버십 가입

3개의 댓글

SNS 계정으로 간편하게 로그인하고 댓글을 남겨주세요.
새로운 알림이 없습니다.