内蔵メディアビューアのローディング表示に進捗ゲージを追加

This commit is contained in:
tateisu 2017-12-27 20:28:20 +09:00
parent 07c8d64b3a
commit 93f40e08a5
5 changed files with 568 additions and 141 deletions

View File

@ -56,10 +56,12 @@ import jp.juggler.subwaytooter.api.TootApiTask;
import jp.juggler.subwaytooter.api.entity.TootAttachment;
import jp.juggler.subwaytooter.dialog.ActionsDialog;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.ProgressResponseBody;
import jp.juggler.subwaytooter.util.Utils;
import jp.juggler.subwaytooter.view.PinchBitmapView;
import okhttp3.Call;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class ActMediaViewer extends AppCompatActivity implements View.OnClickListener {
@ -172,7 +174,7 @@ public class ActMediaViewer extends AppCompatActivity implements View.OnClickLis
exoPlayer = ExoPlayerFactory.newSimpleInstance( this, new DefaultTrackSelector() );
exoPlayer.addListener( player_listener );
exoView.setPlayer( exoPlayer );
}
@ -276,7 +278,7 @@ public class ActMediaViewer extends AppCompatActivity implements View.OnClickLis
pbvImage.setVisibility( View.VISIBLE );
pbvImage.setBitmap( null );
new TootApiTask( this, true ) {
new TootApiTask( this, TootApiTask.PROGRESS_HORIZONTAL ) {
private final BitmapFactory.Options options = new BitmapFactory.Options();
@ -325,7 +327,17 @@ public class ActMediaViewer extends AppCompatActivity implements View.OnClickLis
try{
//noinspection ConstantConditions
data = response.body().bytes();
data = ProgressResponseBody.bytes( response,new ProgressResponseBody.Callback() {
@Override public void progressBytes( long bytesRead, long bytesTotal ){
// 50MB以上のデータはキャンセルする
if( Math.max(bytesRead,bytesTotal) >= 50000000 ){
throw new RuntimeException( "media attachment is larger than 50000000" );
}
publishApiProgressRatio( (int) bytesRead, (int) bytesTotal );
}
} );
return new TootApiResult( "" );
}catch( Throwable ex ){
return new TootApiResult( Utils.formatError( ex, "content error." ) );
@ -388,15 +400,17 @@ public class ActMediaViewer extends AppCompatActivity implements View.OnClickLis
}
}
static class DownloadHistory{
static class DownloadHistory {
final long time;
@NonNull final String url;
DownloadHistory(long time,@NonNull String url){
DownloadHistory( long time, @NonNull String url ){
this.time = time;
this.url = url;
}
}
static final LinkedList<DownloadHistory> download_history_list = new LinkedList<>( );
static final LinkedList< DownloadHistory > download_history_list = new LinkedList<>();
static final long DOWNLOAD_REPEAT_EXPIRE = 3000L;
void download( @NonNull TootAttachment ta ){
@ -412,7 +426,7 @@ public class ActMediaViewer extends AppCompatActivity implements View.OnClickLis
Utils.showToast( this, false, "download manager is not on your device." );
return;
}
String url = ta.getLargeUrl( App1.pref );
if( url == null ) return;
@ -437,24 +451,24 @@ public class ActMediaViewer extends AppCompatActivity implements View.OnClickLis
download_history_list.addLast( new DownloadHistory( now, url ) );
}
String fileName =null;
String fileName = null;
try{
List<String> pathSegments = Uri.parse( url ).getPathSegments();
List< String > pathSegments = Uri.parse( url ).getPathSegments();
if( pathSegments != null ){
int size = pathSegments.size();
for( int i= size-1;i>=0;--i){
String s = pathSegments.get(i);
if( !TextUtils.isEmpty( s )){
for( int i = size - 1 ; i >= 0 ; -- i ){
String s = pathSegments.get( i );
if( ! TextUtils.isEmpty( s ) ){
fileName = s;
break;
}
}
}
}catch(Throwable ex){
}catch( Throwable ex ){
log.trace( ex );
}
if( fileName == null ){
fileName = url
.replaceFirst( "https?://", "" )

View File

@ -20,6 +20,7 @@ import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
import com.bumptech.glide.load.model.GlideUrl;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.concurrent.BlockingQueue;
@ -46,12 +47,14 @@ import jp.juggler.subwaytooter.table.UserRelation;
import jp.juggler.subwaytooter.util.CustomEmojiCache;
import jp.juggler.subwaytooter.util.CustomEmojiLister;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.ProgressResponseBody;
import jp.juggler.subwaytooter.util.Utils;
import okhttp3.Cache;
import okhttp3.CacheControl;
import okhttp3.Call;
import okhttp3.CipherSuite;
import okhttp3.ConnectionSpec;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Response;
import uk.co.chrisjenx.calligraphy.CalligraphyConfig;
@ -107,8 +110,7 @@ public class App1 extends Application {
super( context, DB_NAME, null, DB_VERSION );
}
@Override
public void onCreate( SQLiteDatabase db ){
@Override public void onCreate( SQLiteDatabase db ){
LogData.onDBCreate( db );
//
SavedAccount.onDBCreate( db );
@ -125,8 +127,7 @@ public class App1 extends Application {
TagSet.onDBCreate( db );
}
@Override
public void onUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){
@Override public void onUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){
LogData.onDBUpgrade( db, oldVersion, newVersion );
//
SavedAccount.onDBUpgrade( db, oldVersion, newVersion );
@ -217,7 +218,8 @@ public class App1 extends Application {
.readTimeout( 60, TimeUnit.SECONDS )
.writeTimeout( 60, TimeUnit.SECONDS )
.pingInterval( 10, TimeUnit.SECONDS )
.connectionSpecs( spec_list );
.connectionSpecs( spec_list )
.addInterceptor( ProgressResponseBody.makeInterceptor() );
return builder;
}
@ -237,7 +239,7 @@ public class App1 extends Application {
@SuppressLint("StaticFieldLeak")
public static CustomEmojiCache custom_emoji_cache;
@SuppressLint("StaticFieldLeak")
public static CustomEmojiLister custom_emoji_lister;
@ -400,15 +402,16 @@ public class App1 extends Application {
}
public static void setActivityTheme( @NonNull Activity activity, boolean bNoActionBar ){
setActivityTheme( activity, bNoActionBar ,false);
setActivityTheme( activity, bNoActionBar, false );
}
public static void setActivityTheme( @NonNull Activity activity, boolean bNoActionBar ,boolean forceDark){
public static void setActivityTheme( @NonNull Activity activity, boolean bNoActionBar, boolean forceDark ){
prepare( activity.getApplicationContext() );
int theme_idx = pref.getInt( Pref.KEY_UI_THEME, 0 );
if( forceDark ) theme_idx =1;
if( forceDark ) theme_idx = 1;
switch( theme_idx ){
@ -490,16 +493,14 @@ public class App1 extends Application {
}
}
// Activity開始時に設定を読み直す
public static boolean disable_emoji_animation;
public static boolean allow_non_space_before_emoji_shortcode;
private static void reloadConfig(){
disable_emoji_animation = pref.getBoolean( Pref.KEY_DISABLE_EMOJI_ANIMATION,false );
allow_non_space_before_emoji_shortcode = pref.getBoolean( Pref.KEY_ALLOW_NON_SPACE_BEFORE_EMOJI_SHORTCODE,false );
}
private static void reloadConfig(){
disable_emoji_animation = pref.getBoolean( Pref.KEY_DISABLE_EMOJI_ANIMATION, false );
allow_non_space_before_emoji_shortcode = pref.getBoolean( Pref.KEY_ALLOW_NON_SPACE_BEFORE_EMOJI_SHORTCODE, false );
}
// Chrome Custom Tab を開く
public static void openCustomTab( @NonNull Activity activity, @NonNull String url ){
@ -523,13 +524,13 @@ public class App1 extends Application {
builder.setToolbarColor( Styler.getAttributeColor( activity, R.attr.colorPrimary ) ).setShowTitle( true );
CustomTabsIntent customTabsIntent = builder.build();
customTabsIntent.launchUrl( activity, Uri.parse( url ) );
}catch(Throwable ex){
}catch( Throwable ex ){
log.e( ex, "openCustomTab: failed." );
}
}
public static void openCustomTab( @NonNull Activity activity, @NonNull TootAttachment ta){
String url = ta.getLargeUrl(pref);
public static void openCustomTab( @NonNull Activity activity, @NonNull TootAttachment ta ){
String url = ta.getLargeUrl( pref );
if( url != null ){
openCustomTab( activity, url );
}

View File

@ -22,21 +22,43 @@ public abstract class TootApiTask extends AsyncTask< Void, Void, TootApiResult >
@NonNull protected final TootApiClient client;
protected TootApiTask( Activity _activity, SavedAccount access_info, boolean bShowProgress ){
@SuppressWarnings("WeakerAccess") public static final int PROGRESS_NONE = - 1;
@SuppressWarnings("WeakerAccess") public static final int PROGRESS_SPINNER = ProgressDialog.STYLE_SPINNER;
@SuppressWarnings("WeakerAccess") public static final int PROGRESS_HORIZONTAL = ProgressDialog.STYLE_HORIZONTAL;
@SuppressWarnings("WeakerAccess")
public TootApiTask( Activity _activity, SavedAccount access_info, boolean bShowProgress ){
this( _activity, access_info, bShowProgress ? PROGRESS_SPINNER : PROGRESS_NONE );
}
@SuppressWarnings("WeakerAccess")
public TootApiTask( Activity _activity, SavedAccount access_info, int progress_style ){
this.client = new TootApiClient( _activity, this );
client.setAccount( access_info );
if( bShowProgress ) showProgress( _activity );
showProgress( _activity, progress_style );
}
protected TootApiTask( Activity _activity, String instance, boolean bShowProgress ){
@SuppressWarnings("WeakerAccess")
public TootApiTask( Activity _activity, String instance, boolean bShowProgress ){
this( _activity, instance, bShowProgress ? PROGRESS_SPINNER : PROGRESS_NONE );
}
@SuppressWarnings("WeakerAccess")
public TootApiTask( Activity _activity, String instance, int progress_style ){
this.client = new TootApiClient( _activity, this );
client.setInstance( instance );
if( bShowProgress ) showProgress( _activity );
showProgress( _activity, progress_style );
}
protected TootApiTask( Activity _activity, boolean bShowProgress ){
@SuppressWarnings("WeakerAccess")
public TootApiTask( Activity _activity, boolean bShowProgress ){
this( _activity, bShowProgress ? PROGRESS_SPINNER : PROGRESS_NONE );
}
@SuppressWarnings("WeakerAccess")
public TootApiTask( Activity _activity, int progress_style ){
this.client = new TootApiClient( _activity, this );
if( bShowProgress ) showProgress( _activity );
showProgress( _activity, progress_style );
}
public TootApiTask setProgressPrefix( String s ){
@ -54,6 +76,7 @@ public abstract class TootApiTask extends AsyncTask< Void, Void, TootApiResult >
if( progress != null ){
Utils.runOnMainThread( new Runnable() {
@Override public void run(){
progress.setIndeterminate( true );
if( ! TextUtils.isEmpty( progress_prefix ) ){
progress.setMessage( progress_prefix + "\n" + s );
}else{
@ -64,6 +87,18 @@ public abstract class TootApiTask extends AsyncTask< Void, Void, TootApiResult >
}
}
public void publishApiProgressRatio( final int value, final int max ){
if( progress != null ){
Utils.runOnMainThread( new Runnable() {
@Override public void run(){
progress.setIndeterminate( false );
progress.setProgress( value );
progress.setMax( max );
}
} );
}
}
//////////////////////////////////////////////////////
@Override protected abstract TootApiResult doInBackground( Void... voids );
@ -84,13 +119,21 @@ public abstract class TootApiTask extends AsyncTask< Void, Void, TootApiResult >
private ProgressDialog progress;
private String progress_prefix;
private void showProgress( Activity activity ){
private void showProgress( Activity activity, int progressStyle ){
if( progressStyle == PROGRESS_NONE ) return;
//noinspection deprecation
this.progress = new ProgressDialog( activity );
progress.setIndeterminate( true );
progress.setCancelable( true );
progress.setProgressStyle( progressStyle );
progress.setIndeterminate( true );
progress.setMax( 1 );
if( ! TextUtils.isEmpty( progress_prefix ) ){
progress.setMessage( progress_prefix );
}else{
// HORIZONTALスタイルの場合初期メッセージがないと後からメッセージを指定しても表示されない
progress.setMessage( " " );
}
progress.setOnCancelListener( new DialogInterface.OnCancelListener() {
@Override

View File

@ -0,0 +1,365 @@
package jp.juggler.subwaytooter.util;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.nio.charset.Charset;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.Buffer;
import okio.BufferedSource;
import okio.ByteString;
import okio.Options;
import okio.Sink;
import okio.Source;
import okio.Timeout;
public class ProgressResponseBody extends ResponseBody {
static final LogCategory log = new LogCategory( "ProgressResponseBody" );
@NonNull private final ResponseBody originalBody;
// please append this for OkHttpClient.Builder#addInterceptor().
// ex) builder.addInterceptor( ProgressResponseBody.makeInterceptor() );
public static Interceptor makeInterceptor(){
return new Interceptor() {
@Override public Response intercept( @NonNull Chain chain ) throws IOException{
Response originalResponse = chain.proceed( chain.request() );
if( originalResponse == null )
throw new RuntimeException( "makeInterceptor: chain.proceed() returns null." );
ResponseBody originalBody = originalResponse.body();
if( originalBody == null )
throw new RuntimeException( "makeInterceptor: originalResponse.body() reruens null." );
return originalResponse.newBuilder()
.body( new ProgressResponseBody( originalBody ) )
.build();
}
};
}
/*
then you can read response body's bytes() with progress callback.
example:
byte[] data = ProgressResponseBody.bytes( response, new ProgressResponseBody.Callback() {
@Override public void progressBytes( long bytesRead, long bytesTotal ){
publishApiProgressRatio( (int) bytesRead, (int) bytesTotal );
}
} );
*/
public interface Callback {
void progressBytes(
long bytesRead
, long bytesTotal // initial value is Content-Length, but it may only hint, it may glows greater.
);
}
@SuppressWarnings("WeakerAccess")
public static byte[] bytes( @NonNull Response response, @Nullable Callback callback ) throws IOException{
ResponseBody body = response.body();
if( body == null ) throw new RuntimeException( "response.body() is null." );
return bytes( body, callback );
}
@SuppressWarnings("WeakerAccess")
public static byte[] bytes( @NonNull ResponseBody body, @Nullable Callback callback ) throws IOException{
if( body instanceof ProgressResponseBody ){
( (ProgressResponseBody) body ).callback = callback;
}
return body.bytes();
}
/////////////////////////////////////////////////////
// internal
private ProgressResponseBody( @NonNull ResponseBody originalBody ){
this.originalBody = originalBody;
}
@Nullable private Callback callback;
@Override public MediaType contentType(){
return originalBody.contentType();
}
@Override public long contentLength(){
return originalBody.contentLength();
}
/*
RequestBody.bytes() is defined as final, We can't override it.
Make WrappedBufferedSource to capture BufferedSource.readByteArray().
*/
private BufferedSource wrappedSource;
@Override public BufferedSource source(){
if( wrappedSource == null ){
BufferedSource originalSource = originalBody.source();
if( originalSource == null ) return null;
try{
// if it is RealBufferedSource, I can access to source public field via reflection.
final Field field_source = originalSource.getClass().getField( "source" );
// If there is the method, create the wrapper.
wrappedSource = new ForwardingBufferedSource( originalSource ) {
@Override public byte[] readByteArray() throws IOException{
/*
RealBufferedSource.readByteArray() does:
- buffer.writeAll(source);
- return buffer.readByteArray(buffer.size());
We do same things using Reflection, with progress.
*/
try{
long contentLength = originalBody.contentLength();
Buffer buffer = originalSource.buffer();
Source source = (Source) field_source.get( originalSource );
if( source == null )
throw new IllegalArgumentException( "source == null" );
// same thing of Buffer.writeAll(), with counting.
long nRead = 0;
if( callback != null ){
callback.progressBytes( 0, Math.max( contentLength, 1 ) );
}
for( ; ; ){
long delta = source.read( buffer, 8192 );
if( delta == - 1L ) break;
nRead += delta;
if( nRead > 0 && callback != null ){
callback.progressBytes( nRead, Math.max( contentLength, nRead ) );
}
}
// EOS時の進捗
callback.progressBytes( nRead, Math.max( contentLength, nRead ) );
return buffer.readByteArray();
}catch( Throwable ex ){
log.trace( ex );
log.e( "readByteArray() failed. " );
return originalSource.readByteArray();
}
}
};
}catch( Throwable ex ){
log.e( "can't access to RealBufferedSource#source field." );
wrappedSource = originalSource;
}
}
return wrappedSource;
}
// To avoid double buffering, We have to make ForwardingBufferedSource.
static class ForwardingBufferedSource implements BufferedSource {
@NonNull final BufferedSource originalSource;
ForwardingBufferedSource( @NonNull BufferedSource originalSource ){
this.originalSource = originalSource;
}
@Override public Buffer buffer(){
return originalSource.buffer();
}
@Override public boolean exhausted() throws IOException{
return originalSource.exhausted();
}
@Override public void require( long byteCount ) throws IOException{
originalSource.require( byteCount );
}
@Override public boolean request( long byteCount ) throws IOException{
return originalSource.request( byteCount );
}
@Override public byte readByte() throws IOException{
return originalSource.readByte();
}
@Override public short readShort() throws IOException{
return originalSource.readShort();
}
@Override public short readShortLe() throws IOException{
return originalSource.readShortLe();
}
@Override public int readInt() throws IOException{
return originalSource.readInt();
}
@Override public int readIntLe() throws IOException{
return originalSource.readIntLe();
}
@Override public long readLong() throws IOException{
return originalSource.readLong();
}
@Override public long readLongLe() throws IOException{
return originalSource.readLongLe();
}
@Override public long readDecimalLong() throws IOException{
return originalSource.readDecimalLong();
}
@Override public long readHexadecimalUnsignedLong() throws IOException{
return originalSource.readHexadecimalUnsignedLong();
}
@Override public void skip( long byteCount ) throws IOException{
originalSource.skip( byteCount );
}
@Override public ByteString readByteString() throws IOException{
return originalSource.readByteString();
}
@Override public ByteString readByteString( long byteCount ) throws IOException{
return originalSource.readByteString( byteCount );
}
@Override public int select( @NonNull Options options ) throws IOException{
return originalSource.select( options );
}
@Override public byte[] readByteArray() throws IOException{
return originalSource.readByteArray();
}
@Override public byte[] readByteArray( long byteCount ) throws IOException{
return originalSource.readByteArray( byteCount );
}
@Override public int read( @NonNull byte[] sink ) throws IOException{
return originalSource.read( sink );
}
@Override public void readFully( @NonNull byte[] sink ) throws IOException{
originalSource.readFully( sink );
}
@Override
public int read( @NonNull byte[] sink, int offset, int byteCount ) throws IOException{
return originalSource.read( sink, offset, byteCount );
}
@Override public void readFully( @NonNull Buffer sink, long byteCount ) throws IOException{
originalSource.readFully( sink, byteCount );
}
@Override public long readAll( @NonNull Sink sink ) throws IOException{
return originalSource.readAll( sink );
}
@Override public String readUtf8() throws IOException{
return originalSource.readUtf8();
}
@Override public String readUtf8( long byteCount ) throws IOException{
return originalSource.readUtf8( byteCount );
}
@Nullable @Override public String readUtf8Line() throws IOException{
return originalSource.readUtf8Line();
}
@Override public String readUtf8LineStrict() throws IOException{
return originalSource.readUtf8LineStrict();
}
@Override public String readUtf8LineStrict( long limit ) throws IOException{
return originalSource.readUtf8LineStrict( limit );
}
@Override public int readUtf8CodePoint() throws IOException{
return originalSource.readUtf8CodePoint();
}
@Override public String readString( @NonNull Charset charset ) throws IOException{
return originalSource.readString( charset );
}
@Override
public String readString( long byteCount, @NonNull Charset charset ) throws IOException{
return originalSource.readString( byteCount, charset );
}
@Override public long indexOf( byte b ) throws IOException{
return originalSource.indexOf( b );
}
@Override public long indexOf( byte b, long fromIndex ) throws IOException{
return originalSource.indexOf( b, fromIndex );
}
@Override public long indexOf( byte b, long fromIndex, long toIndex ) throws IOException{
return originalSource.indexOf( b, fromIndex, toIndex );
}
@Override public long indexOf( @NonNull ByteString bytes ) throws IOException{
return originalSource.indexOf( bytes );
}
@Override
public long indexOf( @NonNull ByteString bytes, long fromIndex ) throws IOException{
return originalSource.indexOf( bytes, fromIndex );
}
@Override public long indexOfElement( @NonNull ByteString targetBytes ) throws IOException{
return originalSource.indexOfElement( targetBytes );
}
@Override
public long indexOfElement( @NonNull ByteString targetBytes, long fromIndex ) throws IOException{
return originalSource.indexOfElement( targetBytes, fromIndex );
}
@Override
public boolean rangeEquals( long offset, @NonNull ByteString bytes ) throws IOException{
return originalSource.rangeEquals( offset, bytes );
}
@Override
public boolean rangeEquals( long offset, @NonNull ByteString bytes, int bytesOffset, int byteCount ) throws IOException{
return originalSource.rangeEquals( offset, bytes, bytesOffset, byteCount );
}
@Override public InputStream inputStream(){
return originalSource.inputStream();
}
@Override public long read( @NonNull Buffer sink, long byteCount ) throws IOException{
return originalSource.read( sink, byteCount );
}
@Override public Timeout timeout(){
return originalSource.timeout();
}
@Override public void close() throws IOException{
originalSource.close();
}
}
}

View File

@ -92,6 +92,11 @@ public class PinchBitmapView extends View {
@Override protected void onSizeChanged( int w, int h, int oldw, int oldh ){
super.onSizeChanged( w, h, oldw, oldh );
view_w = Math.max( 1f, w );
view_h = Math.max( 1f, h );
view_aspect = view_w / view_h;
initializeScale();
}
@ -101,19 +106,15 @@ public class PinchBitmapView extends View {
return true;
}
// ビットマップを変更した時とビューのサイズが変わった時と画像をクリックした時に呼ばれる
// 表示位置を再計算して再描画
// 表示位置の初期化
// 呼ばれるのはビットマップを変更した時ビューのサイズが変わった時画像をクリックした時
void initializeScale(){
if( bitmap != null && ! bitmap.isRecycled() ){
if( bitmap != null && ! bitmap.isRecycled() && view_w >= 1f ){
bitmap_w = Math.max( 1f, bitmap.getWidth() );
bitmap_h = Math.max( 1f, bitmap.getHeight() );
bitmap_aspect = bitmap_w / bitmap_h;
view_w = Math.max( 1f, this.getWidth() );
view_h = Math.max( 1f, this.getHeight() );
view_aspect = view_w / view_h;
if( view_aspect > bitmap_aspect ){
current_scale = view_h / bitmap_h;
}else{
@ -127,6 +128,7 @@ public class PinchBitmapView extends View {
current_trans_y = ( view_h - draw_h ) / 2f;
}
// 画像がnullに変化した時も再描画が必要
invalidate();
}
@ -148,17 +150,20 @@ public class PinchBitmapView extends View {
@Override public boolean onTouchEvent( MotionEvent ev ){
if( bitmap == null || bitmap.isRecycled() ) return false;
if( bitmap == null
|| bitmap.isRecycled()
|| view_w < 1f
) return false;
int action = ev.getAction();
if( action == MotionEvent.ACTION_DOWN ){
if( velocityTracker != null ){
velocityTracker.recycle();
velocityTracker = null;
velocityTracker.clear();
}else{
velocityTracker = VelocityTracker.obtain();
}
velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement( ev );
bDrag = bPointerCountChanged = false;
@ -174,8 +179,7 @@ public class PinchBitmapView extends View {
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_POINTER_UP:
// タッチ操作中に指の数を変えた
bDrag = true;
bPointerCountChanged = true;
bDrag = bPointerCountChanged = true;
startTracking( ev );
break;
@ -186,53 +190,7 @@ public class PinchBitmapView extends View {
case MotionEvent.ACTION_UP:
nextTracking( ev );
if( ! bDrag ){
// 指を動かしていないならクリック操作だったのだろう
performClick();
}else if( ! bPointerCountChanged ){
// 指の数を変えていないならページめくり操作かもしれない
velocityTracker.computeCurrentVelocity( 1000 );
final float xv = velocityTracker.getXVelocity();
float yv = velocityTracker.getYVelocity();
float image_move_x = Math.abs( current_trans_x - start_image_trans_x );
float image_move_y = Math.abs( current_trans_y - start_image_trans_y );
float draw_w = bitmap_w * current_scale;
if( Math.abs( xv ) < Math.abs( yv ) / 8 ){
// 指を動かした方向の角度が左右ではなかった
log.d( "flick is vertical." );
}else if( Math.abs( xv ) < ( draw_w <= view_w ? swipe_velocity2 : swipe_velocity ) ){
// 左右方向の強さが足りなかった
log.d( "velocity %f not enough to paging", xv );
}else if( image_move_x >= drag_width
|| image_move_y >= drag_width * 5f
){
// 画像を動かしたかどうかの最終チェック
log.d( "image was moved. not paging action. %f %f "
, image_move_x / drag_width
, image_move_y / drag_width
);
}else{
log.d( "paging! %f %f %f"
, image_move_x / drag_width
, image_move_y / drag_width
,xv
);
Utils.runOnMainThread( new Runnable() {
@Override public void run(){
if( callback != null ) callback.onSwipe( xv >= 0f ? - 1 : 1 );
}
} );
}
}
checkClickOrPaging();
if( velocityTracker != null ){
velocityTracker.recycle();
@ -244,6 +202,57 @@ public class PinchBitmapView extends View {
return true;
}
void checkClickOrPaging(){
if( ! bDrag ){
// 指を動かしていないならクリック操作だったのだろう
performClick();
}else if( ! bPointerCountChanged && velocityTracker != null ){
// 指の数を変えていないならページめくり操作かもしれない
velocityTracker.computeCurrentVelocity( 1000 );
final float xv = velocityTracker.getXVelocity();
final float yv = velocityTracker.getYVelocity();
float image_move_x = Math.abs( current_trans_x - start_image_trans_x );
float image_move_y = Math.abs( current_trans_y - start_image_trans_y );
float draw_w = bitmap_w * current_scale;
if( Math.abs( xv ) < Math.abs( yv ) / 8 ){
// 指を動かした方向の角度が左右ではなかった
log.d( "flick is vertical." );
}else if( Math.abs( xv ) < ( draw_w <= view_w ? swipe_velocity2 : swipe_velocity ) ){
// 左右方向の強さが足りなかった
log.d( "velocity %f not enough to paging", xv );
}else if( image_move_x >= drag_width
|| image_move_y >= drag_width * 5f
){
// 画像を動かしたかどうかの最終チェック
log.d( "image was moved. not paging action. %f %f "
, image_move_x / drag_width
, image_move_y / drag_width
);
}else{
log.d( "paging! %f %f %f"
, image_move_x / drag_width
, image_move_y / drag_width
, xv
);
Utils.runOnMainThread( new Runnable() {
@Override public void run(){
if( callback != null ) callback.onSwipe( xv >= 0f ? - 1 : 1 );
}
} );
}
}
}
// マルチタッチの中心位置の計算
static class PointerAvg {
@ -305,16 +314,15 @@ public class PinchBitmapView extends View {
float view_aspect;
void startTracking( MotionEvent ev ){
// 追跡開始時の指の位置
start_pos.update( ev );
pos.update( ev );
// 追跡開始時の画像の位置
start_image_trans_x = current_trans_x;
start_image_trans_y = current_trans_y;
start_image_scale = current_scale;
view_w = Math.max( 1f, this.getWidth() );
view_h = Math.max( 1f, this.getHeight() );
view_aspect = view_w / view_h;
if( view_aspect > bitmap_aspect ){
scale_min = view_h / bitmap_h / 2f;
scale_max = view_w / bitmap_w * 8f;
@ -325,13 +333,13 @@ public class PinchBitmapView extends View {
if( scale_max < scale_min ) scale_max = scale_min * 16f;
}
final Matrix tracking_matrix = new Matrix();
final Matrix tracking_matrix_inv = new Matrix();
final float[] avg_on_image1 = new float[ 2 ];
final float[] avg_on_image2 = new float[ 2 ];
private final Matrix tracking_matrix = new Matrix();
private final Matrix tracking_matrix_inv = new Matrix();
private final float[] avg_on_image1 = new float[ 2 ];
private final float[] avg_on_image2 = new float[ 2 ];
// 画面上の指の位置から画像中の指の位置を調べる
void getCoordinateOnImage( @NonNull float[] dst, @NonNull float[] src ){
private void getCoordinateOnImage( @NonNull float[] dst, @NonNull float[] src ){
tracking_matrix.reset();
tracking_matrix.postScale( current_scale, current_scale );
tracking_matrix.postTranslate( current_trans_x, current_trans_y );
@ -357,9 +365,7 @@ public class PinchBitmapView extends View {
getCoordinateOnImage( avg_on_image1, pos.avg );
// ズーム率を変更する
float new_scale = start_image_scale * pos.max_radius / start_pos.max_radius;
new_scale = new_scale < scale_min ? scale_min : new_scale > scale_max ? scale_max : new_scale;
current_scale = new_scale;
current_scale = clip( scale_min, scale_max, start_image_scale * pos.max_radius / start_pos.max_radius );
// 再び調べる
getCoordinateOnImage( avg_on_image2, pos.avg );
@ -368,7 +374,6 @@ public class PinchBitmapView extends View {
start_image_trans_x += current_scale * ( avg_on_image2[ 0 ] - avg_on_image1[ 0 ] );
start_image_trans_y += current_scale * ( avg_on_image2[ 1 ] - avg_on_image1[ 1 ] );
invalidate();
}
// 平行移動
@ -384,33 +389,32 @@ public class PinchBitmapView extends View {
bDrag = true;
}
// 画像の移動量
float trans_x = start_image_trans_x + move_x;
float trans_y = start_image_trans_y + move_y;
// 画像サイズとビューサイズを使って移動可能範囲をクリッピング
float draw_w = bitmap_w * current_scale;
float draw_h = bitmap_h * current_scale;
if( draw_w <= view_w ){
float remain = view_w - draw_w;
trans_x = remain / 2f;
}else{
float remain = draw_w - view_w;
trans_x = trans_x >= 0f ? 0f : trans_x < - remain ? - remain : trans_x;
}
if( draw_h <= view_h ){
float remain = view_h - draw_h;
trans_y = remain / 2f;
}else{
float remain = draw_h - view_h;
trans_y = trans_y >= 0f ? 0f : trans_y < - remain ? - remain : trans_y;
}
// 画像の表示位置を変更して再描画
current_trans_x = trans_x;
current_trans_y = trans_y;
invalidate();
// 画像の表示位置を更新
current_trans_x = clipTranslate( view_w, bitmap_w, current_scale, start_image_trans_x + move_x );
current_trans_y = clipTranslate( view_h, bitmap_h, current_scale, start_image_trans_y + move_y );
}
invalidate();
}
// 数値を範囲内にクリップする
private static float clip( float min, float max, float v ){
return v < min ? min : v > max ? max : v;
}
// ビューの幅と画像の描画サイズを元に描画位置をクリップする
private static float clipTranslate(
float view_w // ビューの幅
, float bitmap_w // 画像の幅
, float current_scale // 画像の拡大率
, float trans_x // タッチ操作による表示位置
){
// 余白(拡大率が小さい場合はプラス拡大率が大きい場合はマイナス)
float padding = view_w - bitmap_w * current_scale;
// 余白が>=0なら画像を中心に表示する <0なら操作された位置をクリップする
return padding >= 0f ? padding / 2f : clip( padding, 0f, trans_x );
}
}