实现 android 程序导出数据到 Excel 表格

我学会了很多东西,勤俭 隐忍 坚持 淡然 息事宁人,却没学会享受生活。


数据如何输出到 Excel 表格中?如下。

先看下生成的Excel效果:




本demo地址 https://github.com/yuqianglianshou/ExportExcelFile
以VSCode方式打开代码 https://github1s.com/yuqianglianshou/ExportExcelFile

jxl && POI
jxl是一个韩国人写的java操作excel的工具,是一个开源的Java Excel API项目,通过Jxl,Java可以很方便的操作微软的Excel文档。除了Jxl之外,  
还有Apache的一个POI项目,也可以操作Excel,两者相比之下:Jxl使用方便,但功能相对POI比较弱,很多时候,一个软件应用程序需要生成  
Microsoft Excel文件格式的报告。有时,一个应用程序甚至希望将Excel文件作为输入数据。例如,一个公司开发的应用程序将财务部门需要所有输出生成自己的Excel。  
Apache POI是一种流行的API,它允许程序员使用Java程序创建,修改和显示MS Office文件。这由Apache软件基金会开发使用Java分布式设计或修改  
Microsoft Office文件的开源库。它包含类和方法对用户输入数据或文件到MS Office文档进行解码。

更多关于jxl的相关方法可以参考文档:
http://jexcelapi.sourceforge.net/resources/javadocs/current/docs/
poi的相关内容:
https://poi.apache.org/

整体代码结构:



首先下载并导入 jxl.jar 包
jxl.jar包的下载地址:http://jexcelapi.sourceforge.net/ 可以到我的项目工程中直接拷贝。




布局文件 activity_main.xml:



<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="导出"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.229" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:padding="20dp"
        android:text="路径显示区"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btn"
        app:layout_constraintVertical_bias="0.136" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.kt:



package com.moon.exportexcelfile

import android.os.Bundle
import android.os.Environment
import android.util.Log
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import java.io.File
import java.util.*

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //显示存储路径
        val textView = findViewById<TextView>(R.id.textView)
        //导出按键
        findViewById<Button>(R.id.btn).setOnClickListener {
            val file = creatFile()
            ExcelUtils.writeDataToExcel(file, getData())
            Toast.makeText(this, "创建成功", Toast.LENGTH_SHORT).show()
            Log.i("lq ", " ok")
            textView.text = "文件目录为  " + file.absolutePath
        }
    }
    /**
     * 创建一个本地存储路径
     * /storage/emulated/0/Android/data/com.moon.exportexcelfile/files/Documents/export.xls
     */
    private fun creatFile(): File {
        val file =
            File(this.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)?.absolutePath + File.separator + "export.xls")

        Log.i("lq ", " file == " + file.absolutePath)
        return file
    }
    /**
     * 生成 20行8列 数据,第一行为表头
     */
    private fun getData(): ArrayList<ArrayList<String>> {
        val list = ArrayList<ArrayList<String>>()
        for (j in 1..20) {
            val listRow = ArrayList<String>()
            for (i in 1..8) {
                if (j == 1) {
                    //添加表头
                    listRow.add("表头 $i ")
                } else {
                    listRow.add("$j 行-$i 列")
                }
            }
            list.add(listRow)
        }
        Log.i("lq ", " list == $list")
        return list
    }

}

工具类 ExcelUtils.kt:



package com.moon.exportexcelfile

import android.util.Log
import jxl.Workbook
import jxl.write.Label
import jxl.write.WritableCellFormat
import jxl.write.WritableFont
import java.io.File

/**
 *
 *@author : lq
 *@date   : 2021/6/17
 *@desc   : 在指定全路径的 Excel表中写入数据
 *
 */
object ExcelUtils {
    //表头样式
    private lateinit var tableFormat: WritableCellFormat
    init {
        Log.i("lq ", "ExcelUtils init")
        format()
    }
    /**
     * excel 样式设置
     */
    private fun format() {
        val tableFont1 = WritableFont(WritableFont.ARIAL, 11, WritableFont.BOLD)//11号字,加粗
//        tableFont1.colour = jxl.format.Colour.LIGHT_BLUE // 可设置颜色

        tableFormat = WritableCellFormat(tableFont1)
        tableFormat.alignment = jxl.format.Alignment.CENTRE // 表格数据居中显示
        tableFormat.setBorder(jxl.format.Border.ALL, jxl.format.BorderLineStyle.THIN)
        tableFormat.setBackground(jxl.format.Colour.GRAY_25)//表格添加背景颜色
    }

