Android 打开 office 文档的几种方式

Android 在开发项目的过程中遇到了关于office 文件打开浏览的问题,在解决完后,在这里做个记录,也给各位未接触过的小伙伴们一点方向(会在文末附上该项目的git链接地址)。在查阅相关资料后,给出了几种方案:

方案一:用谷歌或微软的url链接格式拼接后,通过控件webView内置预览。

方案二:使用第三方手机软件WPS 打开本地文档。

方案三:使用国内腾讯 X5内核浏览控件 打开本地文档。

在自己亲自尝试后,方案一由于国内Android手机各种魔改和需要翻墙不可行,故介绍其它两种可行的方案与其缺陷。

结论先行:
方案二:通过第三方软件打开,若手机设备未安装WPS等能打开文档的相关软件则无法调用打开
方案三:能在自己APP内置打开,若手机设备未安装QQ或微信,则无法调用打开
方案三的基础上的优化:将x5内核放在自己工程中或者后台服务器上下载下来,静态集成进来,有大佬已经写过相关文章,亲测可用,就不献丑了,链接如下:Android静态集成X5内核

点击跳转无效的话直接使用以下链接:
https://blog.csdn.net/qq_34205629/article/details/122375262#comments_20014428

方案二效果图如下:


wps打开录屏.gif

一般情况是由后台生成文件链接,我们下载保存到手机中再打开,由于没有服务器支持,我这里是将assets文件夹的test.docx文件保存到手机中去。再调用WPS打开文件

在这之前借鉴了大佬的博客:
https://blog.csdn.net/qq_31939617/article/details/83443440?utm_medium=distribute.pc_relevant.none-task-blog-baidujs-1

开始上代码:
由于我们要将文件写入到手机中,需要写入权限

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

读取目标文件可能会遇到7.0以上手机 FileUriExposedException ,这里动态申请权限和解决FileUri异常相信大多是博客写的很详细,我就不赘诉了

权限初始化判断授权,6.0以上动态申请

  checkPermissions();

checkPermissions 方法

   private void checkPermissions() {
        //检查是否获得权限
        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            //没有获得授权,申请授权
            if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
                //弹窗解释为何需要该权限,再次请求权限
                Toast.makeText(MainActivity.this, "请授权,否则无法存储test.docx 文档", Toast.LENGTH_LONG).show();
                //跳转到应用设置界面
                Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                Uri uri = Uri.fromParts("package", getPackageName(), null);
                intent.setData(uri);
                startActivity(intent);
            } else {
                //不需要解释为何需要授权直接请求授权
                ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, MY_PERMISSIONS_REQUEST_CALL_PHONE);
            }
        } else {
            //获得授权,将文件写入到
            saveFileToPhone();
            
        }

    }

onRequestPermissionsResult 权限申请回调:

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        switch (requestCode) {
            case MY_PERMISSIONS_REQUEST_CALL_PHONE: {
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    //授权成功,写入文件
                    saveFileToPhone();
                } else {
                    //授权失败
                    Toast.makeText(this, "授权失败!", Toast.LENGTH_LONG).show();
                }
                break;
            }
        }
    }

动态申请权限成功之后直接调用 saveFileToPhone() 将 assets中的文件保存复制到手机中

    private void saveFileToPhone() {
        FileUtils.getInstance(getApplicationContext()).copyAssetsToSD("", "officeShow/office").setFileOperateCallback(new FileUtils.FileOperateCallback() {
            @Override
            public void onSuccess() {
                Toast.makeText(MainActivity.this,"保存成功,可以打开啦",Toast.LENGTH_SHORT).show();
                fileUrl= Environment.getExternalStorageDirectory()+"/officeShow/office/test.docx";
            }

            @Override
            public void onFailed(String error) {
                Toast.makeText(MainActivity.this,"保存失败:"+error,Toast.LENGTH_SHORT).show();
            }
        });

    }

工具类代码如下:

public class FileUtils {
    private static FileUtils instance;
    private static final int SUCCESS = 1;
    private static final int FAILED = 0;
    private Context context;
    private FileOperateCallback callback;
    private volatile boolean isSuccess;
    private String errorStr;

    public static FileUtils getInstance(Context context) {
        if (instance == null)
            instance = new FileUtils(context);
        return instance;
    }

    private FileUtils(Context context) {
        this.context = context;
    }

