[Android] Tích hợp Emoticon và ảnh GIF vào ứng dụng chat trong Android

I. Mở bài

Các ứng dụng chat hay nhắn tin đang dần trở nên phổ biến, có thể kể đến 1 số ứng dụng như: Facebook Messenger, Google Hangouts, WhatsApp, Skype... Ngoài các tính nắng nổi trội thì bên cạnh đó, mỗi ứng dụng đều mang đến những emoticon rất dễ thương và thú vị. Bên cạnh đó, gần đây những đoạn chat + comment sử dụng những ảnh gif đang dần trở nên xu thế và tạo nhiều cảm xúc thú vị cho người dùng Trong bài viết này, mình sẽ giới thiệu và hướng dẫn mọi người tích hợp bàn phím có các emoticon và ảnh gif và hiển thị lên ứng dụng của mình Cụ thể ứng dụng sẽ như hình:

II. Phân tích Emoticon

2.1 Database

Dù các ứng dụng có thể có các emoticon khác nhau, nhưng nhìn chung, các emoticon đều chia thành 8 các category: people, nature, food, sport, cars, electron, symbol, flag Ở đây mình có 1 database sẵn về các emoticon. Mọi người có thể thao khảo ở đây: emoticon.db

Mẫu db như sau

_id unicode name category
1 😀 Grinning Face 1
2 😁 Grinning Face With Smiling Eyes 1
3 😂 Face With Tears of Joy 1
4 😃 Smiling Face With Open Mouth 1
... ... ... ...

Với các category lần lượt

category_id name
1 people
2 nature
3 food
4 sport
5 cars
6 electron
7 symbol
8 flag

2.2 Emoticon icon packs

Tương ứng với mỗi unicode sẽ map với 1 ảnh icon theo từng gói. Có rất nhiều gói cho các bạn lựa chọn:

Icon Emoticon Pack Gradle Dependency
Apple compile 'com.kevalpatel2106:emoticonpack-ios:1.1'
Android 7.0 compile 'com.kevalpatel2106:emoticonpack-android7:1.1'
Android 8.0 compile 'com.kevalpatel2106:emoticonpack-android8:1.1'
Samsung compile 'com.kevalpatel2106:emoticonpack-samsung:1.1'
Twitter compile 'com.kevalpatel2106:emoticonpack-twitter:1.1'
Facebook compile 'com.kevalpatel2106:emoticonpack-facebook:1.1'
Messenger compile 'com.kevalpatel2106:emoticonpack-messenger:1.1'

2.3 GIF Packs

Các gói ảnh gif

GIF Provider Module Dependency
giphy.com Giphy compile 'com.kevalpatel2106:gifpack-giphy:1.1'
tenor.com Tenor compile 'com.kevalpatel2106:gifpack-tenor:1.1'

2.4 Keyboard

dependencies {
    compile 'com.kevalpatel2106:emoticongifkeyboard:1.1'
}

III. Tạo ứng dụng

Các bạn có thể compile các thư viện trên về sử dụng, song do thư viện trên đã dựng sẵn giao diện keyboard, do vậy, để có thể custom như ý muốn, mình đã clone về import vào project để có thể handle theo ý muốn Ở đây mình sử dụng emoticon icon pack là Facebook Messenger và GIF pack là Giphy

3.1 Custome widget

3.1.1 EmoticonEditText

public class EmoticonEditText extends AppCompatEditText {
    private static final String TAG = "EmoticonEditText";
    private int mEmoticonSize;
    @Nullable
    private EmoticonProvider mEmoticonProvider;

    public EmoticonEditText(final Context context) {
        super(context);
        mEmoticonSize = (int) getTextSize();
        setText(getText());
    }

    public EmoticonEditText(final Context context, final AttributeSet attrs) {
        super(context, attrs);
        init(attrs);
    }

    public EmoticonEditText(final Context context, final AttributeSet attrs, final int defStyle) {
        super(context, attrs, defStyle);
        init(attrs);
    }

    private void init(AttributeSet attrs) {
        @SuppressLint("CustomViewStyleable")
        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.Emoticon);
        mEmoticonSize = (int) a.getDimension(R.styleable.Emoticon_emojiconSize, getTextSize());
        a.recycle();

