上一篇《【Android】读取sdcard卡上的所有图片并且显示,读取的过程有进度条显示》(点击打开链接)在真机上测试很有问题,经常遇到内存溢出,卡死的情况。因为现在真机上的内存上,2G已经很少见了,基本上都8G的样子了。由于把读取出来的图片一次性地放到app上,而且读取的过程中,又没有正在读取到哪个文件,虽然能够在AVD安卓模拟器上完成基本的功能,但是这个app很不友好。因此采用Handler、Message配合线程等安卓消息机制,完成读取过程,并且利用GridView把读取到的图片显示出来。改进这个内存卡图片读取器,具体效果如下,基本是可以媲美部分安卓系统自带的图库了吧……就差个对文件夹的分类而已了……
一开始,在读取的过程,有读取进度的显示,具体读取到哪个文件夹。这对于大内存卡很有意义,我的8G内存卡亲测,仅30秒完成读取。
之后,用网格视图把图片显示出来。点击图片可以查看图片的大图,与图片的路径。
在app的右上角可以退出。
当然本app也完成对返回键的监听。返回键也能够退出。这个在《【Android】各式各样的弹出框与对菜单键、返回键的监听》(点击打开链接)已经说过了,这里不再赘述。
1、首先修改res\values\strings.xml,设置各个字符,如下。作者什么的,请忽略这些细节!
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">内存卡图片查看器</string>
<string name="menu_author">作者:yongh701</string>
<string name="menu_exit">退出</string>
<string name="img_description">大图</string>
<string name="view_button1">返回</string>
</resources>
2、之后修改菜单的字符文件res\menu\main.xml如下,一个配置到MainActivity.java当中,这个在《【Android】日期拾取器、时间拾取器与菜单》(
点击打开链接)已经说过了。这里不再赘述。
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/menu_exit"
android:title="@string/menu_exit"/>
<item android:title="@string/menu_author"/>
</menu>
3、随后完成AndroidManifest.xml的修改,要求系统对本app赋予SDCard的读写权限。同时,因为app查看大图要用另一个Activity,这里同时在这里注册查看大图的ViewActivity,该文件修改如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.sdcard_read_all"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="18" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- 要求向SDCard读取数据权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- 要求向SDCard写入数据权限 -->
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.sdcard_read_all.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.sdcard_read_all.ViewActivity"
android:label="@string/img_description" > <!-- 注册ViewActivity -->
</activity>
</application>
</manifest>
4、其实之后的过程,与《【Android】图片资源的访问与网格式图片浏览器》(
点击打开链接)是一样的。先修改MainActivity的布局文件res\layout\activity_main.xml,布置一个网格布局GridView,当然,同时布置一个TextView,用于在读取完成之前,显示读取信息,此文件修改之后如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="36sp" />
<GridView
android:id="@+id/gridView1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:numColumns="2" >
</GridView>
</LinearLayout>
5、同时在res\layout\新建一个查看大图的的ViewActivity的布局文件activity_view.xml,这里和《【Android】画廊式的图片浏览器,使用HorizontalScrollView取代Gallery,OnClickListener的参数传递》(
点击打开链接)同理,只不过是在一个垂直滚动布局下,新建一个线性子布局LinearLayout。这样就不用担心组件的超过一屏,可以达到滚动效果。线性布局的滚动条就是这样做的。三个组件皆沾满屏幕宽度,同时分别赋予id,一会儿通过ViewActivity.java赋予相应的值。其中图片视图ImageView配置的adjustViewBounds与scaleType,让其自动缩放大小。android:contentDescription="@string/img_description"是图片的描述,不赋予,Eclipse会出现警告。
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:id="@+id/textView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24sp" />
<ImageView
android:id="@+id/imageView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="centerInside"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:contentDescription="@string/img_description" />
<Button
android:id="@+id/button1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/view_button1"
android:textSize="24sp" />
</LinearLayout>
</ScrollView>
6、在src\当前工程包下(这里是com.sdcard_read_all)中,如《【Android】多个Activity之间利用bundle传递数值》(点击打开链接)一样,新建一个继承android.app.Activity的ViewActivity.java。里面代码如下:
package com.sdcard_read_all;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
public class ViewActivity extends Activity {
private TextView textView1;
private ImageView imageView1;
private Button button1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_view);
//获取组件
textView1 = (TextView) findViewById(R.id.textView1);
imageView1 = (ImageView) findViewById(R.id.imageView1);
button1 = (Button) findViewById(R.id.button1);
//得到MainActivity传递过来的图片路径,进行相应的处理。
Intent intent = getIntent();
Bundle bundle = intent.getExtras();
String imgPath = bundle.getString("imgPath");
textView1.setText("图片路径:" + imgPath);
Bitmap bm = BitmapFactory.decodeFile(imgPath);
imageView1.setImageBitmap(bm);
//按钮的返回事件
button1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
finish();
}
});
}
// 对物理按钮的监听
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_BACK:
finish();// 关闭这个Activity。
break;
}
return super.onKeyDown(keyCode, event);
}
}
7、完成了查看大图ViewActivity之后,便是我们的压轴大戏MainActivity,MainActivity也是整个程序的核心,代码如下。代码在结构上分为五部分:1、遍历sdcard的方法,2、加载图片的缓存工具方法,这两个方法与《【Android】读取sdcard卡上的所有图片并且显示,读取的过程有进度条显示》(
点击打开链接)是同样的。3、程序的执行onCreate方法,4、app的菜单,这《【Android】日期拾取器、时间拾取器与菜单》(
点击打开链接)已经说过了。5、对物理按键的监听,这可以参考《【Android】各式各样的弹出框与对菜单键、返回键的监听》(
点击打开链接)。
程序的入口与其它安卓程序同样为onCreate方法。程序的执行将会分成两部分:
1、读取时候,开一条读取进行,利用安卓的线程消息传递机制,不停在更新textView1。这与《【Android】进度条与线程之间的消息处理》(点击打开链接)其实是同理。只能这样完成更新,通过安卓的线程消息传递机制,不至于app会出现死机的线程。安卓系统会自动对程序上的线程进行资源良好调度与分配。之前的程序就因为对线程的处理不好,导致读大内存卡会卡死。现在通过安卓的线程消息传递机制读多大的内存卡也是没事的!
2、读取完毕,与《【Android】图片资源的访问与网格式图片浏览器》(点击打开链接)同样,利用适配器与网格视图,把所有图片显示。这里由于网格视图自带的适配器是自动会分配好资源的,因此占用系统资源也不大的。同时对网格视图赋予监听,点击图片传递图片路径,并打开查看大图ViewActivity。
package com.sdcard_read_all;
import java.io.File;
import java.util.ArrayList;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.TextView;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
public class MainActivity extends Activity {
private TextView textView1;
private GridView gridView1;
private static Handler handler;// 线程消息处理器
// 用于存放sdcard卡上的所有图片路径
public static ArrayList<String> dirAllStrArr = new ArrayList<String>();
// 用于遍历sdcard卡上所有文件的类
public static void DirAll(File dirFile) throws Exception {
if (dirFile.exists()) {
File files[] = dirFile.listFiles();
for (File file : files) {
String fileName = file.getName();
String filePath = file.getPath();
// 读取文件路径,作为信息发送给handler进行处理
Message msg = new Message();
msg.obj = "正在读取:" + filePath;
handler.sendMessage(msg);
if (file.isDirectory()) {
// 除sdcard上Android这个文件夹以外。
if (!fileName.endsWith("Android")) {
// 如果遇到文件夹则递归调用。
DirAll(file);
}
} else {
// 如果是图片文件压入数组
if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")
|| fileName.endsWith(".bmp")
|| fileName.endsWith(".gif")
|| fileName.endsWith(".png")) {
if (dirFile.getPath().endsWith(File.separator)) {
dirAllStrArr
.add(dirFile.getPath() + file.getName());
} else {
dirAllStrArr.add(dirFile.getPath() + File.separator
+ file.getName());
}
}
}
}
}
}
// 图片加载的缓存工具类,利用安卓自带的方法,根据大小压缩图片
public static BitmapFactory.Options getHeapOpts(File file) {
BitmapFactory.Options opts = new BitmapFactory.Options();
// 数字越大读出的图片占用的内存必须越小,不然总是溢出
if (file.length() < 20480) { // 0-20k
opts.inSampleSize = 1;// 这里意为缩放的大小 ,数字越多缩放得越厉害
} else if (file.length() < 51200) { // 20-50k
opts.inSampleSize = 2;
} else if (file.length() < 307200) { // 50-300k
opts.inSampleSize = 4;
} else if (file.length() < 819200) { // 300-800k
opts.inSampleSize = 6;
} else if (file.length() < 1048576) { // 800-1024k
opts.inSampleSize = 8;
} else {
opts.inSampleSize = 10;
}
return opts;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取组件
textView1 = (TextView) findViewById(R.id.textView1);
gridView1 = (GridView) findViewById(R.id.gridView1);
// 线程开始之前,先把网格视图因此
gridView1.setVisibility(View.INVISIBLE);
/* 遍历sdcard旗下的所有文件夹的线程开始 */
Thread readSdcard = new Thread() {
private String sdpath = Environment.getExternalStorageDirectory()
.getAbsolutePath();// 获取sdcard的根路径
private File dirFile = new File(sdpath);
public void run() {
try {
DirAll(dirFile);// 遍历sdcard旗下的所有文件夹
// 完成之后,在线程自身的消亡之前,发送一条“0”的标志信息,给handler
Message msg = new Message();
msg.obj = "0";
handler.sendMessage(msg);
} catch (Exception e) {
e.printStackTrace();
}
};
};
textView1.setVisibility(View.VISIBLE);
readSdcard.start();// 线程开始,默认不开始。
/* 遍历sdcard旗下的所有文件夹结束 */
// 不停在接受线性的消息,根据消息,进行处理
// 随着线程readSdcard的开始而出现,随其死亡而消亡,与线程是一对的。
handler = new Handler(new Handler.Callback() {// 这样写,就不弹出什么泄漏的警告了
@Override
public boolean handleMessage(Message msg) {
textView1.setText(msg.obj + "");
if (msg.obj.equals("0")) {// 完成读取之后
textView1.setVisibility(View.GONE);// 隐藏读取信息
gridView1.setVisibility(View.VISIBLE);// 显示网格视图
}
return false;
}
});
// 网格视图适配器
BaseAdapter baseAdapter = new BaseAdapter() {
@Override
public View getView(int position, View convertView, ViewGroup arg2) {
ImageView imageView1;
if (convertView == null) {
imageView1 = new ImageView(MainActivity.this);
imageView1.setAdjustViewBounds(true);// 自动缩放为宽高比
imageView1.setScaleType(ScaleType.CENTER_INSIDE);// 设置图片保持宽高比显示
imageView1.setPadding(5, 5, 5, 5);
} else {
imageView1 = (ImageView) convertView;
}
// 把图片加载到网格视图
String filePath = dirAllStrArr.get(position);
File file = new File(filePath);
Bitmap bm = BitmapFactory.decodeFile(filePath,
getHeapOpts(file));
imageView1.setImageBitmap(bm);
return imageView1;
}
// 获取当前选项
@Override
public long getItemId(int position) {
return position;
}
@Override
public Object getItem(int position) {
return position;
}
// 获取数量
@Override
public int getCount() {
return dirAllStrArr.size();
}
};
gridView1.setAdapter(baseAdapter);// 把适配器与网格视图链接起来
gridView1.setOnItemClickListener(new OnItemClickListener() {// 点击网格组件的任意一张图片时候的事件
@Override
public void onItemClick(AdapterView<?> arg0, View arg1,
int position,// position为点击的id
long arg3) {
Intent intent = new Intent(MainActivity.this,
ViewActivity.class);// 激活ViewActivity
Bundle bundle = new Bundle();
bundle.putString("imgPath", dirAllStrArr.get(position));// 传递点击的图片的id到ViewActivity
intent.putExtras(bundle);
startActivity(intent);
}
});
}
// 创建menu的方法,没有该方法,不会在右上角设置菜单。
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// 设置menu界面为res\menu\menu.xml
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
// 处理菜单事件
public boolean onOptionsItemSelected(MenuItem item) {
// 得到当前选中的MenuItem的ID,
int item_id = item.getItemId();
switch (item_id) {
// 设置id为menu_exit的菜单子项所要执行的方法。
case R.id.menu_exit:
System.exit(0);// 结束程序
break;
}
return true;
}
// 对物理按钮的监听
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_BACK:
finish();// 关闭这个Activity。
break;
}
return super.onKeyDown(keyCode, event);
}
}
整个制作过程如上所示了,我还上传了一份Ecllipse,在ADT的编写源码,大家有兴趣可以下载来看一下:
安卓读取SD卡上的所有图片(点击打开链接)