Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android加載網絡GIF完整解決方案

Android加載網絡GIF完整解決方案

編輯:關於Android編程

前言:

加載並顯示gif是App常見的一個功能,像加載普通圖片一樣,大體應該包含以下幾項功能:

1、自動下載GIF到本地文件作為緩存,第二次加載同一個url的圖片不需要下載第二遍

2、由於GIF往往較大,要顯示圓形的進度條提示下載進度

3、在GIF完全下載完之前,先顯示GIF的第一幀圖像進行占位,完全下載完畢之後自動播放動畫。

4、兩個不同的頁面加載同一張GIF,兩個頁面的加載進度應該一致

5、支持ViewPager同時加載多個GIF動圖

效果演示:

\ \

 

實現思路:

1、關於下載和磁盤緩存:

我這裡使用HttpConnection根據url進行下載,在下載之前先將url字符串使用16位MD5進行轉換,讓下載的文件名為url的MD5碼,然後以4096字節為單位,使用ByteStremBuffer進行邊讀邊寫,防止下載過程中內存溢出,而且不時的向磁盤寫入還可以幫助實現GIF第一幀占位的效果。

2、關於進度指示:

我這裡使用了一個圓形的第三方Progress Bar和一個TextView實現,由於在下載過程中以4096為緩沖,所以每下載4096字節就會更新一次進度UI。文件總大小由http返回報文的頭部的Content-length返回,通過已下載大小除以這個length得出下載百分比。

3、關於不同頁面的下載同步:

用戶在首頁會看到一個gif,這時候點擊圖片可以跳進大圖頁繼續這個gif的下載,用戶在首頁的下載進度到帶到大圖頁來,不能讓用戶下載兩遍,也不能在大圖頁打開一個才下載了一半的圖像。

首先在下載開始之前,建立一個MD5.tmp的文件用來存儲下載內容,在下載完畢之後將.tmp文件名後綴去掉,這樣通過文件系統檢索一個GIF是否已被下載的時候,沒有下載完成的圖片就不會被檢索出來。

如果有一個url已經開始了一次下載,這時候又有一個下載請求同一個url,此時會將請求的imageView,textView和progressBar使用一個WeakReference引用起來,防止內存洩漏,然後把這三個空間添加到一個HashMap裡去,這個HashMap的key是url,value就是這些控件的弱引用組成的list。當下載線程更新進度或完成的時候,會從這個HashMap中根據url取出所有和這張gif有關的控件,然後把這些控件統一的更新狀態,這樣就可以保證不同頁面的控件的進度相同,也避免了一個文件下載多次的情況。

4、關於使用GIF的第一幀進行下載占位:

GIF的顯示使用了github上的開源項目:android-gif-drawable,地址:https://github.com/koral--/android-gif-drawable。是一個非常優秀的框架,其內部使用c語言編寫了一些效率非常高的執行代碼。

這個框架的可以直接根據輸入流進行加載,也就是說不用等gif文件完全下載完畢就可以顯示已經下載完畢的內容,甚至可以向浏覽器那樣一行像素一行像素的進行加載,十分好用。

根據框架的這個特性,只需要將還沒有下載好的文件直接傳到Drawable裡,讓道gifImageView中顯示即可,並且在這之前要判斷能否拿到第一幀,然後設置播放選項為暫停。

5、關於VIewPager的使用

在ViewPager的Adapter使用的時候遇到了很多麻煩,主要是由於ViewPager的緩存機制引起的,會引起顯示重復,無控件顯示等等問題,要解決在ViewPager中的使用,並讓GifImageView和普通ImageView一起在ViewPager中和平共處,需要先研究好ViewPager的緩存機制。在這裡我是先根據所有圖片數量生成同等多的imageView放在一個數組裡,然後ViewPager切換到哪張就從數組裡拿出哪張放到ViewPager的Container裡。GIfImageVIew也是這樣,不過是放在另一個數組裡,根據position取得相應的GIFImageView,然後用container來add,這裡對於add過一遍的GIfImageView會報異常,通過catch解決。

 

具體代碼:

加載工具類:

 

import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;

import com.imaginato.qravedconsumer.task.AlxMultiTask;
import com.lidroid.xutils.HttpUtils;
import com.pnikosis.materialishprogress.ProgressWheel;
import com.qraved.app.R;

import java.io.File;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;

import pl.droidsonroids.gif.GifDrawable;
import pl.droidsonroids.gif.GifImageView;

/**
 * Created by Alex on 2016/6/16.
 */
public class AlxGifHelper {