        setText(getText());
    }

    @Override
    @CallSuper
    public void setText(CharSequence rawText, BufferType type) {
        final CharSequence text = rawText == null ? "" : rawText;
        final SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text);
        if (mEmoticonProvider != null)
            EmoticonUtils.replaceWithImages(getContext(), spannableStringBuilder, mEmoticonProvider, mEmoticonSize);
        super.setText(text, type);
    }

    @Override
    @CallSuper
    public void append(CharSequence rawText, int start, int end) {
        final SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(getText().insert(start, rawText));
        if (mEmoticonProvider != null)
            EmoticonUtils.replaceWithImages(getContext(), spannableStringBuilder, mEmoticonProvider, mEmoticonSize);
        super.setText(spannableStringBuilder);
        setSelection(length());
    }

    @CallSuper
    public void backspace() {
        final KeyEvent event = new KeyEvent(0, 0, 0,
                KeyEvent.KEYCODE_DEL, 0, 0, 0, 0,
                KeyEvent.KEYCODE_ENDCALL);
        dispatchKeyEvent(event);
    }

    /**
     * Set the size of emojicon in pixels.
     */
    @CallSuper
    public void setEmoticonSize(final int pixels) {
        mEmoticonSize = pixels;
        setText(getText());
    }

    /**
     * Set {@link EmoticonProvider} to display custom emoticon icons.
     *
     * @param emoticonProvider {@link EmoticonProvider} of custom icon packs or null to display
     *                         system icons.
     */
    @CallSuper
    public void setEmoticonProvider(@Nullable final EmoticonProvider emoticonProvider) {
        mEmoticonProvider = emoticonProvider;

        //Refresh the emoticon icons
        final SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(getText());
        if (mEmoticonProvider != null)
            EmoticonUtils.replaceWithImages(getContext(), spannableStringBuilder, mEmoticonProvider, mEmoticonSize);
    }
}

3.1.2 EmoticonTextView

public class EmoticonTextView extends AppCompatTextView {
    private int mEmoticonSize;
    @Nullable
    private EmoticonProvider mEmoticonProvider;

    public EmoticonTextView(@NonNull Context context) {
        super(context);
        mEmoticonSize = (int) getTextSize();
        init(null);
    }

    public EmoticonTextView(@NonNull Context context, AttributeSet attrs) {
        super(context, attrs);
        init(attrs);
    }

    public EmoticonTextView(@NonNull Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(attrs);
    }

    private void init(@Nullable final AttributeSet attrs) {
        if (attrs == null) {
            mEmoticonSize = (int) getTextSize();
        } else {
            @SuppressLint("CustomViewStyleable")
            TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.Emoticon);
            mEmoticonSize = (int) a.getDimension(R.styleable.Emoticon_emojiconSize, getTextSize());
            a.recycle();
        }
        setText(getText());
    }

    @Override
    @CallSuper
    public void setText(CharSequence rawText, BufferType type) {
        final CharSequence text = rawText == null ? "" : rawText;
        final SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text);
        if (mEmoticonProvider != null)
            EmoticonUtils.replaceWithImages(getContext(), spannableStringBuilder, mEmoticonProvider, mEmoticonSize);
        super.setText(spannableStringBuilder, type);
    }

    @Override
    @Deprecated
    @CallSuper
    public void append(CharSequence rawText, int start, int end) {
        final String text = getText() + (rawText == null ? "" : rawText).toString();
        final SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text);
        if (mEmoticonProvider != null)
            EmoticonUtils.replaceWithImages(getContext(), spannableStringBuilder, mEmoticonProvider, mEmoticonSize);
        super.setText(spannableStringBuilder);
    }

    /**
     * Set the size of emojicon in pixels.
     */
    @CallSuper
    public void setEmoticonSize(final int pixels) {
        mEmoticonSize = pixels;
        setText(getText());
    }

    /**
     * Set {@link EmoticonProvider} to display custom emoticon icons.
     *
     * @param emoticonProvider {@link EmoticonProvider} of custom icon packs or null to display
     *                         system icons.
     */
    @CallSuper
    public void setEmoticonProvider(@Nullable final EmoticonProvider emoticonProvider) {
        mEmoticonProvider = emoticonProvider;

        //Refresh the emoticon icons
        final SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(getText());
        if (mEmoticonProvider != null)
            EmoticonUtils.replaceWithImages(getContext(), spannableStringBuilder, mEmoticonProvider, mEmoticonSize);
    }
}

