第十七章 相簿實作

Android的UI畫面只能由主執行緒來更新, 所以其他的執行緒需借由Handler發送Message通知主執行緒進行更新的動作.
基於此因素, Google開發了AsyncTask類別, 此類別拆成二部份, 其一為負責更新UI的主執行緒, 另一部份為新的執行緒, 負責後台耗時的工作

要使用此類別, 需實作doInBackground(Void… voids), 將耗時工作寫於此處. 若有回報進度, 可在此方法中調用 publishProgress(Integer)方法, 此時就會執行 onProgressUpdate()方法, 然後於onProgressUpdate()中執行UI畫面的更新.

class ReadSDCard extends AsyncTask<Void, Integer, Void>{
    @Override
    protected Void doInBackground(Void... voids) {
        return null;
    }
    @Override
    protected void onProgressUpdate(Integer... values) {
        super.onProgressUpdate(values);
    }
}

在此特別聲明一下, AsyncTask真的好用嗎?? 此類別使用二個執行緒來執行.  在doInBackground()新增一個執行緒處理耗時的工作, 而其他的方法則使用UI主執行緒來執行以便控制UI畫面.
好問題來了, 當畫面旋轉時, 所有MainActivity的物件變數全都砍掉重練, 只留下doInBackgournd()的執行緒獨立奮戰, 戰後的結果往上回報, 但卻因轉向後所有的物件變數人事全非江山易主, 所以無從報起.
所以還是要建議一下, 要使用AsyncTask時, 最好把AndroidManifest.xml設定檔的 <activity>標簽設定為android:screenOrientation=”landscape|portrait” , 讓整個畫面固定不會旋轉

本人是絕對不用這個類別啦, 但因為教學的關係, 所以只好寫一下. 另外AsyncTask的效能非常的差, 所以下面的範面, 全改為Thread

在這個章節中, 要實作出類似Google 相簿功能的程式碼, 所以需融合Service, 執行緒, BaseAdapter, ImageView等複雜的觀念, 因此獨立成這個章節進行說明.

設計需求

打開Google相簿, 會出現每列都有三格的縮圖, 然後一個一個慢慢的顯示出來, 要等到顯示完畢, 可能要花個好幾分鐘. 而在這段時間, 如果等的不耐煩,  可以直接點選想要放大的圖片看個仔細, 不過此時系統背後還是持續的在截取其他SDCard的縮圖

GridView

要顯示所有的縮圖, 就要將這些縮圖放在GridView中, GridView如同ListView, 具有上下捲動的功能, 所以在activity_main.xml中, 放置一個GridView, Id設為gridView, 並設定每列顯示三行圖片
android:numColumns=”3″

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="net.ddns.mahaljsp.ch15_06_asynctask.MainActivity">
    <GridView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/gridView"
        android:numColumns="3" />
</LinearLayout>

開啟SDCard讀取權限

讀取SDCard屬危險權限等級, 所以需於AndroidManifest.xml設定permission

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

在API 23以上, 還需進行權限開通. 以下程式嗎中, 先把要開通的權限寫在permitItems[]字串陣列, 並將requestCode設定在permitCodes[]陣列中.

於processPermissions裏, 先判斷目前的裝置版本是否大於等於API 23, 如果是, 而且還沒取得讀取SDCard的權限時, 就調用requestPermissions()方法彈出視窗詢問使用者是否接受.

詢問結果後, 會執行onRequestPermissionsResult()方法, 在此決定是否繼續下一步的動作

public class MainActivity extends AppCompatActivity {
    String []permitItems=new String[]{Manifest.permission.READ_EXTERNAL_STORAGE};
    int [] permitCodes={100};
    int permitIndex;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        processPermissions(0);
    }
    public void processPermissions(int index){
        permitIndex=index;
        if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.M){
            int hasPermission = checkSelfPermission(permitItems[index]);
            if (hasPermission != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(new String[]{permitItems[index]}, permitCodes[index]);
            }
        }
    }
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if(requestCode==permitCodes[permitIndex]){
            if(grantResults[0]==PackageManager.PERMISSION_GRANTED){
                Toast.makeText(this, "got permission", Toast.LENGTH_LONG).show();
            }
            else{
                Toast.makeText(this, "no permission", Toast.LENGTH_LONG).show();
            }
        }
        else {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }
}

背景Thread

ScanPhoto類別為背景Thread負責掃瞄SDCard之圖片縮圖, 使用FilenameFilter過濾 jpg, bmp, png, gif等圖片檔. scan方法中將取得的縮圖放入Common.list資料結構中