    public static class ProgressViews{
        public ProgressViews(WeakReference gifImageViewWeakReference, WeakReference progressWheelWeakReference, WeakReference textViewWeakReference,int displayWidth) {
            this.gifImageViewWeakReference = gifImageViewWeakReference;
            this.progressWheelWeakReference = progressWheelWeakReference;
            this.textViewWeakReference = textViewWeakReference;
            this.displayWidth = displayWidth;
        }

        public WeakReference gifImageViewWeakReference;//gif顯示控件
        public WeakReference progressWheelWeakReference;//用來裝飾的圓形進度條
        public WeakReference textViewWeakReference;//用來顯示當前進度的文本框
        public int displayWidth;//imageView的控件寬度
    }

    public static ConcurrentHashMap> memoryCache;//防止同一個gif文件建立多個下載線程,url和imageView是一對多的關系,如果一個imageView建立了一次下載,那麼其他請求這個url的imageView不需要重新開啟一次新的下載,這幾個imageView同時回調
    //為了防止內存洩漏,這個一對多的關系均使用LRU緩存

    /**
     * 通過本地緩存或聯網加載一張GIF圖片
     * @param url
     * @param gifView
     */
    public static void displayImage(final String url, GifImageView gifView, ProgressWheel progressBar , TextView tvProgress, int displayWidth){
        //首先查詢一下這個gif是否已被緩存
        String md5Url = getMd5(url);
        String path = gifView.getContext().getCacheDir().getAbsolutePath()+"/"+md5Url;//帶.tmp後綴的是沒有下載完成的,用於加載第一幀,不帶tmp後綴是下載完成的,
        //這樣做的目的是為了防止一個圖片正在下載的時候,另一個請求相同url的imageView使用未下載完畢的文件顯示一半圖像
        JLogUtils.i("AlexGIF","gif圖片的緩存路徑是"+path);
        final File cacheFile = new File(path);
        if(cacheFile.exists()){//如果本地已經有了這個gif的緩存
            JLogUtils.i("AlexGIF","本圖片有緩存");
            if(displayImage(cacheFile,gifView,displayWidth)) {//如果本地緩存讀取失敗就重新聯網下載
                if (progressBar != null) progressBar.setVisibility(View.GONE);
                if (tvProgress!=null)tvProgress.setVisibility(View.GONE);
                return;
            }
        }
        //為了防止activity被finish了但是還有很多gif還沒有加載完成,導致activity沒有及時被內存回收導致內存洩漏,這裡使用弱引用
        final WeakReference imageViewWait= new WeakReference(gifView);
        final WeakReference progressBarWait= new WeakReference(progressBar);
        final WeakReference textViewWait= new WeakReference(tvProgress);
        if(gifView.getId()!= R.id.gif_photo_view)gifView.setImageResource(R.drawable.qraved_bg_default);//設置沒有下載完成前的默認圖片
        if(memoryCache!=null && memoryCache.get(url)!=null){//如果以前有別的imageView加載過
            JLogUtils.i("AlexGIF","以前有別的ImageView申請加載過該gif"+url);
            //可以借用以前的下載進度,不需要新建一個下載線程了
            memoryCache.get(url).add(new ProgressViews(imageViewWait,progressBarWait,textViewWait,displayWidth));
            return;
        }
        if(memoryCache==null)memoryCache = new ConcurrentHashMap<>();
        if(memoryCache.get(url)==null)memoryCache.put(url,new ArrayList());
        //將現在申請加載的這個imageView放到緩存裡,防止重復加載
        memoryCache.get(url).add(new ProgressViews(imageViewWait,progressBarWait,textViewWait,displayWidth));

        final HttpUtils http = new HttpUtils();

        // 下載圖片
        startDownLoad(url, new File(cacheFile.getAbsolutePath()+".tmp"), new DownLoadTask() {
            @Override
            public void onStart() {
                JLogUtils.i("AlexGIF","下載GIF開始");
                ProgressWheel progressBar = progressBarWait.get();
                TextView tvProgress = textViewWait.get();
                if(progressBar!=null){
                    progressBar.setVisibility(View.VISIBLE);
                    progressBar.setProgress(0);
                    if(tvProgress==null)return;
                    tvProgress.setVisibility(View.VISIBLE);
                    tvProgress.setText("1%");
                }
            }

            @Override
            public void onLoading(long total, long current) {
                int progress = 0;
                //得到要下載文件的大小,是通過http報文的header的Content-Length獲得的,如果獲取不到就是-1
                if(total>0)progress = (int)(current*100/total);
                JLogUtils.i("AlexGIF","下載gif的進度是"+progress+"%"+"    現在大小"+current+"   總大小"+total);
                ArrayList viewses = memoryCache.get(url);
                if(viewses ==null)return;
                JLogUtils.i("AlexGIF","該gif的請求數量是"+viewses.size());
                for(ProgressViews vs : viewses){//遍歷所有的進度條,修改同一個url請求的進度顯示
                    ProgressWheel progressBar = vs.progressWheelWeakReference.get();
                    if(progressBar!=null){
                        progressBar.setProgress((float)progress/100f);
                        if(total==-1)progressBar.setProgress(20);//如果獲取不到大小,就讓進度條一直轉
                    }
                    TextView tvProgress = vs.textViewWeakReference.get();
                    if(tvProgress != null)tvProgress.setText(progress+"%");
                }
		//顯示第一幀直到全部下載完之後開始動畫
                getFirstPicOfGIF(new File(cacheFile.getAbsolutePath()+".tmp"),vs.gifImageViewWeakReference.get());
            }

            public void onSuccess(File file) {
                if(file==null)return;
                String path = file.getAbsolutePath();
                if(path==null || path.length()<5)return;
                File downloadFile = new File(path);
                File renameFile = new File(path.substring(0,path.length()-4));
                if(path.endsWith(".tmp"))downloadFile.renameTo(renameFile);//將.tmp後綴去掉
                Log.i("AlexGIF","下載GIf成功,文件路徑是"+path+" 重命名之後是"+renameFile.getAbsolutePath());
                if(memoryCache==null)return;
                ArrayList viewArr = memoryCache.get(url);
                if(viewArr==null || viewArr.size()==0)return;
                for(ProgressViews ws:viewArr){//遍歷所有的進度條和imageView,同時修改所有請求同一個url的進度
                    //顯示imageView
                    GifImageView gifImageView = ws.gifImageViewWeakReference.get();
                    if (gifImageView!=null)displayImage(renameFile,gifImageView,ws.displayWidth);
                    //修改進度條
                    TextView tvProgress = ws.textViewWeakReference.get();
                    ProgressWheel progressBar = ws.progressWheelWeakReference.get();
                    if(progressBar!=null)progressBar.setVisibility(View.GONE);
                    if(tvProgress!=null)tvProgress.setVisibility(View.GONE);
                }
                JLogUtils.i("AlexGIF",url+"的imageView已經全部加載完畢,共有"+viewArr.size()+"個");
                memoryCache.remove(url);//這個url的全部關聯imageView都已經顯示完畢,清除緩存記錄
            }

            @Override
            public void onFailure(Throwable e) {
                Log.i("Alex","下載gif圖片出現異常",e);
                TextView tvProgress = textViewWait.get();
                ProgressWheel progressBar = progressBarWait.get();
                if(progressBar!=null)progressBar.setVisibility(View.GONE);
                if(tvProgress!=null)tvProgress.setText("image download failed");
                if(memoryCache!=null)memoryCache.remove(url);//下載失敗移除所有的弱引用
            }
        });
    }



