diff --git a/twidere/build.gradle b/twidere/build.gradle index f608d8983..12483ee47 100644 --- a/twidere/build.gradle +++ b/twidere/build.gradle @@ -165,9 +165,10 @@ dependencies { compile 'com.github.mariotaku.ObjectCursor:core:0.9.9' compile 'com.github.mariotaku:MultiValueSwitch:0.9.7' compile 'com.github.mariotaku:AbstractTask:0.9.4' - compile 'com.github.mariotaku.CommonsLibrary:parcel:0.9.9-SNAPSHOT' - compile 'com.github.mariotaku.CommonsLibrary:io:0.9.9-SNAPSHOT' - compile 'com.github.mariotaku.CommonsLibrary:text:0.9.9-SNAPSHOT' + compile 'com.github.mariotaku.CommonsLibrary:parcel:0.9.9' + compile 'com.github.mariotaku.CommonsLibrary:io:0.9.9' + compile 'com.github.mariotaku.CommonsLibrary:text:0.9.9' + compile 'com.github.mariotaku.CommonsLibrary:text-kotlin:0.9.9' compile 'com.github.mariotaku:KPreferences:0.9.1' compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile 'nl.komponents.kovenant:kovenant:3.3.0' diff --git a/twidere/src/main/java/org/mariotaku/twidere/util/CodePointArray.java b/twidere/src/main/java/org/mariotaku/twidere/util/CodePointArray.java deleted file mode 100644 index bd1c00380..000000000 --- a/twidere/src/main/java/org/mariotaku/twidere/util/CodePointArray.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Twidere - Twitter client for Android - * - * Copyright (C) 2012-2015 Mariotaku Lee - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.mariotaku.twidere.util; - -import android.support.annotation.NonNull; - -import org.apache.commons.lang3.ArrayUtils; - -/** - * Created by mariotaku on 15/11/4. - */ -public final class CodePointArray { - - private final int[] codePoints; - private final int length; - - CodePointArray(int[] codePoints, int length) { - this.codePoints = codePoints; - this.length = length; - } - - public CodePointArray(@NonNull final CharSequence cs) { - final int inputLength = cs.length(); - codePoints = new int[inputLength]; - int codePointsLength = 0; - for (int offset = 0; offset < inputLength; ) { - final int codePoint = Character.codePointAt(cs, offset); - codePoints[codePointsLength++] = codePoint; - offset += Character.charCount(codePoint); - } - this.length = codePointsLength; - } - - public int get(int pos) { - return codePoints[pos]; - } - - public int length() { - return length; - } - - public int indexOfText(int codePoint, int start) { - int index = 0; - for (int i = 0; i < length; i++) { - final int current = codePoints[i]; - if (current == codePoint && i >= start) return index; - index += Character.charCount(current); - } - return -1; - } - - @NonNull - public String substring(int start, int end) { - final StringBuilder sb = new StringBuilder(); - for (int i = start; i < end; i++) { - sb.appendCodePoint(codePoints[i]); - } - return sb.toString(); - } - - @NonNull - public CodePointArray subCodePointArray(int start, int end) { - return new CodePointArray(ArrayUtils.subarray(codePoints, start, end), end - start); - } - - public int[] subarray(int start, int end) { - return ArrayUtils.subarray(codePoints, start, end); - } - - @Override - public String toString() { - return substring(0, length); - } - - public int charCount(int start, int end) { - int count = 0; - for (int i = start; i < end; i++) { - count += Character.charCount(codePoints[i]); - } - return count; - } -} diff --git a/twidere/src/main/java/org/mariotaku/twidere/util/EmojiSupportUtils.java b/twidere/src/main/java/org/mariotaku/twidere/util/EmojiSupportUtils.java deleted file mode 100644 index eee520b65..000000000 --- a/twidere/src/main/java/org/mariotaku/twidere/util/EmojiSupportUtils.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Twidere - Twitter client for Android - * - * Copyright (C) 2012-2015 Mariotaku Lee - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.mariotaku.twidere.util; - -import android.graphics.drawable.Drawable; -import android.support.annotation.NonNull; -import android.text.Spannable; -import android.text.Spanned; -import android.widget.TextView; - -import org.mariotaku.twidere.text.style.EmojiSpan; -import org.mariotaku.twidere.text.util.EmojiEditableFactory; -import org.mariotaku.twidere.text.util.EmojiSpannableFactory; - -import kotlin.ranges.RangesKt; - -/** - * Created by mariotaku on 15/12/20. - */ -public class EmojiSupportUtils { - - private EmojiSupportUtils() { - } - - public static void initForTextView(TextView textView) { - if (textView.isInEditMode()) return; - textView.setSpannableFactory(new EmojiSpannableFactory(textView)); - textView.setEditableFactory(new EmojiEditableFactory(textView)); - } - - public static void applyEmoji(ExternalThemeManager manager, @NonNull Spannable text) { - applyEmoji(manager, text, 0, text.length()); - } - - public static void applyEmoji(ExternalThemeManager manager, @NonNull Spannable text, - int textStart, int textLength) { - final ExternalThemeManager.Emoji emoji = manager.getEmoji(); - if (emoji == null || !emoji.isSupported()) return; - final CodePointArray array = new CodePointArray(text); - for (int arrayIdx = array.length() - 1; arrayIdx >= 0; arrayIdx--) { - final int codePoint = array.get(arrayIdx); - if (isEmoji(codePoint)) { - int arrayEnd = arrayIdx + 1, arrayIdxOffset = 0; - int textIdx = array.indexOfText(codePoint, arrayIdx), textIdxOffset = 0; - int skippedIndex = 0; - if (textIdx == -1 || textIdx < textStart) { - continue; - } - final int textEnd = textIdx + Character.charCount(codePoint); - if (arrayIdx > 0) { - final int prevCodePoint = array.get(arrayIdx - 1); - if (isRegionalIndicatorSymbol(codePoint)) { - if (isRegionalIndicatorSymbol(prevCodePoint)) { - arrayIdxOffset = -1; - textIdxOffset = -Character.charCount(prevCodePoint); - skippedIndex = -1; - } - } else if (isModifier(codePoint)) { - if (isEmoji(prevCodePoint)) { - arrayIdxOffset = -1; - textIdxOffset = -Character.charCount(prevCodePoint); - skippedIndex = -1; - } - } else if (isKeyCap(codePoint)) { - if (isPhoneNumberSymbol(prevCodePoint)) { - arrayIdxOffset = -1; - textIdxOffset = -Character.charCount(prevCodePoint); - skippedIndex = -1; - } - } else if (isZeroWidthJoin(prevCodePoint)) { - int notValidControlCount = 0; - int charCount = 0; - for (int i = arrayIdx - 1; i >= 0; i--) { - final int cp = array.get(i); - charCount += Character.charCount(cp); - if (isZeroWidthJoin(cp) || isVariationSelector(cp)) { - // Ignore - notValidControlCount = 0; - continue; - } - notValidControlCount++; - if (notValidControlCount > 1 || i == 0) { - arrayIdxOffset = i - arrayIdx + 1; - textIdxOffset = -charCount + Character.charCount(cp); - skippedIndex = i - arrayIdx + 1; - break; - } - } - } - } - if (textEnd > textStart + textLength) continue; - EmojiSpan[] spans = text.getSpans(textIdx + textIdxOffset, textEnd, EmojiSpan.class); - if (spans.length == 0) { - Drawable drawable = emoji.getEmojiDrawableFor(array.subarray(arrayIdx + arrayIdxOffset, - arrayEnd)); - if (drawable == null) { - // Not emoji combination, just use fallback - textIdxOffset = 0; - arrayIdxOffset = 0; - skippedIndex = 0; - spans = text.getSpans(textIdx + textIdxOffset, textEnd, EmojiSpan.class); - if (spans.length == 0) { - drawable = emoji.getEmojiDrawableFor(array.subarray(arrayIdx + arrayIdxOffset, - arrayEnd)); - } - } - if (drawable != null) { - text.setSpan(new EmojiSpan(drawable), textIdx + textIdxOffset, textEnd, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - arrayIdx += skippedIndex; - } - } - } - - private static boolean isVariationSelector(int codePoint) { - return codePoint == 0xfe0f; - } - - private static boolean isZeroWidthJoin(int codePoint) { - return codePoint == 0x200d; - } - - private static boolean isPhoneNumberSymbol(int codePoint) { - return codePoint == 0x0023 || codePoint == 0x002a || TwidereMathUtils.inRange(codePoint, - 0x0030, 0x0039, TwidereMathUtils.RANGE_INCLUSIVE_INCLUSIVE); - } - - private static boolean isModifier(int codePoint) { - return TwidereMathUtils.inRange(codePoint, 0x1f3fb, 0x1f3ff, - TwidereMathUtils.RANGE_INCLUSIVE_INCLUSIVE); - } - - private static boolean isEmoji(int codePoint) { - return !Character.isLetterOrDigit(codePoint); - } - - private static boolean isRegionalIndicatorSymbol(int codePoint) { - return TwidereMathUtils.inRange(codePoint, 0x1f1e6, 0x1f1ff, - TwidereMathUtils.RANGE_INCLUSIVE_INCLUSIVE); - } - - private static boolean isKeyCap(int codePoint) { - return codePoint == 0x20e3; - } - -} diff --git a/twidere/src/main/java/org/mariotaku/twidere/util/HtmlBuilder.java b/twidere/src/main/java/org/mariotaku/twidere/util/HtmlBuilder.java index fe3c482cc..0af7499a2 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/util/HtmlBuilder.java +++ b/twidere/src/main/java/org/mariotaku/twidere/util/HtmlBuilder.java @@ -22,6 +22,7 @@ package org.mariotaku.twidere.util; import android.support.annotation.NonNull; import android.support.v4.util.Pair; +import org.mariotaku.commons.text.CodePointArray; import org.mariotaku.twidere.model.SpanItem; import java.util.ArrayList; diff --git a/twidere/src/main/java/org/mariotaku/twidere/util/InternalTwitterContentUtils.java b/twidere/src/main/java/org/mariotaku/twidere/util/InternalTwitterContentUtils.java index e7836fdda..170c22c09 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/util/InternalTwitterContentUtils.java +++ b/twidere/src/main/java/org/mariotaku/twidere/util/InternalTwitterContentUtils.java @@ -10,8 +10,7 @@ import android.text.TextUtils; import org.apache.commons.lang3.text.translate.CharSequenceTranslator; import org.apache.commons.lang3.text.translate.EntityArrays; import org.apache.commons.lang3.text.translate.LookupTranslator; -import org.mariotaku.microblog.library.MicroBlog; -import org.mariotaku.microblog.library.MicroBlogException; +import org.mariotaku.commons.text.CodePointArray; import org.mariotaku.microblog.library.twitter.model.DirectMessage; import org.mariotaku.microblog.library.twitter.model.EntitySupport; import org.mariotaku.microblog.library.twitter.model.ExtendedEntitySupport; @@ -19,7 +18,6 @@ import org.mariotaku.microblog.library.twitter.model.MediaEntity; import org.mariotaku.microblog.library.twitter.model.Status; import org.mariotaku.microblog.library.twitter.model.UrlEntity; import org.mariotaku.microblog.library.twitter.model.User; -import org.mariotaku.restfu.http.MultiValueMap; import org.mariotaku.twidere.model.ParcelableStatus; import org.mariotaku.twidere.model.SpanItem; import org.mariotaku.twidere.model.UserKey; @@ -27,8 +25,6 @@ import org.mariotaku.twidere.provider.TwidereDataStore.Filters; import java.util.ArrayList; import java.util.List; -import java.util.Set; -import java.util.regex.Matcher; import java.util.regex.Pattern; /** diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/EmojiSupportUtils.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/EmojiSupportUtils.kt new file mode 100644 index 000000000..60f201b52 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/EmojiSupportUtils.kt @@ -0,0 +1,159 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright (C) 2012-2015 Mariotaku Lee + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.mariotaku.twidere.util + +import android.graphics.drawable.Drawable +import android.text.Spannable +import android.text.Spanned +import android.widget.TextView +import org.mariotaku.commons.text.CodePointArray +import org.mariotaku.commons.text.get + +import org.mariotaku.twidere.text.style.EmojiSpan +import org.mariotaku.twidere.text.util.EmojiEditableFactory +import org.mariotaku.twidere.text.util.EmojiSpannableFactory + +/** + * Created by mariotaku on 15/12/20. + */ +object EmojiSupportUtils { + + fun initForTextView(textView: TextView) { + if (textView.isInEditMode) return + textView.setSpannableFactory(EmojiSpannableFactory(textView)) + textView.setEditableFactory(EmojiEditableFactory(textView)) + } + + fun applyEmoji(manager: ExternalThemeManager, text: Spannable, + textStart: Int = 0, textLength: Int = text.length) { + val emoji = manager.emoji + if (emoji == null || !emoji.isSupported) return + val array = CodePointArray(text) + var arrayIdx = array.length() - 1 + while (arrayIdx >= 0) { + val codePoint = array[arrayIdx] + if (isEmoji(codePoint)) { + val arrayEnd = arrayIdx + 1 + var arrayIdxOffset = 0 + val textIdx = array.indexOfText(codePoint, arrayIdx) + var textIdxOffset = 0 + var skippedIndex = 0 + if (textIdx == -1 || textIdx < textStart) { + arrayIdx-- + continue + } + val textEnd = textIdx + Character.charCount(codePoint) + if (arrayIdx > 0) { + val prevCodePoint = array[arrayIdx - 1] + when { + isRegionalIndicatorSymbol(codePoint) -> if (isRegionalIndicatorSymbol(prevCodePoint)) { + arrayIdxOffset = -1 + textIdxOffset = -Character.charCount(prevCodePoint) + skippedIndex = -1 + } + isModifier(codePoint) -> if (isEmoji(prevCodePoint)) { + arrayIdxOffset = -1 + textIdxOffset = -Character.charCount(prevCodePoint) + skippedIndex = -1 + } + isKeyCap(codePoint) -> if (isPhoneNumberSymbol(prevCodePoint)) { + arrayIdxOffset = -1 + textIdxOffset = -Character.charCount(prevCodePoint) + skippedIndex = -1 + } + isZeroWidthJoin(prevCodePoint) -> { + var notValidControlCount = 0 + var charCount = 0 + for (i in arrayIdx - 1 downTo 0) { + val cp = array.get(i) + charCount += Character.charCount(cp) + if (isZeroWidthJoin(cp) || isVariationSelector(cp)) { + // Ignore + notValidControlCount = 0 + continue + } + notValidControlCount++ + if (notValidControlCount > 1 || i == 0) { + arrayIdxOffset = i - arrayIdx + 1 + textIdxOffset = -charCount + Character.charCount(cp) + skippedIndex = i - arrayIdx + 1 + break + } + } + } + } + } + if (textEnd > textStart + textLength) { + arrayIdx-- + continue + } + var spans = text.getSpans(textIdx + textIdxOffset, textEnd, EmojiSpan::class.java) + if (spans.size == 0) { + var drawable: Drawable? = emoji.getEmojiDrawableFor(*array[arrayIdx + arrayIdxOffset..arrayEnd]) + if (drawable == null) { + // Not emoji combination, just use fallback + textIdxOffset = 0 + arrayIdxOffset = 0 + skippedIndex = 0 + spans = text.getSpans(textIdx + textIdxOffset, textEnd, EmojiSpan::class.java) + if (spans.size == 0) { + drawable = emoji.getEmojiDrawableFor(*array[arrayIdx + arrayIdxOffset..arrayEnd]) + } + } + if (drawable != null) { + text.setSpan(EmojiSpan(drawable), textIdx + textIdxOffset, textEnd, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + arrayIdx += skippedIndex + } + arrayIdx-- + } + } + + private fun isVariationSelector(codePoint: Int): Boolean { + return codePoint == 0xfe0f + } + + private fun isZeroWidthJoin(codePoint: Int): Boolean { + return codePoint == 0x200d + } + + private fun isPhoneNumberSymbol(codePoint: Int): Boolean { + return codePoint == 0x0023 || codePoint == 0x002a || codePoint in 0x0030..0x0039 + } + + private fun isModifier(codePoint: Int): Boolean { + return codePoint in 0x1f3fb..0x1f3ff + } + + private fun isEmoji(codePoint: Int): Boolean { + return !Character.isLetterOrDigit(codePoint) + } + + private fun isRegionalIndicatorSymbol(codePoint: Int): Boolean { + return codePoint in 0x1f1e6..0x1f1ff + } + + private fun isKeyCap(codePoint: Int): Boolean { + return codePoint == 0x20e3 + } + +}