基于TBS编写Android原生视图提供React Navtive使用的总结

TBS的集成流程

1、下载SDK将jar包放在app/libs/目录下(以Project方式展示项目)
2、将Demo工程中的liblbs.so拷贝到main/jniLibs/armeabi/目录下
3、添加build.gradle中添加NDK支持

ndk {
    abiFilters "armeabi-v7a", "x86", "armeabi"
}

4、添加所需要的权限

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

TBS安卓原生插件的编写

  • 以原生module的方式提供React Native使用

如何编写RN的原生插件这里就不演示了,具体可以参考官方网站。
实际上就是RN端跳转原生的Activity加载TBS视图。
项目中使用了TbsReaderView加载PDF、Word等文件,利用WebView加载视频和利用SDK提供的Activity播放视频。

1、Tbs的初始化

在MainApplication的onCreate()方法中初始化
private void initTBS() {
    //初始化X5内核
    QbSdk.initX5Environment(this, new QbSdk.PreInitCallback() {
        @Override
        public void onCoreInitFinished() {
            //x5内核初始化完成回调接口,此接口回调并表示已经加载起来了x5,有可能特殊情况下x5内核加载失败,切换到系统内核。
            Log.e("QbSdk","onCoreInitFinished:");
        }

        @Override
        public void onViewInitFinished(boolean result) {
            //x5內核初始化完成的回调,为true表示x5内核加载成功,否则表示x5内核加载失败,会自动切换到系统内核。
            Log.e("QbSdk","加载内核是否成功:"+ result);
            isLoadX5 = result;
        }
    });
    
    // 监听X5内核的下载情况
    QbSdk.setTbsListener(new TbsListener() {
        @Override
        public void onDownloadFinish(int i) {
            Log.e("QbSdk","onDownloadFinish:"+ i);
        }

        @Override
        public void onInstallFinish(int i) {
            Log.e("QbSdk","onInstallFinish:"+ i);
        }

        @Override
        public void onDownloadProgress(int i) {
            Log.e("QbSdk","onDownloadProgress:"+ i);
        }
    });
}

2、在activity中利用TbsReaderView加载PDF、World等文件

public class TbsActivity extends AppCompatActivity {

    private TbsReaderView mTbsReaderView;
    private RelativeLayout mRelativeLayout;
    private TextView mTextView;
    private Button backButton;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tbs);
        mTbsReaderView = new TbsReaderView(this, new TbsReaderView.ReaderCallback() {
            @Override
            public void onCallBackAction(Integer integer, Object o, Object o1) {
                Log.i("onCallBackAction", "o = " + o + " o1 = " + o1);
            }
        });
        backButton = findViewById(R.id.tbs_back_button);
        backButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });
        mRelativeLayout = findViewById(R.id.tbsRelativeView);
        mRelativeLayout.addView(mTbsReaderView,new RelativeLayout.LayoutParams(-1,-1));
        mTextView = findViewById(R.id.tbs_title);
        String localPath = getIntent().getStringExtra("localPath");
        displayFile(localPath);
        mTextView.setText(FileUtils.fileName(localPath));
    }

    private void displayFile(String localPath) {
        if (!MainApplication.getInstance().getLoadX5()) {
            Toast.makeText(TbsActivity.this, "加载X5内核失败", Toast.LENGTH_SHORT).show();
            return;
        }

        if (localPath.length() == 0 || localPath == null) {
            Toast.makeText(TbsActivity.this, "获取文件名失败", Toast.LENGTH_SHORT).show();
            return;
        }
        Bundle bundle = new Bundle();
        String tempPath =  Environment.getExternalStorageDirectory()
                .getPath();
        String filePath = localPath;
        int index = localPath.indexOf("file:///");
        if (index != -1) {
            filePath = localPath.substring(localPath.indexOf("/") + 2);
        }
        Log.e("displayFile - filePath", filePath);
        Boolean isExists = FileUtils.fileIsExists(filePath);
        if (!isExists) {
            Toast.makeText(TbsActivity.this, "本地文件不存在", Toast.LENGTH_SHORT).show();
            return;
        }
        bundle.putString("filePath", filePath);
        bundle.putString("tempPath",tempPath);
        boolean result = mTbsReaderView.preOpen(parseFormat(localPath), false);
        if (result) {
            mTbsReaderView.openFile(bundle);
        } else {
            Toast.makeText(TbsActivity.this, "预览文件失败", Toast.LENGTH_SHORT).show();
        }
    }

    private String parseFormat(String fileName) {
        return fileName.substring(fileName.lastIndexOf(".") + 1);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mTbsReaderView.onStop();
    }
}

3、利用WebView播放视频,但是这种方式不会自动播放

public class TbsVideoActivity extends AppCompatActivity {

    private X5WebView webView;
    private Button backButton;
    private TextView mTextView;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tbs_video);
        webView = findViewById(R.id.video_webView);
        mTextView = findViewById(R.id.tbs_title);
        backButton = findViewById(R.id.tbs_back_button);
        backButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });
        initDisplayVideoView();
    }

    private void initDisplayVideoView() {
        if (!MainApplication.getInstance().getLoadX5()) {
            Toast.makeText(TbsVideoActivity.this, "加载X5内核失败", Toast.LENGTH_SHORT).show();
            return;
        }
        String localPath = getIntent().getStringExtra("localPath");
        mTextView.setText(FileUtils.fileName(localPath));
        Log.i("TbsVideoActivity", localPath);
        if (localPath.length() == 0 || localPath == null) {
            Toast.makeText(TbsVideoActivity.this, "获取文件名失败", Toast.LENGTH_SHORT).show();
            return;
        }
        Boolean isExists = FileUtils.fileIsExists(localPath);
        if (!isExists) {
            Toast.makeText(TbsVideoActivity.this, "本地文件不存在", Toast.LENGTH_SHORT).show();
            return;
        }
        if (localPath.indexOf("file://") == -1) {
            localPath = "file://" + localPath;
        }
        webView.loadUrl(localPath);
        getWindow().setFormat(PixelFormat.TRANSLUCENT);
        webView.getView().setOverScrollMode(View.OVER_SCROLL_ALWAYS);
        webView.setWebChromeClient(new com.tencent.smtt.sdk.WebChromeClient());
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (webView != null) {
            webView.onPause();
        }
    }
}