    /**
     * 通過本地文件顯示GIF文件
     * @param localFile 本地的文件指針
     * @param gifImageView
     * displayWidth imageView控件的寬度,用於根據gif的實際高度重設控件的高度來保證完整顯示,傳0表示不縮放gif的大小,顯示原始尺寸
     */
    public static boolean displayImage(File localFile,GifImageView gifImageView,int displayWidth){
        if(localFile==null || gifImageView==null)return false;
        JLogUtils.i("AlexGIF","准備加載gif"+localFile.getAbsolutePath()+"顯示寬度為"+displayWidth);
        GifDrawable gifFrom;
        try {
            gifFrom = new GifDrawable(localFile);
            int raw_height = gifFrom.getIntrinsicHeight();
            int raw_width = gifFrom.getIntrinsicWidth();
            JLogUtils.i("AlexGIF","圖片原始height是"+raw_height+"  圖片原始寬是:"+raw_width);
            if(gifImageView.getScaleType() != ImageView.ScaleType.CENTER_CROP && gifImageView.getScaleType()!= ImageView.ScaleType.FIT_XY){
                //如果大小應該自適應的話進入該方法(也就是wrap content),不然高度不會自動變化
                if(raw_width<1 || raw_height<1)return false;
                int imageViewWidth = displayWidth;
                if(imageViewWidth < 1)imageViewWidth = raw_width;//當傳來的控件寬度不大對的時候,就顯示gif的原始大小
                int imageViewHeight = imageViewWidth*raw_height/raw_width;
                JLogUtils.i("AlexGIF","縮放完的gif是"+imageViewWidth+" X "+imageViewHeight);
                ViewGroup.LayoutParams params = gifImageView.getLayoutParams();
                if(params!=null){
                    params.height = imageViewHeight;
                    params.width = imageViewWidth;
                }
            }else {
                JLogUtils.i("AlexGIF","按照固定大小進行顯示");
            }
            gifImageView.setImageDrawable(gifFrom);
            return true;
        } catch (IOException e) {
            JLogUtils.i("AlexGIF","顯示gif出現異常",e);
            return false;
        }
    }