    /**
     * 写入数据到Excel中
     * file  所创建的Excel文件  /storage/emulated/0/Android/data/com.moon.exportexcelfile/files/Documents/export.xls
     * list  数据集合 j 行 i 列
     */
    fun writeDataToExcel(file: File, list: ArrayList<ArrayList<String>>) {

        if (list.isEmpty() || list.size == 0) {
            return
        }
        if (file.exists()) {
            Log.i("lq ", " 文件已经存在,删除重建")
            file.delete()
        }
        file.createNewFile()
        var workbook = Workbook.createWorkbook(file)
        try {
            //一张 execl 表可以创建很多表单(sheet),默认有3个,这里使用第一张表单创建数据
            val sheet = workbook.createSheet("表单1", 0)

            for (j in list.indices) {

                var listRow = list[j]

                for (i in listRow.indices) {
                    if (j == 0) {
                        //为表头添加样式
                        sheet.addCell(Label(i, j, listRow[i], tableFormat))
                    } else {
                        sheet.addCell(Label(i, j, listRow[i]))
                    }

                    //根据数据长度设置合适列宽
                    if (listRow[i].length <= 5) {
                        sheet.setColumnView(i, listRow[i].length + 8)
                    } else {
                        sheet.setColumnView(i, listRow[i].length + 5)
                    }
                }

                //设置行高
                sheet.setRowView(j, 350)

            }
            workbook.write()
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            workbook?.close()
        }
    }

}

注意:需要添加文件写入权限:

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

运行截图:



点击导出后:



后台log:



手机文件管理器找到文件:



wps 打开 Excel :



0618 新增点击导出直接调用程序打开生成的 excel 表格功能,不再需要去文件管理器自己寻找。

AndroidManifest.xml 中 添加代码

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths" />
        </provider>

资源文件夹(res)下创建 xml 目录,创建 provider_paths.xml 文件,内容如下

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path
        name="external_files"
        path="." />
</paths>

新增 FileUtils.kt 工具类,代码如下

package com.moon.exportexcelfile

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.core.content.FileProvider
import java.io.File
import java.util.*

/**
 *
 *@author : lq
 *@date   : 5/10/21
 *@desc   : 文件打开工具类
 *
 */
class FileUtils {

    private var mContext: Context

    constructor(mContext: Context) {
        this.mContext = mContext
    }

    /**声明各种类型文件的dataType */
    private val DATA_TYPE_ALL = "*/*" //未指定明确的文件类型,不能使用精确类型的工具打开,需要用户选择

    private val DATA_TYPE_APK = "application/vnd.android.package-archive"
    private val DATA_TYPE_VIDEO = "video/*"
    private val DATA_TYPE_AUDIO = "audio/*"
    private val DATA_TYPE_HTML = "text/html"
    private val DATA_TYPE_IMAGE = "image/*"
    private val DATA_TYPE_PPT = "application/vnd.ms-powerpoint"
    private val DATA_TYPE_EXCEL = "application/vnd.ms-excel"
    private val DATA_TYPE_WORD = "application/msword"
    private val DATA_TYPE_CHM = "application/x-chm"
    private val DATA_TYPE_TXT = "text/plain"
    private val DATA_TYPE_PDF = "application/pdf"

    /**
     * 产生除了视频、音频、网页文件外,打开其他类型文件的Intent
     * @param filePath 文件路径
     * @param dataType 文件类型
     * @return
     */
    private fun generateCommonIntent(
        filePath: String,
        dataType: String
    ): Intent? {
        val intent = Intent()
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        intent.setAction(Intent.ACTION_VIEW)
        val file = File(filePath)
        val uri = getUri(intent, file)
        intent.setDataAndType(uri, dataType)
        return intent
    }


