1. ホーム
  2. android

[解決済み] Androidで画像をダウンロードし、保存する方法

2023-02-13 03:41:39

質問

Androidで、指定したURLから画像をダウンロードし、保存する方法を教えてください。

どのように解決するのですか?

2015.12.30現在編集中 - 画像ダウンロードの極意


最終メジャー アップデート: 2016 年 3 月 31 日


TL;DR、すなわち、話さなくていいから、コードをくれ!

この記事の最下部までスキップして BasicImageDownloader (javadoc版 ここで ) をプロジェクトに組み込んで OnImageLoaderListener インターフェイスを実装します。 を実装すれば完了です。

ノート : しかし BasicImageDownloader は起こりうるエラーを処理します。 を処理し、何か問題が発生したときにアプリがクラッシュするのを防ぎます。 に対していかなる後処理 (例: サイズ縮小) も行いません。 Bitmaps .


この投稿は非常に多くの注目を集めたので、非推奨の技術や悪いプログラミング手法、あるいは単に愚かなこと、たとえばメインスレッドでネットワークを実行したり、すべての SSL 証明書を受け入れるためのハックを探したりすることを防ぐために、完全に作り直すことにしました。

私は "Image Downloader" という名前のデモ プロジェクトを作成し、私自身のダウンローダー実装、Android の組み込みの DownloadManager と、いくつかの有名なオープンソースライブラリを使用して、画像をダウンロード(および保存)する方法を示す、&qu;Image Downloader&qu;という名前のプロジェクトです。ソースコード全体を見るか、プロジェクトをダウンロードすることができます。 を GitHub で見ることができます。 .

<ブロッククオート

ノート : SDK 23+ (Marshmallow)の権限管理はまだ調整していないので、プロジェクトはSDK 22 (Lollipop)を対象にしています。

私の 結論 この記事の最後に、私は 私の謙虚な意見 を紹介します。

独自の実装から始めましょう(投稿の最後にコードを見つけることができます)。まず第一に、これは 基本的な ImageDownloaderであり、それだけです。これは与えられた url に接続し、データを読み込んで、それを Bitmap としてデコードしようとすること、そして OnImageLoaderListener インターフェイスのコールバックを適宜トリガーします。 この方法の利点は、シンプルで、何が起こっているのかを明確に把握することができることです。メモリやディスクのキャッシュを維持することを気にしない一方で、いくつかの画像をダウンロード/表示/保存する必要があるだけなら、この方法は良い方法です。

注意: 大きな画像の場合は スケールダウン ダウン .

--

アンドロイド ダウンロードマネージャー は、システムにダウンロードを処理させる方法です。実際には、画像だけでなく、あらゆる種類のファイルをダウンロードすることが可能です。ダウンロードはユーザーから見えないように静かに行わせることもできますし、通知領域でダウンロードを確認できるようにすることもできます。また BroadcastReceiver を登録して、ダウンロード完了後に通知を受けることもできます。設定は非常に簡単です。サンプルコードはリンク先のプロジェクトを参照してください。

を使用して DownloadManager を使うことは、一般に、画像を表示したい場合には良いアイデアではありません。なぜなら、ダウンロードした BitmapImageView . そのため DownloadManager は、アプリがダウンロードの進捗を追跡するための API も提供しません。

--

さて、いよいよ素晴らしいもの、ライブラリの紹介です。これらは単に画像をダウンロードして表示するだけでなく、メモリ/ディスク キャッシュの作成と管理、画像のリサイズ、画像の変換など、多くのことを行うことができます。

まず始めに ボレー は、Googleによって作成された強力なライブラリで、公式ドキュメントでカバーされています。画像に特化したものではなく、汎用のネットワーク・ライブラリですが、Volleyは画像を管理するための非常に強力なAPIを備えています。

を実装する必要があります。 シングルトン クラスを実装すれば完了です。

を置き換える必要があるかもしれません。 ImageView をVolleyの NetworkImageView というように、ダウンロードは基本的に一行で済むようになっています。

((NetworkImageView) findViewById(R.id.myNIV)).setImageUrl(url, MySingleton.getInstance(this).getImageLoader());

もっとコントロールが必要な場合は、このように ImageRequest をVolleyで作成したものです。

     ImageRequest imgRequest = new ImageRequest(url, new Response.Listener<Bitmap>() {
             @Override
             public void onResponse(Bitmap response) {
                    //do stuff
                }
            }, 0, 0, ImageView.ScaleType.CENTER_CROP, Bitmap.Config.ARGB_8888, 
             new Response.ErrorListener() {
             @Override
             public void onErrorResponse(VolleyError error) {
                   //do stuff
                }
            });

