序言目前有个uni-app应用内预览Office及PDF文档的需求，Dcloud插件市场：https://ext.dcloud.net.cn/上有比较多的插件，有一些用起来比较省心，但是自己也忍不住想写一个插件，因此本文实现基本的文档预览功能基础上，分享一下如何实现插件。[scode type="blue"]Dcloud官方的uni-app原生插件开发教程：https://nativesupport.dcloud.net.cn/NativePlugin/course/android[/scode]开发环境JAVA环境 jdk1.8Android Studio 下载地址：Android Studio官网 OR Android Studio中文社区App离线SDK下载：请下载2.9.8+版本的android平台SDK导入uni插件原生项目UniPlugin-Hello-AS工程请在App离线SDK中查找点击Android Studio菜单选项File--->New--->Import Project。创建Android Studio的Module模块在现有Android项目中创建library的Module，并配置刚创建的Module的build.gradle信息。apply plugin: 'com.android.library' android { compileSdkVersion 29 defaultConfig { minSdkVersion 16 targetSdkVersion 29 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" // Specifies the ABI configurations of your native // libraries Gradle should build and package with your APK. ndk { abiFilters "armeabi", "armeabi-v7a", "x86", "mips" } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } repositories { flatDir { dirs 'libs' } } dependencies { compileOnly fileTree(dir: 'libs', include: ['*.jar']) compileOnly fileTree(dir: '../app/libs', include: ['uniapp-v8-release.aar']) //noinspection GradleCompatible compileOnly "com.android.support:recyclerview-v7:28.0.0" //noinspection GradleCompatible compileOnly "com.android.support:support-v4:28.0.0" //noinspection GradleCompatible compileOnly "com.android.support:appcompat-v7:28.0.0" implementation 'com.alibaba:fastjson:1.1.46.android' implementation 'com.facebook.fresco:fresco:1.13.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3' testImplementation 'junit:junit:4.12' } 创建TestModule类Module 扩展必须继承 UniModule 类扩展方法必须加上@UniJSMethod (uiThread = false or true) 注解。UniApp 会根据注解来判断当前方法是否要运行在 UI 线程，和当前方法是否是扩展方法。UniApp是根据反射来进行调用 Module 扩展方法，所以Module中的扩展方法必须是 public 类型。package com.siyue.uni.plugin;import android.app.Activity;import android.content.Intent;import android.util.Log;import com.alibaba.fastjson.JSONObject;import io.dcloud.feature.uniapp.annotation.UniJSMethod;import io.dcloud.feature.uniapp.bridge.UniJSCallback;import io.dcloud.feature.uniapp.common.UniDestroyableModule;import io.dcloud.feature.uniapp.common.UniModule;import io.dcloud.feature.uniapp.utils.UniLogUtils;public class SiyueDocumentPreview extends UniDestroyableModule {String TAG = "SiyueDocumentPreview"; public static int REQUEST_CODE = 1000; @UniJSMethod (uiThread = true) public void documentPreview(JSONObject options){ if(mUniSDKInstance != null && mUniSDKInstance.getContext() instanceof Activity) { Intent intent = new Intent(mUniSDKInstance.getContext(), DocumentPreviewActivity.class); intent.putExtra("fileUrl", options.getString("url")); intent.putExtra("title", options.getString("title")); intent.putExtra("titleHeight", options.getString("titleHeight")); intent.putExtra("titleBgColor", options.getString("titleBgColor")); intent.putExtra("titleTextColor", options.getString("titleTextColor")); intent.putExtra("titleTextSize", options.getString("titleTextSize")); intent.putExtra("fileType", options.getString("fileType")); intent.putExtra("fileName", options.getString("fileName")); intent.putExtra("deleteCacheFile", options.getString("deleteCacheFile")); ((Activity)mUniSDKInstance.getContext()).startActivityForResult(intent, REQUEST_CODE); } } @Override public void destroy() { }}引入腾讯TBSTBS文档接入地址将jar放入到插件工程的libs中：初始化X5内核创建一个实体类并实现UniAppHookProxy接口，在onCreate函数中添加组件注册相关参数 或 填写插件需要在启动时初始化的逻辑。package com.siyue.uni.plugin; import android.app.Application; import android.content.Intent; import android.util.Log; import com.tencent.smtt.export.external.TbsCoreSettings; import com.tencent.smtt.sdk.QbSdk; import com.tencent.smtt.sdk.TbsListener; import java.util.HashMap; import io.dcloud.feature.uniapp.UniAppHookProxy; public class SiyueDocument_AppProxy implements UniAppHookProxy { @Override public void onCreate(Application application) { //可写初始化触发逻辑 Log.e("SiyueDocument_AppProxy", "--初始化触发逻辑 开始--"); boolean b = QbSdk.canLoadX5(application.getApplicationContext()); Log.e("SiyueDocument_AppProxy", "是否可以加载X5内核 -->" + b); QbSdk.setDownloadWithoutWifi(true); QbSdk.setTbsListener(new TbsListener() { @Override public void onDownloadFinish(int i) { Log.e("SiyueDocument_AppProxy", "onDownloadFinish -->下载X5内核完成：" + i); } @Override public void onInstallFinish(int i) { Log.e("SiyueDocument_AppProxy", "onInstallFinish -->安装X5内核进度：" + i); } @Override public void onDownloadProgress(int i) { Log.e("SiyueDocument_AppProxy", "onDownloadProgress -->安装X5内核进度：" + i); } }); QbSdk.initX5Environment(application.getApplicationContext(), cb); Log.e("SiyueDocument_AppProxy", "--初始化触发逻辑 结束--"); } QbSdk.PreInitCallback cb = new QbSdk.PreInitCallback() { @Override public void onCoreInitFinished() { //x5内核初始化完成回调接口，此接口回调并表示已经加载起来了x5，有可能特殊情况下x5内核加载失败，切换到系统内核。 Log.e("SiyueDocument_AppProxy", "onCoreInitFinished-->"); } @Override public void onViewInitFinished(boolean b) { //x5內核初始化完成的回调，为true表示x5内核加载成功，否则表示x5内核加载失败，会自动切换到系统内核。 Log.e("SiyueDocument_AppProxy", "onViewInitFinished: 加载X5内核是否成功:" + b); } }; @Override public void onSubProcessCreate(Application application) { //子进程初始化回调 } } 文档预览功能package com.siyue.uni.plugin; import android.Manifest; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; import android.app.DownloadManager; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.database.ContentObserver; import android.database.Cursor; import android.graphics.Color; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.Window; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import com.tencent.smtt.sdk.QbSdk; import com.tencent.smtt.sdk.TbsReaderView; import com.tencent.smtt.sdk.TbsReaderView.ReaderCallback; import java.io.File; import java.text.DecimalFormat; public class DocumentPreviewActivity extends Activity implements ReaderCallback, OnClickListener { private TextView txtTitle; private TbsReaderView mTbsReaderView; private TextView txtDownload; //rl_tbsView为装载TbsReaderView的视图 private RelativeLayout rlTbsReaderView; private RelativeLayout rlTitleBar; private ProgressBar pbDownload; private DownloadManager mDownloadManager; private long mRequestId; private DownloadObserver mDownloadObserver; //文件url 由文件url截取的文件名 上个页面传过来用于显示的文件名 private String mFileUrl = "", mFileName, fileName; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.activity_document_preview); findViewById(); getFileUrlByIntent(); mTbsReaderView = new TbsReaderView(this, this); rlTbsReaderView.addView(mTbsReaderView, new RelativeLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); if ((mFileUrl == null) || (mFileUrl.length() <= 0)) { Toast.makeText(DocumentPreviewActivity.this, "获取文件url出错了", Toast.LENGTH_SHORT).show(); return; } mFileName = parseName(mFileUrl); if (isLocalExist()) { txtDownload.setText("打开文件"); txtDownload.setVisibility(View.GONE); displayFile(); } else { if (!mFileUrl.contains("http")) { new AlertDialog.Builder(DocumentPreviewActivity.this) .setTitle("温馨提示:") .setMessage("文件的url地址不合法，无法进行下载！") .setCancelable(false) .setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface arg0, int arg1) { return; } }).create().show(); } startDownload(); } } /** * 将url进行encode，解决部分手机无法下载含有中文url的文件的问题（如OPPO R9） * * @param url * @return * @author xch */ private String toUtf8String(String url) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < url.length(); i++) { char c = url.charAt(i); if (c >= 0 && c <= 255) { sb.append(c); } else { byte[] b; try { b = String.valueOf(c).getBytes("utf-8"); } catch (Exception ex) { System.out.println(ex); b = new byte[0]; } for (int j = 0; j < b.length; j++) { int k = b[j]; if (k < 0) k += 256; sb.append("%" + Integer.toHexString(k).toUpperCase()); } } } return sb.toString(); } private void findViewById() { txtDownload = (TextView) findViewById(R.id.tv_download); ImageView back_icon = (ImageView) findViewById(R.id.back_icon); txtTitle = (TextView) findViewById(R.id.title); pbDownload = (ProgressBar) findViewById(R.id.progressBar_download); rlTbsReaderView = (RelativeLayout) findViewById(R.id.rl_tbsView); rlTitleBar = (RelativeLayout) findViewById(R.id.title_bar); back_icon.setOnClickListener(this); } /** * 获取传过来的文件url和文件名 */ private void getFileUrlByIntent() { Intent intent = getIntent(); mFileUrl = intent.getStringExtra("fileUrl"); fileName = intent.getStringExtra("fileName")!=null ? intent.getStringExtra("fileName") : parseName(mFileUrl); if(intent.getStringExtra("fileType")!=null){ } if(intent.getStringExtra("titleBgColor")!=null){ rlTitleBar.setBackgroundColor(Color.parseColor(intent.getStringExtra("titleBgColor"))); } txtTitle.setText(intent.getStringExtra("title")!=null ? intent.getStringExtra("title") : fileName); if(intent.getStringExtra("titleTextColor")!=null){ txtTitle.setTextColor(Color.parseColor(intent.getStringExtra("titleTextColor"))); } if(intent.getStringExtra("titleTextSize")!=null){ txtTitle.setTextSize(Float.parseFloat(intent.getStringExtra("titleTextSize"))); } android.view.ViewGroup.LayoutParams titleLayout = txtTitle.getLayoutParams(); if(intent.getStringExtra("titleHeight")!=null){ titleLayout.height = Integer.parseInt(intent.getStringExtra("titleHeight")); } txtTitle.setLayoutParams(titleLayout); } /** * 加载显示文件内容 */ private void displayFile() { if (ContextCompat.checkSelfPermission(DocumentPreviewActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(DocumentPreviewActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); } else { Bundle bundle = new Bundle(); bundle.putString("filePath", getLocalFile().getPath()); bundle.putString("tempPath", Environment.getExternalStorageDirectory() .getPath()); boolean result = mTbsReaderView.preOpen(parseFormat(mFileName), false); if (result) { mTbsReaderView.openFile(bundle); } else { File file = new File(getLocalFile().getPath()); if (file.exists()) { Intent openintent = new Intent(); openintent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); String type = getMIMEType(file); // 设置intent的data和Type属性。 openintent.setDataAndType(/* uri */Uri.fromFile(file), type); // 跳转 startActivity(openintent); finish(); } } } } private String getMIMEType(File file) { String type = "*/*"; String fName = file.getName(); // 获取后缀名前的分隔符"."在fName中的位置。 int dotIndex = fName.lastIndexOf("."); if (dotIndex < 0) { return type; } /* 获取文件的后缀名 */ String end = fName.substring(dotIndex, fName.length()).toLowerCase(); if (end == "") return type; // 在MIME和文件类型的匹配表中找到对应的MIME类型。 for (int i = 0; i < MIME_MapTable.length; i++) { if (end.equals(MIME_MapTable[i][0])) type = MIME_MapTable[i][8]; } return type; } private String parseFormat(String fileName) { return fileName.substring(fileName.lastIndexOf(".") + 1); } /** * 利用文件url转换出文件名 * * @param url * @return */ private String parseName(String url) { String fileName = null; try { fileName = url.substring(url.lastIndexOf("/") + 1); } finally { if (TextUtils.isEmpty(fileName)) { fileName = String.valueOf(System.currentTimeMillis()); } } return fileName; } private boolean isLocalExist() { return getLocalFile().exists(); } private File getLocalFile() { return new File( Environment .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), mFileName); } /** * 下载文件 */ @SuppressLint("NewApi") private void startDownload() { mDownloadObserver = new DownloadObserver(new Handler()); getContentResolver().registerContentObserver( Uri.parse("content://downloads/my_downloads"), true, mDownloadObserver); mDownloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE); //将含有中文的url进行encode String fileUrl = toUtf8String(mFileUrl); try { DownloadManager.Request request = new DownloadManager.Request( Uri.parse(fileUrl)); request.setDestinationInExternalPublicDir( Environment.DIRECTORY_DOWNLOADS, mFileName); request.allowScanningByMediaScanner(); request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN); mRequestId = mDownloadManager.enqueue(request); } catch (Exception e) { e.printStackTrace(); } } @TargetApi(Build.VERSION_CODES.GINGERBREAD) private void queryDownloadStatus() { DownloadManager.Query query = new DownloadManager.Query() .setFilterById(mRequestId); Cursor cursor = null; try { cursor = mDownloadManager.query(query); if (cursor != null && cursor.moveToFirst()) { // 已经下载的字节数 long currentBytes = cursor .getLong(cursor .getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)); // 总需下载的字节数 long totalBytes = cursor .getLong(cursor .getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)); // 状态所在的列索引 int status = cursor.getInt(cursor .getColumnIndex(DownloadManager.COLUMN_STATUS)); txtDownload.setText("下载中...(" + formatKMGByBytes(currentBytes) + "/" + formatKMGByBytes(totalBytes) + ")"); // 将当前下载的字节数转化为进度位置 int progress = (int) ((currentBytes * 1.0) / totalBytes * 100); pbDownload.setProgress(progress); Log.i("downloadUpdate: ", currentBytes + " " + totalBytes + " " + status + " " + progress); if (DownloadManager.STATUS_SUCCESSFUL == status && txtDownload.getVisibility() == View.VISIBLE) { txtDownload.setVisibility(View.GONE); txtDownload.performClick(); if (isLocalExist()) { txtDownload.setVisibility(View.GONE); displayFile(); } } } } finally { if (cursor != null) { cursor.close(); } } } @Override public void onCallBackAction(Integer integer, Object o, Object o1) { } @Override protected void onDestroy() { super.onDestroy(); mTbsReaderView.onStop(); if (mDownloadObserver != null) { getContentResolver().unregisterContentObserver(mDownloadObserver); } } @SuppressLint("Override") public void onPointerCaptureChanged(boolean hasCapture) { } private class DownloadObserver extends ContentObserver { private DownloadObserver(Handler handler) { super(handler); } @Override public void onChange(boolean selfChange, Uri uri) { queryDownloadStatus(); } } @Override public void onClick(View v) { // TODO Auto-generated method stub if (v.getId() == R.id.back_icon) { finish(); } } /** * 将字节数转换为KB、MB、GB * * @param size 字节大小 * @return */ private String formatKMGByBytes(long size) { StringBuffer bytes = new StringBuffer(); DecimalFormat format = new DecimalFormat("###.00"); if (size >= 1024 * 1024 * 1024) { double i = (size / (1024.0 * 1024.0 * 1024.0)); bytes.append(format.format(i)).append("GB"); } else if (size >= 1024 * 1024) { double i = (size / (1024.0 * 1024.0)); bytes.append(format.format(i)).append("MB"); } else if (size >= 1024) { double i = (size / (1024.0)); bytes.append(format.format(i)).append("KB"); } else if (size < 1024) { if (size <= 0) { bytes.append("0B"); } else { bytes.append((int) size).append("B"); } } return bytes.toString(); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { case 1: if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { } else { Toast.makeText(this, "没有获取到系统权限，无法预览文件！", Toast.LENGTH_SHORT).show(); } break; default: } } private final String[][] MIME_MapTable = { // {后缀名，MIME类型} {".3gp", "video/3gpp"}, {".apk", "application/vnd.android.package-archive"}, {".asf", "video/x-ms-asf"}, {".avi", "video/x-msvideo"}, {".bin", "application/octet-stream"}, {".bmp", "image/bmp"}, {".c", "text/plain"}, {".class", "application/octet-stream"}, {".conf", "text/plain"}, {".cpp", "text/plain"}, {".doc", "application/msword"}, {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, {".xls", "application/vnd.ms-excel"}, {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, {".exe", "application/octet-stream"}, {".gif", "image/gif"}, {".gtar", "application/x-gtar"}, {".gz", "application/x-gzip"}, {".h", "text/plain"}, {".htm", "text/html"}, {".html", "text/html"}, {".jar", "application/java-archive"}, {".java", "text/plain"}, {".jpeg", "image/jpeg"}, {".jpg", "image/jpeg"}, {".js", "application/x-javascript"}, {".log", "text/plain"}, {".m3u", "audio/x-mpegurl"}, {".m4a", "audio/mp4a-latm"}, {".m4b", "audio/mp4a-latm"}, {".m4p", "audio/mp4a-latm"}, {".m4u", "video/vnd.mpegurl"}, {".m4v", "video/x-m4v"}, {".mov", "video/quicktime"}, {".mp2", "audio/x-mpeg"}, {".mp3", "audio/x-mpeg"}, {".mp4", "video/mp4"}, {".mpc", "application/vnd.mpohun.certificate"}, {".mpe", "video/mpeg"}, {".mpeg", "video/mpeg"}, {".mpg", "video/mpeg"}, {".mpg4", "video/mp4"}, {".mpga", "audio/mpeg"}, {".msg", "application/vnd.ms-outlook"}, {".ogg", "audio/ogg"}, {".pdf", "application/pdf"}, {".png", "image/png"}, {".pps", "application/vnd.ms-powerpoint"}, {".ppt", "application/vnd.ms-powerpoint"}, {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, {".prop", "text/plain"}, {".rc", "text/plain"}, {".rmvb", "audio/x-pn-realaudio"}, {".rtf", "application/rtf"}, {".sh", "text/plain"}, {".tar", "application/x-tar"}, {".tgz", "application/x-compressed"}, {".txt", "text/plain"}, {".wav", "audio/x-wav"}, {".wma", "audio/x-ms-wma"}, {".wmv", "audio/x-ms-wmv"}, {".wps", "application/vnd.ms-works"}, {".xml", "text/plain"}, {".z", "application/x-compress"}, {".zip", "application/x-zip-compressed"}, {"", "*/*"}}; } 插件调试本地注册插件将UniPlugin-Hello-AS工程下 “app” Module根目录assets/dcloud_uniplugins.json文件。 在moudles节点下 添加要注册的Module{ "nativePlugins": [ { "hooksClass": "com.siyue.uni.plugin.SiyueDocument_AppProxy", "plugins": [ { "type": "module", "name": "SiyueDocumentPreview", "class": "com.siyue.uni.plugin.SiyueDocumentPreview" } ] } ] } 集成uni-app项目测试插件安装最新HbuilderX 大于等于1.4.0+创建uni-app工程或在已有的uni-app工程编写相关的.nvue 和.vue文件。使用uni-app插件中的module 或 component。xxx.vue 示例代码（源码请参考UniPlugin-Hello-AS项目中uniapp示例工程源码文件夹的unipluginDemo工程）最终测试结果：