こんにちは。Typetalk開発チームの後藤です。Android用の画像読み込みライブラリを使うことで、自前のコードがだいぶ削減できましたので、ご紹介したいと思います。
このようなライブラリを使う利点は以下の通りです。
- 基本的に画像URLと表示するImageViewを指定するだけ
- メモリキャッシュ、ファイルキャッシュを自動に行ってくれる
- Gifアニメーションが表示できる
Glideとは
もともとはBumpという会社が開発していたようですが、この会社はGoogleに買収されており、CopyrightにはGoogleと記載されています。同じ目的のライブラリはいくつかあるようですが、Googleだと安心感があり、採用の十分な根拠になりますね!
はじめに
まずはじめに、build.gradleのdependenciesにglideを追加しましょう。現在のバージョンは4.4.0なので、この記事はこのバージョンのGlideライブラリの説明となります。
implementation 'com.github.bumptech.glide:glide:4.4.0'
インターネット上の画像を読むので、AndroidManifest.xmlに忘れないように設定を追加します。
<uses-permission android:name="android.permission.INTERNET" />
レイアウトファイルにImageViewを追加します。
<ImageView android:id="@+id/match_image" android:layout_width="match_parent" android:layout_height="wrap_content" />
ActivityクラスのonCreateで画像を読み込んで表示させるコードを書きます。以前このブログに貼られたアニメーションGIFを読み込んでみました。簡単ですね!
ImageView matchImage = findViewById(R.id.match_image); String gifUrl = "https://d1u2e6wtudxkpm.cloudfront.net/wp-content/uploads/sites/2/2017/10/techblog-vue-kanban-06.gif"; Glide.with(this).load(gifUrl).into(matchImage);
アイコンを丸くする
読み込む画像を丸く表示するなどの拡張機能を使うには、annotationProcessorを使う必要があります。build.gradleのdependenciesにannotationProcessorを追加し、AppGlideModuleを継承してGlideModuleアノテーションを付けたJavaクラスも追加します。
annotationProcessor 'com.github.bumptech.glide:compiler:4.4.0'
MyAppGlideModule.java
package com.example.myapp; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; @GlideModule public class MyAppGlideModule extends AppGlideModule { }
アイコン用のImageViewをレイアウトファイルに追加してみます。
<ImageView android:id="@+id/icon" android:layout_width="40dp" android:layout_height="40dp"/>
Glide.with(this)の代わりに、GlideApp.with(this)を使い、circleCrop()を間に挟むだけで、アイコンが丸くなります。これもまた簡単ですね!
ImageView iconImage = findViewById(R.id.icon); String iconUrl = "https://typetalk.com/accounts/5/profile_image.png"; GlideApp.with(this).load(iconUrl).circleCrop().into(iconImage);
画像の読み込みサイズ
アプリのログを見ているとこんなのが出力されています。
Glide treats LayoutParams.WRAP_CONTENT as a request for an image the size of this device’s screen dimensions. If you want to load the original image and are ok with the corresponding memory cost and OOMs (depending on the input size), use .override(Target.SIZE_ORIGINAL). Otherwise, use LayoutParams.MATCH_PARENT, set layout_width and layout_height to fixed dimension, or use .override() with fixed dimensions. |
ImageViewがWRAP_CONTENTだとデバイスのスクリーンサイズで画像を読み込もうとするので、元画像サイズのまま読み込んでよいなら.override(Target.SIZE_ORIGINAL)を指定、または画像サイズにMATCH_PARENTを指定するか、layout_widthとlayout_heightか.overrideで固定のサイズを指定したほうが良いようです。一体どういうことか確認してみましょう。
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:gravity="center_horizontal"> <ImageView android:id="@+id/match_image" android:layout_width="match_parent" android:layout_height="wrap_content" android:adjustViewBounds="true"/> <ImageView android:id="@+id/wrap_image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:adjustViewBounds="true"/> <ImageView android:id="@+id/w100_image" android:layout_width="100dp" android:layout_height="wrap_content" android:adjustViewBounds="true"/> </LinearLayout>
同じ画像を違うImageViewに読み込んで、RequestListenerでログを出力し、Bitmapのサイズを確認してみます。
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); RequestManager rm = Glide.with(this); String imageUrl = "https://d1u2e6wtudxkpm.cloudfront.net/wp-content/uploads/sites/2/2017/12/FullSizeRender-11-e1513846838609.jpg"; rm.load(imageUrl) .listener(createLoggerListener("match_image")) .into((ImageView)findViewById(R.id.match_image)); rm.load(imageUrl) .listener(createLoggerListener("wrap_image")) .into((ImageView)findViewById(R.id.wrap_image)); rm.load(imageUrl) .listener(createLoggerListener("w100_image")) .into((ImageView)findViewById(R.id.w100_image)); } private RequestListener<Drawable> createLoggerListener(final String name) { return new RequestListener<Drawable>(){ @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { return false; } @Override public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { if (resource instanceof BitmapDrawable) { Bitmap bitmap = ((BitmapDrawable) resource).getBitmap(); Log.d("GlideApp", String.format("Ready %s bitmap %,d bytes, size: %d x %d", name, bitmap.getByteCount(), bitmap.getWidth(), bitmap.getHeight())); } return false; } }; }
Ready w100_image bitmap 256,800 bytes, size: 300 x 214 Ready match_image bitmap 3,330,720 bytes, size: 1080 x 771 Ready wrap_image bitmap 9,007,872 bytes, size: 1776 x 1268 |
確かに、wrap_contentを指定しているImageViewに反映させるときのBitmapのサイズが大きくなっています。メモリを食うので注意した方がよさそうです。
Overrideを使ってプログラムから読み込む画像サイズを指定することもできます。
GlideApp.with(this) .load(imageUrl) .listener(createLoggerListener("override200")) .override(200, 200) .into(matchImage);
Ready override200 bitmap 114,400 bytes, size: 200 x 143 |
サイズがわからない画像を読み込む
写真やアイコンなど何かわからない画像を読み込むこともよくあるかもしれません。小さなアイコン画像を大きな領域のImageViewに読み込むと拡大されたBitmapが作成されてしまいます。描画のパフォーマンスにはよいかもしれませんが、メモリやキャッシュ領域には優しくなさそうです。
Glide.with(this).load(iconUrl) .listener(createLoggerListener("match image")) .into((ImageView) findViewById(R.id.match_image));
Ready match image bitmap 4,665,600 bytes, size: 1080 x 1080 |
そこで、ImageViewにscaleType=”centerInside”を指定してみましょう。元画像の大きさ以上には拡大されなくなります。
<ImageView android:id="@+id/center_image" android:layout_width="match_parent" android:layout_height="wrap_content" android:scaleType="centerInside"/>
Glide.with(this).load(iconUrl) .listener(createLoggerListener("center image")) .into((ImageView) findViewById(R.id.center_image));
Ready center image bitmap 65,536 bytes, size: 128 x 128 |
以下のようにプログラムからオプションの指定を行えば、ImageViewの属性を使わずに拡大させないようにすることも可能です。
GlideApp.with(this).load(iconUrl) .downsample(DownsampleStrategy.CENTER_INSIDE) .dontTransform() .listener(createLoggerListener("set downsample")) .into((ImageView) findViewById(R.id.wrap_image));
Ready set downsample bitmap 65,536 bytes, size: 128 x 128 |
このように、オプションの指定をしない場合は、ImageViewの属性値によってがらりと挙動が変わるので、注意する必要があります。
HTTP Headerをつけてリクエスト
GlideUrlを使うとリクエストヘッダをつけてリクエストすることができます。Typetalk上の添付ファイル画像を表示する例です。認証が必要な画像の表示も簡単です。
String protectedUrl = "https://typetalk.com/api/v1/topics/xxx/posts/xxxxxxxxxx/attachments/1/sample.jpg"; Headers headers = new LazyHeaders.Builder() .addHeader("Authorization", "Bearer XXXXXXXXXXXXXXXXXXXXXXXX") .build(); Glide.with(this).load(new GlideUrl(protectedUrl, headers)).into(matchImage);
サムネイルの表示
サムネイル用のリクエストを別に指定することができます。この例では、Typetalkの添付ファイル取得APIにtype=smallパラメータを付けると小さめの画像を取得することができるので、そのリクエストをthumbnailに指定しています。こうすることで、まず小さな画像を読み込んでからきれいな画像を表示することができます。例えば一覧画面と詳細画面があったときに、一覧画面で表示しキャッシュされた小さめの画像を、詳細画面を開いたときにまず読み込んでからオリジナルの画像を表示する、といったことも可能になります。便利ですね。
GlideRequests gr = GlideApp.with(this); gr.load(new GlideUrl(protectedUrl, headers)) .thumbnail(gr .load(new GlideUrl(protectedUrl + "?type=small", headers)) .override(Target.SIZE_ORIGINAL) .listener(createLoggerListener("small"))) .listener(createLoggerListener("original")) .into(matchImage);
Ready small bitmap 691,200 bytes, size: 480 x 360 Ready original bitmap 3,499,200 bytes, size: 1080 x 810 |
その他
その他にもGlideにはいろいろな機能があるので、興味のある方はドキュメントを確認してみてください。
- 読み込み中画像指定 (placeholder)
- エラー時の画像指定 (error)
- 画像ロード時のフェードインなどのアニメーション (transition)
- メモリキャッシュやディスクキャッシュの無効化
- キャッシュからのみの読み込み
- SVG表示 (サンプル)
これまで面倒だった部分が簡単になり、アプリ開発の本質的な部分に注力できると良いですね!