您的位置:首页 > 移动开发

kotlin实现的简单个人账户管理APP(二) 文件选择浏览/文件导入导出

2017-12-22 10:37 399 查看
转载请注明出处:http://blog.csdn.net/a512337862/article/details/78870646

前言

1.本篇博客相关的项目介绍请参考基于kotlin实现的简单个人账户管理APP

2.本篇博客是介绍利用kotlin指定账户的导入文件/导出到文件。

3.因为本人是kotlin初学者,博客如果有任何问题请指出。

代码分析

FileExploreFragment

截图如下:



FileExploreFragment主要实现文件/文件夹的选择,并通过接口将选中的文件路径回调,相当于一个文件浏览选择器。这里是通过DialogFragment以Dialog的形式表现。代码如下:

/**
* Author : BlackHao
* Time : 2017/11/16 13:27
* Description : 文件选择框
*/
class FileExploreFragment : DialogFragment() {

//控件
private lateinit var backIb: ImageButton
private lateinit var fileListView: ListView
private lateinit var ensureExport: ImageButton
private lateinit var showTypeTv: TextView
//文件选择回调
var listener: FileExploreClickListener? = null
//导入导出标识
private var isImport = false
//当前路径
private lateinit var currentPath: String
private lateinit var originalPath: String
//文件列表adapter
private var adapter: FileListAdapter? = null
private var fileList: ArrayList<File>? = null

override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater?.inflate(R.layout.fragment_file_explore, container, false)
fileListView = view?.findViewById(R.id.file_list_lv) as ListView
backIb = view.findViewById(R.id.back_ib) as ImageButton
ensureExport = view.findViewById(R.id.ensure_ib) as ImageButton
showTypeTv = view.findViewById(R.id.show_im_ex_tv) as TextView
//设置原始路径
originalPath = Environment.getExternalStorageDirectory().absolutePath
fileList = arrayListOf()
adapter = FileListAdapter(fileList!!, activity)
fileListView.adapter = adapter
//点击事件
backIb.setOnClickListener {
if (currentPath == Environment.getExternalStorageDirectory().toString()) {
//已经是最开始的路径
//隐藏fragment
dismiss()
} else {
//显示父目录下文件
setFilePath(File(currentPath).parentFile.absolutePath)
}
}
fileListView.setOnItemClickListener { _, _, position, _ ->
val selectedFile = fileList!![position]
if (selectedFile.isDirectory) {
setFilePath(selectedFile.absolutePath)
} else {
//导入模式下,文件可选择
if (isImport) {
listener?.selectedFile(selectedFile, true)
}
}
}
ensureExport.setOnClickListener {
listener?.selectedFile(File(currentPath), false)
}
return view
}

override fun onResume() {
super.onResume()
setFilePath(originalPath)
isImport = tag == "Import"
if (isImport) {
ensureExport.visibility = View.GONE
showTypeTv.text = getString(R.string.select_import_file)
} else {
ensureExport.visibility = View.VISIBLE
showTypeTv.text = getString(R.string.select_export_folder)
}
}

/**
* 显示指定路径下的文件
*
* @param folderPath 文件夹路径
*/
private fun setFilePath(folderPath: String) {
currentPath = folderPath
val file = File(folderPath)
//文件存在并且是文件夹
if (file.exists() && file.isDirectory) {
val files = file.listFiles()
fileList?.clear()
files.filterNot { it.isHidden }.forEach { fileList?.add(it) }
adapter?.notifyDataSetChanged()
}
}
}


这里主要解释以下几点:

1.因为这里涉及到导入导出两种情况,导入需要选中文件,而导出则需要选择文件夹。这里以DialogFragment.show(FragmentManager manager, String tag)中的tag(Import/Export)来判断。主要在DialogFragment的onResume中来进行判断:



2.ensureExport是左上角的一个确定按钮,在导出到文件夹时,可以通过点击ensureExport来触发将文件夹路径回调。导入时只需选择文件即可,所以无需ensureExport。

3.originalPath则是原始路径,每次显示DialogFragment都会默认显示originalPath下文件,setFilePath方法是用来显示指定文件夹下的所有未隐藏文件,并通知adapter更新ListView:



4.回退按钮没有太多的可以介绍,主要的一点就是当返回到最原始的路径originalPath时,直接隐藏FileExploreFragment。



FileListAdapter

FileListAdapter是用来显示FileExploreFragment的文件列表,直接贴代码:

