読者です 読者をやめる 読者になる 読者になる

PicassoとGlide

android gradle data_binding

はじめに

この記事は Android その3 Advent Calendar 2016 - Qiita

の十日目の記事です。

一般的に見ると

日本国内のアプリだとかなりPicasso押しの風潮ですが、

Picassoでうまくできなかった事例がGlideで実現できたケースが有ったので備忘録として書いてみます

どのようなケースでうまくいかなかったのか?

  • 画像が読み込まれるまでは View.GONE
  • 読み込まれた段階で View.VISIBLE に切り替える

なんでこんな風な仕様にしたの?

等を表示したかったからです。*1

ちゃんとG様のApiKeyを取得すれば、確実に表示されることは保証されるのですが

  • かなり凝ったアプリを作ったからといって収益に反映しない
    • ココらへんがフリーランスに世の中が厳しかったり、クラウドワークの案件単価が安すぎたり みたいな話につながっているのかも。。
  • 企画/営業の方は今あるリソースでなんとか良くしようと知恵を絞る

みたいなところがあって、フリーの範囲でできるだけ頑張るみたいな形になってしまうのは仕方ないんでしょうね


で、なにがうまくいかなかったの?

下記に記載例を記述しますが、

  • CallbackがSuccessなのに画面にImageViewの領域がうまく表示されない*2
  • 表示されても、画像が白っぽく表示されたり正常に表示されない

現象が発生しました。

通常のレイアウトの場合

<!--中略-->

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <!--横ピッタリ、縦は等比-->    
    <ImageView
        android:visibility="View.GONE"
        android:scaleType="fitStart"
        android:adjustViewBounds="true"
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <!--ボタン-->
    <Button
           android:layout_alignParentRight="true"
          android:layout_alignParentBottom="true" 
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="ボタン"/>
</RelativeLayout>

<!--中略-->
  • hogeActivity
       final ImageView imageView = (ImageView)findViewById(R.id.imageView);
        Picasso.with(context)
               .load(item.getUrl())
               .into(imageView, new Callback() {
            @Override
            public void onSuccess() {
                //成功したときだけImageViewを表示モードにする
                imageView.setVisibility(View.VISIBLE);
            }
            @Override
            public void onError() { 
            }
        });

InfoWindowの場合

  • layout/info_window.xml
<layout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto" >
    <data>
        <variable
            name="dto"
            type="hoge.fuga.itemDto" />
    </data>
    
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <!--画像のタイトルとか-->
    <TextView 
           android:gravity="center_horizontal" 
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:text="@{dto.title}"/>
           
    <!--横ピッタリ、縦は等比-->    
    <ImageView
        android:visibility="View.GONE"
        android:scaleType="fitStart"
        android:adjustViewBounds="true"
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>
  • MyInfoWindowAdapter.java
public class MyInfoWindowAdapter implements InfoWindowAdapter {

    private Context context;
    private InfoWindowBinding binding;
    private String            lastMarkerId;

    public MyInfoWindowAdapter(Context context) {
        this.context = context;
        binding = InfoWindowBinding.inflate(LayoutInflater.from(context));
    }

    @Override
    public View getInfoWindow(Marker marker) {
        return render(marker);
    }

    @Override
    public View getInfoContents(Marker marker) {
        if (marker != null && marker.isInfoWindowShown()) {
            marker.showInfoWindow();
        }
        return null;
    }

    private boolean isRender = false; //マーカー連打対策

    private View render(final Marker marker) {
        if(isRender)return;
        isRender=true;

        itemDto dto = (itemDto)marker.getTag(); //★ 配列やDBとかからもってくるならIdでも可
        if (marker.getId().equals(lastMarkerId)){
            binding.setDto(dto);
            binding.executePendingBindings();//即時反映をお願いする
            isRender=false;
            return;
        }   
        lastMarkerId = marker.getId();

        binding.setDto(dto);
        binding.executePendingBindings();//即時反映をお願いする
        
//======PicassoでImageを読み込む処理 =====     
        final ImageView imageView = binding.imageView;
        Picasso.with(context)
               .load(item.getUrl())
               .into(imageView, new Callback() {
            @Override
            public void onSuccess() {
                //成功したときだけImageViewを表示モードにしてInfoWindowを読み直す
                imageView.setVisibility(View.VISIBLE);
                getInfoContents(marker);
            }
            @Override
            public void onError() { 
            }
        });
        isRender=false;
    }
}

