- GIFアニメの最適化
- それでもバッテリー的に厳しいのでデフォルトOFFに変更
This commit is contained in:
tateisu 2017-08-12 06:31:27 +09:00
parent 8159a60012
commit d81b86ea99
14 changed files with 705 additions and 207 deletions

View File

@ -9,8 +9,8 @@ android {
applicationId "jp.juggler.subwaytooter"
minSdkVersion 21
targetSdkVersion 25
versionCode 114
versionName "1.1.4"
versionCode 115
versionName "1.1.5"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

View File

@ -0,0 +1,201 @@
package com.bumptech.glide.load.resource.bitmap;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.util.DisplayMetrics;
import android.view.Gravity;
import com.bumptech.glide.load.resource.drawable.GlideDrawable;
import java.lang.reflect.Field;
import jp.juggler.subwaytooter.util.LogCategory;
@SuppressWarnings("unused") public class MyGlideBitmapDrawable extends GlideDrawable {
private static final LogCategory log = new LogCategory( "MyGlideBitmapDrawable" );
private final Rect destRect = new Rect();
private int width;
private int height;
private boolean applyGravity;
private boolean mutated;
private GlideBitmapDrawable.BitmapState state;
private static Field field_state;
private static GlideBitmapDrawable.BitmapState cloneState( GlideBitmapDrawable other ){
try{
if( field_state == null ){
field_state = GlideBitmapDrawable.class.getDeclaredField( "state" );
field_state.setAccessible( true );
}
GlideBitmapDrawable.BitmapState other_state = (GlideBitmapDrawable.BitmapState) field_state.get( other );
return new GlideBitmapDrawable.BitmapState( other_state );
}catch( Throwable ex ){
throw new RuntimeException( "cloning GlideBitmapDrawable.BitmapState failed.", ex );
}
}
private float mCornerRadius;
public MyGlideBitmapDrawable( Resources res,GlideBitmapDrawable other, float radius ){
this( res, cloneState( other ) );
this.mCornerRadius = radius;
}
private MyGlideBitmapDrawable(Resources res, GlideBitmapDrawable.BitmapState state) {
if (state == null) {
throw new NullPointerException("BitmapState must not be null");
}
this.state = state;
final int targetDensity;
if (res != null) {
final int density = res.getDisplayMetrics().densityDpi;
targetDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density;
state.targetDensity = targetDensity;
} else {
targetDensity = state.targetDensity;
}
width = state.bitmap.getScaledWidth(targetDensity);
height = state.bitmap.getScaledHeight(targetDensity);
}
@Override
public int getIntrinsicWidth() {
return width;
}
@Override
public int getIntrinsicHeight() {
return height;
}
@Override
public boolean isAnimated() {
return false;
}
@Override
public void setLoopCount(int loopCount) {
// Do nothing.
}
@Override
public void start() {
// Do nothing.
}
@Override
public void stop() {
// Do nothing.
}
@Override
public boolean isRunning() {
return false;
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
applyGravity = true;
}
@Override
public ConstantState getConstantState() {
return state;
}
@Override
public void draw( @NonNull Canvas canvas) {
if (applyGravity) {
Gravity.apply(Gravity.FILL, width, height, getBounds(), destRect);
applyGravity = false;
mBitmapShader = null;
}
Bitmap toDraw = state.bitmap;
if( mCornerRadius <= 0f ){
state.paint.setShader( null );
canvas.drawBitmap( toDraw, null, destRect, state.paint );
mBitmapShader = null;
}else{
drawRoundImage(canvas,toDraw );
}
}
private final Matrix mShaderMatrix = new Matrix();
private final RectF mDstRectF = new RectF();
private BitmapShader mBitmapShader;
private void drawRoundImage( Canvas canvas, Bitmap src){
if( src == null ) return;
int src_w = src.getWidth();
int src_h = src.getHeight();
if( src_w < 1 || src_h < 1 ) return;
if( mBitmapShader == null ){
int outWidth = destRect.width();
int outHeight = destRect.height();
mDstRectF.set( destRect );
mShaderMatrix.preScale( mDstRectF.width() / src_w, mDstRectF.height() / src_h );
mBitmapShader = new BitmapShader( src, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP );
mBitmapShader.setLocalMatrix( mShaderMatrix );
}
state.paint.setShader( mBitmapShader );
canvas.drawRoundRect( mDstRectF, mCornerRadius, mCornerRadius, state.paint );
}
@Override
public void setAlpha(int alpha) {
int currentAlpha = state.paint.getAlpha();
if (currentAlpha != alpha) {
state.setAlpha(alpha);
invalidateSelf();
}
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
state.setColorFilter(colorFilter);
invalidateSelf();
}
@Override
public int getOpacity() {
Bitmap bm = state.bitmap;
return bm == null || bm.hasAlpha() || state.paint.getAlpha() < 255
? PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
}
@NonNull @Override
public Drawable mutate() {
if (!mutated && super.mutate() == this) {
state = new GlideBitmapDrawable.BitmapState(state);
mutated = true;
}
return this;
}
public Bitmap getBitmap() {
return state.bitmap;
}
}

View File

@ -0,0 +1,370 @@
package com.bumptech.glide.load.resource.gif;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.support.annotation.NonNull;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.View;
import com.bumptech.glide.gifdecoder.GifDecoder;
import com.bumptech.glide.load.Transformation;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.drawable.GlideDrawable;
import java.lang.reflect.Field;
import jp.juggler.subwaytooter.util.LogCategory;
import static com.bumptech.glide.gifdecoder.GifDecoder.TOTAL_ITERATION_COUNT_FOREVER;
@SuppressWarnings({ "WeakerAccess", "unused", "ObsoleteSdkInt" })
public class MyGifDrawable extends GlideDrawable implements GifFrameLoader.FrameCallback {
static final LogCategory log = new LogCategory( "MyGifDrawable" );
private final Paint paint;
private final Rect destRect = new Rect();
private final GifDrawable.GifState state;
private final GifDecoder decoder;
private final GifFrameLoader frameLoader;
/**
* True if the drawable is currently animating.
*/
private boolean isRunning;
/**
* True if the drawable should animate while visible.
*/
private boolean isStarted;
/**
* True if the drawable's resources have been recycled.
*/
private boolean isRecycled;
/**
* True if the drawable is currently visible. Default to true because on certain platforms (at least 4.1.1),
* setVisible is not called on {@link android.graphics.drawable.Drawable Drawables} during
* {@link android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)}. See issue #130.
*/
private boolean isVisible = true;
/**
* The number of times we've looped over all the frames in the gif.
*/
private int loopCount;
/**
* The number of times to loop through the gif animation.
*/
private int maxLoopCount = LOOP_FOREVER;
private boolean applyGravity;
static Field field_state;
static GifDrawable.GifState cloneState( GifDrawable other ){
try{
if( field_state == null ){
field_state = GifDrawable.class.getDeclaredField( "state" );
field_state.setAccessible( true );
}
GifDrawable.GifState other_state = (GifDrawable.GifState) field_state.get( other );
return new GifDrawable.GifState(
other_state.gifHeader,
other_state.data,
other_state.context,
other.getFrameTransformation(),
other_state.targetWidth,
other_state.targetHeight,
other_state.bitmapProvider,
other_state.bitmapPool,
other.getFirstFrame()
);
}catch( Throwable ex ){
throw new RuntimeException( "cloning GifDrawable.GifState failed.", ex );
}
}
float mCornerRadius;
public MyGifDrawable( GifDrawable other, float radius ){
this( cloneState( other ) );
this.mCornerRadius = radius;
}
private MyGifDrawable( GifDrawable.GifState state ){
if( state == null ){
throw new NullPointerException( "GifState must not be null" );
}
this.state = state;
this.decoder = new GifDecoder( state.bitmapProvider );
this.paint = new Paint( Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG );
decoder.setData( state.gifHeader, state.data );
frameLoader = new GifFrameLoader( state.context, this, decoder, state.targetWidth, state.targetHeight );
frameLoader.setFrameTransformation( state.frameTransformation );
}
// Visible for testing.
MyGifDrawable( GifDecoder decoder, GifFrameLoader frameLoader, Bitmap firstFrame, BitmapPool bitmapPool, Paint paint ){
this.decoder = decoder;
this.frameLoader = frameLoader;
this.state = new GifDrawable.GifState( null );
this.paint = paint;
state.bitmapPool = bitmapPool;
state.firstFrame = firstFrame;
}
public Bitmap getFirstFrame(){
return state.firstFrame;
}
public void setFrameTransformation( Transformation< Bitmap > frameTransformation, Bitmap firstFrame ){
if( firstFrame == null ){
throw new NullPointerException( "The first frame of the GIF must not be null" );
}
if( frameTransformation == null ){
throw new NullPointerException( "The frame transformation must not be null" );
}
state.frameTransformation = frameTransformation;
state.firstFrame = firstFrame;
frameLoader.setFrameTransformation( frameTransformation );
}
public GifDecoder getDecoder(){
return decoder;
}
public Transformation< Bitmap > getFrameTransformation(){
return state.frameTransformation;
}
public byte[] getData(){
return state.data;
}
public int getFrameCount(){
return decoder.getFrameCount();
}
private void resetLoopCount(){
loopCount = 0;
}
@Override
public void start(){
log.d("start");
isStarted = true;
resetLoopCount();
if( isVisible ){
startRunning();
}
}
@Override
public void stop(){
isStarted = false;
stopRunning();
// On APIs > honeycomb we know our drawable is not being displayed anymore when it's callback is cleared and so
// we can use the absence of a callback as an indication that it's ok to clear our temporary data. Prior to
// honeycomb we can't tell if our callback is null and instead eagerly reset to avoid holding on to resources we
// no longer need.
if( Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB ){
reset();
}
}
/**
* Clears temporary data and resets the drawable back to the first frame.
*/
private void reset(){
frameLoader.clear();
invalidateSelf();
}
private void startRunning(){
// If we have only a single frame, we don't want to decode it endlessly.
if( decoder.getFrameCount() == 1 ){
invalidateSelf();
}else if( ! isRunning ){
isRunning = true;
frameLoader.start();
invalidateSelf();
}
}
private void stopRunning(){
isRunning = false;
frameLoader.stop();
}
@Override
public boolean setVisible( boolean visible, boolean restart ){
isVisible = visible;
if( ! visible ){
stopRunning();
}else if( isStarted ){
startRunning();
}
return super.setVisible( visible, restart );
}
@Override
public int getIntrinsicWidth(){
return state.firstFrame.getWidth();
}
@Override
public int getIntrinsicHeight(){
return state.firstFrame.getHeight();
}
@Override
public boolean isRunning(){
return isRunning;
}
// For testing.
void setIsRunning( boolean isRunning ){
this.isRunning = isRunning;
}
@Override
protected void onBoundsChange( Rect bounds ){
super.onBoundsChange( bounds );
applyGravity = true;
}
final Matrix mShaderMatrix = new Matrix();
final RectF mDstRectF = new RectF();
@Override
public void draw( @NonNull Canvas canvas ){
if( isRecycled ){
return;
}
if( applyGravity ){
Gravity.apply( Gravity.FILL, getIntrinsicWidth(), getIntrinsicHeight(), getBounds(), destRect );
applyGravity = false;
}
Bitmap currentFrame = frameLoader.getCurrentFrame();
Bitmap toDraw = currentFrame != null ? currentFrame : state.firstFrame;
if( mCornerRadius <= 0f ){
paint.setShader( null );
canvas.drawBitmap( toDraw, null, destRect, paint );
}else{
drawRoundImage(canvas,toDraw );
}
}
private void drawRoundImage( Canvas canvas, Bitmap src){
if( src == null ) return;
int src_w = src.getWidth();
int src_h = src.getHeight();
if( src_w < 1 || src_h < 1 ) return;
int outWidth = destRect.width();
int outHeight = destRect.height();
mDstRectF.set( destRect );
mShaderMatrix.preScale( mDstRectF.width() / src_w, mDstRectF.height() / src_h );
BitmapShader mBitmapShader = new BitmapShader( src, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP );
mBitmapShader.setLocalMatrix( mShaderMatrix );
paint.setShader( mBitmapShader );
canvas.drawRoundRect( mDstRectF, mCornerRadius, mCornerRadius, paint );
}
@Override
public void setAlpha( int i ){
paint.setAlpha( i );
}
@Override
public void setColorFilter( ColorFilter colorFilter ){
paint.setColorFilter( colorFilter );
}
@Override
public int getOpacity(){
// We can't tell, so default to transparent to be safe.
return PixelFormat.TRANSPARENT;
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@Override
public void onFrameReady( int frameIndex ){
if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && getCallback() == null ){
stop();
reset();
return;
}
invalidateSelf();
if( frameIndex == decoder.getFrameCount() - 1 ){
loopCount++;
}
if( maxLoopCount != LOOP_FOREVER && loopCount >= maxLoopCount ){
stop();
}
}
@Override
public ConstantState getConstantState(){
return state;
}
/**
* Clears any resources for loading frames that are currently held on to by this object.
*/
public void recycle(){
isRecycled = true;
state.bitmapPool.put( state.firstFrame );
frameLoader.clear();
frameLoader.stop();
}
// For testing.
boolean isRecycled(){
return isRecycled;
}
@Override
public boolean isAnimated(){
return true;
}
@Override
public void setLoopCount( int loopCount ){
if( loopCount <= 0 && loopCount != LOOP_FOREVER && loopCount != LOOP_INTRINSIC ){
throw new IllegalArgumentException( "Loop count must be greater than 0, or equal to "
+ "GlideDrawable.LOOP_FOREVER, or equal to GlideDrawable.LOOP_INTRINSIC" );
}
if( loopCount == LOOP_INTRINSIC ){
int intrinsicCount = decoder.getTotalIterationCount();
maxLoopCount = ( intrinsicCount == TOTAL_ITERATION_COUNT_FOREVER ) ? LOOP_FOREVER : intrinsicCount;
}else{
maxLoopCount = loopCount;
}
}
}

View File

@ -98,7 +98,7 @@ public class ActAppSetting extends AppCompatActivity
Switch swPostButtonBarTop;
Switch swDontDuplicationCheck;
Switch swQuickTootBar;
Switch swDisableGifAnimation;
Switch swEnableGifAnimation;
Spinner spBackButtonAction;
Spinner spUITheme;
@ -197,8 +197,8 @@ public class ActAppSetting extends AppCompatActivity
swQuickTootBar = (Switch) findViewById( R.id.swQuickTootBar );
swQuickTootBar.setOnCheckedChangeListener( this );
swDisableGifAnimation = (Switch) findViewById( R.id.swDisableGifAnimation );
swDisableGifAnimation.setOnCheckedChangeListener( this );
swEnableGifAnimation = (Switch) findViewById( R.id.swEnableGifAnimation );
swEnableGifAnimation.setOnCheckedChangeListener( this );
cbNotificationSound = (CheckBox) findViewById( R.id.cbNotificationSound );
cbNotificationVibration = (CheckBox) findViewById( R.id.cbNotificationVibration );
@ -340,7 +340,7 @@ public class ActAppSetting extends AppCompatActivity
swPostButtonBarTop.setChecked( pref.getBoolean( Pref.KEY_POST_BUTTON_BAR_AT_TOP, false ) );
swDontDuplicationCheck.setChecked( pref.getBoolean( Pref.KEY_DONT_DUPLICATION_CHECK, false ) );
swQuickTootBar.setChecked( pref.getBoolean( Pref.KEY_QUICK_TOOT_BAR, false ) );
swDisableGifAnimation.setChecked( pref.getBoolean( Pref.KEY_DISABLE_GIF_ANIMATION, false ) );
swEnableGifAnimation.setChecked( pref.getBoolean( Pref.KEY_ENABLE_GIF_ANIMATION, false ) );
// Switch with default true
swDisableFastScroller.setChecked( pref.getBoolean( Pref.KEY_DISABLE_FAST_SCROLLER, true ) );
@ -403,7 +403,7 @@ public class ActAppSetting extends AppCompatActivity
.putBoolean( Pref.KEY_POST_BUTTON_BAR_AT_TOP, swPostButtonBarTop.isChecked() )
.putBoolean( Pref.KEY_DONT_DUPLICATION_CHECK, swDontDuplicationCheck.isChecked() )
.putBoolean( Pref.KEY_QUICK_TOOT_BAR, swQuickTootBar.isChecked() )
.putBoolean( Pref.KEY_DISABLE_GIF_ANIMATION, swDisableGifAnimation.isChecked() )
.putBoolean( Pref.KEY_ENABLE_GIF_ANIMATION, swEnableGifAnimation.isChecked() )
.putBoolean( Pref.KEY_NOTIFICATION_SOUND, cbNotificationSound.isChecked() )
.putBoolean( Pref.KEY_NOTIFICATION_VIBRATION, cbNotificationVibration.isChecked() )

View File

@ -313,7 +313,7 @@ public class AppDataExporter {
case Pref.KEY_POST_BUTTON_BAR_AT_TOP:
case Pref.KEY_DONT_DUPLICATION_CHECK:
case Pref.KEY_QUICK_TOOT_BAR:
case Pref.KEY_DISABLE_GIF_ANIMATION:
case Pref.KEY_ENABLE_GIF_ANIMATION:
boolean bv = reader.nextBoolean();
e.putBoolean( k, bv );
break;
@ -373,6 +373,7 @@ public class AppDataExporter {
// just ignore
case "device_token":
case "install_id":
case "disable_gif_animation":
reader.skipValue();
e.remove( k );
break;

View File

@ -72,7 +72,7 @@ public class Pref {
public static final String KEY_QUOTE_NAME_FORMAT = "quote_name_format";
public static final String KEY_DISABLE_GIF_ANIMATION = "disable_gif_animation";
public static final String KEY_ENABLE_GIF_ANIMATION = "enable_gif_animation";
// 項目を追加したらAppDataExporter#importPref のswitch文も更新すること
}

View File

@ -1,6 +1,54 @@
package jp.juggler.subwaytooter.api_msp.entity;
import android.content.Context;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import org.json.JSONObject;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jp.juggler.subwaytooter.api.entity.TootAccount;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.HTMLDecoder;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
public class MSPAccount extends TootAccount {
private static final Pattern reAccountUrl = Pattern.compile( "\\Ahttps://([^/#?]+)/@([^/#?]+)\\z" );
static TootAccount parseAccount( @NonNull Context context, LogCategory log, SavedAccount access_info, JSONObject src ){
if( src == null ) return null;
MSPAccount dst = new MSPAccount();
dst.url = Utils.optStringX( src, "url" );
dst.username = Utils.optStringX( src, "username" );
dst.avatar = dst.avatar_static = Utils.optStringX( src, "avatar" );
String sv = Utils.optStringX( src, "display_name" );
dst.setDisplayName( context, dst.username , sv );
dst.id = src.optLong( "id" );
dst.note = Utils.optStringX( src, "note" );
dst.decoded_note = HTMLDecoder.decodeHTML( context,access_info, ( dst.note != null ? dst.note : null ), true, true,null );
if( TextUtils.isEmpty( dst.url ) ){
log.e( "parseAccount: missing url" );
return null;
}
Matcher m = reAccountUrl.matcher( dst.url );
if( ! m.find() ){
log.e( "parseAccount: not account url: %s", dst.url );
return null;
}else{
dst.acct = dst.username + "@" + m.group( 1 );
}
return dst;
}
}

View File

@ -15,7 +15,6 @@ import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jp.juggler.subwaytooter.api.entity.TootAccount;
import jp.juggler.subwaytooter.api.entity.TootStatusLike;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.HTMLDecoder;
@ -29,40 +28,6 @@ public class MSPToot extends TootStatusLike {
}
private static final Pattern reAccountUrl = Pattern.compile( "\\Ahttps://([^/#?]+)/@([^/#?]+)\\z" );
private static TootAccount parseAccount( @NonNull Context context, LogCategory log, SavedAccount access_info, JSONObject src ){
if( src == null ) return null;
MSPAccount dst = new MSPAccount();
dst.url = Utils.optStringX( src, "url" );
dst.username = Utils.optStringX( src, "username" );
dst.avatar = dst.avatar_static = Utils.optStringX( src, "avatar" );
String sv = Utils.optStringX( src, "display_name" );
dst.setDisplayName( context, dst.username , sv );
dst.id = src.optLong( "id" );
dst.note = Utils.optStringX( src, "note" );
dst.decoded_note = HTMLDecoder.decodeHTML( context,access_info, ( dst.note != null ? dst.note : null ), true, true,null );
if( TextUtils.isEmpty( dst.url ) ){
log.e( "parseAccount: missing url" );
return null;
}
Matcher m = reAccountUrl.matcher( dst.url );
if( ! m.find() ){
log.e( "parseAccount: not account url: %s", dst.url );
return null;
}else{
dst.acct = dst.username + "@" + m.group( 1 );
}
return dst;
}
// private static final Pattern reTime = Pattern.compile( "\\A(\\d+)\\D+(\\d+)\\D+(\\d+)\\D+(\\d+)\\D+(\\d+)\\D+(\\d+)" );
//
// private static final TimeZone tz_tokyo = TimeZone.getTimeZone( "Asia/Tokyo" );
@ -103,7 +68,7 @@ public class MSPToot extends TootStatusLike {
if( src == null ) return null;
MSPToot dst = new MSPToot();
dst.account = parseAccount( context,log, access_info, src.optJSONObject( "account" ) );
dst.account = MSPAccount.parseAccount( context, log, access_info, src.optJSONObject( "account" ) );
if( dst.account == null ){
log.e( "missing status account" );
return null;
@ -136,20 +101,20 @@ public class MSPToot extends TootStatusLike {
// dst.msp_id = src.optLong( "msp_id" );
dst.sensitive = ( src.optInt( "sensitive", 0 ) != 0 );
dst.setSpoilerText(context,Utils.optStringX( src, "spoiler_text" ));
dst.setSpoilerText( context, Utils.optStringX( src, "spoiler_text" ) );
dst.content = Utils.optStringX( src, "content" );
dst.decoded_content = HTMLDecoder.decodeHTML( context,access_info, dst.content, true,true, null );
dst.decoded_content = HTMLDecoder.decodeHTML( context, access_info, dst.content, true, true, null );
return dst;
}
public static List parseList( @NonNull Context context, LogCategory log, SavedAccount access_info, JSONArray array ){
public static List parseList( @NonNull Context context, LogCategory log, SavedAccount access_info, JSONArray array ){
List list = new List();
for( int i = 0, ie = array.length() ; i < ie ; ++ i ){
JSONObject src = array.optJSONObject( i );
if( src == null ) continue;
MSPToot item = parse( context,log, access_info, src );
MSPToot item = parse( context, log, access_info, src );
if( item == null ) continue;
list.add( item );
}

View File

@ -1,15 +0,0 @@
package jp.juggler.subwaytooter.view;
import android.graphics.Bitmap;
import com.bumptech.glide.load.Transformation;
import com.bumptech.glide.load.resource.gif.GifDrawable;
class MyGifDrawable extends GifDrawable {
MyGifDrawable( GifDrawable other, Bitmap firstFrame, Transformation< Bitmap > frameTransformation ){
super( other, firstFrame, frameTransformation );
}
// このクラスに追加された機能は特にないがGifDrawableから差し替え済みであることを検出できるようにするため派生クラスとしている
}

View File

@ -4,14 +4,7 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
@ -19,22 +12,19 @@ import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.support.v7.widget.AppCompatImageView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
import com.bumptech.glide.load.resource.bitmap.GlideBitmapDrawable;
import com.bumptech.glide.load.resource.bitmap.MyGlideBitmapDrawable;
import com.bumptech.glide.load.resource.drawable.GlideDrawable;
import com.bumptech.glide.load.resource.gif.GifDrawable;
import com.bumptech.glide.load.resource.gif.MyGifDrawable;
import com.bumptech.glide.request.animation.GlideAnimation;
import com.bumptech.glide.request.target.BaseTarget;
import com.bumptech.glide.request.target.ImageViewTarget;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.target.SquaringDrawable;
import com.bumptech.glide.request.target.Target;
import jp.juggler.subwaytooter.Pref;
@ -97,7 +87,7 @@ public class MyNetworkImageView extends AppCompatImageView {
mCornerRadius = r;
}
if( pref.getBoolean( Pref.KEY_DISABLE_GIF_ANIMATION, false ) ){
if( ! pref.getBoolean( Pref.KEY_ENABLE_GIF_ANIMATION, false ) ){
gif_url = null;
}
@ -116,6 +106,14 @@ public class MyNetworkImageView extends AppCompatImageView {
private void cancelLoading(){
if( mTarget != null ){
Drawable d = getDrawable();
if( d instanceof GlideDrawable ){
GlideDrawable gd =(GlideDrawable)d;
if( gd.isRunning() ){
log.d("cancelLoading: GlideDrawable.stop()");
gd.stop();
}
}
setImageDrawable( null );
Glide.clear( mTarget );
mTarget = null;
@ -124,6 +122,16 @@ public class MyNetworkImageView extends AppCompatImageView {
// デフォルト画像かnullを表示する
private void setDefaultImageOrNull(){
Drawable d = getDrawable();
if( d instanceof GlideDrawable ){
GlideDrawable gd =(GlideDrawable)d;
if( gd.isRunning() ){
log.d("setDefaultImageOrNull: GlideDrawable.stop()");
gd.stop();
}
}
if( mDefaultImageId != 0 ){
setImageResource( mDefaultImageId );
}else{
@ -171,17 +179,9 @@ public class MyNetworkImageView extends AppCompatImageView {
}
if( mMayGif ){
if( mCornerRadius > 0f ){
mTarget = Glide.with( getContext() )
.load( mUrl )
.transform( new RoundTransformation( mCornerRadius ) )
.into( new MyTargetGif( mUrl ) );
}else{
mTarget = Glide.with( getContext() )
.load( mUrl )
.into( new MyTargetGif( mUrl ) );
}
mTarget = Glide.with( getContext() )
.load( mUrl )
.into( new MyTargetGif( mUrl ) );
}else{
mTarget = Glide.with( getContext() )
.load( mUrl )
@ -310,38 +310,47 @@ public class MyNetworkImageView extends AppCompatImageView {
// このViewは別の画像を表示するように指定が変わっていた
if( ! url.equals( mUrl ) ) return;
// ディスクキャッシュから読んだ画像は角丸が正しく扱われない
// transformを設定しなおす
if( ( resource instanceof GifDrawable ) && ! ( resource instanceof MyGifDrawable ) ){
GifDrawable src = (GifDrawable) resource;
if( app_context != null && mCornerRadius > 0f ){
RoundTransformation t = new RoundTransformation( mCornerRadius );
try{
BitmapPool pool = Glide.get( app_context ).getBitmapPool();
Bitmap first_frame = t.transform( pool, src.getFirstFrame(), getWidth(), getHeight() );
resource = new MyGifDrawable( src,first_frame, t );
}catch( Throwable ex ){
ex.printStackTrace();
// view#getWidth() 0 だと firstFrame transform がnullを返してGifDrawableを作れない
resource = new MyGifDrawable( src,src.getFirstFrame(), t );
if( mCornerRadius > 0f ){
if( resource instanceof GifDrawable ){
// ディスクキャッシュから読んだ画像は角丸が正しく扱われない
// MyGifDrawable に差し替えて描画させる
GifDrawable src = (GifDrawable) resource;
if( app_context != null ){
try{
resource = new MyGifDrawable( src, mCornerRadius );
}catch( Throwable ex ){
ex.printStackTrace();
}
}
}else if ( resource instanceof GlideBitmapDrawable ){
GlideBitmapDrawable src = (GlideBitmapDrawable) resource;
if( app_context != null ){
try{
resource = new MyGlideBitmapDrawable( getResources(), src, mCornerRadius );
}catch( Throwable ex ){
ex.printStackTrace();
}
}
}else{
throw new RuntimeException( String.format("unsupported draw type : %s", resource.getClass()) );
}
}
if( ! resource.isAnimated() ){
//TODO: Try to generalize this to other sizes/shapes.
// This is a dirty hack that tries to make loading square thumbnails and then square full images less costly
// by forcing both the smaller thumb and the larger version to have exactly the same intrinsic dimensions.
// If a drawable is replaced in an ImageView by another drawable with different intrinsic dimensions,
// the ImageView requests a layout. Scrolling rapidly while replacing thumbs with larger images triggers
// lots of these calls and causes significant amounts of jank.
float viewRatio = view.getWidth() / (float) view.getHeight();
float drawableRatio = resource.getIntrinsicWidth() / (float) resource.getIntrinsicHeight();
if( Math.abs( viewRatio - 1f ) <= SQUARE_RATIO_MARGIN
&& Math.abs( drawableRatio - 1f ) <= SQUARE_RATIO_MARGIN ){
resource = new SquaringDrawable( resource, view.getWidth() );
}
}
// if( ! resource.isAnimated() ){
// //TODO: Try to generalize this to other sizes/shapes.
// // This is a dirty hack that tries to make loading square thumbnails and then square full images less costly
// // by forcing both the smaller thumb and the larger version to have exactly the same intrinsic dimensions.
// // If a drawable is replaced in an ImageView by another drawable with different intrinsic dimensions,
// // the ImageView requests a layout. Scrolling rapidly while replacing thumbs with larger images triggers
// // lots of these calls and causes significant amounts of jank.
// float viewRatio = view.getWidth() / (float) view.getHeight();
// float drawableRatio = resource.getIntrinsicWidth() / (float) resource.getIntrinsicHeight();
// if( Math.abs( viewRatio - 1f ) <= SQUARE_RATIO_MARGIN
// && Math.abs( drawableRatio - 1f ) <= SQUARE_RATIO_MARGIN ){
// resource = new SquaringDrawable( resource, view.getWidth() );
// }
// }
super.onResourceReady( resource, animation );
this.glide_drawable = resource;
resource.setLoopCount( maxLoopCount );
@ -366,16 +375,16 @@ public class MyNetworkImageView extends AppCompatImageView {
@Override
public void onStart(){
// log.d( "MyTargetGif onStart glide_drawable=%s", glide_drawable );
if( glide_drawable != null ){
if( glide_drawable != null && ! glide_drawable.isRunning() ){
log.d( "MyTargetGif onStart glide_drawable=%s", glide_drawable );
glide_drawable.start();
}
}
@Override
public void onStop(){
log.d( "MyTargetGif onStop glide_drawable=%s", glide_drawable );
if( glide_drawable != null ){
if( glide_drawable != null && glide_drawable.isRunning() ){
log.d( "MyTargetGif onStop glide_drawable=%s", glide_drawable );
glide_drawable.stop();
}
}
@ -388,86 +397,6 @@ public class MyNetworkImageView extends AppCompatImageView {
}
private static class RoundTransformation extends BitmapTransformation {
private final float radius;
RoundTransformation( float radius ){
super( app_context );
this.radius = radius;
mPaint.setAntiAlias( true );
mPaint.setFilterBitmap( true );
}
@Override public String getId(){
return getClass().getName();
}
final Matrix mShaderMatrix = new Matrix();
final Rect mViewContainer = new Rect();
final Rect mDstRect = new Rect();
final RectF mDstRectF = new RectF();
final Paint mPaint = new Paint( Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG );
@Override
protected Bitmap transform( BitmapPool pool, Bitmap source, int outWidth, int outHeight ){
if( source == null ) return null;
int src_w = source.getWidth();
int src_h = source.getHeight();
if( src_w < 1 || src_h < 1 ) return null;
if( outWidth < 1 || outHeight < 1 ) return null;
int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT;
int mBitmapWidth = source.getScaledWidth( mTargetDensity );
int mBitmapHeight = source.getScaledHeight( mTargetDensity );
mViewContainer.left = 0;
mViewContainer.top = 0;
mViewContainer.right = outWidth;
mViewContainer.bottom = outHeight;
int mGravity = Gravity.FILL;
Gravity.apply( mGravity, mBitmapWidth, mBitmapHeight, mViewContainer, mDstRect, View.LAYOUT_DIRECTION_LTR );
mDstRectF.set( mDstRect );
mShaderMatrix.setTranslate( mDstRectF.left, mDstRectF.top );
mShaderMatrix.preScale( mDstRectF.width() / src_w, mDstRectF.height() / src_h );
BitmapShader mBitmapShader = new BitmapShader( source, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP );
mBitmapShader.setLocalMatrix( mShaderMatrix );
Bitmap result = pool.getDirty( outWidth, outHeight, Bitmap.Config.ARGB_8888 );
if( result == null ){
result = Bitmap.createBitmap( outWidth, outHeight, Bitmap.Config.ARGB_8888 );
}
Canvas canvas = new Canvas( result );
canvas.drawColor( Color.TRANSPARENT, PorterDuff.Mode.CLEAR );
mPaint.setShader( mBitmapShader );
// mPaint.setColor( 0xffff0000 );
canvas.drawRoundRect( mDstRectF, radius, radius, mPaint );
// log.d("transform radius=%.2f,outWidth=%d,outHeight=%d",radius,outWidth,outHeight);
// int dst_wh = Math.min( src_w,src_h );
// int offset_x = ( src_w - dst_wh ) / 2;
// int offset_y = ( src_h - dst_wh ) / 2;
//
//
// // TODO this could be acquired from the pool too
// pool.
// Bitmap squared = Bitmap.createBitmap( source, x, y, size, size );
//
//
// Paint paint = new Paint();
// paint.setShader( new BitmapShader( squared, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP ) );
// paint.setAntiAlias( true );
// float r = size / 2f;
// canvas.drawCircle( r, r, r, paint );
// // canvas.drawRoundRect( float left, float top, float right, float bottom, float rx, float ry,
return result;
}
}
final Runnable proc_load_image = new Runnable() {
@Override public void run(){
loadImageIfNecessary();

View File

@ -542,13 +542,13 @@
<TextView
style="@style/setting_row_label"
android:text="@string/disable_gif_animation"
android:text="@string/enable_gif_animation"
/>
<LinearLayout style="@style/setting_row_form">
<Switch
android:id="@+id/swDisableGifAnimation"
android:id="@+id/swEnableGifAnimation"
style="@style/setting_horizontal_stretch"
android:gravity="center"
/>

View File

@ -411,7 +411,7 @@
<string name="acct_color">Acct text color</string>
<string name="content_color">Content text color</string>
<string name="content_sample">(sample)Le cœur déçu mais l\'âme plutôt naïve, Louÿs rêva de crapaüter en canoë au delà des îles, près du mälströn où brûlent les novæ.</string>
<string name="disable_gif_animation">Disable GIF animation (app restart required)</string>
<string name="enable_gif_animation">Enable GIF animation (It wastes the battery very much)</string>
<string name="acct_sample">(sample)username@instance</string>
<!--<string name="abc_action_bar_home_description">Revenir à l\'accueil</string>-->

View File

@ -698,7 +698,6 @@
<string name="background_image_alpha">背景画像のアルファ値</string>
<string name="content_color">本文の文字色</string>
<string name="content_sample">(見本)色はにほへど 散りぬるを 我が世たれぞ 常ならむ 有為の奥山 今日越えて 浅き夢見じ 酔ひもせず</string>
<string name="disable_gif_animation">GIFアニメ表示を無効(アプリ再起動が必要)</string>
<string name="acct_sample">(見本)username@instance</string>
<string name="enable_gif_animation">GIFアニメーションを有効にする(バッテリーをとても浪費します)</string>
</resources>

View File

@ -406,7 +406,7 @@
<string name="acct_color">Acct text color</string>
<string name="content_color">Content text color</string>
<string name="content_sample">(sample)The quick brown fox jumps over the lazy dog.</string>
<string name="disable_gif_animation">Disable GIF animation (app restart required)</string>
<string name="enable_gif_animation">Enable GIF animation (It wastes the battery very much)</string>
<string name="acct_sample">(sample)username@instance</string>
</resources>