/**
* Author : BlackHao
* Time : 2017/11/16 14:07
* Description : 文件列表 Adapter
*/
class FileListAdapter(private var list: ArrayList<File>, private val context: Context) : BaseAdapter() {

override fun getItem(position: Int): Any = list[position]

override fun getItemId(position: Int): Long = position.toLong()

override fun getCount(): Int = list.size

override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val holder: ViewHolder
val file = list[position]
val view : View
if (convertView == null) {
view = View.inflate(context, R.layout.item_file_list, null)
holder = ViewHolder(view)
view.tag = holder
} else {
view = convertView
holder = view.tag as ViewHolder
}
if (file.isDirectory) {
holder.iconIv.setImageResource(R.drawable.dir)
} else {
if (file.name.endsWith(".txt")) {
holder.iconIv.setImageResource(R.drawable.file)
} else if (file.name.endsWith(".xls") || file.name.endsWith(".xlxs")) {
holder.iconIv.setImageResource(R.drawable.xls)
} else if (file.name.endsWith(".pdf")) {
holder.iconIv.setImageResource(R.drawable.pdf)
} else if (file.name.endsWith(".ppt") || file.name.endsWith(".pptx")) {
holder.iconIv.setImageResource(R.drawable.ppt)
} else if (file.name.endsWith(".doc") || file.name.endsWith(".docx")) {
holder.iconIv.setImageResource(R.drawable.doc)
} else if (file.name.endsWith(".png") || file.name.endsWith(".jpg")) {
holder.iconIv.setImageResource(R.drawable.photo)
}  else {
holder.iconIv.setImageResource(R.drawable.unknown_file)
}
}
holder.nameTv.text = file.name
return view
}

private inner class ViewHolder(view: View) {
var nameTv: TextView = view.findViewById(R.id.file_name_tv) as TextView
var iconIv: ImageView = view.findViewById(R.id.file_icon_iv) as ImageView
}
}


这里没什么可以讲的东西,唯一能提的就是通过文件类型显示不同的图标,截图如下:



导入文件解析

DecodeFileThread线程是用来进行解析文件,并将结果以ArrayList<AccountBean>的形式回调,直接贴代码:

/**
* Author : BlackHao
* Time : 2017/11/16 15:44
* Description : 解析文件
*/
class DecodeFileThread(filePath: String, callBack: AccountsCallBack) : Thread() {

val path: String = filePath
val callback = callBack

override fun run() {
super.run()
val br = BufferedReader(FileReader(path))
val list = arrayListOf<AccountBean>()
var bean = AccountBean()
var text = br.readLine()
while (text != null) {
when {text.isEmpty() -> {
addBeanToList(bean, list)
bean = AccountBean()
}
text.contains(":") -> {
splitData(":", text, bean)
}
text.contains(":") -> {
splitData(":", text, bean)
}
else -> {
bean.name = text
}
}
text = br.readLine()
}
//是否存在最后一个未保存
addBeanToList(bean, list)
callback.searchFinish(list)
}

//分理数据
private fun splitData(splitTag: String, splitData: String, saveData: AccountBean) {
//去掉空格
val texts = splitData.replace(" ", "").split(splitTag)
when {
texts[0].contains("备注") -> saveData.notes = texts[1].trim()
texts[0].contains("账户") -> saveData.account = texts[1].trim()
texts[0].contains("密码") -> saveData.psw = texts[1].trim()
else -> {
if (saveData.notes!!.isEmpty()) {
saveData.notes = saveData.notes + splitData
} else {
saveData.notes = saveData.notes + "\n" + splitData
}
}
}
}

//将实体类添加到list中
private fun addBeanToList(bean: AccountBean, list: ArrayList<AccountBean>) {
if (bean.account.isEmpty() && bean.name.isNotEmpty()) {
//当账户为空时,将账户设置为账户类型
bean.account = bean.name
list.add(bean)
} else if (bean.account.isNotEmpty()) {
list.add(bean)
}
}
}


这里导入的文件的格式进行了严格的限定,账户类型不能为空,且必须以空行作为两个账户分隔符,不然就会解析失败,大概的文件格式如下图所示:



这里简单分析一下部分代码:

在run()里面文件解析的代码是关键部分,截图如下:



这部分代码的思路就是:通过BufferedReader按行读取指定文件,读取完一行之后,通过判断该行:

1.不为空行的情况下,是否包含中英文的冒号,包含冒号则是“账户”/“密码”/“备注”之一,不包含则是账户类型。

2.空行则认为一个账户解析完成,开始解析下一个。

3.读完文件最后一行不为空行,也认为账户解析完成。

