序言
目前有个uni-app应用内预览Office及PDF文档的需求,Dcloud插件市场:https://ext.dcloud.net.cn/上有比较多的插件,有一些用起来比较省心,但是自己也忍不住想写一个插件,因此本文实现基本的文档预览功能基础上,分享一下如何实现插件。
开发环境
JAVA环境 jdk1.8
Android 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() { }
}
引入腾讯TBS
将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工程)
最终测试结果:
此处评论已关闭