SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/util/EmojiDecoder.java

281 lines
8.4 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package jp.juggler.subwaytooter.util;
import android.content.Context;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import java.util.ArrayList;
import java.util.regex.Pattern;
import jp.juggler.subwaytooter.R;
import jp.juggler.subwaytooter.api.entity.CustomEmoji;
@SuppressWarnings("WeakerAccess")
public class EmojiDecoder {
private static class DecodeEnv {
@NonNull final Context context;
@NonNull final SpannableStringBuilder sb = new SpannableStringBuilder();
DecodeEnv( @NonNull Context context ){
this.context = context;
}
void addUnicodeString( String s ){
int i = 0;
int end = s.length();
while( i < end ){
int remain = end - i;
String emoji = null;
Integer image_id = null;
for( int j = EmojiMap201709.utf16_max_length ; j > 0 ; -- j ){
if( j > remain ) continue;
String check = s.substring( i, i + j );
image_id = EmojiMap201709.sUTF16ToImageId.get( check );
if( image_id != null ){
if( j < remain && s.charAt( i + j ) == 0xFE0E ){
// 絵文字バリエーション・シーケンスEVSのU+FE0EVS-15が直後にある場合
// その文字を絵文字化しない
emoji = s.substring( i, i + j + 1 );
image_id = 0;
}else{
emoji = check;
}
break;
}
}
if( image_id != null ){
if( image_id == 0 ){
// 絵文字バリエーション・シーケンスEVSのU+FE0EVS-15が直後にある場合
// その文字を絵文字化しない
sb.append( emoji );
}else{
addImageSpan( emoji, image_id );
}
i += emoji.length();
continue;
}
int length = Character.charCount( s.codePointAt( i ) );
if( length == 1 ){
sb.append( s.charAt( i ) );
++ i;
}else{
sb.append( s.substring( i, i + length ) );
i += length;
}
}
}
void addImageSpan( String text, @DrawableRes int res_id ){
int start = sb.length();
sb.append( text );
int end = sb.length();
sb.setSpan( new EmojiImageSpan( context, res_id ), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
}
void addNetworkEmojiSpan( String text, @NonNull String url ){
int start = sb.length();
sb.append( text );
int end = sb.length();
sb.setSpan( new NetworkEmojiSpan( url ), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
}
}
public static boolean isWhitespaceBeforeEmoji( int cp ){
switch( cp ){
case 0x0009: // HORIZONTAL TABULATION
case 0x000A: // LINE FEED
case 0x000B: // VERTICAL TABULATION
case 0x000C: // FORM FEED
case 0x000D: // CARRIAGE RETURN
case 0x001C: // FILE SEPARATOR
case 0x001D: // GROUP SEPARATOR
case 0x001E: // RECORD SEPARATOR
case 0x001F: // UNIT SEPARATOR
case 0x0020:
case 0x00A0: //非区切りスペース
case 0x1680:
case 0x180E:
case 0x2000:
case 0x2001:
case 0x2002:
case 0x2003:
case 0x2004:
case 0x2005:
case 0x2006:
case 0x2007: //非区切りスペース
case 0x2008:
case 0x2009:
case 0x200A:
case 0x200B:
case 0x202F: //非区切りスペース
case 0x205F:
case 0x2060:
case 0x3000:
case 0x3164:
case 0xFEFF:
return true;
default:
return Character.isWhitespace( cp );
}
}
public static boolean isShortCodeCharacter( int cp ){
return ( 'A' <= cp && cp <= 'Z' )
|| ( 'a' <= cp && cp <= 'z' )
|| ( '0' <= cp && cp <= '9' )
|| cp == '-'
|| cp == '+'
|| cp == '_'
;
}
interface ShortCodeSplitterCallback {
void onString( @NonNull String part );
void onShortCode( @NonNull String part, @NonNull String name );
}
static void splitShortCode( @NonNull String s, int start, int end, @NonNull ShortCodeSplitterCallback callback ){
int i = start;
while( i < end ){
// 絵文字パターンの開始位置を探索する
start = i;
while( i < end ){
int c = s.codePointAt( i );
if( c == ':' ){
// ショートコードの手前は始端か改行か空白文字でないとならない
// 空白文字の判定はサーバサイドのそれにあわせる
if( i == 0 || isWhitespaceBeforeEmoji( s.codePointBefore( i ) ) ){
break;
}
}
i += Character.charCount( c );
}
if( i > start ){
callback.onString( s.substring( start, i ) );
}
if( i >= end ) break;
start = i++; // start=コロンの位置 i=その次の位置
int emoji_end = - 1;
while( i < end ){
int c = s.codePointAt( i );
if( c == ':' ){
emoji_end = i;
break;
}
if( ! isShortCodeCharacter( c ) ){
break;
}
i += Character.charCount( c );
}
// 絵文字がみつからなかったら、startの位置のコロンだけを処理して残りは次のループで処理する
if( emoji_end == - 1 || emoji_end - start < 3 ){
callback.onString( ":" );
i = start + 1;
continue;
}
callback.onShortCode(
s.substring( start, emoji_end + 1 ) // ":shortcode:"
, s.substring( start + 1, emoji_end ) // "shortcode"
);
i = emoji_end + 1;// コロンの次の位置
}
}
private static final Pattern reNicoru = Pattern.compile( "\\Anicoru\\d*\\z", Pattern.CASE_INSENSITIVE );
private static final Pattern reHohoemi = Pattern.compile( "\\Ahohoemi\\d*\\z", Pattern.CASE_INSENSITIVE );
public static Spannable decodeEmoji( @NonNull final Context context, @NonNull final String s, @Nullable final CustomEmoji.Map custom_map ){
final DecodeEnv decode_env = new DecodeEnv( context );
splitShortCode( s, 0, s.length(), new ShortCodeSplitterCallback() {
@Override public void onString( @NonNull String part ){
decode_env.addUnicodeString( part );
}
@Override public void onShortCode( @NonNull String part, @NonNull String name ){
EmojiMap201709.EmojiInfo info = EmojiMap201709.sShortNameToImageId.get( name.toLowerCase().replace( '-', '_' ) );
if( info != null ){
decode_env.addImageSpan( part, info.image_id );
return;
}
String url = ( custom_map == null ? null : custom_map.get( name ) );
if( ! TextUtils.isEmpty( url ) ){
decode_env.addNetworkEmojiSpan( part, url );
return;
}
if( reHohoemi.matcher( name ).find() ){
decode_env.addImageSpan( part, R.drawable.emoji_hohoemi );
}else if( reNicoru.matcher( name ).find() ){
decode_env.addImageSpan( part, R.drawable.emoji_nicoru );
}else{
decode_env.addUnicodeString( part );
}
}
} );
return decode_env.sb;
}
// 投稿などの際、表示は不要だがショートコード=>Unicodeの解決を行いたい場合がある
// カスタム絵文字の変換も行わない
public static String decodeShortCode( @NonNull final String s ){
final StringBuilder sb = new StringBuilder();
splitShortCode( s, 0, s.length(), new ShortCodeSplitterCallback() {
@Override public void onString( @NonNull String part ){
sb.append( part );
}
@Override public void onShortCode( @NonNull String part, @NonNull String name ){
EmojiMap201709.EmojiInfo info = EmojiMap201709.sShortNameToImageId.get( name.toLowerCase().replace( '-', '_' ) );
sb.append( info != null ? info.unified : part );
}
} );
return sb.toString();
}
// 入力補完用。絵文字ショートコード一覧を部分一致で絞り込む
static ArrayList< CharSequence > searchShortCode( Context context, String prefix, int limit ){
ArrayList< CharSequence > dst = new ArrayList<>();
for( String shortCode : EmojiMap201709.sShortNameList ){
if( dst.size() >= limit ) break;
if( ! shortCode.contains( prefix ) ) continue;
EmojiMap201709.EmojiInfo info = EmojiMap201709.sShortNameToImageId.get( shortCode );
if( info == null ) continue;
SpannableStringBuilder sb = new SpannableStringBuilder();
sb.append( ' ' );
int start = 0;
int end = sb.length();
sb.setSpan( new EmojiImageSpan( context, info.image_id ), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
sb.append( ' ' );
sb.append( ':' );
sb.append( shortCode );
sb.append( ':' );
dst.add( sb );
}
return dst;
}
}