    /**
     * 产生打开视频或音频的Intent
     * @param filePath 文件路径
     * @param dataType 文件类型
     * @return
     */
    private fun generateVideoAudioIntent(
        filePath: String,
        dataType: String
    ): Intent? {
        val intent =
            Intent(Intent.ACTION_VIEW)
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
        intent.putExtra("oneshot", 0)
        intent.putExtra("configchange", 0)
        val file = File(filePath)
        intent.setDataAndType(getUri(intent, file), dataType)
        return intent
    }

    /**
     * 产生打开网页文件的Intent
     * @param filePath 文件路径
     * @return
     */
    private fun generateHtmlFileIntent(filePath: String): Intent? {
        val uri = Uri.parse(filePath)
            .buildUpon()
            .encodedAuthority("com.android.htmlfileprovider")
            .scheme("content")
            .encodedPath(filePath)
            .build()
        val intent =
            Intent(Intent.ACTION_VIEW)
        intent.setDataAndType(uri, DATA_TYPE_HTML)
        return intent
    }

    /**
     * 获取对应文件的Uri
     * @param intent 相应的Intent
     * @param file 文件对象
     * @return
     */
    private fun getUri(intent: Intent, file: File): Uri? {
        var uri: Uri? = null
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            //判断版本是否在7.0以上
            uri = FileProvider.getUriForFile(
                mContext, mContext.getPackageName().toString() + ".fileprovider",
                file
            )
            //添加这一句表示对目标应用临时授权该Uri所代表的文件
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        } else {
            uri = Uri.fromFile(file)
        }
        return uri
    }

    /**
     * 打开文件
     * @param filePath 文件的全路径,包括到文件名
     */
    fun openFile(filePath: String) {
        Log.i("lq", "打开文件 == $filePath")
        val file = File(filePath)
        if (!file.exists()) {
            //如果文件不存在
            Toast.makeText(mContext, "文件打开失败,文件已经被移动或者删除", Toast.LENGTH_SHORT).show()
            return
        }
        /* 取得扩展名 */
        val end =
            file.name.substring(file.name.lastIndexOf(".") + 1, file.name.length)
                .toLowerCase(Locale.getDefault())
        /* 依扩展名的类型决定MimeType */
        var intent: Intent? = null
        if (end == "m4a" || end == "mp3" || end == "mid" || end == "xmf" || end == "ogg" || end == "wav") {
            intent = generateVideoAudioIntent(filePath, DATA_TYPE_AUDIO)
        } else if (end == "3gp" || end == "mp4") {
            intent = generateVideoAudioIntent(filePath, DATA_TYPE_VIDEO)
        } else if (end == "jpg" || end == "gif" || end == "png" || end == "jpeg" || end == "bmp") {
            intent = generateCommonIntent(filePath, DATA_TYPE_IMAGE)
        } else if (end == "apk") {
            intent = generateCommonIntent(filePath, DATA_TYPE_APK)
        } else if (end == "html" || end == "htm") {
            intent = generateHtmlFileIntent(filePath)
        } else if (end == "ppt") {
            intent = generateCommonIntent(filePath, DATA_TYPE_PPT)
        } else if (end == "xls" || end == "xlsx") {
            intent = generateCommonIntent(filePath, DATA_TYPE_EXCEL)
        } else if (end == "doc" || end == "docx") {
            intent = generateCommonIntent(filePath, DATA_TYPE_WORD)
        } else if (end == "pdf") {
            intent = generateCommonIntent(filePath, DATA_TYPE_PDF)
        } else if (end == "chm") {
            intent = generateCommonIntent(filePath, DATA_TYPE_CHM)
        } else if (end == "txt") {
            intent = generateCommonIntent(filePath, DATA_TYPE_TXT)
        } else {
            intent = generateCommonIntent(filePath, DATA_TYPE_ALL)
        }
        try {
            mContext.startActivity(intent)
        } catch (e: Exception) {
            e.printStackTrace()
            Toast.makeText(mContext, "未找到可以打开文件的软件", Toast.LENGTH_SHORT).show()
        }

    }
}

在导出后调用即可

     FileUtils(this).openFile(file.absolutePath)

新的代码结构图:




问题:
混淆打包需要在 proguard-rules.pro 混淆文件中添加

#表格导出相关
-keep class jxl.** {*;}
-keep class common.** {*;}







转载请注明:劉清揚的博客 » 实现 android 程序导出数据到 Excel 表格