本文围绕以下三个部分展开:
一、BaseAdapter 自定义适配器
二、一个案例
附 代码补充
一、BaseAdapter 自定义适配器
它是 Android 应用程序中经常用到的基础适配器,它的主要用途是将一组数据传到 ListView、Spinner、Gallery 及 GridView 等 UI 显示组件。
二、一个案例
案例说明:
读取手机的SD卡中的目录和文件(全部读出/过滤不允许读的文件和隐藏文件),并显示成上图的样子。
目录:显示出目录(文件夹)的图标、目录名、包含几个文件。文件:显示出文件的图标、文件名、文件大小。
点击右边的按钮,出现弹出菜单,里面有“复制”和“删除”两项。分别点击,会弹出提示信息。
1. 在 styles.xml(v21) 中设置标题栏颜色。
<item name="android:colorPrimaryDark">@android:color/holo_blue_dark</item> <item name="android:colorPrimary">@android:color/holo_blue_light</item> <item name="android:navigationBarColor">@android:color/transparent</item>
2. 在功能清单中授予读取SD卡的权限,这样,本APP才可以读取到手机SD卡。
<!-- 授予此App读取SD卡的权限 (注意大小写,不能写为:ANDROID.PERMISSION.READ_EXTERNAL_STORAGE)--> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
3. 启动后,会先启动MainAcitivity,然后加载主活动的布局文件(主界面)。
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 加载主活动的布局文件 setContentView(R.layout.activity_main); }
因此要写主界面 activity_main.xml :列表框(ListView)。
<!-- 主活动的布局文件:列表框--> <ListView android:id="@+id/listView" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
因为ListView里面也有布局文件,用来放图片、文件/目录名等控件,因此接下来写里面的布局文件。该布局文件中,有一些字符串,因此先在 strings.xml 中写需要的字符串。
<string name="filename">文件名</string> <string name="summary">文件/目录 概要信息</string> <string name="copy">复制</string> <string name="delete">删除</string> <string name="sd_card_error">读取SD卡出错</string>
然后再创建里面的布局文件:file_item.xml ,然后写布局。
<ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:src="@drawable/ic_file" android:contentDescription="@string/filename"/> <ImageButton android:id="@+id/btnOperator" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:src="@drawable/ic_more_vert_grey600_16dp" style="@android:style/Widget.DeviceDefault.Button.Borderless.Small"/> <TextView android:id="@+id/tvFilename" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignTop="@id/imageView" android:layout_marginLeft="18dp" android:layout_toRightOf="@id/imageView" android:text="@string/filename" android:textSize="16sp"/> <TextView android:id="@+id/tvSummary" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@id/tvFilename" android:layout_alignParentBottom="true" android:layout_alignStart="@id/tvFilename" android:text="@string/summary" android:textSize="12sp"/>
4. 判断SD卡状态,并读取SD卡内容。
(1)判断SD卡状态是否有效。如果有效,弹出提示“SD card is ok”;否则,弹出提示:“读取SD卡出错”。
// 获得当前设备的SD卡的状态:Environment.getExternalStorageState() // SD卡正在使用:Environment.MEDIA_MOUNTED if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { Toast.makeText(this,"SD card is ok",Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, getString(R.string.sd_card_error), Toast.LENGTH_SHORT).show(); }
然后在手机上部署,显示以下效果,说明SD卡正在使用。
(2)获得SD卡的根目录下的目录/文件列表。(此处获得的,是所有的目录和文件列表)
public class MainActivity extends Activity { private ListView listView; private File[] files; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listView = (ListView) findViewById(R.id.listView); // 获得当前设备的SD卡的状态:Environment.getExternalStorageState() // SD卡正在使用:Environment.MEDIA_MOUNTED if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { Toast.makeText(this,"SD card is ok",Toast.LENGTH_SHORT).show(); // 获得SD卡的根目录下的目录/文件 files = Environment.getExternalStorageDirectory().listFiles(); } else { Toast.makeText(this, getString(R.string.sd_card_error), Toast.LENGTH_SHORT).show(); } }
(3)先初始化根路径,然后通过创建文件过滤器,过滤不允许读的文件和隐藏文件,接着获得SD卡根目录下面允许读的文件/目录列表。
public class MainActivity extends Activity { private ListView listView; private File[] files; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listView = (ListView) findViewById(R.id.listView); // 获得当前设备的SD卡的状态:Environment.getExternalStorageState() // SD卡正在使用:Environment.MEDIA_MOUNTED if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { // 初始化根路径(/storage/emulated/0) File sdPath = Environment.getExternalStorageDirectory(); // 创建文件过滤器(过滤不允许读的文件/隐藏文件) SdFileFilter sdFileFilter = new SdFileFilter(); // 获得SD卡的根目录下的目录/文件(文件列表) files = sdPath.listFiles(sdFileFilter); } else { Toast.makeText(this, getString(R.string.sd_card_error), Toast.LENGTH_SHORT).show(); } }
其中,用到了SD卡文件过滤器 SdFileFilter ,因此需创建此文件过滤器。
package com.xiangdong.baseadapter; import java.io.File; import java.io.FileFilter; /** * SD Card文件过滤器 * Created by Xiangdong on 2015/6/4. */ public class SdFileFilter implements FileFilter { /** * 不允许读的文件/隐藏文件 不显示 */ @Override public boolean accept(File pathname) { if (pathname.isHidden() || !pathname.canRead()) { return false; } return true; } }
注意:以上只是获得了文件/目录列表,并放入了数组中,但并未进行填充。
5. 创建文件适配器 FileAdapter 类,继承自 BaseAdapter。然后将数据与该适配器关联,接着将布局与适配器关联。
public class MainActivity extends Activity { private ListView listView; private File[] files; private FileAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listView = (ListView) findViewById(R.id.listView); if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { File sdPath = Environment.getExternalStorageDirectory(); SdFileFilter sdFileFilter = new SdFileFilter(); files = sdPath.listFiles(sdFileFilter); // 数据(文件列表)显示的方式:数据与适配器关联 adapter = new FileAdapter(this, files); // 布局与适配器关联 listView.setAdapter(adapter); } else { Toast.makeText(this, getString(R.string.sd_card_error), Toast.LENGTH_SHORT).show(); } }
6. 进入 FileAdapter 类,写该文件适配器。
(1)创建构造方法。
/** * 自定义文件适配器类 * Created by Xiangdong on 2015/6/4. */ public class FileAdapter extends BaseAdapter { // 上下文 private Context context; // 数据(文件列表) private File[] files; // 布局填充器(加载布局文件) private LayoutInflater inflater; /** * 4.1 构造方法 * * @param context 上下文 * @param files 数据(文件列表) */ public FileAdapter(Context context, File[] files) { this.context = context; this.files = files; inflater = LayoutInflater.from(context); } }
(2)继承了 BaseAdapter 类,要重写(override)四个方法。
(2.1)重写 getCount() 方法。
/** * 获得数据的总数 * * @return 文件列表的长度 */ @Override public int getCount() { return files.length; }
(2.2)重写 getItem(int position) 方法。
/** * 获得特定位置的数据 * * @param position 位置 * @return 特定位置的数据 */ @Override public Object getItem(int position) { return files[position]; }
(2.3)重写 getItemId(int position) 方法。
/** * 获得特定位置数据的 id (使用SQLite的场景) * * @param position 位置 * @return 特定位置数据的 id */ @Override public long getItemId(int position) { // 位置是列表项在 ListView 中的索引 // 在排序规则改变时,位置的数值会改变 // id 是数据的唯一标识,不会改变 return 0; }
(2.4)重写 getView(int position, View convertView, ViewGroup parent) 方法。
/** * (2.4) 获得特定位置加载了数据的视图(列表项) * * @param position 位置 * @param convertView 可重用的视图 * @param parent 父元素 * @return 列表项 */ @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder = null; ImageButtonListener listener = null; if (convertView == null) { // 若无【可重用的视图】,才实例化 xml 文件创建视图 // inflate 方法执行的次数为屏幕上能显示的列表项的最大值 convertView = inflater.inflate(R.layout.file_item, parent, false); // 创建【结构持有者】,获得视图中的各个控件 holder = new ViewHolder(); holder.imageView = (ImageView) convertView.findViewById(R.id.imageView); holder.btnOperator = (ImageButton) convertView.findViewById(R.id.btnOperator); holder.tvFilename = (TextView) convertView.findViewById(R.id.tvFilename); holder.tvSummary = (TextView) convertView.findViewById(R.id.tvSummary); // 创建按钮的监听器 listener = new ImageButtonListener(); // 注册监听器 holder.btnOperator.setOnClickListener(listener); // 将监听器存储【绑定】到按钮中 holder.btnOperator.setTag(listener); // 将【结构持有者】存储到视图中 convertView.setTag(holder); } else { // 有【可重用的视图】,则从中取出它的【结构持有者】 holder = (ViewHolder) convertView.getTag(); // 获得按钮的监听器 listener = (ImageButtonListener) holder.btnOperator.getTag(); } // 在【结构持有者】中加载 position 位置的数据 // 获得的是根据位置取出的一个,返回的是一个文件 File file = files[position]; // 进行填充 holder.tvFilename.setText(file.getName()); holder.tvSummary.setText(file.isFile() ? String.format(context.getString(R.string.fileSize), file.length()) : String.format(context.getString(R.string.contents), file.listFiles().length)); holder.imageView.setImageResource(file.isFile() ? R.drawable.ic_file : R.drawable.ic_folder); // 修改监听器的监听的数据 listener.setData(file); // 返回视图 return convertView; }
注:上面在填充的时候,需要填充 “文件大小:%,d 字节”和“目录: %d 个字节”,因此把这两个字符串也写进 strings.xml 中。
<string name="fileSize">文件大小:%,d 字节</string> <string name="contents">目录: %d 个文件</string>
(2.4.1)列表项的【结构持有者】:ViewHolder 。
/** * (2.4.1)列表项的 结构持有者 * 存储 file_item.xml 文件中的控件结构 * 直接通过字段访问,为提高性能 */ private static class ViewHolder { ImageView imageView; ImageButton btnOperator; TextView tvFilename; TextView tvSummary; }
(2.4.2)自定义的 ImageButton 点击监听器:ImageButtonListener 。
/** * (2.4.2)FileAdapter 的内部类 * 自定义的 ImageButton 点击监听器 */ private class ImageButtonListener implements View.OnClickListener { // 点击时获得的数据(文件/目录) private File data; public void setData(File data) { this.data = data; } @Override public void onClick(View v) { // 创建弹出菜单 PopupMenu menu = new PopupMenu(context, v); // 加载菜单文件 menu.inflate(R.menu.popup); // 注册菜单选项事件 menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { String text = ""; switch (item.getItemId()) { case R.id.action_copy: // copyFile(); text = context.getString(R.string.copy); break; case R.id.action_delete: // deleteFile(); text = context.getString(R.string.delete); } text += ": " + data.getName(); Toast.makeText(context, text, Toast.LENGTH_SHORT).show(); return true; } }); // 显示菜单 menu.show(); } }
注:在 ImageButtonListener 中,这儿实现的是,在点击“复制”或“删除”后,弹出提示信息。也可以,通过调用 copyFile() 或者 deleteFile() 方法,然后通过流来实现文件/目录的复制和删除。
上面用到了弹出菜单 PopupMenu ,因此要创建弹出菜单: popup.xml。
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/action_copy" android:title="@string/copy"/> <item android:id="@+id/action_delete" android:title="@string/delete"/> </menu>
附 代码补充
项目目录结构如下:
(1)styles.xml(v21)
<?xml version="1.0" encoding="utf-8"?> <resources> <style name="AppTheme" parent="android:Theme.Material.Light"> <item name="android:colorPrimaryDark">@android:color/holo_blue_dark</item> <item name="android:colorPrimary">@android:color/holo_blue_light</item> <item name="android:navigationBarColor">@android:color/transparent</item> </style> </resources>
(2)AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.xiangdong.baseadapter" > <!--1. 授予此App读取SD卡的权限 (注意大小写,不能写为:ANDROID.PERMISSION.READ_EXTERNAL_STORAGE)--> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name=".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> </application> </manifest>
(3)strings.xml
<resources> <string name="app_name">BaseAdapter</string> <string name="action_settings">Settings</string> <string name="filename">文件名</string> <string name="summary">文件/目录 概要信息</string> <string name="copy">复制</string> <string name="delete">删除</string> <string name="sd_card_error">读取SD卡出错</string> <string name="fileSize">文件大小:%,d 字节</string> <string name="contents">目录: %d 个文件</string> </resources>
(4)activity_main.xml
<RelativeLayout 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:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity"> <!-- 2.主活动的布局文件:列表框--> <ListView android:id="@+id/listView" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </RelativeLayout>
(5)file_item.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="72dp" android:padding="@dimen/activity_horizontal_margin" android:descendantFocusability="blocksDescendants"> <ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:src="@drawable/ic_file" android:contentDescription="@string/filename"/> <ImageButton android:id="@+id/btnOperator" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:src="@drawable/ic_more_vert_grey600_16dp" style="@android:style/Widget.DeviceDefault.Button.Borderless.Small"/> <TextView android:id="@+id/tvFilename" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignTop="@id/imageView" android:layout_marginLeft="18dp" android:layout_toRightOf="@id/imageView" android:text="@string/filename" android:textSize="16sp"/> <TextView android:id="@+id/tvSummary" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@id/tvFilename" android:layout_alignParentBottom="true" android:layout_alignStart="@id/tvFilename" android:text="@string/summary" android:textSize="12sp"/> </RelativeLayout>
(6)MainActivity
package com.xiangdong.baseadapter; import android.app.Activity; import android.os.Bundle; import android.os.Environment; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.widget.ListView; import android.widget.Toast; import java.io.File; public class MainActivity extends Activity { private ListView listView; private File[] files; private FileAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 2.加载主活动的布局文件 setContentView(R.layout.activity_main); listView = (ListView) findViewById(R.id.listView); // 3.获得当前设备的SD卡的状态:Environment.getExternalStorageState() // SD卡正在使用:Environment.MEDIA_MOUNTED if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { //Toast.makeText(this,"SD card is ok",Toast.LENGTH_SHORT).show(); // 获得SD卡的根目录下的目录/文件 //files = Environment.getExternalStorageDirectory().listFiles(); // 3.1 初始化根路径(/storage/emulated/0) File sdPath = Environment.getExternalStorageDirectory(); // 3.2 创建文件过滤器(过滤不允许读的文件/隐藏文件) SdFileFilter sdFileFilter = new SdFileFilter(); // 3.3获得SD卡的根目录下的目录/文件(文件列表) files = sdPath.listFiles(sdFileFilter); // 4.数据(文件列表)显示的方式:数据与适配器关联 adapter = new FileAdapter(this, files); // 布局与适配器关联 listView.setAdapter(adapter); } else { Toast.makeText(this, getString(R.string.sd_card_error), Toast.LENGTH_SHORT).show(); } } //------------------------------------------------------------------------------------------- @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } }
(7)SdFileFilter
package com.xiangdong.baseadapter; import java.io.File; import java.io.FileFilter; /** * SD Card文件过滤器 * Created by Xiangdong on 2015/6/4. */ public class SdFileFilter implements FileFilter { /** * 不允许读的文件/隐藏文件 不显示 */ @Override public boolean accept(File pathname) { if (pathname.isHidden() || !pathname.canRead()) { return false; } return true; } }
(8)FileAdapter
package com.xiangdong.baseadapter; import android.content.Context; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.PopupMenu; import android.widget.TextView; import android.widget.Toast; import java.io.File; /** * 自定义文件适配器类 * Created by Xiangdong on 2015/6/4. */ public class FileAdapter extends BaseAdapter { // 上下文 private Context context; // 数据(文件列表) private File[] files; // 布局填充器(加载布局文件) private LayoutInflater inflater; /** * 4.1 构造方法 * * @param context 上下文 * @param files 数据(文件列表) */ public FileAdapter(Context context, File[] files) { this.context = context; this.files = files; inflater = LayoutInflater.from(context); } /** * 4.2.1 获得数据的总数 * * @return 文件列表的长度 */ @Override public int getCount() { return files.length; } /** * 4.2.2 获得特定位置的数据 * * @param position 位置 * @return 特定位置的数据 */ @Override public Object getItem(int position) { return files[position]; } /** * 4.2.3 获得特定位置数据的 id (使用SQLite的场景) * * @param position 位置 * @return 特定位置数据的 id */ @Override public long getItemId(int position) { // 位置是列表项在 ListView 中的索引 // 在排序规则改变时,位置的数值会改变 // id 是数据的唯一标识,不会改变 return 0; } /** * 4.3 获得特定位置加载了数据的视图(列表项) * * @param position 位置 * @param convertView 可重用的视图 * @param parent 父元素 * @return 列表项 */ @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder = null; ImageButtonListener listener = null; if (convertView == null) { // 若无【可重用的视图】,才实例化 xml 文件创建视图 // inflate 方法执行的次数为屏幕上能显示的列表项的最大值 convertView = inflater.inflate(R.layout.file_item, parent, false); // 创建【结构持有者】,获得视图中的各个控件的作用 holder = new ViewHolder(); holder.imageView = (ImageView) convertView.findViewById(R.id.imageView); holder.btnOperator = (ImageButton) convertView.findViewById(R.id.btnOperator); holder.tvFilename = (TextView) convertView.findViewById(R.id.tvFilename); holder.tvSummary = (TextView) convertView.findViewById(R.id.tvSummary); // 创建按钮的监听器 listener = new ImageButtonListener(); // 注册监听器 holder.btnOperator.setOnClickListener(listener); // 将监听器存储【绑定】到按钮中 holder.btnOperator.setTag(listener); // 将【结构持有者】存储到视图中 convertView.setTag(holder); } else { // 有【可重用的视图】,则从中取出它的【结构持有者】 holder = (ViewHolder) convertView.getTag(); // 获得按钮的监听器 listener = (ImageButtonListener) holder.btnOperator.getTag(); } // 在【结构持有者】中加载 position 位置的数据 File file = files[position]; holder.tvFilename.setText(file.getName()); holder.tvSummary.setText(file.isFile() ? String.format(context.getString(R.string.fileSize), file.length()) : String.format(context.getString(R.string.contents), file.listFiles().length)); holder.imageView.setImageResource(file.isFile() ? R.drawable.ic_file : R.drawable.ic_folder); // 修改监听器的监听的数据 listener.setData(file); // 返回视图 return convertView; } /** * 4.3.1 列表项的 结构持有者 * 存储 file_item.xml 文件中的控件结构 * 直接通过字段访问,为提高性能 */ private static class ViewHolder { ImageView imageView; ImageButton btnOperator; TextView tvFilename; TextView tvSummary; } /** * 4.3.2 FileAdapter 的内部类 * 自定义的 ImageButton 点击监听器 */ private class ImageButtonListener implements View.OnClickListener { // 点击时获得的数据(文件/目录) private File data; public void setData(File data) { this.data = data; } @Override public void onClick(View v) { // 创建弹出菜单 PopupMenu menu = new PopupMenu(context, v); // 加载菜单文件 menu.inflate(R.menu.popup); // 注册菜单选项事件 menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { String text = ""; switch (item.getItemId()) { case R.id.action_copy: // copyFile(); text = context.getString(R.string.copy); break; case R.id.action_delete: // deleteFile(); text = context.getString(R.string.delete); } text += ": " + data.getName(); Toast.makeText(context, text, Toast.LENGTH_SHORT).show(); return true; } }); // 显示菜单 menu.show(); } } }
(9)popup.xml
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/action_copy" android:title="@string/copy"/> <item android:id="@+id/action_delete" android:title="@string/delete"/> </menu>