splitData()用于分离数据,即通过中英文冒号来将需要的信息分离出来,这里只是简单的认为冒号右边的数据为有效数据。另外,如果出现备注/账户/密码之外包含冒号的信息,则统一认为备注部分,该行数据直接添加到备注。截图如下:



文件导出

文件导出没有任何需要介绍的东西,就是将选中的账户信息写入文件,写完一个账户就以空行作为分隔符,代码一眼就能看懂,截图如下:



SelectImportActivity

文件导入,以及导出所有的逻辑全部在SelectImportActivity中实现,因为这两个功能除了几个文字不同,界面基本上全部一样,导入界面截图如下:



SelectImportActivity代码如下:

/**
* Author : BlackHao
* Time : 2017/11/16 15:15
* Description : 选择需要导入/导出账户信息
*/
class SelectImportActivity : BaseActivity(), AccountsCallBack {

private lateinit var loadFragment: LoadFragment
//导入实体类
private lateinit var importList: ArrayList<ImportBean>
//Adapter
private lateinit var adapter: ImportListAdapter
//控件
private lateinit var listView: ListView
private lateinit var selectAll: CheckBox
private lateinit var importBt: Button
private lateinit var titleTv: TextView
//导入/导出路径
private lateinit var path: String
//数据库操作类
private lateinit var dao: DbDao
//导入/导出
private var isImport = false

override fun initView() {
setContentView(R.layout.activity_import)
//获取数据库工具类
dao = (this.application as AccountApp).dao
loadFragment = LoadFragment()
loadFragment.show(supportFragmentManager, "Load")
//初始化控件
listView = findViewById(R.id.import_list_view) as ListView
importBt = findViewById(R.id.ensure_import) as Button
selectAll = findViewById(R.id.select_all_ib) as CheckBox
titleTv = findViewById(R.id.title) as TextView
//设置监听
selectAll.setOnCheckedChangeListener { _, checked ->
if (importList.size > 0) {
importList.forEach { it.isSelect = checked }
adapter.notifyDataSetChanged()
}
}
importBt.setOnClickListener {
loadFragment.show(supportFragmentManager, "import")
//开启线程更新数据库
Thread(Runnable {
if (isImport) {
//筛选选中的账户添加数据库
importList.filter { it.isSelect }.forEach { dao.addNewAccount(it.bean) }
} else {
//筛选选中的写入文件
val fos = FileOutputStream(path + File.separator + Constant.EXPORT_FILE_NAME)
importList.filter { it.isSelect }.forEach {
if (it.bean.name.isNotEmpty()) {
fos.write((it.bean.name + "\n").toByteArray())
}
if (it.bean.account.isNotEmpty()) {
fos.write(("账户 : " + it.bean.account + "\n").toByteArray())
}
if (it.bean.psw.isNotEmpty()) {
fos.write(("密码 : " + it.bean.psw + "\n").toByteArray())
}
if (it.bean.notes!!.isNotEmpty()) {
fos.write(("备注 : " + it.bean.notes + "\n").toByteArray())
}
//换行
fos.write("\n".toByteArray())
fos.flush()
}
//关闭流
fos.close()
}
//结束当前activity
setResult(200, intent)
finish()
}).start()

}
}

override fun initData() {
path = intent.getStringExtra("path")
isImport = intent.getBooleanExtra("isImport", false)
//初始化ListView相关
importList = arrayListOf()
adapter = ImportListAdapter(importList, this)
listView.adapter = adapter
if (isImport) {
importBt.text = getString(R.string.import_)
titleTv.text = getString(R.string.select_import)
//导入则解析文件
DecodeFileThread(path, this).start()
} else {
importBt.text = getString(R.string.export)
titleTv.text = getString(R.string.select_export)
//导出则获取数据库数据
SearchDataThread("", this, dao).start()
}
}

override fun searchFinish(list: ArrayList<AccountBean>) {
runOnUiThread {
//导入时去掉已经存在的账户
importList.clear()
list.forEach {
if (!dao.isAccountExist(it) || !isImport) {
importList.add(ImportBean(false, it))
}
}
adapter.notifyDataSetChanged()
loadFragment.dismiss()
}
}
}


简单分析一下代码:

1.在initData()中通过intent获取文件/文件夹路径,已经当前是导入/导出,并对应修改界面显示:



结语

1.因为文字功底有限,所以介绍性的文字不多,但是基本上每句代码都加了注释,理解起来应该不难,如果有任何问题,可以留言。

2.这里只附上了我认为比较关键的代码,布局文件等都未涉及,需要的可以去下载源码。

3.项目源码下载地址:http://download.csdn.net/download/a512337862/10151418
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息