3.2 Tạo Object

3.2.1 Emoticon

public final class Emoticon implements Parcelable {

    public static final Creator<Emoticon> CREATOR = new Creator<Emoticon>() {
        @Override
        public Emoticon createFromParcel(Parcel in) {
            return new Emoticon(in);
        }

        @Override
        public Emoticon[] newArray(int size) {
            return new Emoticon[size];
        }
    };
    /**
     * Unicode value of the emoticon.
     */
    @NonNull
    private final String unicode;
    /**
     * Custom icon for the emoticon. (If you don't want to use system default ones.)
     */
    @DrawableRes
    private int icon = -1;

    /**
     * Public constructor.
     *
     * @param unicode Unicode of the emoticon. This cannot be null.
     */
    public Emoticon(@NonNull String unicode) {
        //noinspection ConstantConditions
        if (unicode == null) throw new RuntimeException("Unicode cannot be null.");
        this.unicode = unicode;
    }

    /**
     * Public constructor.
     *
     * @param unicode Unicode of the emoticon. This cannot be null.
     * @param icon    Drawable resource id for the emoticon.
     */

    public Emoticon(@NonNull String unicode, @DrawableRes int icon) {
        this(unicode);
        this.icon = icon;
    }

    /**
     * Constructor for parcelable object.
     */
    public Emoticon(Parcel in) {
        this.icon = in.readInt();
        this.unicode = in.readString();

        //noinspection ConstantConditions
        if (unicode == null) throw new RuntimeException("Unicode cannot be null.");
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(icon);
        dest.writeString(unicode);
    }

    /**
     * @return Drawable resource for the image of emoticon. If there is no icon, it will return -1.
     */
    @DrawableRes
    public int getIcon() {
        return icon;
    }

    /**
     * @return Unicode for the emoticon.
     */
    @NonNull
    public String getUnicode() {
        return unicode;
    }

    @Override
    public boolean equals(Object o) {
        return o == this || (o instanceof Emoticon && unicode.equals(((Emoticon) o).unicode));
    }

    @Override
    public int hashCode() {
        return unicode.hashCode();
    }
}

3.2.2 GIF

public final class Gif implements Parcelable {

    public static final Creator<Gif> CREATOR = new Creator<Gif>() {
        @Override
        public Gif createFromParcel(Parcel in) {
            return new Gif(in);
        }

        @Override
        public Gif[] newArray(int size) {
            return new Gif[size];
        }
    };

    /**
     * Original GIF image url.
     */
    @NonNull
    private final String gifUrl;

    /**
     * Preview GIF url to use as thumbnail.
     */
    @Nullable
    private final String previewGifUrl;

    /**
     * MP4 video url for the image.
     */
    @Nullable
    private final String mp4Url;

    /**
     * Public construction.
     *
     * @param gifUrl        Full scale GIF URL.
     * @param previewGifUrl Preview scale GIF URL.
     * @param mp4Url MP4 video url for the image.
     */
    @SuppressWarnings("ConstantConditions")
    public Gif(@NonNull String gifUrl, @Nullable String previewGifUrl, @Nullable String mp4Url) {
        if (gifUrl == null) throw new IllegalArgumentException("GIF url cannot be null.");

        this.gifUrl = gifUrl;
        this.previewGifUrl = previewGifUrl;
        this.mp4Url = mp4Url;
    }

    /**
     * Public construction.
     *
     * @param gifUrl Full scale GIF URL.
     */
    @SuppressWarnings("ConstantConditions")
    public Gif(@NonNull String gifUrl) {
        if (gifUrl == null) throw new IllegalArgumentException("GIF url cannot be null.");

        this.gifUrl = gifUrl;
        this.previewGifUrl = null;
        this.mp4Url = null;
    }

    /**
     * Constructor for parcelable object.
     */
    public Gif(Parcel in) {
        this.previewGifUrl = in.readString();
        this.gifUrl = in.readString();
        this.mp4Url = in.readString();

        //noinspection ConstantConditions
        if (gifUrl == null) throw new IllegalArgumentException("GIF url cannot be null.");
    }