特筆すべきは、Volleyが優れたエラー処理機構を備えていることです。 VolleyError クラスを提供することで、エラーの正確な原因を特定するのに役立っています。もし、あなたのアプリが多くのネットワーキングを行い、画像の管理が主な目的でないなら、Volley はあなたにぴったりです。

--

スクウェアの ピカソ は、画像の読み込みをすべて代行してくれる有名なライブラリです。Picassoを使って画像を表示させるだけなら、以下のように簡単です。

 Picasso.with(myContext)
       .load(url)
       .into(myImageView); 

デフォルトでは、Picasso はディスク/メモリキャッシュを管理するので、それについて心配する必要はありません。もっとコントロールしたい場合は Target インターフェイスを実装し、画像を読み込むためにそれを使用します - これは Volley の例と同様のコールバックを提供します。この場合、Volley の例と同様のコールバックが提供されます。例については、デモ プロジェクトを参照してください。

Picassoでは、ダウンロードした画像に変形を加えることもできます。 他のライブラリ があり、それらのAPIを拡張しています。また RecyclerView / ListView / GridView .

--

ユニバーサルイメージローダ は、画像管理を目的とした、もう一つの非常に人気のあるライブラリです。これは、独自の ImageLoader を使用し、(一旦初期化されると)グローバルインスタンスを持ち、1行のコードで画像をダウンロードするために使用することができます。

  ImageLoader.getInstance().displayImage(url, myImageView);

ダウンロードの進行状況を確認したり、ダウンロードした Bitmap :

 ImageLoader.getInstance().displayImage(url, myImageView, opts, 
 new ImageLoadingListener() {
     @Override
     public void onLoadingStarted(String imageUri, View view) {
                     //do stuff
                }

      @Override
      public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
                   //do stuff
                }

      @Override
      public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
                   //do stuff
                }

      @Override
      public void onLoadingCancelled(String imageUri, View view) {
                   //do stuff
                }
            }, new ImageLoadingProgressListener() {
      @Override
      public void onProgressUpdate(String imageUri, View view, int current, int total) {
                   //do stuff
                }
            });

opts の引数は、この例では DisplayImageOptions オブジェクトを返します。詳しくは、デモプロジェクトを参照してください。

Volleyと同様に、UILでは FailReason クラスがあり、ダウンロードの失敗時に何が問題だったかをチェックすることができます。デフォルトでは、UIL は明示的にそうしないように指示しない限り、メモリ/ディスク キャッシュを維持します。

注意 : 作者は2015年11月27日現在、プロジェクトの保守を終了していることを述べています。しかし、多くの貢献者がいるため、Universal Image Loaderが生き続けることを期待することができます。

--

フェイスブックの フレスコ画 は、画像管理を新たなレベルに引き上げる、最新かつ (IMO) 最先端のライブラリです。 Bitmaps を java ヒープから切り離すことから(Lollipop より前)、サポートする アニメーションのフォーマット プログレッシブ JPEG ストリーミング .

Fresco の背後にあるアイデアや技術について詳しく知るには、以下を参照してください。 この記事 .

基本的な使い方は非常にシンプルです。注意することは Fresco.initialize(Context); を一度だけ、出来れば Application クラスで行うのが望ましいです。Frescoを複数回初期化すると、予測できない動作やOOMエラーにつながる可能性があります。

Frescoは Drawee を使用して画像を表示します。 ImageView s:

    <com.facebook.drawee.view.SimpleDraweeView
    android:id="@+id/drawee"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    fresco:fadeDuration="500"
    fresco:actualImageScaleType="centerCrop"
    fresco:placeholderImage="@drawable/placeholder_grey"
    fresco:failureImage="@drawable/error_orange"
    fresco:placeholderImageScaleType="fitCenter"
    fresco:failureImageScaleType="centerInside"
    fresco:retryImageScaleType="centerCrop"
    fresco:progressBarImageScaleType="centerInside"
    fresco:progressBarAutoRotateInterval="1000"
    fresco:roundAsCircle="false" />

見ての通り、多くのもの(変換オプションを含む)は既にXMLで定義されているので、画像を表示するのに必要なのは一行だけです。

 mDrawee.setImageURI(Uri.parse(url));

Fresco は拡張されたカスタマイズ API を提供します。これは状況によっては非常に複雑で、ユーザがドキュメントを注意深く読む必要があります (そう、時には が必要です。 RTFM)。