    private Handler handler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (callback != null) {
                if (msg.what == SUCCESS) {
                    callback.onSuccess();
                }
                if (msg.what == FAILED) {
                    callback.onFailed(msg.obj.toString());
                }
            }
        }
    };

    public FileUtils copyAssetsToSD(final String srcPath, final String sdPath) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                copyAssetsToDst(context, srcPath, sdPath);
                if (isSuccess)
                    handler.obtainMessage(SUCCESS).sendToTarget();
                else
                    handler.obtainMessage(FAILED, errorStr).sendToTarget();
            }
        }).start();
        return this;
    }

    public void setFileOperateCallback(FileOperateCallback callback) {
        this.callback = callback;
    }

    private void copyAssetsToDst(Context context, String srcPath, String dstPath) {
        try {
            String fileNames[] = context.getAssets().list(srcPath);
            if (fileNames.length > 0) {
                File file = new File(Environment.getExternalStorageDirectory(), dstPath);
                if (!file.exists()) file.mkdirs();
                for (String fileName : fileNames) {
                    if (!srcPath.equals("")) { // assets 文件夹下的目录
                        copyAssetsToDst(context, srcPath + File.separator + fileName, dstPath + File.separator + fileName);
                    } else { // assets 文件夹
                        copyAssetsToDst(context, fileName, dstPath + File.separator + fileName);
                    }
                }
            } else {
                File outFile = new File(Environment.getExternalStorageDirectory(), dstPath);
                InputStream is = context.getAssets().open(srcPath);
                FileOutputStream fos = new FileOutputStream(outFile);
                byte[] buffer = new byte[1024];
                int byteCount;
                while ((byteCount = is.read(buffer)) != -1) {
                    fos.write(buffer, 0, byteCount);
                }
                fos.flush();
                is.close();
                fos.close();
            }
            isSuccess = true;
        } catch (Exception e) {
            e.printStackTrace();
            errorStr = e.getMessage();
            isSuccess = false;
        }
    }

    public interface FileOperateCallback {
        void onSuccess();

        void onFailed(String error);
    }

}

保存成功之后,通过点击按钮启动意图打开手机中的第三方软件:WPS,打开之前先通过isInstall函数判断是否已安装

    private boolean isInstall(Context context, String packageName) {
        final PackageManager packageManager = context.getPackageManager();
        // 获取所有已安装程序的包信息
        List<PackageInfo> pinfo = packageManager.getInstalledPackages(0);
        for (int i = 0; i < pinfo.size(); i++) {
            if (pinfo.get(i).packageName.equalsIgnoreCase(packageName))
                return true;
        }
        return false;
    }
    @Override
    public void onClick(View view) {
        switch (view.getId()){
            case R.id.btn_openWithWPS:
                if (!isInstall(this, "cn.wps.moffice_eng")) {
                    Toast.makeText(this,"请下载安装WPS",Toast.LENGTH_SHORT).show();
                    return;
                }

                startActivity(getWordFileIntent(fileUrl));
                break;
        }
    }

若已经安装,则通过getWordFileIntent(fileUrl) 启动意图

   private Intent getWordFileIntent(String Path) {
        File file = new File(Path);
        Intent intent = new Intent("android.intent.action.VIEW");
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        Uri uri;
        //Uri uri = Uri.fromFile(file);//解决FileUriExposedException
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            uri = FileProvider.getUriForFile(getApplicationContext(),
                    getPackageName() + ".fileprovider", new File(String.valueOf(file)));
        } else {
            uri = Uri.fromFile(new File(String.valueOf(file)));
        }


        String type = getMIMEType(file);
        if (type.contains("pdf") || type.contains("vnd.ms-powerpoint") || type.contains("vnd.ms-word") || type.contains("vnd.ms-excel") || type.contains("text/plain") || type.contains("text/html")) {
            if (isInstall(this, "cn.wps.moffice_eng")) {
                intent.setClassName("cn.wps.moffice_eng",
                        "cn.wps.moffice.documentmanager.PreStartActivity2");
                intent.setData(uri);
            } else {
                intent.addCategory("android.intent.category.DEFAULT");
                intent.setDataAndType(uri, type);
            }
        } else {
            intent.addCategory("android.intent.category.DEFAULT");
            intent.setDataAndType(uri, type);
        }
        return intent;
    }

    /**
     * 判断文件类型
     */
    private static String getMIMEType(File f) {
        String type = "";
        String fName = f.getName();
        /* 取得扩展名 */
        String end = fName.substring(fName.lastIndexOf(".") + 1, fName.length()).toLowerCase();
        /* 依扩展名的类型决定MimeType */
        if (end.equals("pdf")) {
            type = "application/pdf";
        } else if (end.equals("m4a") || end.equals("mp3") || end.equals("mid") ||
                end.equals("xmf") || end.equals("ogg") || end.equals("wav")) {
            type = "audio/*";
        } else if (end.equals("3gp") || end.equals("mp4")) {
            type = "video/*";
        } else if (end.equals("jpg") || end.equals("gif") || end.equals("png") ||
                end.equals("jpeg") || end.equals("bmp")) {
            type = "image/*";
        } else if (end.equals("apk")) {
            type = "application/vnd.android.package-archive";
        } else if (end.equals("pptx") || end.equals("ppt")) {
            type = "application/vnd.ms-powerpoint";
        } else if (end.equals("docx") || end.equals("doc")) {
            type = "application/vnd.ms-word";
        } else if (end.equals("xlsx") || end.equals("xls")) {
            type = "application/vnd.ms-excel";
        } else if (end.equals("txt")) {
            type = "text/plain";
        } else if (end.equals("html") || end.equals("htm")) {
            type = "text/html";
        } else {
            //如果无法直接打开,就跳出软件列表给用户选择
            type = "*/*";
        }
        return type;
    }