    /**
     * Get the Url of the preview GIF. If there is no preview url for the GIF, this will return
     * full scale GIF url.
     *
     * @return URL of the preview scale GIF.
     */
    @NonNull
    public String getPreviewGifUrl() {
        return previewGifUrl == null ? gifUrl : previewGifUrl;
    }

    /**
     * @return Full scale GIF URL.
     */
    @NonNull
    public String getGifUrl() {
        return gifUrl;
    }

    @Override
    public boolean equals(Object obj) {
        return obj == this || (obj instanceof Gif && ((Gif) obj).gifUrl.equals(gifUrl));
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(previewGifUrl);
        dest.writeString(gifUrl);
        dest.writeString(mp4Url);
    }

    @Override
    public int hashCode() {
        return gifUrl.hashCode();
    }
}

Để lấy thông tin các ảnh gif từ Giphy về, chúng ta có thể lấy qua các api như sau

interface GiphyApiService {

    String GIPHY_BASE_URL = "http://api.giphy.com/";


    @GET("v1/gifs/trending")
    Call<ResponseBody> getTrending(@Query("api_key") String apiKey,
                                   @Query("limit") int limit,
                                   @Query("fmt") String format);
    @GET("/v1/gifs/search")
    Call<ResponseBody> searchGif(@Query("api_key") String apiKey,
                                 @Query("q") String query,
                                 @Query("limit") int limit,
                                 @Query("fmt") String format);
}

3.3 Emoticon Messenger

3.3.1 EmoticonList

Tạo danh sách các unicode map với các drawable trong project

public class EmoticonList {
    static final HashMap<String, Integer> EMOTICONS = new HashMap<>();

    static {
        EMOTICONS.put("😀", R.drawable.emoji_messenger_1f600);
        EMOTICONS.put("😁", R.drawable.emoji_messenger_1f601);
        EMOTICONS.put("😂", R.drawable.emoji_messenger_1f602);
        EMOTICONS.put("😃", R.drawable.emoji_messenger_1f603);
        EMOTICONS.put("😄", R.drawable.emoji_messenger_1f604);
        EMOTICONS.put("😅", R.drawable.emoji_messenger_1f605);
        ...
        ...
        EMOTICONS.put("🇻🇳", R.drawable.emoji_messenger_1f1fb_1f1f3);
        EMOTICONS.put("🇿🇦", R.drawable.emoji_messenger_1f1ff_1f1e6);
    }
}
        

3.3.2 MessengerEmoticonProvider

public class MessengerEmoticonProvider implements EmoticonProvider {

    private MessengerEmoticonProvider() {
    }

    /**
     * return {@link MessengerEmoticonProvider}
     */
    public static MessengerEmoticonProvider create() {
        return new MessengerEmoticonProvider();
    }

    /**
     * Get the drawable resource for the given unicode.
     *
     * @param unicode Unicode for which icon is required.
     * @return Icon drawable resource id or -1 if there is no drawable for given unicode.
     */
    @Override
    public int getIcon(String unicode) {
        return hasEmoticonIcon(unicode) ? EmoticonList.EMOTICONS.get(unicode) : -1;
    }

    /**
     * Check if the icon pack contains the icon image for given unicode/emoticon?
     *
     * @param unicode Unicode to check.
     * @return True if the icon found else false.
     */
    @Override
    public boolean hasEmoticonIcon(String unicode) {
        return EmoticonList.EMOTICONS.containsKey(unicode);
    }
}

3.4 MainActivity

3.4.1 Layout

