編輯:關於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(WeakReferencegifImageViewWeakReference, 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 AlxMultiTaskextends 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); } }
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; } }
布局文件
dependencies { compile 'com.pnikosis:materialish-progress:1.7' }
前言 一個好的應用需要一個有良好的用戶體驗的登錄界面,現如今,許多應用的的登錄界面都有著用戶名,密碼一鍵刪除,用戶名,密碼
最近在處理一些lowmemorykiller相關的問題,於是對lowmemorykiller機制作了一個簡單的了解。在這裡總結一下。首先,是lowmemorykiller
將gradle更好應用到你的應用開發上面Gradle深入淺出以下部分可以讓你將一個基於gradle建立的android程序跑起來,並將重點介紹gradle為安卓開發過程中
程序的最主要的功能在於對數據進行操作,通過對數據進行操作來實現某個功能。而數據庫就是很重要的一個方面的,Android中內置了小巧輕便,功能卻很強的一個數據庫–SQLit