public class ScanPhoto extends Thread{
    Handler handler;
    FilenameFilter filter=new FilenameFilter() {
        private String []fileType={"jpg","bmp","png","gif"};
        @Override
        public boolean accept(File file, String fileName) {
            for(int i=0;i<fileType.length;i++){
                if(fileName.toLowerCase().indexOf(fileType[i])!=-1)return true;
            }
            return false;
        }
    };
    public void setHandler(Handler handler){
        this.handler=handler;
    }
    public ScanPhoto(Handler handler){
        this.handler=handler;
    }
    @Override
    public void run() {
        String path= Environment.getExternalStorageDirectory().getPath()+"/Pictures";
        Common.photoCount=0;
        readTree(new File(path));
        Common.scanProgress=0;
        scan(new File(path));
    }
    private void readTree(File file){ //遞回統計檔案數
        Common.photoCount+=file.listFiles(filter).length;
        handler.sendEmptyMessage(0);
        for(File f:file.listFiles()){
            if(f.isDirectory())readTree(f);
        }
    }
    private void scan(File file){//遞回讀取圖片
        for(File f:file.listFiles(filter)){
            HashMap<String, Object> hashMap=new HashMap<>();
            hashMap.put("file", f);

            BitmapFactory.Options options=new BitmapFactory.Options();
            options.inJustDecodeBounds=true;//設定只抓取寬高等相關資料, 不取圖片資料
            BitmapFactory.decodeFile(f.getAbsolutePath(), options);//開始抓取寬高相關資料,放入option
            int ratioW=Math.round(options.outWidth/200.0f);
            int ratioH=Math.round(options.outHeight/200.0f);
            int inSimpleSize=ratioW>ratioH ? ratioW:ratioH;
            options.inSampleSize=inSimpleSize;
            options.inJustDecodeBounds=false;
            hashMap.put("picture",(BitmapFactory.decodeFile(f.getAbsolutePath(), options)));
            Log.d("Thomas", f.getAbsolutePath());
            Common.list.add(hashMap);
            handler.sendEmptyMessage(1);
        }
        for(File f:file.listFiles()){
            if(f.isDirectory())scan(f);
        }
    }
}

檔案樹狀結構拜訪

上述readTree()及scan()使用遞回讀取檔案及統計檔案數量. 當取得圖片資料更改list後, 立即使用Handler發送Message, 通知MainActivity使用adapter.notifyDataSetChanged()通知要改變GridView的顯示內容

縮圖技巧

上述scan()方法中, 使用BitmapFactory.Options()取得option物件, 先把option.inJustDecodeBounds 設為true, 設定只抓取寬高值相關資料, 再用BitmapFactory.decodeFile()把寬高值放入option中.
最後設定option的inSimpleSize屬性, 再把inJustDecodeBounds關掉, 正式取得圖片資料

大圖示顯示方式

相機畫素超過1200M, 會造成圖檔過大, 顯示在螢幕會造成out of memory的錯誤. 所以可以使用上述縮圖的技巧, 設定只截取跟螢幕一樣大小的圖片即可.

BaseAdapter設計

以下同一般BaseAdapter設計, 覆寫getCount()及getItem()方法後, 再新增ViewHolder將要顯示的元件列出, 然後於getView()中將畫面元件物件化,  並設定圖片即可

public class PhotoAdapter extends BaseAdapter {
    int width;
    private LayoutInflater mInflater;
    public PhotoAdapter(Context context){
        this.mInflater = LayoutInflater.from(context);
        width=Common.screenWidth/3;
    }
    @Override
    public int getCount() {
        return Common.list.size();
    }
    @Override
    public Object getItem(int position) {
        return Common.list.get(position).get("picture");
    }
    @Override
    public long getItemId(int position) {
        return position;
    }
    @Override
    public View getView(int position, View convertView, ViewGroup viewGroup) {
        ViewHolder holder;
        if(convertView==null){
            holder= new ViewHolder();
            convertView = mInflater.inflate(R.layout.photo_layout, null);
            holder.photo=(ImageView)convertView.findViewById(R.id.photo);
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(width, width);
            holder.photo.setLayoutParams(params);
            convertView.setTag(holder);
        }
        else {
            holder = (ViewHolder)convertView.getTag();
        }
        holder.photo.setImageBitmap((Bitmap)Common.list.get(position).get("picture"));
        return convertView;
    }
    public final class ViewHolder{
        ImageView photo;
    }
}

完整程式碼下載

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *