カスタム絵文字4k個のタンスで絵文字ピッカーの挙動を最適化した

This commit is contained in:
tateisu 2017-09-26 22:39:37 +09:00
parent 2d4b107879
commit 33584b5689
19 changed files with 13207 additions and 13032 deletions

View File

@ -16,6 +16,7 @@
<w>gifv</w>
<w>hashtag</w>
<w>hashtags</w>
<w>hohoemi</w>
<w>idempotency</w>
<w>kenglxn</w>
<w>mailto</w>

View File

@ -443,16 +443,22 @@ for(@fix_name){
updateCodeMap();
updateNameMap();
my %name_chars;
my $bad_name = 0;
for my $name (sort keys %name_map){
for( split //,$name ){
$name_chars{$_}=1;
}
my $rh = $name_map{$name};
my @res_list = values %$rh;
next if @res_list == 1;
warn "name $name has multiple resource. ",join(',',map{ $_->{res_name} } @res_list),"\n";
$bad_name = 1;
}
$bad_name and die "please fix name=>resource duplicate.\n";
warn "name_chars: [",join('',sort keys %name_chars),"]\n";
sub decodeUnified($){
my($chars) = @_;
@ -552,6 +558,8 @@ my $utf8 = Encode::find_encoding("utf8");
my $utf16 = Encode::find_encoding("UTF-16BE");
my $utf16_max_length = 0;
# 画像リソースIDとUnidoceシーケンスの関連付けを出力する
for my $res_name ( sort keys %res_map ){
my $res_info = $res_map{$res_name};
@ -576,12 +584,29 @@ for my $res_name ( sort keys %res_map ){
}
}
#for my $res_name ( sort keys %res_map ){
# my $res_info = $res_map{$res_name};
# for my $short_name ( sort keys %{$res_info->{shortname_map}} ){
# addCode( qq{name( R.drawable.$res_name, "$short_name" );});
# }
#}
# 画像リソースIDとshortcodeの関連付けを出力する
for my $res_name ( sort keys %res_map ){
my $res_info = $res_map{$res_name};
for my $short_name ( sort keys %{$res_info->{shortname_map}} ){
addCode( qq{name( R.drawable.$res_name, "$short_name" );});
}
# 投稿時にshortcodeをユニコードに変換するため、shortcodeとUTF-16シーケンスの関連付けを出力する
for my $name (sort keys %name_map){
my $rh = $name_map{$name};
my @res_list = values %$rh;
my $res_info = $res_list[0];
my $chars = parseCodePoint( $res_info->{unified} );
# コードポイントのリストからperl内部表現の文字列にする
my $str = join '',map{ chr hex $_ } @$chars;
# perl内部表現からUTF-16に変換する
my $str_utf16 = $utf16->encode( $str );
my @utf16_chars = unpack("n*",$str_utf16);
# UTF-16の文字のリストをJavaのエスケープ表現に直す
my $java_chars = join('',map{ sprintf qq(\\u%04x),$_} @utf16_chars );
addCode( qq{name( "$name", R.drawable.$res_info->{res_name}, "$java_chars" );});
}
# カテゴリを書きだす

File diff suppressed because it is too large Load Diff

View File

@ -866,7 +866,7 @@ public class ActAccountSetting extends AppCompatActivity
return;
}
}
updateCredential( "display_name=" + Uri.encode(sv ) );
updateCredential( "display_name=" + Uri.encode(EmojiDecoder.decodeShortCode(sv) ) );
}
private void sendNote(boolean bConfirmed){
@ -890,7 +890,7 @@ public class ActAccountSetting extends AppCompatActivity
return;
}
}
updateCredential( "note=" + Uri.encode(sv ) );
updateCredential( "note=" + Uri.encode(EmojiDecoder.decodeShortCode(sv) ) );
}
private static final int PERMISSION_REQUEST_AVATAR = 1;

View File