サンプルプロジェクトにプログレッシブJPEGとアニメーション画像の例を入れました。


結論 - "素晴らしいものを学んだので、今度は何を使うべきですか?

以下の文章は、私の個人的な意見を述べたものであり、仮定としてとらえるべきものではないことに注意してください。 ので、ご注意ください。

  • いくつかの画像をダウンロード/保存/表示する必要があるだけなら、それらを Recycler-/Grid-/ListView で使用する予定がなく、大量の画像を表示できるようにする必要がない場合は BasicImageDownloader はあなたのニーズに合うはずです。
  • あなたのアプリがユーザーまたは自動化されたアクションの結果として画像(または他のファイル)を保存し、画像を頻繁に表示する必要がない場合、Androidの ダウンロードマネージャー .
  • アプリが多くのネットワーク接続を行う場合、送信/受信する JSON データを送受信したり、画像を扱ったりしますが、それらはアプリの主目的ではありません。 ボレー .
  • 画像やメディアに特化したアプリで、画像に何らかの変換を適用したいが、複雑なAPIは使いたくない場合。 Picasso (注意: 中間ダウンロードの状態を追跡するAPIは提供していません) または ユニバーサルイメージローダ
  • もしあなたのアプリが画像に関するもので、アニメーション形式の表示などの高度な機能が必要で、ドキュメントを読む準備ができているのなら フレスコ .

因みに、この Github リンク をクリックすると、デモプロジェクトが表示されます。


そして、ここでは BasicImageDownloader.java

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashSet;
import java.util.Set;

public class BasicImageDownloader {

    private OnImageLoaderListener mImageLoaderListener;
    private Set<String> mUrlsInProgress = new HashSet<>();
    private final String TAG = this.getClass().getSimpleName();

    public BasicImageDownloader(@NonNull OnImageLoaderListener listener) {
        this.mImageLoaderListener = listener;
    }

    public interface OnImageLoaderListener {
        void onError(ImageError error);      
        void onProgressChange(int percent);
        void onComplete(Bitmap result);
    }


