mirror of
https://github.com/tateisu/SubwayTooter
synced 2025-01-26 16:56:28 +01:00
絵文字データをJavaコードではなくデータファイルから読む
This commit is contained in:
parent
0f490f6a59
commit
7450b837e7
@ -120,6 +120,7 @@
|
||||
<w>renotes</w>
|
||||
<w>repeatly</w>
|
||||
<w>samsung</w>
|
||||
<w>scna</w>
|
||||
<w>sephiroth</w>
|
||||
<w>sharedpref</w>
|
||||
<w>shortcode</w>
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,64 +0,0 @@
|
||||
package jp.juggler.subwaytooter.emoji
|
||||
|
||||
import java.io.*
|
||||
|
||||
class JavaCodeWriter(file: File) : AutoCloseable {
|
||||
|
||||
companion object {
|
||||
const val lineFeed = "\u000a"
|
||||
}
|
||||
|
||||
private val writer = OutputStreamWriter(BufferedOutputStream(FileOutputStream(file)), Charsets.UTF_8)
|
||||
|
||||
private var linesInFunction = 0
|
||||
var functionsCount = 0
|
||||
|
||||
override fun close() {
|
||||
writer.flush()
|
||||
writer.close()
|
||||
}
|
||||
|
||||
fun print(x: String) {
|
||||
writer.write(x, 0, x.length)
|
||||
}
|
||||
|
||||
fun println(x: String) {
|
||||
print(x)
|
||||
print(lineFeed)
|
||||
writer.flush()
|
||||
}
|
||||
|
||||
|
||||
fun addCode(code: String) {
|
||||
// open new function
|
||||
if (linesInFunction == 0) {
|
||||
++functionsCount
|
||||
println("\n\tprivate static void init$functionsCount(EmojiMap e){")
|
||||
}
|
||||
// write code
|
||||
print("\t\t")
|
||||
println(code)
|
||||
|
||||
// close function
|
||||
if (++linesInFunction > 100) {
|
||||
println("\t}")
|
||||
linesInFunction = 0
|
||||
}
|
||||
}
|
||||
|
||||
fun closeFunction() {
|
||||
if (linesInFunction > 0) {
|
||||
println("\t}")
|
||||
linesInFunction = 0
|
||||
}
|
||||
}
|
||||
|
||||
fun writeDefinition(s: String) {
|
||||
println("\t$s")
|
||||
println("")
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@ -749,19 +749,13 @@ class App {
|
||||
}
|
||||
log.w("nameChars: [${nameChars.sorted().joinToString("")}]")
|
||||
|
||||
// JSONコードを出力する
|
||||
val outFile = "EmojiMapInitializer.java"
|
||||
JavaCodeWriter(File(outFile)).use { jcw ->
|
||||
jcw.println(
|
||||
"""
|
||||
package jp.juggler.emoji;
|
||||
|
||||
public final class EmojiMapInitializer {
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val outFile = "emoji_map.txt"
|
||||
UnixPrinter(File(outFile)).use { writer ->
|
||||
for (emoji in emojiMap.values.sortedBy { it.key }) {
|
||||
val codeSet = emoji.codeSet
|
||||
|
||||
val codeSet = emoji.codeSet.sorted()
|
||||
|
||||
// asciiコードだけの絵文字は処理しない
|
||||
if (codeSet.isEmpty()) {
|
||||
log.w("skip emoji ${emoji.unified} ${emoji.resName} that has no valid codes")
|
||||
@ -775,16 +769,15 @@ public final class EmojiMapInitializer {
|
||||
|
||||
// 画像リソースIDとUnicodeシーケンスの関連付けを出力する
|
||||
val strResName = emoji.resName
|
||||
if (File("assets/$strResName.svg").isFile) {
|
||||
writer.println("s1:$strResName.svg//${emoji.imageFiles.first().second}")
|
||||
} else {
|
||||
writer.println("s1d:$strResName//${emoji.imageFiles.first().second}")
|
||||
}
|
||||
codeSet.forEach { code ->
|
||||
val javaChars = code.makeUtf16()
|
||||
|
||||
if(javaChars.isEmpty()) error("too short code! ${emoji.resName}")
|
||||
|
||||
if (File("assets/$strResName.svg").isFile) {
|
||||
jcw.addCode("e.code(\"$javaChars\", \"$strResName.svg\"); // ${code.from} ${emoji.imageFiles.first().second}")
|
||||
} else {
|
||||
jcw.addCode("e.code(\"$javaChars\", R.drawable.$strResName); // ${code.from} ${emoji.imageFiles.first().second}")
|
||||
}
|
||||
val raw = code.toRawString()
|
||||
if(raw.isEmpty()) error("too short code! ${emoji.resName}")
|
||||
writer.println("s2:$raw//${code.from}")
|
||||
}
|
||||
}
|
||||
|
||||
@ -798,39 +791,29 @@ public final class EmojiMapInitializer {
|
||||
// 投稿時にshortcodeをユニコードに変換するため、shortcodeとUTF-16シーケンスの関連付けを出力する
|
||||
for (name in emoji.shortNames.map { it.name }.toSet().sorted()) {
|
||||
val froms = emoji.shortNames.filter { it.name == name }.map { it.cameFrom }.sorted()
|
||||
val javaChars = unified.makeUtf16()
|
||||
jcw.addCode("e.name(\"${name}\", \"$javaChars\"); // ${froms.joinToString(",")}")
|
||||
writer.println("n:$name,${unified.toRawString()}//${froms.joinToString(",")}")
|
||||
}
|
||||
}
|
||||
|
||||
categoryNames.values.forEach { category ->
|
||||
writer.println("c1:${category.enumId}")
|
||||
category.eachEmoji { emoji ->
|
||||
if (emoji.skip) return@eachEmoji
|
||||
val shortName = emoji.shortNames.first()
|
||||
jcw.addCode("e.category( EmojiMap.${category.enumId}, \"${shortName.name}\"); // ${shortName.cameFrom}")
|
||||
writer.println("c2:${shortName.name}//${shortName.cameFrom}")
|
||||
}
|
||||
}
|
||||
|
||||
val enumId = "CATEGORY_OTHER"
|
||||
writer.println("c1:${enumId}")
|
||||
emojiMap.values
|
||||
.filter { it.usedInCategory == null && !it.isToneVariation }
|
||||
.sortedBy { it.shortNames.first() }
|
||||
.forEach { emoji ->
|
||||
if (emoji.skip) return@forEach
|
||||
val enumId = "CATEGORY_OTHER"
|
||||
val shortName = emoji.shortNames.first()
|
||||
jcw.addCode("e.category( EmojiMap.$enumId, \"${shortName.name}\"); // ${shortName.cameFrom}")
|
||||
writer.println("c2:${shortName.name}//${shortName.cameFrom}")
|
||||
}
|
||||
|
||||
jcw.closeFunction()
|
||||
|
||||
jcw.println("\tstatic void initAll(EmojiMap e){")
|
||||
jcw.println("\t\te.utf16MaxLength=$utf16_max_length;")
|
||||
for (i in 1..jcw.functionsCount) {
|
||||
jcw.println("\t\tinit$i(e);")
|
||||
}
|
||||
jcw.println("\t}")
|
||||
|
||||
jcw.println("}")
|
||||
}
|
||||
|
||||
log.d("wrote $outFile")
|
||||
|
@ -423,6 +423,8 @@ class ActMain : AsyncActivity(), View.OnClickListener,
|
||||
app_state = App1.getAppState(this)
|
||||
pref = App1.pref
|
||||
|
||||
EmojiDecoder.handleUnicodeEmoji = Pref.bpInAppUnicodeEmoji(pref)
|
||||
|
||||
density = app_state.density
|
||||
acct_pad_lr = (0.5f + 4f * density).toInt()
|
||||
|
||||
|
@ -18,6 +18,7 @@ import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
||||
import com.bumptech.glide.load.engine.executor.GlideExecutor
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import jp.juggler.emoji.EmojiMap
|
||||
import jp.juggler.subwaytooter.api.TootApiClient
|
||||
import jp.juggler.subwaytooter.table.*
|
||||
import jp.juggler.subwaytooter.util.CustomEmojiCache
|
||||
@ -289,8 +290,11 @@ class App1 : Application() {
|
||||
fun prepare(app_context : Context, caller : String) : AppState {
|
||||
var state = appStateX
|
||||
if(state != null) return state
|
||||
|
||||
|
||||
log.d("initialize AppState. caller=$caller")
|
||||
|
||||
// initialize EmojiMap
|
||||
EmojiMap.load(app_context)
|
||||
|
||||
// initialize Conscrypt
|
||||
Security.insertProviderAt(
|
||||
|
@ -766,6 +766,7 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
|
||||
sw(Pref.bpEmojioneShortcode, R.string.emojione_shortcode_support) {
|
||||
desc = R.string.emojione_shortcode_support_desc
|
||||
}
|
||||
sw(Pref.bpInAppUnicodeEmoji, R.string.in_app_unicode_emoji)
|
||||
}
|
||||
|
||||
section(R.string.color) {
|
||||
|
@ -2630,7 +2630,7 @@ internal class ItemViewHolder(
|
||||
|
||||
if (code == null) {
|
||||
EmojiPicker(activity, access_info, closeOnSelected = true) { name, instance, _, _, _ ->
|
||||
val item = EmojiMap.sMap.shortNameToEmojiInfo[name]
|
||||
val item = EmojiMap.shortNameToEmojiInfo[name]
|
||||
val newCode = if (item == null || instance != null) {
|
||||
":$name:"
|
||||
} else {
|
||||
|
@ -445,6 +445,11 @@ object Pref {
|
||||
false
|
||||
)
|
||||
|
||||
val bpInAppUnicodeEmoji = BooleanPref(
|
||||
"InAppUnicodeEmoji",
|
||||
true
|
||||
)
|
||||
|
||||
// int
|
||||
|
||||
val ipBackButtonAction = IntPref("back_button_action", 0)
|
||||
|
@ -387,7 +387,7 @@ fun SpannableStringBuilder.appendMisskeyReaction(
|
||||
val end = this.length
|
||||
|
||||
this.setSpan(
|
||||
EmojiMap.sMap.utf16ToEmojiResource[emojiUtf16] !!.createSpan(context),
|
||||
EmojiMap.utf16ToEmojiResource[emojiUtf16] !!.createSpan(context),
|
||||
start, end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
|
@ -326,7 +326,7 @@ internal class ViewHolderHeaderProfile(
|
||||
|
||||
if(whoDetail?.locked ?: who.locked) {
|
||||
append(" ")
|
||||
val info = EmojiMap.sMap.shortNameToEmojiInfo["lock"]
|
||||
val info = EmojiMap.shortNameToEmojiInfo["lock"]
|
||||
if(info != null) {
|
||||
appendSpan("locked", info.er.createSpan(activity))
|
||||
} else {
|
||||
@ -336,7 +336,7 @@ internal class ViewHolderHeaderProfile(
|
||||
|
||||
if(who.bot) {
|
||||
append(" ")
|
||||
val info = EmojiMap.sMap.shortNameToEmojiInfo["robot_face"]
|
||||
val info = EmojiMap.shortNameToEmojiInfo["robot_face"]
|
||||
if(info != null) {
|
||||
appendSpan("bot", info.er.createSpan(activity))
|
||||
} else {
|
||||
@ -346,7 +346,7 @@ internal class ViewHolderHeaderProfile(
|
||||
|
||||
if(who.suspended) {
|
||||
append(" ")
|
||||
val info = EmojiMap.sMap.shortNameToEmojiInfo["x"]
|
||||
val info = EmojiMap.shortNameToEmojiInfo["x"]
|
||||
if(info != null) {
|
||||
appendSpan("suspended", info.er.createSpan(activity))
|
||||
} else {
|
||||
|
@ -1324,7 +1324,7 @@ object Action_Toot {
|
||||
if (code == null) {
|
||||
if (!bSet) error("will not happen")
|
||||
EmojiPicker(activity, access_info, closeOnSelected = true) { name, instance, _, _, _ ->
|
||||
val item = EmojiMap.sMap.shortNameToEmojiInfo[name]
|
||||
val item = EmojiMap.shortNameToEmojiInfo[name]
|
||||
val newCode = if (item == null || instance != null) {
|
||||
":$name:"
|
||||
} else {
|
||||
|
@ -334,7 +334,7 @@ class EmojiPicker(
|
||||
val tone = viewRoot.findViewById<View>(selected_tone).tag as SkinTone
|
||||
for(suffix in tone.suffix_list) {
|
||||
val new_name = name + suffix
|
||||
val info = EmojiMap.sMap.shortNameToEmojiInfo[new_name]
|
||||
val info = EmojiMap.shortNameToEmojiInfo[new_name]
|
||||
if(info != null) return new_name
|
||||
}
|
||||
return name
|
||||
@ -383,7 +383,7 @@ class EmojiPicker(
|
||||
}
|
||||
|
||||
else -> ArrayList<EmojiItem>().apply {
|
||||
EmojiMap.sMap.categoryMap.get(category_id)?.emoji_list?.forEach { name ->
|
||||
EmojiMap.categoryMap.get(category_id)?.emoji_list?.forEach { name ->
|
||||
add(EmojiItem(name, null))
|
||||
}
|
||||
}
|
||||
@ -481,8 +481,10 @@ class EmojiPicker(
|
||||
item.name
|
||||
}
|
||||
|
||||
val info = EmojiMap.sMap.shortNameToEmojiInfo[name]
|
||||
if(info != null) {
|
||||
val info = EmojiMap.shortNameToEmojiInfo[name]
|
||||
if(info==null){
|
||||
log.w("missing emoji for $name")
|
||||
}else {
|
||||
val er = info.er
|
||||
if(er.isSvg) {
|
||||
Glide.with(activity)
|
||||
@ -520,11 +522,11 @@ class EmojiPicker(
|
||||
selected(name, item.instance, customEmoji = emoji_url_map[item.name])
|
||||
} else {
|
||||
// 普通の絵文字
|
||||
var ei = EmojiMap.sMap.shortNameToEmojiInfo[name] ?: return
|
||||
var ei = EmojiMap.shortNameToEmojiInfo[name] ?: return
|
||||
|
||||
if(page.hasSkinTone) {
|
||||
val sv = applySkinTone(name)
|
||||
val tmp = EmojiMap.sMap.shortNameToEmojiInfo[sv]
|
||||
val tmp = EmojiMap.shortNameToEmojiInfo[sv]
|
||||
if(tmp != null) {
|
||||
ei = tmp
|
||||
name = sv
|
||||
|
@ -32,6 +32,8 @@ object EmojiDecoder {
|
||||
|
||||
private const val cpZwsp = '\u200B'.toInt()
|
||||
|
||||
var handleUnicodeEmoji = true
|
||||
|
||||
fun customEmojiSeparator(pref: SharedPreferences) = if (Pref.bpCustomEmojiSeparatorZwsp(pref)) {
|
||||
'\u200B'
|
||||
} else {
|
||||
@ -184,13 +186,20 @@ object EmojiDecoder {
|
||||
}
|
||||
|
||||
fun addUnicodeString(s: String) {
|
||||
|
||||
if(!handleUnicodeEmoji){
|
||||
openNormalText()
|
||||
sb.append(s)
|
||||
return
|
||||
}
|
||||
|
||||
var i = 0
|
||||
val end = s.length
|
||||
|
||||
// 絵文字ではない部分をコピーする
|
||||
fun normalCopy(initialJ: Int): Boolean {
|
||||
var j = initialJ
|
||||
while (j < end && !EmojiMap.sMap.isStartChar(s[j])) {
|
||||
while (j < end && !EmojiMap.isStartChar(s[j])) {
|
||||
j += min(end - j, Character.charCount(s.codePointAt(j)))
|
||||
}
|
||||
if (j <= i) return false
|
||||
@ -207,7 +216,7 @@ object EmojiDecoder {
|
||||
if (normalCopy(i) && i >= end) break
|
||||
|
||||
// 絵文字コードを探索
|
||||
val result = EmojiMap.sMap.utf16Trie.get(s,i,end)
|
||||
val result = EmojiMap.utf16Trie.get(s,i,end)
|
||||
if (result == null) {
|
||||
// 見つからなかったら、通常テキストを1文字以上コピーする
|
||||
normalCopy(i + min(end-i, Character.charCount(s.codePointAt(i))))
|
||||
@ -268,7 +277,6 @@ object EmojiDecoder {
|
||||
val urlList = ArrayList<IntRange>().apply {
|
||||
val m = reUrl.matcher(s)
|
||||
while (m.find()) {
|
||||
log.d("urlList ${m.start()}..${m.end()}")
|
||||
add(m.start()..m.end())
|
||||
}
|
||||
}
|
||||
@ -374,7 +382,7 @@ object EmojiDecoder {
|
||||
// 通常の絵文字
|
||||
if (useEmojioneShortcode) {
|
||||
val info =
|
||||
EmojiMap.sMap.shortNameToEmojiInfo[name.toLowerCase(Locale.JAPAN).replace('-', '_')]
|
||||
EmojiMap.shortNameToEmojiInfo[name.toLowerCase(Locale.JAPAN).replace('-', '_')]
|
||||
if (info != null) {
|
||||
builder.addImageSpan(part, info.er)
|
||||
return
|
||||
@ -426,7 +434,7 @@ object EmojiDecoder {
|
||||
|
||||
// カスタム絵文字ではなく通常の絵文字のショートコードなら絵文字に変換する
|
||||
val info =
|
||||
EmojiMap.sMap.shortNameToEmojiInfo[name.toLowerCase(Locale.JAPAN).replace('-', '_')]
|
||||
EmojiMap.shortNameToEmojiInfo[name.toLowerCase(Locale.JAPAN).replace('-', '_')]
|
||||
sb.append(info?.unified ?: part)
|
||||
}
|
||||
})
|
||||
@ -441,11 +449,11 @@ object EmojiDecoder {
|
||||
limit: Int
|
||||
): ArrayList<CharSequence> {
|
||||
val dst = ArrayList<CharSequence>()
|
||||
for (shortCode in EmojiMap.sMap.shortNameList) {
|
||||
for (shortCode in EmojiMap.shortNameList) {
|
||||
if (dst.size >= limit) break
|
||||
if (!shortCode.contains(prefix)) continue
|
||||
|
||||
val info = EmojiMap.sMap.shortNameToEmojiInfo[shortCode] ?: continue
|
||||
val info = EmojiMap.shortNameToEmojiInfo[shortCode] ?: continue
|
||||
|
||||
val sb = SpannableStringBuilder()
|
||||
val start = 0
|
||||
|
@ -1026,7 +1026,7 @@ class PostHelper(
|
||||
bInstanceHasCustomEmoji: Boolean
|
||||
): SpannableStringBuilder {
|
||||
|
||||
val item = EmojiMap.sMap.shortNameToEmojiInfo[name]
|
||||
val item = EmojiMap.shortNameToEmojiInfo[name]
|
||||
val separator = EmojiDecoder.customEmojiSeparator(pref)
|
||||
if (item == null || instance != null) {
|
||||
// カスタム絵文字は常にshortcode表現
|
||||
|
@ -1071,5 +1071,6 @@
|
||||
<string name="visibility_limited">限定(サークル)</string>
|
||||
<string name="visibility_mutual">相互</string>
|
||||
<string name="server_has_no_support_of_visibility">公開範囲\'%1$s\'はこのサーバで使えません</string>
|
||||
<string name="in_app_unicode_emoji">アプリ内蔵のUnicode絵文字を使う(アプリ再起動が必要)</string>
|
||||
|
||||
</resources>
|
||||
|
@ -1085,4 +1085,5 @@
|
||||
<string name="visibility_mutual">Mutual</string>
|
||||
<string name="server_has_no_support_of_visibility">Server has no support of visibility \'%1$s\'.</string>
|
||||
<string name="show_emoji_picker_other_category">Show emoji picker other category</string>
|
||||
<string name="in_app_unicode_emoji">use in-app Unicode Emoji (app restart required)</string>
|
||||
</resources>
|
||||
|
@ -28,12 +28,6 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation "junit:junit:$junit_version"
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation "androidx.appcompat:appcompat:$appcompat_version"
|
||||
}
|
||||
|
17894
emoji/src/main/assets/emoji_map.txt
Normal file
17894
emoji/src/main/assets/emoji_map.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,140 +0,0 @@
|
||||
package jp.juggler.emoji;
|
||||
|
||||
import android.util.SparseArray;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
|
||||
public class EmojiMap {
|
||||
|
||||
public static final int CATEGORY_PEOPLE = 0;
|
||||
public static final int CATEGORY_NATURE = 1;
|
||||
public static final int CATEGORY_FOODS = 2;
|
||||
public static final int CATEGORY_ACTIVITY = 3;
|
||||
public static final int CATEGORY_PLACES = 4;
|
||||
public static final int CATEGORY_OBJECTS = 5;
|
||||
public static final int CATEGORY_SYMBOLS = 6;
|
||||
public static final int CATEGORY_FLAGS = 7;
|
||||
public static final int CATEGORY_OTHER = 8;
|
||||
|
||||
|
||||
public static class Category {
|
||||
public final int category_id; // this is String resource id;
|
||||
public final ArrayList< String > emoji_list = new ArrayList<>();
|
||||
|
||||
public Category( int category_id ){
|
||||
this.category_id = category_id;
|
||||
}
|
||||
}
|
||||
|
||||
public static class EmojiResource {
|
||||
@DrawableRes
|
||||
public final int drawableId;
|
||||
@Nullable
|
||||
public final String assetsName;
|
||||
|
||||
public EmojiResource( @DrawableRes int drawableId ){
|
||||
this.drawableId = drawableId;
|
||||
this.assetsName = null;
|
||||
}
|
||||
|
||||
EmojiResource( @NonNull String assetsName ){
|
||||
this.drawableId = 0;
|
||||
this.assetsName = assetsName;
|
||||
}
|
||||
|
||||
public boolean isSvg(){
|
||||
return assetsName != null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class EmojiInfo {
|
||||
@NonNull public final String unified;
|
||||
@NonNull public final EmojiResource er;
|
||||
|
||||
public EmojiInfo( @NonNull String unified, @NonNull EmojiResource er ){
|
||||
this.unified = unified;
|
||||
this.er = er;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 表示に使う。絵文字のユニコードシーケンスから画像リソースIDへのマップ
|
||||
public int utf16MaxLength;
|
||||
|
||||
public final HashMap< String, EmojiResource> utf16ToEmojiResource = new HashMap<>();
|
||||
|
||||
public final EmojiTrie<EmojiResource> utf16Trie = new EmojiTrie<>();
|
||||
|
||||
// 表示と投稿に使う。絵文字のショートコードから画像リソースIDとユニコードシーケンスへのマップ
|
||||
public final HashMap< String, EmojiInfo> shortNameToEmojiInfo = new HashMap<>();
|
||||
|
||||
// 入力補完に使う。絵文字のショートコードのソートされたリスト
|
||||
public final ArrayList< String > shortNameList = new ArrayList<>();
|
||||
|
||||
// ピッカーに使う。カテゴリのリスト
|
||||
public final SparseArray<Category> categoryMap = new SparseArray<>();
|
||||
|
||||
// 素の数字とcopyright,registered, trademark は絵文字にしない
|
||||
private boolean isIgnored( @NonNull String code ){
|
||||
int c = code.charAt( 0 );
|
||||
return code.length() == 1 && c <= 0xae || c == 0x2122;
|
||||
}
|
||||
|
||||
// PNG
|
||||
// private void code( @NonNull String code, @DrawableRes int resId ){
|
||||
// if( isIgnored( code ) ) return;
|
||||
// EmojiResource res = new EmojiResource( resId );
|
||||
// utf16ToEmojiResource.put( code, res);
|
||||
// utf16Trie.append(code,0,res);
|
||||
// }
|
||||
|
||||
// Assets
|
||||
void code(@NonNull String code, @NonNull String assetsName ){
|
||||
if( isIgnored( code ) ) return;
|
||||
EmojiResource res = new EmojiResource( assetsName );
|
||||
utf16ToEmojiResource.put( code, res);
|
||||
utf16Trie.append(code,0,res);
|
||||
}
|
||||
|
||||
void name( @NonNull String name, @NonNull String unified ){
|
||||
if( isIgnored( unified ) ) return;
|
||||
EmojiResource er = utf16ToEmojiResource.get( unified );
|
||||
if( er == null ) throw new IllegalStateException( "missing emoji for code " + unified );
|
||||
shortNameToEmojiInfo.put( name, new EmojiInfo( unified, er ) );
|
||||
shortNameList.add( name );
|
||||
}
|
||||
|
||||
void category(@StringRes int string_id, @NonNull String name ){
|
||||
EmojiMap.Category c = categoryMap.get( string_id );
|
||||
if( c == null ){
|
||||
c = new EmojiMap.Category( string_id );
|
||||
categoryMap.put( string_id, c );
|
||||
}
|
||||
c.emoji_list.add( name );
|
||||
}
|
||||
|
||||
private EmojiMap(){
|
||||
EmojiMapInitializer.initAll(this);
|
||||
Collections.sort( shortNameList );
|
||||
}
|
||||
|
||||
public static final EmojiMap sMap = new EmojiMap();
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
|
||||
public boolean isStartChar(char c) {
|
||||
return utf16Trie.hasNext(c);
|
||||
}
|
||||
|
||||
}
|
||||
|
190
emoji/src/main/java/jp/juggler/emoji/EmojiMap.kt
Normal file
190
emoji/src/main/java/jp/juggler/emoji/EmojiMap.kt
Normal file
@ -0,0 +1,190 @@
|
||||
package jp.juggler.emoji
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.AssetManager
|
||||
import android.util.Log
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import java.io.EOFException
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
|
||||
object EmojiMap {
|
||||
|
||||
const val CATEGORY_PEOPLE = 0
|
||||
const val CATEGORY_NATURE = 1
|
||||
const val CATEGORY_FOODS = 2
|
||||
const val CATEGORY_ACTIVITY = 3
|
||||
const val CATEGORY_PLACES = 4
|
||||
const val CATEGORY_OBJECTS = 5
|
||||
const val CATEGORY_SYMBOLS = 6
|
||||
const val CATEGORY_FLAGS = 7
|
||||
const val CATEGORY_OTHER = 8
|
||||
|
||||
class Category{
|
||||
val emoji_list = ArrayList<String>()
|
||||
}
|
||||
|
||||
class EmojiResource(
|
||||
// SVGの場合はasset resourceの名前
|
||||
val assetsName: String? = null,
|
||||
// PNGの場合はdrawable id
|
||||
@DrawableRes val drawableId: Int =0,
|
||||
){
|
||||
val isSvg: Boolean
|
||||
get() = assetsName != null
|
||||
}
|
||||
|
||||
class EmojiInfo(val unified: String, val er: EmojiResource)
|
||||
|
||||
// 表示に使う。絵文字のユニコードシーケンスから画像リソースIDへのマップ
|
||||
val utf16ToEmojiResource = HashMap<String, EmojiResource>()
|
||||
val utf16Trie = EmojiTrie<EmojiResource>()
|
||||
|
||||
// 表示と投稿に使う。絵文字のショートコードから画像リソースIDとユニコードシーケンスへのマップ
|
||||
val shortNameToEmojiInfo = HashMap<String, EmojiInfo>()
|
||||
|
||||
// 入力補完に使う。絵文字のショートコードのソートされたリスト
|
||||
val shortNameList = ArrayList<String>()
|
||||
|
||||
// ピッカーに使う。カテゴリのリスト
|
||||
val categoryMap = SparseArrayCompat<Category>()
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
|
||||
private fun readStream(assetManager:AssetManager, inStream:InputStream){
|
||||
|
||||
val categoryNameMap = HashMap<String,Category>().apply{
|
||||
fun a(@StringRes id:Int,name:String){
|
||||
val c = categoryMap.get(id)
|
||||
?: Category().also{ categoryMap.put(id,it)}
|
||||
put(name, c)
|
||||
}
|
||||
a(CATEGORY_PEOPLE,"CATEGORY_PEOPLE")
|
||||
a(CATEGORY_NATURE,"CATEGORY_NATURE")
|
||||
a(CATEGORY_FOODS,"CATEGORY_FOODS")
|
||||
a(CATEGORY_ACTIVITY,"CATEGORY_ACTIVITY")
|
||||
a(CATEGORY_PLACES,"CATEGORY_PLACES")
|
||||
a(CATEGORY_OBJECTS,"CATEGORY_OBJECTS")
|
||||
a(CATEGORY_SYMBOLS,"CATEGORY_SYMBOLS")
|
||||
a(CATEGORY_FLAGS,"CATEGORY_FLAGS")
|
||||
a(CATEGORY_OTHER,"CATEGORY_OTHER")
|
||||
}
|
||||
|
||||
// 素の数字とcopyright,registered, trademark は絵文字にしない
|
||||
fun isIgnored(code: String): Boolean {
|
||||
val c = code[0].toInt()
|
||||
return code.length == 1 && c <= 0xae || c == 0x2122
|
||||
}
|
||||
|
||||
fun addCode(code: String, er:EmojiResource) {
|
||||
if (isIgnored(code)) return
|
||||
utf16ToEmojiResource[code] = er
|
||||
utf16Trie.append(code, 0, er)
|
||||
}
|
||||
|
||||
// private fun addCode(code: String, @DrawableRes resId:Int ) =
|
||||
// addCode(code,EmojiResource(resId))
|
||||
//
|
||||
// private fun addCode(code: String, assetsName: String) =
|
||||
// addCode(code,EmojiResource(assetsName))
|
||||
|
||||
fun addName(name: String, unified: String) {
|
||||
if (isIgnored(unified)) return
|
||||
val er = utf16ToEmojiResource[unified]
|
||||
?: throw IllegalStateException("missing emoji for code $unified")
|
||||
shortNameToEmojiInfo[name] = EmojiInfo(unified, er)
|
||||
shortNameList.add(name)
|
||||
}
|
||||
|
||||
fun addCategoryItem(name: String,category:Category) {
|
||||
category.emoji_list.add(name)
|
||||
}
|
||||
|
||||
|
||||
val reComment="""\s*//.*""".toRegex()
|
||||
val reLineHeader="""\A(\w+):""".toRegex()
|
||||
val assetsSet = assetManager.list("")!!.toSet()
|
||||
var lastEmoji : EmojiResource? = null
|
||||
var lastCategory:Category? = null
|
||||
var lno = 0
|
||||
fun readEmojiDataLine(rawLine:String){
|
||||
++lno
|
||||
var line = rawLine.replace(reComment, "").trim()
|
||||
val head = reLineHeader.find(line)?.groupValues?.elementAtOrNull(1)
|
||||
?: error("missing line header. line=$lno $line")
|
||||
line = line.substring(head.length + 1)
|
||||
try {
|
||||
when (head) {
|
||||
"s1" -> {
|
||||
if (!assetsSet.contains(line)) error("missing assets. line=$lno $line")
|
||||
lastEmoji = EmojiResource(assetsName = line)
|
||||
}
|
||||
"s2" -> addCode(
|
||||
line,
|
||||
lastEmoji
|
||||
?: error("missing lastEmoji. line=$lno $line")
|
||||
)
|
||||
"n" -> {
|
||||
val cols = line.split(",", limit = 2)
|
||||
if (cols.size != 2) error("invalid name,code. line=$lno $line")
|
||||
addName(cols[0], cols[1])
|
||||
}
|
||||
"c1" -> {
|
||||
lastCategory = categoryNameMap[line]
|
||||
?: error("missing category name. line=$lno $line")
|
||||
}
|
||||
"c2" -> addCategoryItem(
|
||||
line,
|
||||
lastCategory
|
||||
?: error("missing lastCategory. line=$lno $line")
|
||||
)
|
||||
}
|
||||
}catch(ex:Throwable){
|
||||
Log.e("SubwayTooter", "EmojiMap load error: lno=$lno line=$line",ex)
|
||||
}
|
||||
}
|
||||
|
||||
val lineFeed = 0x0a.toByte()
|
||||
val buffer = ByteArray(4096)
|
||||
var used = inStream.read(buffer,0,buffer.size)
|
||||
if(used <=0) throw EOFException("unexpected EOF")
|
||||
while(true) {
|
||||
var lineStart = 0
|
||||
while (lineStart < used) {
|
||||
var i = lineStart
|
||||
while (i < used && buffer[i] != lineFeed) ++i
|
||||
if (i >= used) break
|
||||
if (i > lineStart) {
|
||||
val line = String(buffer, lineStart, i - lineStart, Charsets.UTF_8)
|
||||
readEmojiDataLine(line)
|
||||
}
|
||||
lineStart = i + 1
|
||||
}
|
||||
buffer.copyInto(buffer, 0, lineStart, used)
|
||||
used -= lineStart
|
||||
val nRead = inStream.read(buffer, used, buffer.size - used)
|
||||
if (nRead <= 0) {
|
||||
if (used > 0) throw EOFException("unexpected EOF")
|
||||
break
|
||||
}
|
||||
used += nRead
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun load(appContext: Context){
|
||||
val assetManager = appContext.assets !!
|
||||
assetManager.open("emoji_map.txt").use{
|
||||
readStream(assetManager,it)
|
||||
}
|
||||
shortNameList.sort()
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
|
||||
fun isStartChar(c: Char): Boolean {
|
||||
return utf16Trie.hasNext(c)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -2,9 +2,10 @@ package jp.juggler.emoji
|
||||
|
||||
import androidx.collection.SparseArrayCompat
|
||||
|
||||
data class EmojiTrieResult<T>(val data: T, val endPos: Int)
|
||||
|
||||
class EmojiTrie<T> {
|
||||
|
||||
data class Result<T>(val data: T, val endPos: Int)
|
||||
|
||||
var data: T? = null
|
||||
val map = SparseArrayCompat<EmojiTrie<T>>()
|
||||
|
||||
@ -21,13 +22,11 @@ class EmojiTrie<T> {
|
||||
|
||||
fun hasNext(c: Char) = map.containsKey(c.toInt())
|
||||
|
||||
fun get(src: String, offset: Int, end: Int): EmojiTrieResult<T>? {
|
||||
fun get(src: String, offset: Int, end: Int): Result<T>? {
|
||||
// 長い方を優先するので、先に子を調べる
|
||||
if (offset < end)
|
||||
map[src[offset].toInt()]?.get(src, offset + 1, end)
|
||||
?.let { return it }
|
||||
return this.data?.let { EmojiTrieResult(it, offset) }
|
||||
return this.data?.let { Result(it, offset) }
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user