@ -7,8 +7,9 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.graphics.Typeface;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
@ -43,11 +44,13 @@ import jp.juggler.subwaytooter.util.CustomEmojiCache;
import jp.juggler.subwaytooter.util.CustomEmojiLister;
import jp.juggler.subwaytooter.util.LogCategory;
import okhttp3.Cache;
import okhttp3.CacheControl;
import okhttp3.Call;
import okhttp3.CipherSuite;
import okhttp3.ConnectionSpec;
import okhttp3.OkHttpClient;
import okhttp3.Response;
import uk.co.chrisjenx.calligraphy.CalligraphyConfig;
import uk.co.chrisjenx.calligraphy.TypefaceUtils;
public class App1 extends Application {
@ -215,10 +218,10 @@ public class App1 extends Application {
public static OkHttpClient ok_http_client;
public static OkHttpClient ok_http_client2;
private static OkHttpClient ok_http_client2;
public static final boolean USE_OLD_EMOJIONE = false;
public static Typeface typeface_emoji;
// public static final boolean USE_OLD_EMOJIONE = false;
// public static Typeface typeface_emoji;
public static SharedPreferences pref;
@ -292,11 +295,11 @@ public class App1 extends Application {
AcctSet.deleteOld( System.currentTimeMillis() );
}
if( USE_OLD_EMOJIONE ){
if( typeface_emoji == null ){
typeface_emoji = TypefaceUtils.load( app_context.getAssets(), "emojione_android.ttf" );
}
}
// if( USE_OLD_EMOJIONE ){
// if( typeface_emoji == null ){
// typeface_emoji = TypefaceUtils.load( app_context.getAssets(), "emojione_android.ttf" );
// }
// }
// if( image_loader == null ){
// image_loader = new MyImageLoader(
@ -406,4 +409,74 @@ public class App1 extends Application {
}
}
static final CacheControl CACHE_5MIN = new CacheControl.Builder()
.maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS) // キャッシュをいつまで保持するか
//s .minFresh( 1, TimeUnit.HOURS ) // キャッシュが新鮮であると考えられる時間
.maxAge( 1, TimeUnit.HOURS ) // キャッシュが新鮮であると考えられる時間
.build();
@Nullable public static byte[] getHttpCached( @NonNull String url ){
Response response;
long t_start = SystemClock.elapsedRealtime();
try{
okhttp3.Request.Builder request_builder = new okhttp3.Request.Builder();
request_builder.url( url );
request_builder.cacheControl( CACHE_5MIN );
Call call = App1.ok_http_client2.newCall( request_builder.build() );
response = call.execute();
}catch( Throwable ex ){
log.e( ex, "getHttp network error." );
return null;
}finally{
long t_delta = SystemClock.elapsedRealtime() -t_start;
log.d("getHttp: time=%dms",t_delta);
}
if( ! response.isSuccessful() ){
log.e( "getHttp response error. %s", response );
return null;
}
try{
//noinspection ConstantConditions
return response.body().bytes();
}catch( Throwable ex ){
log.e( ex, "getHttp content error." );
return null;
}
}
@Nullable public static String getHttpCachedString( @NonNull String url ){
Response response;
long t_start = SystemClock.elapsedRealtime();
try{
okhttp3.Request.Builder request_builder = new okhttp3.Request.Builder();
request_builder.url( url );
request_builder.cacheControl( CACHE_5MIN );
Call call = App1.ok_http_client2.newCall( request_builder.build() );
response = call.execute();
}catch( Throwable ex ){
log.e( ex, "getHttp network error." );
return null;
}finally{
long t_delta = SystemClock.elapsedRealtime() -t_start;
log.d("getHttp: time=%dms",t_delta);
}
if( ! response.isSuccessful() ){
log.e( "getHttp response error. %s", response );
return null;
}
try{
//noinspection ConstantConditions
return response.body().string();
}catch( Throwable ex ){
log.e( ex, "getHttp content error." );
return null;
}
}
}

View File

@ -11,6 +11,7 @@ import jp.juggler.subwaytooter.api.entity.TootAccount;
import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.table.UserRelation;
import jp.juggler.subwaytooter.util.EmojiDecoder;
import jp.juggler.subwaytooter.util.EmojiMap201709;
import jp.juggler.subwaytooter.view.MyLinkMovementMethod;
import jp.juggler.subwaytooter.view.MyNetworkImageView;
@ -37,20 +38,20 @@ class HeaderViewHolderProfile extends HeaderViewHolderBase implements View.OnCli
, arg_activity.getLayoutInflater().inflate( R.layout.lv_header_account, parent, false )
);
ivBackground = (MyNetworkImageView) viewRoot.findViewById( R.id.ivBackground );
ivBackground = viewRoot.findViewById( R.id.ivBackground );
llProfile = viewRoot.findViewById( R.id.llProfile );
tvCreated = (TextView) viewRoot.findViewById( R.id.tvCreated );
ivAvatar = (MyNetworkImageView) viewRoot.findViewById( R.id.ivAvatar );
tvDisplayName = (TextView) viewRoot.findViewById( R.id.tvDisplayName );
tvAcct = (TextView) viewRoot.findViewById( R.id.tvAcct );
btnFollowing = (Button) viewRoot.findViewById( R.id.btnFollowing );
btnFollowers = (Button) viewRoot.findViewById( R.id.btnFollowers );
btnStatusCount = (Button) viewRoot.findViewById( R.id.btnStatusCount );
tvNote = (TextView) viewRoot.findViewById( R.id.tvNote );
tvCreated = viewRoot.findViewById( R.id.tvCreated );
ivAvatar = viewRoot.findViewById( R.id.ivAvatar );
tvDisplayName = viewRoot.findViewById( R.id.tvDisplayName );
tvAcct = viewRoot.findViewById( R.id.tvAcct );
btnFollowing = viewRoot.findViewById( R.id.btnFollowing );
btnFollowers = viewRoot.findViewById( R.id.btnFollowers );
btnStatusCount = viewRoot.findViewById( R.id.btnStatusCount );
tvNote = viewRoot.findViewById( R.id.tvNote );
View btnMore = viewRoot.findViewById( R.id.btnMore );
btnFollow = (ImageButton) viewRoot.findViewById( R.id.btnFollow );
ivFollowedBy = (ImageView) viewRoot.findViewById( R.id.ivFollowedBy );
tvRemoteProfileWarning = (TextView) viewRoot.findViewById( R.id.tvRemoteProfileWarning );
btnFollow = viewRoot.findViewById( R.id.btnFollow );
ivFollowedBy = viewRoot.findViewById( R.id.ivFollowedBy );
tvRemoteProfileWarning = viewRoot.findViewById( R.id.tvRemoteProfileWarning );
ivBackground.setOnClickListener( this );
btnFollowing.setOnClickListener( this );
@ -103,7 +104,7 @@ class HeaderViewHolderProfile extends HeaderViewHolderBase implements View.OnCli
String s = "@" + access_info.getFullAcct( who );
if( who.locked ){
s += " " + EmojiDecoder.map_name2unicode.get( "lock" );
s += " " + EmojiMap201709.sShortNameToImageId.get("lock" ).unified;
}
tvAcct.setText( EmojiDecoder.decodeEmoji( activity, s ,null) );