<?xml version="1.0" encoding="utf-8"?>
<layout>

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <FrameLayout
            android:id="@+id/keyboard_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true" />

        <RelativeLayout
            android:id="@+id/bottom_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_above="@id/keyboard_container"
            android:elevation="4dp"
            android:gravity="center_vertical"
            android:orientation="horizontal">

            <View
                android:layout_width="match_parent"
                android:layout_height="1dp"
                android:background="#CECECE" />

            <ImageView
                android:id="@+id/emoji_open_close_btn"
                android:layout_width="40dp"
                android:layout_height="40dp"
                android:layout_centerVertical="true"
                android:background="?selectableItemBackground"
                android:padding="6dp"
                android:src="@mipmap/ic_emoticon"
                android:tint="@color/colorPrimary" />

            <ImageView
                android:id="@+id/emoji_send_btn"
                android:layout_width="40dp"
                android:layout_height="40dp"
                android:layout_alignParentEnd="true"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:background="?selectableItemBackground"
                android:padding="6dp"
                android:src="@mipmap/ic_action_name"
                android:tint="@color/colorPrimary" />

            <manhnd.com.demoemotiongif.keyboard.widget.EmoticonEditText
                android:id="@+id/selected_emoticons_et"
                style="@style/Base.TextAppearance.AppCompat.Medium"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginBottom="6dp"
                android:layout_marginEnd="6dp"
                android:layout_marginRight="6dp"
                android:layout_marginTop="6dp"
                android:layout_toEndOf="@id/emoji_open_close_btn"
                android:layout_toLeftOf="@id/emoji_send_btn"
                android:layout_toRightOf="@id/emoji_open_close_btn"
                android:layout_toStartOf="@id/emoji_send_btn"
                android:background="@drawable/bg_search"
                android:padding="10dp"
                app:emojiconSize="30sp" />
        </RelativeLayout>

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_above="@id/bottom_container"
            android:background="#ffffff"
            android:fillViewport="true">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">

                <manhnd.com.demoemotiongif.keyboard.widget.EmoticonTextView
                    android:id="@+id/selected_emoticons_tv"
                    style="@style/Base.TextAppearance.AppCompat.Medium"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:padding="10dp"
                    android:textColor="@android:color/white"
                    app:emojiconSize="30sp" />

                <android.support.v7.widget.AppCompatImageView
                    android:id="@+id/selected_git_iv"
                    android:layout_width="match_parent"
                    android:layout_height="200dp"
                    android:padding="10dp"
                    android:textColor="@android:color/white" />
            </LinearLayout>
        </ScrollView>
    </RelativeLayout>

</layout>

3.4.2 Config

EmoticonConfig

EmoticonGIFKeyboardFragment.EmoticonConfig emoticonConfig = new EmoticonGIFKeyboardFragment.EmoticonConfig()
                .setEmoticonProvider(MessengerEmoticonProvider.create())
                .setEmoticonSelectListener(new EmoticonSelectListener() {

                    @Override
                    public void emoticonSelected(Emoticon emoticon) {
                        Log.d(TAG, "emoticonSelected: " + emoticon.getUnicode());
                        mainBinding.selectedEmoticonsEt.append(emoticon.getUnicode(),
                                mainBinding.selectedEmoticonsEt.getSelectionStart(),
                                mainBinding.selectedEmoticonsEt.getSelectionEnd());
                    }

                    @Override
                    public void onBackSpace() {
                    }
                });

GIFConfig

EmoticonGIFKeyboardFragment.GIFConfig gifConfig = new EmoticonGIFKeyboardFragment
                .GIFConfig(GiphyGifProvider.create(this, "564ce7370bf347f2b7c0e4746593c179"))

                .setGifSelectListener(new GifSelectListener() {
                    @Override
                    public void onGifSelected(@NonNull Gif gif) {
                        //Do something with the selected GIF.
                        Log.d(TAG, "onGifSelected: " + gif.getGifUrl());
                        Glide.with(MainActivity.this)
                                .load(gif.getGifUrl())
                                .asGif()
                                .placeholder(R.mipmap.ic_launcher)
                                .into(mainBinding.selectedGitIv);
                    }
                });

Keyboard Config

 mEmoticonGIFKeyboardFragment = EmoticonGIFKeyboardFragment
                .getNewInstance(findViewById(R.id.keyboard_container), emoticonConfig, gifConfig);
        getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.keyboard_container, mEmoticonGIFKeyboardFragment)
                .commit();
        mEmoticonGIFKeyboardFragment.open();

Bạn cũng có thể cấu hình keyboard hiển thị chỉ emoticon, chỉ gif, hoặc hiển thị cả 2

Only Emoticons Only GIFs

IV. Kết luận

Hiện ứng dụng chỉ dạng sample đơn giản, và có thể chưa thật sự tối ưu và đơn giản, mong mọi người nhiệt tình góp ý Link project: https://github.com/HikaruNguyen/DemoEmoticon Tham khảo thêm tại: https://github.com/kevalpatel2106/EmoticonGIFKeyboard