で直してうまく動いた記述例

   Glide.with(context))
    .load(item.getUrl())
    .asBitmap()
    .into(new SimpleTarget<Bitmap>(600, 400) {
        @Override
        public void onResourceReady(Bitmap bitmap, GlideAnimation anim) {
            imageView.setImageBitmap(bitmap);
            imageView.setVisibility(View.VISIBLE);
            getInfoContents(marker);
        }
    };

ただ下記だとうまく動かなかった

  • サイズ指定がないケース
    • 動くことは動くけど、何故かすごく遅い
Glide.with(context))
    .load(item.getUrl())
    .asBitmap()
    .into(new SimpleTarget<Bitmap>() {
    }
  • ImageViewにサイズ任せなケース*3
    • imageView.setVisibility(View.VISIBLE); してもうまく表示されない
Glide.with(context)
    .load(item.getUrl())
    .asBitmap()
    .into(new BitmapImageViewTarget(imageView)) {
    
    }

ImageViewじたいがView.GONE のためImageViewのサイズが正常に取れないからかもしれない。


Picasso => Glideを置き換えるとした場合の問題点

PicassoだとTransformationという機能が有るよね?

で出て来るお話ですが、一応Glideにも

wasabeef先生が作られたGlide Transformationsがあるようです。下記の記事を参考

ただAnimationみたいに自作ベースはかなり厳しい?かもとは感じました

PicasoだとOkhttpと親和性が高いよね?

Picassoサイド

が可能なのは強み

  • ユーザーがアップロードした画像とかは https
  • 一般的公開画像は http

な話もありましたが、Apple様次第でどうなるんでしょうね

全部httpsでアクセスみたいな話になるのでしょうか・・

  • app/build.gradle
dependencies {
    compile 'com.squareup.picasso:picasso:2.5.2'
    compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0'
}
OkHttpClient client = XXXX;
Picasso picasso = new Picasso.Builder(context)
    .downloader(new OkHttp3Downloader(client))
    .build()

Glideサイド

起動時にしか初期化ができないので切り替えができない*5

の話と合わせて書くと、下記みたいな記述になるイメージなんでしょうね。。*6

  • app/build.gradle
dependencies {
    compile 'com.github.bumptech.glide:glide:3.7.0'
    compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
}
public class LimitCacheSizeGlideModule implements GlideModule {
    // Modern device should have 8GB (=7.45GiB) or more!
    private static final int SMALL_INTERNAL_STORAGE_THRESHOLD_GIB = 6;
    private static final int DISK_CACHE_SIZE_FOR_SMALL_INTERNAL_STORAGE_MIB = 50;

    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        if (MyApplication.from(context).isTest()) return; // NOTE: StatFs will crash on robolectric.

        double totalGiB = getTotalBytesOfInternalStorage() / 1024.0 / 1024.0 / 1024.0;
        Log.i(String.format(Locale.US, "Internal Storage Size: %.1fGiB", totalGiB));
        if (totalGiB < SMALL_INTERNAL_STORAGE_THRESHOLD_GIB) {
            Log.i("Limiting image cache size to " + DISK_CACHE_SIZE_FOR_SMALL_INTERNAL_STORAGE_MIB + "MiB");
            builder.setDiskCache(new InternalCacheDiskCacheFactory(context, DISK_CACHE_SIZE_FOR_SMALL_INTERNAL_STORAGE_MIB));
        }
    }

    @Override
    public void registerComponents(Context context, Glide glide) {
        OkHttpClient okHttpClient = XXXX;
        glide.register(GlideUrl.class, InputStream.class,
                new OkHttpUrlLoader.Factory(okHttpClient));
    }

    private long getTotalBytesOfInternalStorage() {
        // http://stackoverflow.com/a/4595449/1474113
        StatFs stat = new StatFs(Environment.getDataDirectory().getPath());
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            return getTotalBytesOfInternalStorageWithStatFs(stat);
        } else {
            return getTotalBytesOfInternalStorageWithStatFsPreJBMR2(stat);
        }
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    private long getTotalBytesOfInternalStorageWithStatFs(StatFs stat) {
        return stat.getTotalBytes();
    }

    @SuppressWarnings("deprecation")
    private long getTotalBytesOfInternalStorageWithStatFsPreJBMR2(StatFs stat) {
        return (long) stat.getBlockSize() * stat.getBlockCount();
    }
}
  • AndroidManifest.xml
<application>
    <meta-data
        android:name="hoge.fuga.application.LimitCacheSizeGlideModule"
        android:value="GlideModule"
        />
</application>

この場合 hoge.fugaがパッケージ名=applicationId

備忘的なメモ

Proguardはハマるので下記の記事はメモ

qiita.com

Frescoとかはどうなのさ?

  • 特色としてWebP対応が有るけど、WebPのデータって作らないよな・・
    • iOSでもlibwebpというLibraryを使えば表示できるようなのですが
    • Glideの特色としてAnimationGif対応とか有りますけど使ってるの見たことないorz
  • xmlでレイアウト定義で書く
    • G様標準のAppCompatとかでないとlayoutPreviewでエラーor固まるのでかなりストレス

例)マテリアルっぽいインジケータの導入 とか・・

compile ‘com.hannesdorfmann.smoothprogressbar:library:1.0.0@aar’

araiyusuke.hatenadiary.com

*1:ApiKey無しだとかなり制限があり。制限回数が来た時点でエラー。ApiKeyがある場合だと超えた時点で課金

*2:setImageBitmapは発行しているのに

*3:View::onMeasure あたりがうまく動かないからうまく動かない?

*4:通常 or SSLのケース

*5:この場合だと通常タイプのOkHttpClientを登録するしか術がない

*6:確かにGlideに変更してからキャッシュサイズが増えた気はしていました<汗