    /**
     * 用於獲取一個String的md5值
     * @param str
     * @return
     */
    public static String getMd5(String str) {
        if(str==null || str.length()<1)return "no_image.gif";
        MessageDigest md5 = null;
        try {
            md5 = MessageDigest.getInstance("MD5");
            byte[] bs = md5.digest(str.getBytes());
            StringBuilder sb = new StringBuilder(40);
            for(byte x:bs) {
                if((x & 0xff)>>4 == 0) {
                    sb.append("0").append(Integer.toHexString(x & 0xff));
                } else {
                    sb.append(Integer.toHexString(x & 0xff));
                }
            }
            if(sb.length()<24)return sb.toString();
            return sb.toString().substring(8,24);//為了提高磁盤的查找文件速度,讓文件名為16位
        } catch (NoSuchAlgorithmException e) {
            JLogUtils.i("Alex","MD5加密失敗");
            return "no_image.gif";
        }
    }

    public static abstract class DownLoadTask{
        abstract void onStart();
        abstract void onLoading(long total, long current);
        abstract void onSuccess(File target);
        abstract void onFailure(Throwable e);
        boolean isCanceled;
    }

    /**
     * 開啟下載任務到線程池裡,防止多並發線程過多
     * @param uri
     * @param targetFile
     * @param task
     */
    public static void startDownLoad(final String uri, final File targetFile, final DownLoadTask task){
        final Handler handler = new Handler();
        new AlxMultiTask(){//開啟一個多線程池,大小為cpu數量+1

            @Override
            protected Void doInBackground(Void... params) {
                task.onStart();
                downloadToStream(uri,targetFile,task,handler);
                return null;
            }
        }.executeDependSDK();
    }


    /**
     * 通過httpconnection下載一個文件,使用普通的IO接口進行讀寫
     * @param uri
     * @param targetFile
     * @param task
     * @return
     */
    public static long downloadToStream(String uri, final File targetFile, final DownLoadTask task, Handler handler) {

        if (task == null || task.isCanceled) return -1;

        HttpURLConnection httpURLConnection = null;
        BufferedInputStream bis = null;
        OutputStream outputStream = null;

        long result = -1;
        long fileLen = 0;
        long currCount = 0;
        try {

                try {
                    final URL url = new URL(uri);
                    outputStream = new FileOutputStream(targetFile);
                    httpURLConnection = (HttpURLConnection) url.openConnection();
                    httpURLConnection.setConnectTimeout(20000);
                    httpURLConnection.setReadTimeout(10000);

                    final int responseCode = httpURLConnection.getResponseCode();
                    if (HttpURLConnection.HTTP_OK == responseCode) {
                        bis = new BufferedInputStream(httpURLConnection.getInputStream());
                        result = httpURLConnection.getExpiration();
                        result = result < System.currentTimeMillis() ? System.currentTimeMillis() + 40000 : result;
                        fileLen = httpURLConnection.getContentLength();//這裡通過http報文的header Content-Length來獲取gif的總大小,需要服務器提前把header寫好
                    } else {
                        Log.e("Alex","downloadToStream -> responseCode ==> " + responseCode);
                        return -1;
                    }
                } catch (final Exception ex) {
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            task.onFailure(ex);
                        }
                    });
                    return -1;
                }


            if (task.isCanceled) return -1;

            byte[] buffer = new byte[4096];//每4k更新進度一次
            int len = 0;
            BufferedOutputStream out = new BufferedOutputStream(outputStream);
            while ((len = bis.read(buffer)) != -1) {
                out.write(buffer, 0, len);
                currCount += len;
                if (task.isCanceled) return -1;
                final long finalFileLen = fileLen;
                final long finalCurrCount = currCount;
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        task.onLoading(finalFileLen, finalCurrCount);
                    }
                });
            }
            out.flush();
            handler.post(new Runnable() {
                @Override
                public void run() {
                    task.onSuccess(targetFile);
                }
            });
        } catch (Throwable e) {
            result = -1;
            task.onFailure(e);
        } finally {
            if (bis != null) {
                try {
                    bis.close();
                } catch (final Throwable e) {
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            task.onFailure(e);
                        }
                    });
                }
            }
        }
        return result;
    }

 /**
     * 加載gif的第一幀圖像,用於下載完成前占位
     * @param gifFile
     * @param imageView
     */
    public static void getFirstPicOfGIF(File gifFile,GifImageView imageView){
        if(imageView==null)return;
        if(imageView.getTag(R.style.AppTheme) instanceof Integer)return;//之前已經顯示過第一幀了,就不用再顯示了
        try {
            GifDrawable gifFromFile = new GifDrawable(gifFile);
            boolean canSeekForward = gifFromFile.canSeekForward();
            if(!canSeekForward)return;
            JLogUtils.i("AlexGIF","是否能顯示第一幀圖片"+canSeekForward);
            //下面是一些其他有用的信息
//            int frames = gifFromFile.getNumberOfFrames();
//            JLogUtils.i("AlexGIF","已經下載完多少幀"+frames);
//            int bytecount = gifFromFile.getFrameByteCount();
//            JLogUtils.i("AlexGIF","一幀至少多少字節"+bytecount);
//            long memoryCost = gifFromFile.getAllocationByteCount();
//            JLogUtils.i("AlexGIF","內存開銷是"+memoryCost);
            gifFromFile.seekToFrame(0);
            gifFromFile.pause();//靜止在該幀
            imageView.setImageDrawable(gifFromFile);
            imageView.setTag(R.style.AppTheme,1);//標記該imageView已經顯示過第一幀了
        } catch (IOException e) {
            JLogUtils.i("AlexGIF","獲取gif信息出現異常",e);
        }
    }
}