至此,用手机第三方打开WPS已经实现了

接下来实现方案三的功能,参考大佬博客:https://blog.csdn.net/Andy_l1/article/details/78218078?locationNum=5&fps=1
效果图如下

tbs内置浏览office.gif

腾讯浏览服务官网地址
现在引用下腾讯官方文档说明 与贴上它在Android浏览器的文件能力支持情况

x5相关说明.png
office_支持文档.png

目前TSB还不支持在线预览功能,只支持本地文件打开。现在将相关的库依赖到开发项目app目录下的gradle中

api 'com.tencent.tbs.tbssdk:sdk:43903'

添加网络权限和网络监视管理权限


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

初始化QBSDK

  private void initQBSDK() {
        QbSdk.initX5Environment(this, new QbSdk.PreInitCallback() {
            @Override
            public void onCoreInitFinished() {
                Log.e("QbSdk", "QbSdk onCoreInitFinished");
            }

            @Override
            public void onViewInitFinished(boolean b) {

                ifInitSuccess = b;
                Log.e("QbSdk", "QbSdk 初始化是否成功:" + b);
            }
        });
        QbSdk.setDownloadWithoutWifi(true);//设置支持非Wifi下载

    }

在docx文件成功写入手机和QbSdk初始化成功后,跳转到新的页面展示

         case R.id.btn_openWithTbs:
                if(!ifInitSuccess){
                    Toast.makeText(this, "初始化失败,请查看原因", Toast.LENGTH_SHORT).show();
                    return;
                }
                startActivity(new Intent(this,TbsX5ReadOfficeActivity.class)
                .putExtra("fileUrl",fileUrl)
                );
                break;

现在来看看加载office的TbsX5ReadOfficeActivity,贴上整个类的代码:

public class TbsX5ReadOfficeActivity extends AppCompatActivity implements TbsReaderView.ReaderCallback {
    RelativeLayout rootRl;
    private TbsReaderView tbsReaderView;
    private String fileUrl;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tbs_x5_read_office);
        fileUrl = getIntent().getStringExtra("fileUrl");

        initTbs();
    }

    private void initTbs() {
        rootRl = findViewById(R.id.rootRl);
        tbsReaderView = new TbsReaderView(this, this);
        rootRl.addView(tbsReaderView, new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,
                RelativeLayout.LayoutParams.MATCH_PARENT));

        Bundle bundle = new Bundle();
        bundle.putString("filePath", fileUrl);
        //加载插件保存的路径
        bundle.putString("tempPath", Environment.getExternalStorageDirectory() + File.separator + "temp");
        boolean b = tbsReaderView.preOpen("docx", false);
        if (b) {
            tbsReaderView.openFile(bundle);
        }

    }

    @Override
    public void onCallBackAction(Integer integer, Object o, Object o1) {

    }


}

大概流程就是通过上个界面传递过来的文件路径,在tbs初始化后将TbsReaderView 添加到RelativeLayout rootRl中做其子View,通过bundle把文件传给x5,打开的事情交由x5处理,tbsReaderView.preOpen("docx", false) 的第一个参数是目标文件格式,启动相应的格式插件去打开。
至此两个方案的实现均以给出,希望能给小伙伴们借鉴思路,如有讲解错误,希望能不吝惜指正。
这是我项目的github地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352