[Android] Tích hợp Emoticon và ảnh GIF vào ứng dụng chat trong Android
Bài đăng này đã không được cập nhật trong 3 năm
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' |
|
compile 'com.kevalpatel2106:emoticonpack-twitter:1.1' |
||
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
All rights reserved