線程池:

 

 

import android.os.AsyncTask;
import android.os.Build;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Created by Alex on 2016/4/19.
 * 用於替換系統自帶的AsynTask,使用自己的多線程池,執行一些比較復雜的工作,比如select photos,這裡用的是緩存線程池,也可以用和cpu數相等的定長線程池以提高性能
 */
public abstract class AlxMultiTask extends AsyncTask {
    private static ExecutorService photosThreadPool;//用於加載大圖的線程池
    private final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private final int CORE_POOL_SIZE = CPU_COUNT + 1;
    public void executeDependSDK(Params...params){
        if(photosThreadPool==null)photosThreadPool = Executors.newFixedThreadPool(CORE_POOL_SIZE);
        if(Build.VERSION.SDK_INT<11) super.execute(params);
        else super.executeOnExecutor(photosThreadPool,params);
    }

}

ViewPager Adpater的寫法(截取)

 

 

public class PhotoImageViewPageAdapter extends PagerAdapter {
	@Override
    public Object instantiateItem(ViewGroup container, int position) {
 
                String imageUrl = "http://xxx.com/sdf/xxx.gif";
                JLogUtils.i("AlexGIF","當前圖片->"+imageUrl);
                if(imageUrl.endsWith(".gif")){//如果是gif動圖
                JLogUtils.i("AlexGIF","現在是gif大圖");
                View rl_gif = LayoutInflater.from(activity).inflate(R.layout.layout_photo_loading_gif_imageview, null);//這種方式容易導致內存洩漏
                    GifImageView gifImageView = (GifImageView) rl_gif.findViewById(R.id.gif_photo_view);
                    ProgressWheel progressWheel = (ProgressWheel) rl_gif.findViewById(R.id.progress_wheel);
                    CustomTextView tv_progress = (CustomTextView) rl_gif.findViewById(R.id.tv_progress);
                    AlxGifHelper.displayImage(imageUrl,gifImageView,progressWheel,tv_progress,0);//最後一個參數傳0表示不縮放gif的大小,顯示原始尺寸
                    try {
                        container.addView(rl_gif);//這裡要注意由於container是一個復用的控件,所以頻繁的addView會導致多張相同的圖片重疊,必須予以處置
                    }catch (Exception e){
                        JLogUtils.i("AlexGIF","父控件重復!!!!,這裡出現異常很正常",e);
                    }
                    return rl_gif;//這裡有個大坑,千萬不能return container,但是在return之前必須addView
               
            }
        }
        return container;
    }
}

 

 

布局文件

 




    
    
    


中間的ProgressBar使用了一個第三方庫

 

 

dependencies {
    compile 'com.pnikosis:materialish-progress:1.7'
}



 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved