文本阅读器,区别于小说阅读器,不能读取大容量的文件。文件容量过大,就会导致读取的速度变得很慢。
读取一个文本文件需要使用BufferedReader + FileReader来逐行读取内容。
还需要准备一个滚动面板配合TextView来显示内容。
如果文件比较大,读取时间会比较长,就需要使用ProgressDialog来建立进度条,提示用户当前正在加载数据。
注:这里读取文件的必须要转码;因为Android的底层是Linux开发,而Linux默认编码是“UTF-8”,而windows建立的文本文件默认是“简体中文(GBK)”,所以Android中打开的文本文件通常需要转码。 InputStreamReader(new FileInputStream(file),"GBK");
首先,我们看下整体的实现效果:
注:这里设置只显示了SD卡中的文件夹以及文本文件,这是通过扩展名的判断实现的
实行步骤:第一步:
一、创建activity_main布局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <TextView android:id="@+id/title_text" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="center_vertical" android:text="当前位置: /mnt/sdcard" android:textSize="14sp" /> <ListView android:id="@+id/list" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="8" android:cacheColorHint="#00000000" > </ListView> </LinearLayout>
二、创建给列表添加数据的file_line.xml布局:两个TextView,一个是显示文件夹图片的,一个事显示文件夹名字的
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <TextView android:id="@+id/file_img" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" /> <TextView android:id="@+id/file_name" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="4" android:textSize="14sp" /> </LinearLayout>
三、创建显示文本的activity
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ffffff" > <TextView android:id="@+id/detail" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="#000000" android:textSize="14sp" /> </ScrollView>
四、布局写完后,下面是老样子,先编写一个Globals的公共类。
用来动态计算手机的高度和宽度,等比放大缩小。还有初始化所有扩展名和图片的对应关系
import java.util.HashMap; import java.util.Map; import org.liky.txt.R; import android.app.Activity; public class Globals { public static int SCREEN_WIDTH; public static int SCREEN_HEIGHT; // 建立一个Map集合, 里面封装了所有扩展名对应的图标图片, 以便进行文件图标的显示 public static Map<String, Integer> allIconImgs = new HashMap<String, Integer>(); public static void init(Activity a) { SCREEN_WIDTH = a.getWindowManager().getDefaultDisplay().getWidth(); SCREEN_HEIGHT = a.getWindowManager().getDefaultDisplay().getHeight(); // 初始化所有扩展名和图片的对应关系 allIconImgs.put("txt", R.drawable.txt_file); allIconImgs.put("mp3", R.drawable.mp3_file); allIconImgs.put("mp4", R.drawable.mp4_file); allIconImgs.put("bmp", R.drawable.image_file); allIconImgs.put("gif", R.drawable.image_file); allIconImgs.put("png", R.drawable.image_file); allIconImgs.put("jpg", R.drawable.image_file); allIconImgs.put("dir_open", R.drawable.open_dir); allIconImgs.put("dir_close", R.drawable.close_dir); } }
五、创建一个自定义FileAdapter,用于读取,显示界面的列表中的数据,包括通过扩展名取得的图片和文件名:
import java.util.ArrayList; import java.util.List; import java.util.Map; import org.liky.txt.R; import org.liky.txt.util.Globals; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView.LayoutParams; import android.widget.BaseAdapter; import android.widget.TextView; public class FileAdapter extends BaseAdapter { private Context ctx; private List<Map<String, Object>> allValues = new ArrayList<Map<String, Object>>(); public FileAdapter(Context ctx, List<Map<String, Object>> allValues) { this.ctx = ctx; this.allValues = allValues; } @Override public int getCount() { return allValues.size(); } @Override public Object getItem(int arg0) { return allValues.get(arg0); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = LayoutInflater.from(ctx).inflate(R.layout.file_line, null); // 设置高度 convertView.setLayoutParams(new LayoutParams( LayoutParams.MATCH_PARENT, Globals.SCREEN_HEIGHT / 9)); } // 取得组件 TextView fileImg = (TextView) convertView.findViewById(R.id.file_img); fileImg.getLayoutParams().height = Globals.SCREEN_HEIGHT / 9; TextView fileName = (TextView) convertView.findViewById(R.id.file_name); // 取得数据,设置到组件里 Map<String, Object> map = allValues.get(position); // 设置内容, 文字 fileName.setText(map.get("fileName").toString()); // 图片要根据扩展名取得 String extName = map.get("extName").toString(); // 取得图片的id int imgId = Globals.allIconImgs.get(extName); // 设置图片 fileImg.setBackgroundResource(imgId); return convertView; } }
五、创建mainActivity,显示的是activity_main.xml,这中包含一个点击事件,一个退出按钮的监听,还有对扩展名的截取,只显示文件夹和文本文件
package org.liky.txt; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.liky.txt.adapter.FileAdapter; import org.liky.txt.util.Globals; import android.app.Activity; import android.app.AlertDialog.Builder; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.os.Bundle; import android.os.Environment; import android.view.KeyEvent; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemLongClickListener; import android.widget.ListView; import android.widget.TextView; public class MainActivity extends Activity { private TextView titleText; private ListView list; private FileAdapter adapter; private List<Map<String, Object>> allValues = new ArrayList<Map<String, Object>>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Globals.init(this); setContentView(R.layout.activity_main); // 取得组件 titleText = (TextView) findViewById(R.id.title_text); list = (ListView) findViewById(R.id.list); // 准备数据 // 取得SD卡根目录 File root = Environment.getExternalStorageDirectory(); loadFileData(root); // 建立Adapter adapter = new FileAdapter(this, allValues); list.setAdapter(adapter); // 加入监听事件 list.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) { // 取得当前操作的数据 Map<String, Object> map = allValues.get(arg2); // 判断所点的是文件还是文件夹 boolean dirFlag = (Boolean) map.get("dirFlag"); if (dirFlag) { // 文件夹 // 建立该文件夹的File对象 // 取得绝对路径 String fullPath = (String) map.get("fullPath"); // 建立File File dir = new File(fullPath); // 先清空原有数据 allValues.clear(); if (!Environment.getExternalStorageDirectory() .getAbsolutePath().equals(fullPath)) { // 加入返回上一级的操作行 Map<String, Object> parent = new HashMap<String, Object>(); parent.put("fileName", "返回上一级"); parent.put("extName", "dir_open"); parent.put("dirFlag", true); parent.put("fullPath", dir.getParent()); // 保存一个标志 parent.put("flag", "TRUE"); // 将这一行加入到数据集合中 allValues.add(parent); } // 加入新数据 loadFileData(dir); // 使用Adapter通知界面ListView,数据已经被修改了,你也要一起改 adapter.notifyDataSetChanged(); } else { // 弹出该文件的详细信息 File f = new File((String) map.get("fullPath")); // 切换界面 Intent in = new Intent(MainActivity.this, DetailActivity.class); in.putExtra("fullPath", map.get("fullPath").toString()); startActivity(in); } } }); list.setOnItemLongClickListener(new OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> arg0, View arg1, final int arg2, long arg3) { // 取得数据 Map<String, Object> map = allValues.get(arg2); final File f = new File(map.get("fullPath").toString()); if (f.isFile()) { // 弹出确认框 Builder builder = new Builder(MainActivity.this); builder.setTitle("提示"); builder.setMessage("确定要删除该文件(" + f.getName() + ")吗?"); builder.setPositiveButton("确定", new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // 将SD卡中的文件删除 if (f.exists()) { f.delete(); } // 将列表中的数据删除 allValues.remove(arg2); // 通知 adapter.notifyDataSetChanged(); } }); builder.setNegativeButton("取消", new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { } }); builder.create().show(); } return false; } }); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { // 根据keyCode判断用户按下了哪个键 if (keyCode == KeyEvent.KEYCODE_BACK) { // 判断当前是否在SD卡跟目录. // 取得第一行数据 Map<String, Object> map = allValues.get(0); if ("TRUE".equals(map.get("flag"))) { // 里面,需要返回上一级 list.performItemClick(list.getChildAt(0), 0, list.getChildAt(0) .getId()); } else { // 弹出提示框 Builder builder = new Builder(MainActivity.this); builder.setTitle("提示"); builder.setMessage("亲,真的要离开我吗?"); builder.setPositiveButton("真的", new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // 关闭当前Activity finish(); } }); builder.setNegativeButton("再待会儿", new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { } }); builder.create().show(); } return false; } return super.onKeyDown(keyCode, event); } private void loadFileData(File dir) { // 列出该目录下的所有文件 File[] allFiles = dir.listFiles(); // 设置当前位置的提示信息 titleText.setText("当前位置: " + dir.getAbsolutePath()); // 判断 if (allFiles != null) { // 循环 for (int i = 0; i < allFiles.length; i++) { File f = allFiles[i]; Map<String, Object> map = new HashMap<String, Object>(); map.put("fileName", f.getName()); // 多保存一个文件的绝对路径,方便在进行点击时使用 map.put("fullPath", f.getAbsolutePath()); // 判断是文件夹还是文件 if (f.isDirectory()) { // 是文件夹 map.put("extName", "dir_close"); map.put("dirFlag", true); } else { // 是文件 // 截取出扩展名 String extName = f.getName() .substring(f.getName().lastIndexOf(".") + 1) .toLowerCase(); map.put("extName", extName); map.put("dirFlag", false); } // 只有文件夹或文本文件才要显示. if (f.isDirectory() || "txt".equals(map.get("extName"))) { allValues.add(map); } } } } }
六、建立一个DetailActivity的用以读取文本文件,和显示进度条。使用的是ProgressDialog来完成进度条的显示,但同时要加入线程操作。
需要建立一个Handler类,来处理消息接收的功能。
注:测试时提示错误,ANdroid只允许主线程对UI界面显示的内容进行修改,子线程不允许修改界面。
但这里必须在子线程对界面进行修改,这种情况Android是通过消息通道机制来解决的。
package org.liky.txt; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.InputStreamReader; import android.app.Activity; import android.app.ProgressDialog; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.widget.TextView; public class DetailActivity extends Activity { private TextView detail; // 声明这个Handler类 private Handler handler; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_detail); detail = (TextView) findViewById(R.id.detail); // 建立这个handler对象,并覆写handleMessage方法,该方法会在接收到子线程的消息时自动执行,进行界面修改 handler = new Handler() { @Override public void handleMessage(Message msg) { // msg就是子线程发送过来的消息 // 修改text内容 detail.setText(msg.obj.toString()); } }; // 建立进度条 final ProgressDialog dialog = new ProgressDialog(this); // 设置为水平进度条 dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); dialog.setTitle("提示"); dialog.setMessage("正在加载数据, 请稍候..."); // 设置最大值 File f = new File(getIntent().getStringExtra("fullPath")); dialog.setMax((int) f.length()); dialog.show(); Thread t = new Thread() { @Override public void run() { // 接收参数,并根据参数建立文件对象 File f = new File(getIntent().getStringExtra("fullPath")); // 使用IO流读取 try { BufferedReader reader = new BufferedReader( new InputStreamReader(new FileInputStream(f), "GBK")); String line = null; StringBuilder builder = new StringBuilder(); // 定义保存读取内容大小的变量 int size = 0; while ((line = reader.readLine()) != null) { builder.append(line + "\r\n"); // 当读取了一部分内容后,根据读取内容的大小,然后增长进度 size += line.getBytes().length; // 每增加超过10k的内容,就改变一次进度条 if (size >= 10240) { dialog.incrementProgressBy(size); size = 0; // 睡眠一段时间 Thread.sleep(5); } } reader.close(); // detail.setText(builder.toString()); Message msg = new Message(); // 将builder中的内容设置到msg里 msg.obj = builder; // 发送消息 handler.sendMessage(msg); // 关闭进度条 dialog.dismiss(); } catch (Exception e) { e.printStackTrace(); } } }; t.start(); } }
Android中经典的两句话:不允许主线程联网,不允许子线程更改界面;主线程要联网必须通过子线程,子线程要修改界面,必须告知主线程。