View File

@ -133,8 +133,8 @@ public class EmojiPicker implements View.OnClickListener, CustomEmojiLister.Call
SkinTone tone = (SkinTone) viewRoot.findViewById( selected_tone ).getTag();
for( String suffix : tone.suffix_list ){
String new_name = name + suffix;
Integer value = EmojiMap201709.sShortNameToImageId.get( new_name );
if( value != null ) return new_name;
EmojiMap201709.EmojiInfo info = EmojiMap201709.sShortNameToImageId.get( new_name );
if( info != null ) return new_name;
}
return name;
}
@ -334,9 +334,9 @@ public class EmojiPicker implements View.OnClickListener, CustomEmojiLister.Call
view.setTag( item );
ImageView iv = (ImageView) view;
if( page != null ){
Integer image_id = EmojiMap201709.sShortNameToImageId.get( item.name );
if( image_id != null ){
iv.setImageResource( image_id );
EmojiMap201709.EmojiInfo info = EmojiMap201709.sShortNameToImageId.get( item.name );
if( info != null ){
iv.setImageResource( info.image_id );
}
}
}else{
@ -361,8 +361,8 @@ public class EmojiPicker implements View.OnClickListener, CustomEmojiLister.Call
EmojiItem item = page.emoji_list.get( idx );
if( TextUtils.isEmpty( item.instance ) ){
String name = item.name;
Integer image_id = EmojiMap201709.sShortNameToImageId.get( name );
if( image_id == null ) return;
EmojiMap201709.EmojiInfo info = EmojiMap201709.sShortNameToImageId.get( name );
if( info == null ) return;
if( selected_tone != 0 ){
name = applySkinTone( name );
}

View File

@ -459,7 +459,6 @@ import java.util.ArrayList;
if( result != null ) result.onParseComplete();
return result;
}catch( Throwable ex ){
log.trace( ex );
handler.dispose();
throw ex;
}

View File

@ -9,16 +9,16 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.ByteArrayInputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import jp.juggler.subwaytooter.App1;
import okhttp3.Call;
import okhttp3.Response;
@SuppressWarnings("WeakerAccess")
public class CustomEmojiCache {
@ -62,24 +62,41 @@ public class CustomEmojiCache {
// リクエスト
public interface Callback {
void onAPNGLoadComplete( APNGFrames b );
void onAPNGLoadComplete();
}
static class Request {
@NonNull String url;
@NonNull Callback callback;
@NonNull final WeakReference<Object> refTarget;
@NonNull final String url;
@NonNull final Callback callback;
public Request( @NonNull String url, @NonNull Callback callback ){
public Request( @NonNull Object target_tag, @NonNull String url, @NonNull Callback callback ){
this.refTarget = new WeakReference<>( target_tag );
this.url = url;
this.callback = callback;
}
}
final ConcurrentLinkedQueue< Request > queue = new ConcurrentLinkedQueue<>();
final LinkedList< Request > queue = new LinkedList<>();
////////////////////////////////
@Nullable public APNGFrames get( @NonNull String url, @NonNull Callback callback ){
public void cancelRequest( @NonNull Object target_tag){
synchronized( queue ){
Iterator<Request> it = queue.iterator();
while( it.hasNext() ){
Request request = it.next();
Object tag = request.refTarget.get();
if( tag == null || tag == target_tag ){
it.remove();
}
}
}
}
@Nullable public APNGFrames get( @NonNull Object target_tag,@NonNull String url, @NonNull Callback callback ){
cancelRequest( target_tag );
synchronized( cache ){
long now = getNow();
@ -97,7 +114,9 @@ public class CustomEmojiCache {
return null;
}
}
queue.add( new Request( url, callback ) );
synchronized( queue ){
queue.addLast( new Request( target_tag, url, callback ) );
}
worker.notifyEx();
return null;
}
@ -124,19 +143,27 @@ public class CustomEmojiCache {
@Override public void run(){
while( ! bCancelled.get() ){
Request request = queue.poll();
Request request;
synchronized( queue ){
request = queue.isEmpty() ? null : queue.removeFirst();
}
if( request == null ){
waitEx( 86400000L );
continue;
}
if( request.refTarget.get() == null ){
continue;
}
long now = getNow();
synchronized( cache ){
// 成功キャッシュ
CacheItem item = cache.get( request.url );
if( item != null ){
fireCallback( request.callback, item.frames );
fireCallback( request.callback );
continue;
}
@ -151,7 +178,7 @@ public class CustomEmojiCache {
APNGFrames frames = null;
try{
byte[] data = getHttp( request.url );
byte[] data = App1.getHttpCached( request.url );
if( data != null ){
frames = decodeAPNG( data, request.url );
}
@ -169,7 +196,7 @@ public class CustomEmojiCache {
item.frames.dispose();
item.frames = frames;
}
fireCallback( request.callback, frames );
fireCallback( request.callback );
}else{
cache_error.put( request.url, getNow() );
}
@ -177,40 +204,15 @@ public class CustomEmojiCache {
}
}
private void fireCallback( final Callback callback, final APNGFrames frames ){
private void fireCallback( final Callback callback ){
handler.post( new Runnable() {
@Override public void run(){
callback.onAPNGLoadComplete( frames );
callback.onAPNGLoadComplete();
}
} );
}
private byte[] getHttp( String url ){
Response response;
try{
okhttp3.Request.Builder request_builder = new okhttp3.Request.Builder();
request_builder.url( url );
Call call = App1.ok_http_client2.newCall( request_builder.build() );
response = call.execute();
}catch( Throwable ex ){
log.e( ex, "getHttp network error." );
return null;
}
if( ! response.isSuccessful() ){
log.e( "getHttp response error. %s", response );
return null;
}
try{
//noinspection ConstantConditions
return response.body().bytes();
}catch( Throwable ex ){
log.e( ex, "getHttp content error." );
return null;
}
}
private void sweep_cache(){
// キャッシュの掃除
@ -244,7 +246,7 @@ public class CustomEmojiCache {
APNGFrames frames = APNGFrames.parseAPNG( new ByteArrayInputStream( data ), 64 );
if( frames != null ) return frames;
}catch( Throwable ex ){
log.e( ex, "PNG decode failed. %s", url );
log.e( ex, "PNG decode failed. %s ", url );
// PngFeatureException Interlaced images are not yet supported
}

View File

@ -1,8 +1,6 @@
package jp.juggler.subwaytooter.util;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.SystemClock;
import android.support.annotation.NonNull;
@ -11,7 +9,6 @@ import android.text.TextUtils;
import org.json.JSONArray;
import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@ -21,8 +18,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
import jp.juggler.subwaytooter.App1;
import jp.juggler.subwaytooter.api.entity.CustomEmoji;
import okhttp3.Call;
import okhttp3.Response;
@SuppressWarnings("WeakerAccess")
public class CustomEmojiLister {
@ -175,7 +170,7 @@ public class CustomEmojiLister {
CustomEmoji.List list = null;
try{
String data = getHttp( request.instance );
String data = App1.getHttpCachedString("https://"+ request.instance + "/api/v1/custom_emojis");
if( data != null ){
list = decodeEmojiList( data, request.instance );
}
@ -209,31 +204,7 @@ public class CustomEmojiLister {
} );
}
private String getHttp( String instance ){
Response response;
try{
okhttp3.Request.Builder request_builder = new okhttp3.Request.Builder();
request_builder.url( "https://"+ instance + "/api/v1/custom_emojis" );
Call call = App1.ok_http_client2.newCall( request_builder.build() );
response = call.execute();
}catch( Throwable ex ){
log.e( ex, "getHttp network error." );
return null;
}
if( ! response.isSuccessful() ){
log.e( "getHttp response error. %s", response );
return null;
}
try{
//noinspection ConstantConditions
return response.body().string();
}catch( Throwable ex ){
log.e( ex, "getHttp content error." );
return null;
}
}
private void sweep_cache(){

View File

@ -1,6 +1,7 @@
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;
@ -9,52 +10,20 @@ import android.text.Spanned;
import android.text.TextUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jp.juggler.subwaytooter.App1;
import jp.juggler.subwaytooter.R;
import jp.juggler.subwaytooter.api.entity.CustomEmoji;
public abstract class EmojiDecoder {
private static final Pattern SHORTNAME_PATTERN = Pattern.compile( ":([-+\\w]+):" );
public static final HashMap< String, String > map_name2unicode = EmojiMap._shortNameToUnicode;
private static final HashSet< String > set_unicode = EmojiMap._unicode_set;
@SuppressWarnings("WeakerAccess")
public class EmojiDecoder {
private static class DecodeEnv {
@NonNull final Context context;
@NonNull final SpannableStringBuilder sb = new SpannableStringBuilder();
SpannableStringBuilder sb = new SpannableStringBuilder();
int last_span_start = - 1;
int last_span_end = - 1;
@Nullable CustomEmoji.Map custom_map;
DecodeEnv( @NonNull Context context, @Nullable CustomEmoji.Map custom_map ){
DecodeEnv( @NonNull Context context ){
this.context = context;
this.custom_map = custom_map;
}
void closeSpan(){
if( last_span_start >= 0 ){
if( last_span_end > last_span_start && App1.typeface_emoji != null ){
EmojiSpan typefaceSpan = new EmojiSpan( App1.typeface_emoji );
sb.setSpan( typefaceSpan, last_span_start, last_span_end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
}
last_span_start = - 1;
}
}
void addEmoji( String s ){
if( last_span_start < 0 ){
last_span_start = sb.length();
}
sb.append( s );
last_span_end = sb.length();
}
void addUnicodeString( String s ){
@ -63,53 +32,36 @@ public abstract class EmojiDecoder {
while( i < end ){
int remain = end - i;
String emoji = null;
if( App1.USE_OLD_EMOJIONE ){
for( int j = EmojiMap.max_length ; j > 0 ; -- j ){
if( j > remain ) continue;
String check = s.substring( i, i + j );
if( set_unicode.contains( check ) ){
emoji = check;
break;
}
}
if( emoji != null ){
addEmoji( emoji );
i += emoji.length();
continue;
}
}else{
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;
}
}
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( image_id == 0 ){
if( j < remain && s.charAt( i + j ) == 0xFE0E ){
// 絵文字バリエーションシーケンスEVSのU+FE0EVS-15が直後にある場合
// その文字を絵文字化しない
closeSpan();
sb.append( emoji );
emoji = s.substring( i, i + j + 1 );
image_id = 0;
}else{
addImageSpan( emoji, image_id );
emoji = check;
}
i += emoji.length();
continue;
break;
}
}
closeSpan();
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 ) );
@ -121,8 +73,7 @@ public abstract class EmojiDecoder {
}
}
void addImageSpan( String text, int res_id ){
closeSpan();
void addImageSpan( String text, @DrawableRes int res_id ){
int start = sb.length();
sb.append( text );
int end = sb.length();
@ -130,7 +81,6 @@ public abstract class EmojiDecoder {
}
void addNetworkEmojiSpan( String text, @NonNull String url ){
closeSpan();
int start = sb.length();
sb.append( text );
int end = sb.length();
@ -138,78 +88,191 @@ public abstract class EmojiDecoder {
}
}
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( Context context, String s, @Nullable CustomEmoji.Map custom_map ){
public static Spannable decodeEmoji( @NonNull final Context context, @NonNull final String s, @Nullable final CustomEmoji.Map custom_map ){
DecodeEnv decode_env = new DecodeEnv( context, custom_map );
Matcher matcher = SHORTNAME_PATTERN.matcher( s );
int last_end = 0;
while( matcher.find() ){
int start = matcher.start();
int end = matcher.end();
if( start > last_end ){
decode_env.addUnicodeString( s.substring( last_end, start ) );
}
last_end = end;
//
if( App1.USE_OLD_EMOJIONE ){
String unicode = map_name2unicode.get( matcher.group( 1 ) );
if( unicode != null ){
decode_env.addEmoji( unicode );
continue;
}
}else{
String name = matcher.group( 1 ).toLowerCase().replace( '-', '_' );
Integer image_id = EmojiMap201709.sShortNameToImageId.get( name );
if( image_id != null ){
decode_env.addImageSpan( s.substring( start, end ), image_id );
continue;
}
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 );
}
String url = ( custom_map == null ? null : custom_map.get( matcher.group( 1 ) ) );
if( ! TextUtils.isEmpty( url ) ){
decode_env.addNetworkEmojiSpan( s.substring( start, end ), url );
@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;
}
}else if( reHohoemi.matcher( matcher.group( 1 ) ).find() ){
decode_env.addImageSpan( s.substring( start, end ), R.drawable.emoji_hohoemi );
}else if( reNicoru.matcher( matcher.group( 1 ) ).find() ){
decode_env.addImageSpan( s.substring( start, end ), R.drawable.emoji_nicoru );
}else{
decode_env.addUnicodeString( s.substring( start, end ) );
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 );
}
}
}
// copy remain
int end = s.length();
if( end > last_end ){
decode_env.addUnicodeString( s.substring( last_end, end ) );
}
// close span
decode_env.closeSpan();
} );
return decode_env.sb;
}
public static ArrayList< CharSequence > searchShortCode( Context context, String prefix, int limit ){
ArrayList< CharSequence > dst = new ArrayList<>();
if( ! App1.USE_OLD_EMOJIONE ){
for( String shortCode : EmojiMap201709.sShortNameList ){
if( dst.size() >= limit ) break;
if( ! shortCode.contains( prefix )) continue;
SpannableStringBuilder sb = new SpannableStringBuilder();
sb.append( ' ' );
int start = 0;
int end = sb.length();
sb.setSpan( new EmojiImageSpan( context, EmojiMap201709.sShortNameToImageId.get( shortCode ) ), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
sb.append( ' ' );
sb.append( ':' );
sb.append( shortCode );
sb.append( ':' );
dst.add( 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;
}

File diff suppressed because it is too large Load Diff

View File

@ -164,10 +164,10 @@ public class HTMLDecoder {
, LinkClickContext account
, SpannableStringBuilder sb
, @NonNull DecodeOptions options
){
){
if( TAG_TEXT.equals( tag ) ){
if( options.bDecodeEmoji ){
sb.append( EmojiDecoder.decodeEmoji( context, decodeEntity( text ) ,options.customEmojiMap ) );
sb.append( EmojiDecoder.decodeEmoji( context, decodeEntity( text ), options.customEmojiMap ) );
}else{
sb.append( decodeEntity( text ) );
}
@ -188,7 +188,7 @@ public class HTMLDecoder {
sb_tmp.append( "<img/>" );
}else{
for( Node child : child_nodes ){
child.encodeSpan( context, account, sb_tmp, options);
child.encodeSpan( context, account, sb_tmp, options );
}
}
@ -205,8 +205,8 @@ public class HTMLDecoder {
if( end > start && "a".equals( tag ) ){
String href = getHref();
if( href != null ){
String link_text = sb.subSequence( start,end ).toString();
MyClickableSpan span = new MyClickableSpan( account, link_text, href, account.findAcctColor( href ),options.link_tag );
String link_text = sb.subSequence( start, end ).toString();
MyClickableSpan span = new MyClickableSpan( account, link_text, href, account.findAcctColor( href ), options.link_tag );
sb.setSpan( span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE );
}
}
@ -281,16 +281,12 @@ public class HTMLDecoder {
}
if( is_media_attachment( list_attachment, href ) ){
if( App1.USE_OLD_EMOJIONE ){
return EmojiDecoder.decodeEmoji( context, ":frame_photo:",null );
}else{
SpannableStringBuilder sb = new SpannableStringBuilder();
sb.append( href );
int start = 0;
int end = sb.length();
sb.setSpan( new EmojiImageSpan( context, R.drawable.emj_1f5bc ), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
return sb;
}
SpannableStringBuilder sb = new SpannableStringBuilder();
sb.append( href );
int start = 0;
int end = sb.length();
sb.setSpan( new EmojiImageSpan( context, R.drawable.emj_1f5bc ), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
return sb;
}
try{
@ -319,12 +315,11 @@ public class HTMLDecoder {
return Character.isWhitespace( c ) || c == 0x0a || c == 0x0d;
}
public static SpannableStringBuilder decodeHTML(
Context context
, LinkClickContext account
, String src
,@NonNull DecodeOptions options
, @NonNull DecodeOptions options
){
prepareTagInformation();
SpannableStringBuilder sb = new SpannableStringBuilder();
@ -336,7 +331,7 @@ public class HTMLDecoder {
rootNode.addChild( tracker, "" );
}
rootNode.encodeSpan( context, account, sb,options);
rootNode.encodeSpan( context, account, sb, options );
int end = sb.length();
while( end > 0 && isWhitespace( sb.charAt( end - 1 ) ) ) -- end;
if( end < sb.length() ){
@ -376,7 +371,7 @@ public class HTMLDecoder {
// return sb;
// }
public static Spannable decodeMentions( final SavedAccount access_info, TootMention.List src_list ,@Nullable Object link_tag ){
public static Spannable decodeMentions( final SavedAccount access_info, TootMention.List src_list, @Nullable Object link_tag ){
if( src_list == null || src_list.isEmpty() ) return null;
SpannableStringBuilder sb = new SpannableStringBuilder();
for( TootMention item : src_list ){
@ -390,8 +385,8 @@ public class HTMLDecoder {
}
int end = sb.length();
if( end > start ){
String link_text = sb.subSequence( start,end ).toString();
MyClickableSpan span = new MyClickableSpan( access_info, link_text,item.url, access_info.findAcctColor( item.url ) ,link_tag );
String link_text = sb.subSequence( start, end ).toString();
MyClickableSpan span = new MyClickableSpan( access_info, link_text, item.url, access_info.findAcctColor( item.url ), link_tag );
sb.setSpan( span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE );
}

View File

@ -1,6 +1,7 @@
package jp.juggler.subwaytooter.util;
import android.os.Handler;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Spannable;
@ -36,4 +37,25 @@ public class NetworkEmojiInvalidator implements Runnable, NetworkEmojiSpan.Inval
view.postInvalidateOnAnimation();
}
}
// 最後に描画した時刻
private long t_last_draw;
// アニメーション開始時刻
private long t_start;
@Override public long getTimeFromStart(){
long now = SystemClock.elapsedRealtime();
// アニメーション開始時刻を計算する
if( t_start == 0L || now - t_last_draw >= 60000L ){
t_start = now;
}
t_last_draw = now;
return now - t_start;
}
}

View File

@ -33,6 +33,7 @@ public class NetworkEmojiSpan extends ReplacementSpan implements CustomEmojiCach
}
public interface InvalidateCallback {
long getTimeFromStart();
void delayInvalidate( long delay );
}
@ -43,7 +44,7 @@ public class NetworkEmojiSpan extends ReplacementSpan implements CustomEmojiCach
}
// implements CustomEmojiCache.Callback
@Override public void onAPNGLoadComplete( APNGFrames b ){
@Override public void onAPNGLoadComplete(){
if( invalidate_callback != null ){
invalidate_callback.delayInvalidate( 0 );
}
@ -73,11 +74,7 @@ public class NetworkEmojiSpan extends ReplacementSpan implements CustomEmojiCach
// フレーム探索結果を格納する構造体を確保しておく
private final APNGFrames.FindFrameResult mFrameFindResult = new APNGFrames.FindFrameResult();
// 最後に描画した時刻
private long t_last_draw;
// アニメーション開始時刻
private long t_start;
@Override public void draw(
@NonNull Canvas canvas
@ -88,19 +85,13 @@ public class NetworkEmojiSpan extends ReplacementSpan implements CustomEmojiCach
if( invalidate_callback == null ) return;
// APNGデータの取得
APNGFrames frames = App1.custom_emoji_cache.get( url, this );
APNGFrames frames = App1.custom_emoji_cache.get( this, url, this );
if( frames == null ) return;
long now = SystemClock.elapsedRealtime();
// アニメーション開始時刻を計算する
if( t_start == 0L || now - t_last_draw >= 60000L ){
t_start = now;
}
t_last_draw = now;
long t = invalidate_callback.getTimeFromStart();
// アニメーション開始時刻からの経過時間に応じたフレームを探索
frames.findFrame( mFrameFindResult, now - t_start );
frames.findFrame( mFrameFindResult, t );
Bitmap b = mFrameFindResult.bitmap;
if( b == null || b.isRecycled() ) return;

View File

@ -18,6 +18,7 @@ import java.util.ArrayList;
import jp.juggler.subwaytooter.R;
import jp.juggler.subwaytooter.Styler;
import jp.juggler.subwaytooter.view.MyEditText;
@SuppressWarnings("WeakerAccess") class PopupAutoCompleteAcct {
final Activity activity;
@ -61,7 +62,8 @@ import jp.juggler.subwaytooter.Styler;
}
void setList(
final int sel_start
final MyEditText et
, final int sel_start
, final int sel_end
, @Nullable ArrayList< CharSequence > acct_list
, @Nullable String picker_caption
@ -115,11 +117,11 @@ import jp.juggler.subwaytooter.Styler;
}
v.setOnClickListener( new View.OnClickListener() {
@Override public void onClick( View v ){
String s = etContent.getText().toString();
String s = et.getText().toString();
CharSequence svInsert = ( acct.charAt( 0 ) == ' ' ? acct.subSequence( 2, acct.length() ) : acct );
s = s.substring( 0, sel_start ) + svInsert + " " + ( sel_end >= s.length() ? "" : s.substring( sel_end ) );
etContent.setText( s );
etContent.setSelection( sel_start + svInsert.length() + 1 );
et.setText( s );
et.setSelection( sel_start + svInsert.length() + 1 );
acct_popup.dismiss();
}
} );

View File

@ -251,7 +251,7 @@ public class PostHelper implements CustomEmojiLister.Callback, EmojiPicker.Callb
StringBuilder sb = new StringBuilder();
sb.append( "status=" );
sb.append( Uri.encode( content ) );
sb.append( Uri.encode( EmojiDecoder.decodeShortCode(content ) ) );
if( visibility_checked != null ){
sb.append( "&visibility=" );
@ -264,7 +264,7 @@ public class PostHelper implements CustomEmojiLister.Callback, EmojiPicker.Callb
if( spoiler_text != null ){
sb.append( "&spoiler_text=" );
sb.append( Uri.encode( spoiler_text ) );
sb.append( Uri.encode( EmojiDecoder.decodeShortCode(spoiler_text) ) );
}
if( in_reply_to_id != - 1L ){
@ -289,12 +289,12 @@ public class PostHelper implements CustomEmojiLister.Callback, EmojiPicker.Callb
JSONObject json = new JSONObject();
try{
json.put( "status", content );
json.put( "status", EmojiDecoder.decodeShortCode(content) );
if( visibility_checked != null ){
json.put( "visibility", visibility_checked );
}
json.put( "sensitive", bNSFW );
json.put( "spoiler_text", TextUtils.isEmpty( spoiler_text ) ? "" : spoiler_text );
json.put( "spoiler_text", TextUtils.isEmpty( spoiler_text ) ? "" : EmojiDecoder.decodeShortCode(spoiler_text) );
json.put( "in_reply_to_id", in_reply_to_id == - 1L ? null : in_reply_to_id );
JSONArray array = new JSONArray();
if( attachment_list != null ){
@ -308,7 +308,7 @@ public class PostHelper implements CustomEmojiLister.Callback, EmojiPicker.Callb
json.put( "isEnquete", true );
array = new JSONArray();
for( String item : enquete_items ){
array.put( item );
array.put( EmojiDecoder.decodeShortCode(item) );
}
json.put( "enquete_items", array );
}catch( JSONException ex ){
@ -590,7 +590,7 @@ public class PostHelper implements CustomEmojiLister.Callback, EmojiPicker.Callb
if( popup == null || ! popup.isShowing() ){
popup = new PopupAutoCompleteAcct( activity, et, formRoot, bMainScreen );
}
popup.setList( start, end, acct_list, null, null );
popup.setList( et, start, end, acct_list, null, null );
}
}
@ -622,10 +622,12 @@ public class PostHelper implements CustomEmojiLister.Callback, EmojiPicker.Callb
if( popup == null || ! popup.isShowing() ){
popup = new PopupAutoCompleteAcct( activity, et, formRoot, bMainScreen );
}
popup.setList( last_sharp, end, tag_list, null, null );
popup.setList( et, last_sharp, end, tag_list, null, null );
}
}
private void checkEmoji(){
int end = et.getSelectionEnd();
@ -644,12 +646,20 @@ public class PostHelper implements CustomEmojiLister.Callback, EmojiPicker.Callb
return;
}
// : の手前は始端か改行か空白でなければならない
if( last_colon > 0 && ! EmojiDecoder.isWhitespaceBeforeEmoji( src.codePointBefore( last_colon ) ) ){
log.d( "checkEmoji: invalid character before shortcode." );
closeAcctPopup();
return;
}
if( part.length() == 0 ){
if( popup == null || ! popup.isShowing() ){
popup = new PopupAutoCompleteAcct( activity, et, formRoot, bMainScreen );
}
popup.setList(
last_colon, end
et, last_colon, end
, null
, picker_caption_emoji
, open_picker_emoji
@ -684,12 +694,10 @@ public class PostHelper implements CustomEmojiLister.Callback, EmojiPicker.Callb
}
}
if( code_list.isEmpty() ){
closeAcctPopup();
}else{
if( popup == null || ! popup.isShowing() ){
popup = new PopupAutoCompleteAcct( activity, et, formRoot, bMainScreen );
}
popup.setList( last_colon, end, code_list, picker_caption_emoji, open_picker_emoji );
popup.setList( et, last_colon, end, code_list, picker_caption_emoji, open_picker_emoji );
}
}
};

View File

@ -59,7 +59,7 @@ public class NetworkEmojiView extends View implements CustomEmojiCache.Callback
super.onDraw( canvas );
// APNGデータの取得
APNGFrames frames = App1.custom_emoji_cache.get( url, this );
APNGFrames frames = App1.custom_emoji_cache.get( this, url, this );
if( frames == null ) return;
long now = SystemClock.elapsedRealtime();
@ -87,7 +87,7 @@ public class NetworkEmojiView extends View implements CustomEmojiCache.Callback
}
}
@Override public void onAPNGLoadComplete( APNGFrames b ){
@Override public void onAPNGLoadComplete(){
postInvalidateOnAnimation();;
}
}