    public void download(@NonNull final String imageUrl, final boolean displayProgress) {
        if (mUrlsInProgress.contains(imageUrl)) {
            Log.w(TAG, "a download for this url is already running, " +
                    "no further download will be started");
            return;
        }

        new AsyncTask<Void, Integer, Bitmap>() {

            private ImageError error;

            @Override
            protected void onPreExecute() {
                mUrlsInProgress.add(imageUrl);
                Log.d(TAG, "starting download");
            }

            @Override
            protected void onCancelled() {
                mUrlsInProgress.remove(imageUrl);
                mImageLoaderListener.onError(error);
            }

            @Override
            protected void onProgressUpdate(Integer... values) {
                mImageLoaderListener.onProgressChange(values[0]);
            }

        @Override
        protected Bitmap doInBackground(Void... params) {
            Bitmap bitmap = null;
            HttpURLConnection connection = null;
            InputStream is = null;
            ByteArrayOutputStream out = null;
            try {
                connection = (HttpURLConnection) new URL(imageUrl).openConnection();
                if (displayProgress) {
                    connection.connect();
                    final int length = connection.getContentLength();
                    if (length <= 0) {
                        error = new ImageError("Invalid content length. The URL is probably not pointing to a file")
                                .setErrorCode(ImageError.ERROR_INVALID_FILE);
                        this.cancel(true);
                    }
                    is = new BufferedInputStream(connection.getInputStream(), 8192);
                    out = new ByteArrayOutputStream();
                    byte bytes[] = new byte[8192];
                    int count;
                    long read = 0;
                    while ((count = is.read(bytes)) != -1) {
                        read += count;
                        out.write(bytes, 0, count);
                        publishProgress((int) ((read * 100) / length));
                    }
                    bitmap = BitmapFactory.decodeByteArray(out.toByteArray(), 0, out.size());
                } else {
                    is = connection.getInputStream();
                    bitmap = BitmapFactory.decodeStream(is);
                }
            } catch (Throwable e) {
                if (!this.isCancelled()) {
                    error = new ImageError(e).setErrorCode(ImageError.ERROR_GENERAL_EXCEPTION);
                    this.cancel(true);
                }
            } finally {
                try {
                    if (connection != null)
                        connection.disconnect();
                    if (out != null) {
                        out.flush();
                        out.close();
                    }
                    if (is != null)
                        is.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return bitmap;
        }

            @Override
            protected void onPostExecute(Bitmap result) {
                if (result == null) {
                    Log.e(TAG, "factory returned a null result");
                    mImageLoaderListener.onError(new ImageError("downloaded file could not be decoded as bitmap")
                            .setErrorCode(ImageError.ERROR_DECODE_FAILED));
                } else {
                    Log.d(TAG, "download complete, " + result.getByteCount() +
                            " bytes transferred");
                    mImageLoaderListener.onComplete(result);
                }
                mUrlsInProgress.remove(imageUrl);
                System.gc();
            }
        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    public interface OnBitmapSaveListener {
        void onBitmapSaved();
        void onBitmapSaveError(ImageError error);
    }


    public static void writeToDisk(@NonNull final File imageFile, @NonNull final Bitmap image,
                               @NonNull final OnBitmapSaveListener listener,
                               @NonNull final Bitmap.CompressFormat format, boolean shouldOverwrite) {

    if (imageFile.isDirectory()) {
        listener.onBitmapSaveError(new ImageError("the specified path points to a directory, " +
                "should be a file").setErrorCode(ImageError.ERROR_IS_DIRECTORY));
        return;
    }

    if (imageFile.exists()) {
        if (!shouldOverwrite) {
            listener.onBitmapSaveError(new ImageError("file already exists, " +
                    "write operation cancelled").setErrorCode(ImageError.ERROR_FILE_EXISTS));
            return;
        } else if (!imageFile.delete()) {
            listener.onBitmapSaveError(new ImageError("could not delete existing file, " +
                    "most likely the write permission was denied")
                    .setErrorCode(ImageError.ERROR_PERMISSION_DENIED));
            return;
        }
    }

    File parent = imageFile.getParentFile();
    if (!parent.exists() && !parent.mkdirs()) {
        listener.onBitmapSaveError(new ImageError("could not create parent directory")
                .setErrorCode(ImageError.ERROR_PERMISSION_DENIED));
        return;
    }

    try {
        if (!imageFile.createNewFile()) {
            listener.onBitmapSaveError(new ImageError("could not create file")
                    .setErrorCode(ImageError.ERROR_PERMISSION_DENIED));
            return;
        }
    } catch (IOException e) {
        listener.onBitmapSaveError(new ImageError(e).setErrorCode(ImageError.ERROR_GENERAL_EXCEPTION));
        return;
    }

    new AsyncTask<Void, Void, Void>() {

        private ImageError error;

        @Override
        protected Void doInBackground(Void... params) {
            FileOutputStream fos = null;
            try {
                fos = new FileOutputStream(imageFile);
                image.compress(format, 100, fos);
            } catch (IOException e) {
                error = new ImageError(e).setErrorCode(ImageError.ERROR_GENERAL_EXCEPTION);
                this.cancel(true);
            } finally {
                if (fos != null) {
                    try {
                        fos.flush();
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            return null;
        }

        @Override
        protected void onCancelled() {
            listener.onBitmapSaveError(error);
        }

        @Override
        protected void onPostExecute(Void result) {
            listener.onBitmapSaved();
        }
    }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
  }

public static Bitmap readFromDisk(@NonNull File imageFile) {
    if (!imageFile.exists() || imageFile.isDirectory()) return null;
    return BitmapFactory.decodeFile(imageFile.getAbsolutePath());
}

public interface OnImageReadListener {
    void onImageRead(Bitmap bitmap);
    void onReadFailed();
}

public static void readFromDiskAsync(@NonNull File imageFile, @NonNull final OnImageReadListener listener) {
    new AsyncTask<String, Void, Bitmap>() {
        @Override
        protected Bitmap doInBackground(String... params) {
            return BitmapFactory.decodeFile(params[0]);
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            if (bitmap != null)
                listener.onImageRead(bitmap);
            else
                listener.onReadFailed();
        }
    }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, imageFile.getAbsolutePath());
}

    public static final class ImageError extends Throwable {

        private int errorCode;
        public static final int ERROR_GENERAL_EXCEPTION = -1;
        public static final int ERROR_INVALID_FILE = 0;
        public static final int ERROR_DECODE_FAILED = 1;
        public static final int ERROR_FILE_EXISTS = 2;
        public static final int ERROR_PERMISSION_DENIED = 3;
        public static final int ERROR_IS_DIRECTORY = 4;


        public ImageError(@NonNull String message) {
            super(message);
        }

        public ImageError(@NonNull Throwable error) {
            super(error.getMessage(), error.getCause());
            this.setStackTrace(error.getStackTrace());
        }

        public ImageError setErrorCode(int code) {
            this.errorCode = code;
            return this;
        }

        public int getErrorCode() {
            return errorCode;
        }
      }
   }