4、利用SDK自带Activity自动播放视频

activyty配置
<activity
    android:name="com.tencent.smtt.sdk.VideoActivity"
    android:alwaysRetainTaskState="true"
    android:configChanges="orientation|screenSize|keyboardHidden"
    android:exported="false"
    android:launchMode="singleTask">
    <intent-filter>
        <action android:name="com.tencent.smtt.tbs.video.PLAY" />

        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

// 播放视频
public void autoPreviewVideoByTBSForRN(String localPath) {
    Log.i("autoPlayVideo", localPath);
    if (localPath.length() == 0) {
        Toast.makeText(mReactContext, "获取文件名失败", Toast.LENGTH_SHORT).show();
        return;
    }
    Boolean isExists = FileUtils.fileIsExists(localPath);
    if (!isExists) {
        Toast.makeText(mReactContext, "本地文件不存在", Toast.LENGTH_SHORT).show();
        return;
    }
    if (localPath.indexOf("file://") == -1) {
        localPath = "file://" + localPath;
    }
    if (TbsVideo.canUseTbsPlayer(mReactContext)){
        //播放视频
        TbsVideo.openVideo(mReactContext, localPath);
    } else {
        Toast.makeText(mReactContext, "播放失败", Toast.LENGTH_SHORT).show();
    }
}
  • 提供原生视图View的方式预览PDF、Word等文件

遇到的问题:RN中无法刷新原生View,发现ReactRootView中重写的onLayout是空实现

public class TbsPreviewView extends RelativeLayout {

    private TbsReaderView mTbsReaderView;
    private RelativeLayout mRelativeLayout;
    private Context mContext;
    public String localPath;

    public TbsPreviewView(Context context) {
        super(context);
        LayoutInflater.from(context).inflate(R.layout.tbs_preview, this);
        mContext = context;
        mRelativeLayout = findViewById(R.id.tbs_preview_relativeView);
        mTbsReaderView = new TbsReaderView(MainApplication.getInstance().getMainActivity(), new TbsReaderView.ReaderCallback() {
            @Override
            public void onCallBackAction(Integer integer, Object o, Object o1) {
                Log.i("onCallBackAction", "o = " + o + " o1 = " + o1);
            }
        });
        mRelativeLayout.addView(mTbsReaderView,new RelativeLayout.LayoutParams(-1,-1));
    }

    public void setLocalPath(String localPath) {
        this.localPath = localPath;
        if (localPath.length() != 0) {
            Log.i("TbsReaderView", "displayFile setLocalPath = " + localPath);
            displayFile(localPath);
        }
    }

    private void displayFile(String localPath) {
        if (!MainApplication.getInstance().getLoadX5()) {
            Toast.makeText(mContext, "加载X5内核失败", Toast.LENGTH_SHORT).show();
            return;
        }

        if (localPath.length() == 0) {
            Toast.makeText(mContext, "获取文件名失败", Toast.LENGTH_SHORT).show();
            return;
        }
        Bundle bundle = new Bundle();
        String tempPath =  Environment.getExternalStorageDirectory()
                .getPath();

        String filePath = localPath;
        int index = localPath.indexOf("file:///");
        if (index != -1) {
            filePath = localPath.substring(localPath.indexOf("/") + 2);
        }
        Log.e("displayFile - filePath", filePath);
        Boolean isExists = FileUtils.fileIsExists(filePath);
        if (!isExists) {
            Toast.makeText(mContext, "本地文件不存在", Toast.LENGTH_SHORT).show();
            return;
        }
        bundle.putString("filePath", filePath);
        bundle.putString("tempPath",tempPath);
        boolean result = mTbsReaderView.preOpen(parseFormat(localPath), false);
        if (result) {
            mTbsReaderView.openFile(bundle);
        } else {
            Toast.makeText(mContext, "预览文件失败", Toast.LENGTH_SHORT).show();
        }
    }

    private String parseFormat(String fileName) {
        return fileName.substring(fileName.lastIndexOf(".") + 1);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        Log.i("TbsPreviewView", "onDetachedFromWindow");
        if (mTbsReaderView != null) {
            mTbsReaderView.onStop();
        }
    }

    /**
     *
     * 在ReactRootView中是空实现
     * protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}
     *
     */
    @Override
    public void requestLayout() {
        super.requestLayout();
        if (getWidth() > 0 && getHeight() > 0) {
            int w = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY);
            int h = MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY);
            measure(w, h);
            layout(getPaddingLeft() + getLeft(), getPaddingTop() + getTop(), getWidth() + getPaddingLeft() + getLeft(), getHeight() + getPaddingTop() + getTop());
        }
    }
}

// RN端使用
render() {
    return (
        <View style={styles.container}>
            <TbsPreviewView style={styles.previewViewStyle} 
            localPath={this.state.localPath}/>
        </View>
    )
}

这种方式存在的问题:预览相关文件会有失败异常的问题,不稳定,建议还是使用RN跳转原生Activity的方式比较好。

完整代码地址