webview系列:Html5页面和Native App怎么进行交互

webview系列:Html5页面和Native App怎么进行交互

混合开发的App(Hybrid App)就是在一个App中内嵌一个轻量级的浏览器,一部分原生的功能改为Html5来开发,这部分功能不仅能够在不升级App的情况下动态更新,而且可以在Android或iOS的App上同时运行,让用户的体验更好又可以节省开发的资源。

我觉得一个Hybrid开发的App中必须要要有的功能就是Html5页面和Native App怎么进行交互。比如,我点了一个Html 5页面上的一个按钮或链接,我能不能够跳转到Native App的某个页面;比如我点了Html 5页面上的分享按钮,我能不能调用Native App的分享功能;比如Html加载的时候能不能获取Native App的用户信息等等。

一般来讲,我所知道的两种主流的方式就是:

js调用Native中的代码
Schema:WebView拦截页面跳转

第2种方式实现起来很简单,但是一个致命的问题就是这种交互方式是单向的,Html5无法实现回调。如果需求变得复杂,假如Html5需要获取Native App中的用户信息,那么最好使用js调用的方式。例如我们通过淘宝客户端进入天猫的h5页面购物,在这种情况下,你就需要在webview页面获取登陆用户的信息。

一.方式1:js和Native进行交互

demo

webview相关页面配置

WebSettings webSettings = mWebview.getSettings();
        //①设置WebView允许调用js
        webSettings.setJavaScriptEnabled(true);
        webSettings.setDefaultTextEncodingName("UTF-8");
        //②将object对象暴露给Js,调用addjavascriptInterface
        mWebview.addJavascriptInterface(new MyObject(TestWebViewActivity.this),"myObj");

静态页面

<html>
<head>
    <title>Js调用Android</title>
</head>

<body>
<a href="xl://goods:8888/goodsDetail?goodsId=10011002">test scheme</a>

<input type="button" value="Toast提示" onclick="myObj.showToast('曹神前来日狗~');"/>
<input type="button" value="列表对话框" onclick="myObj.showDialog();"/>
</body>
</html>

MyObject

/** * Created by niehongtao on 16/10/9. */
public class MyObject {
    private Context context;

    public MyObject(Context context) {
        this.context = context;
    }

    //将显示Toast和对话框的方法暴露给JS脚本调用
    @JavascriptInterface
    public void showToast(String name) {
        Toast.makeText(context,name,Toast.LENGTH_SHORT).show();
    }

    @JavascriptInterface
    public void showDialog() {
        new AlertDialog.Builder(context)
                .setTitle("联系人列表").setIcon(R.mipmap.ic_launcher)
                .setItems(new String[]{"基神","B神","曹神","街神","翔神"},null)
                .setPositiveButton("确定",null).create().show();
    }
}

可参考
Android基础入门教程——7.5.2 WebView和JavaScrip交互基础
http://www.voidcn.com/article/p-gddujpel-gg.html
这个讲解的最为具体

二.方式2:Scheme:WebView拦截页面跳转

demo

点击网页里的超链接,跳转到原生页面

webviewActivity

package com.ht.fyforandroid.test.webview;

import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.KeyEvent;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import com.ht.fyforandroid.R;
import com.ht.fyforandroid.base.BaseActivity;

import butterknife.InjectView;

/** * Created by niehongtao on 16/7/7. * 进行仿微信加载WebView显示进度条,直接调用start()方法进行跳转. */
public class TestWebViewActivity extends BaseActivity {
    @InjectView(R.id.webview)
    WebView mWebview;


    @Override
    protected int getLayoutId() {
        return R.layout.activity_webview;
    }

    @Override
    protected void init(Bundle savedInstanceState) {
        TestWebViewActivity.super.mLoadingDialog.hideLoading();
        mWebview.loadUrl("file:///android_asset/test.html");
        mWebview.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view,String url) {
                Uri uri = Uri.parse(url);
                if (uri.getScheme().equals("xl")) {
                    startActivity(new Intent(Intent.ACTION_VIEW,Uri.parse(url)));
                } else {
                    view.loadUrl(url);
                }
                return true;
            }
        });

    }


    public static void startActivity(Context context) {
        Intent intent = new Intent(context,TestWebViewActivity.class);
        context.startActivity(intent);
    }


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

}

静态页面test.html

<html>

<body>

<a href="xl://goods:8888/goodsDetail?goodsId=10011002">test scheme</a>
</body>

</html>

需要跳转到的页面

配置信息

<activity android:name=".test.scheme.Test1Activity">
            <!--要想在别的App上能成功调起App,必须添加intent过滤器-->
            <intent-filter>
                <!--协议部分,随便设置-->
                <data  android:host="goods" android:path="/goodsDetail" android:port="8888" android:scheme="xl" />
                <!--下面这几行也必须得设置-->
                <category android:name="android.intent.category.DEFAULT"/>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.BROWSABLE"/>
            </intent-filter>
        </activity>

页面

package com.ht.fyforandroid.test.scheme;

import android.os.Bundle;

import com.ht.fyforandroid.R;
import com.ht.fyforandroid.base.BaseActivity;

/** * Created by niehongtao on 16/10/8. */
public class Test1Activity extends BaseActivity {
    @Override
    protected int getLayoutId() {
        return R.layout.activity_test1;
    }

    @Override
    protected void init(Bundle savedInstanceState) {

    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" >

    <TextView  android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text="scheme跳转测试" />

</LinearLayout>

应用场景:

Android中的scheme是一种非常好的实现机制,通过定义自己的scheme协议,可以非常方便跳转app中的各个页面;
通过scheme协议,服务器可以定制化告诉App跳转那个页面,可以通过通知栏消息定制化跳转页面,可以通过H5页面跳转页面等。
已经可以适应多数的应用场景

三.一些开源项目的实现方案

coding上的实现方案

public static boolean openActivityByUri(Context context,String uri,boolean newTask,boolean defaultIntent,boolean share) {

        final String ProjectPath = "/u/([\\w.-]+)/p/([\\w\\.-]+)";
        final String Host = Global.HOST;
        final String UserPath = "/u/([\\w.-]+)";

        Intent intent = new Intent();
        if (newTask) {
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        }

        final String uriString = uri.replace("/team/","/user/").replace("/t/","/u/"); // 添加 team 后导致的 api 失效问题

        final String NAME = "([\\w.-]+)";

        final String uriPath = uriString.replace(Global.HOST,"");

        final String projectPattern = String.format("^/u/%s/p/%s(.*)",NAME,NAME);
        Pattern pattern = Pattern.compile(projectPattern);
        Matcher matcher = pattern.matcher(uriPath);
        if (matcher.find()) {
            String user = matcher.group(1);
            String project = matcher.group(2);
            String simplePath = matcher.group(3); // 去除了 /u/*/p/* 的路径
            final String projectPath = String.format("/user/%s/project/%s",user,project);

            // 代码中的文件 https://coding.net/u/8206503/p/TestPrivate/git/blob/master/jumpto
            final String gitFile = String.format("^/git/blob/%s/(.*)$",NAME);
            pattern = Pattern.compile(gitFile);
            matcher = pattern.matcher(simplePath);
            if (matcher.find()) {
                String version = matcher.group(1);
                String path = matcher.group(2);

                intent.setClass(context,GitViewActivity_.class);
                intent.putExtra("mProjectPath",projectPath);
                intent.putExtra("mVersion",version);
                intent.putExtra("mGitFileInfoObject",new GitFileInfoObject(path));
                context.startActivity(intent);
                return true;
            }
        }

        // 用户名
        final String atSomeOne = "^(?:https://[\\w.]*)?/u/([\\w.-]+)$";
        pattern = Pattern.compile(atSomeOne);
        matcher = pattern.matcher(uriString);
        if (matcher.find()) {
            String global = matcher.group(1);
            intent.setClass(context,UserDetailActivity_.class);
            intent.putExtra("globalKey",global);
            context.startActivity(intent);
            return true;
        }

        // 项目讨论列表
        // https://coding.net/u/8206503/p/TestIt2/topic/mine
        final String topicList = "^(?:https://[\\w.]*)?/u/([\\w.-]+)/p/([\\w.-]+)/topic/(mine|all)$";
        pattern = Pattern.compile(topicList);
        matcher = pattern.matcher(uriString);
        if (matcher.find()) {
            intent.setClass(context,ProjectActivity_.class);
            ProjectActivity.ProjectJumpParam param = new ProjectActivity.ProjectJumpParam(
                    matcher.group(1),matcher.group(2)
            );
            intent.putExtra("mJumpParam",param);
            intent.putExtra("mJumpType",ProjectActivity.ProjectJumpParam.JumpType.typeTopic);
            context.startActivity(intent);
            return true;
        }

        // 单个项目讨论
        // https://coding.net/u/8206503/p/AndroidCoding/topic/9638?page=1
        final String topic = "^(?:https://[\\w.]*)?/u/([\\w.-]+)/p/([\\w.-]+)/topic/([\\w.-]+)(?:\\?[\\w=&-]*)?$";
        pattern = Pattern.compile(topic);
        matcher = pattern.matcher(uriString);
        if (matcher.find()) {
            intent.setClass(context,TopicListDetailActivity_.class);
            TopicListDetailActivity.TopicDetailParam param =
                    new TopicListDetailActivity.TopicDetailParam(matcher.group(1),matcher.group(2),matcher.group(3));
            intent.putExtra("mJumpParam",param);
            context.startActivity(intent);
            return true;
        }

        // 项目
        // https://coding.net/u/8206503/p/AndroidCoding
        // https://coding.net/u/8206503/p/FireEye/git
        //
        final String project = "^(?:https://[\\w.]*)?/u/([\\w.-]+)/p/([\\w.-]+)(/git)?$";
        pattern = Pattern.compile(project);
        matcher = pattern.matcher(uriString);
        if (matcher.find()) {
            intent.setClass(context,ProjectHomeActivity_.class);
            ProjectActivity.ProjectJumpParam param = new ProjectActivity.ProjectJumpParam(
                    matcher.group(1),param);
            context.startActivity(intent);
            return true;
        }

        // 冒泡
        // https://coding.net/u/8206503/pp/9275
        final String maopao = "^(?:https://[\\w.]*)?/u/([\\w.-]+)/pp/([\\w.-]+)$";
        pattern = Pattern.compile(maopao);
        matcher = pattern.matcher(uriString);
        if (matcher.find()) {
            intent.setClass(context,MaopaoDetailActivity_.class);
            MaopaoDetailActivity.ClickParam param = new MaopaoDetailActivity.ClickParam(
                    matcher.group(1),matcher.group(2));
            intent.putExtra("mClickParam",param);
            context.startActivity(intent);
            return true;
        }

        // 项目内冒泡
        // https://coding.net/t/superrocket/p/TestPrivate?pp=2417
        final String projectMaopao = String.format("^%s%s\\?pp=([\\d]+)",Host,ProjectPath);
        pattern = Pattern.compile(projectMaopao);
        matcher = pattern.matcher(uriString);
        if (matcher.find()) {
            intent.setClass(context,matcher.group(3));
            intent.putExtra("mClickParam",param);
            context.startActivity(intent);
            return true;
        }

            // 冒泡话题
        // https://coding.net/u/8206503/pp/9275
        final String maopaoTopic = "^(?:(?:https://[\\w.]*)?/u/(?:[\\w.-]+))?/pp/topic/([\\w.-]+)$";
        pattern = Pattern.compile(maopaoTopic);
        matcher = pattern.matcher(uriString);
        if (matcher.find()) {
            intent.setClass(context,SubjectDetailActivity_.class);
            intent.putExtra("topicId",Integer.valueOf(matcher.group(1)));
            context.startActivity(intent);
            return true;
        }

        // 还是冒泡话题 https://coding.net/pp/topic/551
        final String maopao2 = "^https://[\\w.]*/pp/topic/([\\w.-]+)$";
        pattern = Pattern.compile(maopao2);
        matcher = pattern.matcher(uriString);
        if (matcher.find()) {
            intent.setClass(context,Integer.valueOf(matcher.group(1)));
            context.startActivity(intent);
            return true;
        }

        // 任务详情
        // https://coding.net/u/wzw/p/coding/task/9220
        final String task = "^(?:https://[\\w.]*)?/u/([\\w.-]+)/p/([\\w\\.-]+)/task/(\\w+)$";
        pattern = Pattern.compile(task);
        matcher = pattern.matcher(uriString);
        if (matcher.find()) {
            Log.d("","gg " + matcher.group(1) + " " + matcher.group(2) + " " + matcher.group(3));
            intent.setClass(context,TaskAddActivity_.class);
            intent.putExtra("mJumpParams",new TaskJumpParams(matcher.group(1),matcher.group(3)));
            context.startActivity(intent);
            return true;
        }

//      我的已过期任务  "/user/tasks"
        final String myExpireTask = String.format("(%s)?%s",Global.DEFAULT_HOST,"/user/tasks");
//        final String myExpireTask = "/user/tasks";
        pattern = Pattern.compile(myExpireTask);
        matcher = pattern.matcher(uriString);
        if (matcher.find()) {
            intent.setClass(context,AllTasksActivity_.class);
            context.startActivity(intent);
            return true;
        }

        // 私信推送
        // https://coding.net/user/messages/history/1984
        final String message = PATTERN_URL_MESSAGE;
        pattern = Pattern.compile(message);
        matcher = pattern.matcher(uriString);
        if (matcher.find()) {
            Log.d("","gg " + matcher.group(1));
            intent.setClass(context,MessageListActivity_.class);
            intent.putExtra("mGlobalKey",matcher.group(1));
            context.startActivity(intent);
            return true;
        }

        // 跳转到文件夹,与服务器相同
        pattern = Pattern.compile(FileUrlActivity.PATTERN_DIR);
        matcher = pattern.matcher(uriString);
        if (matcher.find()) {
            FileUrlActivity_.intent(context)
                    .url(uriString)
                    .start();
            return true;
        }

        // 文件夹,这个url后面的字段是添加上去的
        // https://coding.net/u/8206503/p/TestIt2/attachment/65138/projectid/5741/name/aa.jpg
        final String dir = "^(?:https://[\\w.]*)?/u/([\\w.-]+)/p/([\\w.-]+)/attachment/([\\w.-]+)/projectid/([\\d]+)/name/(.*+)$";
        pattern = Pattern.compile(dir);
        matcher = pattern.matcher(uriString);
        if (matcher.find()) {
            AttachmentFolderObject folder = new AttachmentFolderObject();
            folder.file_id = matcher.group(3);
            folder.name = matcher.group(5);
            AttachmentsActivity_.intent(context)
                    .mAttachmentFolderObject(folder)
                    .mProjectObjectId(Integer.valueOf(matcher.group(4)))
                    .start();
            return true;
        }

        pattern = Pattern.compile(FileUrlActivity.PATTERN_DIR_FILE);
        matcher = pattern.matcher(uriString);
        if (matcher.find()) {
            FileUrlActivity_.intent(context)
                    .url(uriString)
                    .start();
            return true;
        }

        // 文件,这个url后面的字段是添加上去的
        // https://coding.net/u/8206503/p/TestIt2/attachment/65138/preview/66171/projectid/5741/name/aa.jpg
        final String dirFile = "^(?:https://[\\w.]*)?/u/([\\w.-]+)/p/([\\w.-]+)/attachment/([\\w.-]+)/preview/([\\d]+)/projectid/([\\d]+)/name/(.*+)$";
        pattern = Pattern.compile(dirFile);
        matcher = pattern.matcher(uriString);
        if (matcher.find()) {
            AttachmentFolderObject folder = new AttachmentFolderObject();
            folder.name = matcher.group(3);

            AttachmentFileObject folderFile = new AttachmentFileObject();
            folderFile.file_id = matcher.group(4);
            folderFile.setName(matcher.group(6));

            int projectId = Integer.valueOf(matcher.group(5));

            String extension = folderFile.getName().toLowerCase();
            final String imageType = ".*\\.(gif|png|jpeg|jpg)$";
            final String htmlMdType = ".*\\.(html|htm|markd|markdown|md|mdown)$";
            final String txtType = ".*\\.(sh|txt)$";
            if (extension.matches(imageType)) {
                AttachmentsPicDetailActivity_.intent(context)
                        .mProjectObjectId(projectId)
                        .mAttachmentFolderObject(folder)
                        .mAttachmentFileObject(folderFile)
                        .start();

            } else if (extension.matches(htmlMdType)) {
                AttachmentsHtmlDetailActivity_.intent(context)
                        .mProjectObjectId(projectId)
                        .mAttachmentFolderObject(folder)
                        .mAttachmentFileObject(folderFile)
                        .start();

            } else if (extension.matches(txtType)) {
                AttachmentsTextDetailActivity_.intent(context)
                        .mProjectObjectId(projectId)
                        .mAttachmentFolderObject(folder)
                        .mAttachmentFileObject(folderFile)
                        .start();
            } else {
                AttachmentsDownloadDetailActivity_.intent(context)
                        .mProjectObjectId(projectId)
                        .mAttachmentFolderObject(folder)
                        .mAttachmentFileObject(folderFile)
                        .start();
            }

            return true;
        }

        // 图片链接
        final String imageSting = "(http|https):.*?.[.]{1}(gif|jpg|png|bmp)";
        pattern = Pattern.compile(imageSting);
        matcher = pattern.matcher(uriString);
        if (matcher.find()) {
            intent.setClass(context,ImagePagerActivity_.class);
            intent.putExtra("mSingleUri",uriString);
            context.startActivity(intent);
            return true;
        }

        // 跳转图片链接
        // https://coding.net/api/project/78813/files/137849/imagePreview
        final String imageJumpString = Global.HOST_API + "/project/\\d+/files/\\d+/imagePreview";
        pattern = Pattern.compile(imageJumpString);
        matcher = pattern.matcher(uriString);
        if (matcher.find()) {
            intent.setClass(context,uriString);
            context.startActivity(intent);
            return true;
        }

        // 跳转到merge或pull
        final String mergeString = "^(?:https://[\\w.]*)?/u/([\\w.-]+)/p/([\\w\\.-]+)/git/(merge)?(pull)?/(\\d+)";
        pattern = Pattern.compile(mergeString);
        matcher = pattern.matcher(uriString);
        if (matcher.find()) {
            intent.setClass(context,MergeDetailActivity_.class);
            intent.putExtra("mMergeUrl",uriString);
            context.startActivity(intent);
            return true;
        }

        // 跳转到commit
        final String commitString = "^(?:https://[\\w.]*)?/u/([\\w.-]+)/p/([\\w\\.-]+)/git/commit/.+$";
        pattern = Pattern.compile(commitString);
        matcher = pattern.matcher(uriString);
        if (matcher.find()) {
            intent.setClass(context,CommitFileListActivity_.class);
            intent.putExtra("mCommitUrl",uriString);
            context.startActivity(intent);
            return true;
        }

        // 跳转到branch
        final String branchString = "^(?:https://[\\w.]*)?/u/([\\w.-]+)/p/([\\w\\.-]+)/git/tree/(.+)$";
        pattern = Pattern.compile(branchString);
        matcher = pattern.matcher(uriString);
        if (matcher.find()) {
            intent.setClass(context,BranchMainActivity_.class);
            String userString = matcher.group(1);
            String projectString = matcher.group(2);
            String version = matcher.group(3);
            String projectPath = String.format("/user/%s/project/%s",userString,projectString);

            intent.putExtra("mProjectPath",projectPath);
            intent.putExtra("mVersion",version);
            context.startActivity(intent);
            return true;
        }

        String s = PushUrl.URL_2FA;
        if (uriString.equals(s)) {
            intent.setClass(context,AuthListActivity.class);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(intent);
            return true;
        }

        try {
            if (defaultIntent) {
                intent = new Intent(context,WebActivity_.class);

                if (newTask) {
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                }
                if (uri.startsWith("/u/")) {
                    uri = Global.HOST + uri;
                }

                if (share) {
                    intent.putExtra("share",true);
                }

                intent.putExtra("url",uri);
                context.startActivity(intent);
            }
        } catch (Exception e) {
            Toast.makeText(context,"" + uri,Toast.LENGTH_LONG).show();
            Global.errorLog(e);
        }

        return false;
    }

coding的实现其实是和服务器商量了一种协议,看起来非常繁琐。感觉作用和scheme类似。

参考资料

Android产品研发(十一)–>应用内跳转Scheme协议
http://www.voidcn.com/article/p-cujwcfaw-ym.html

谈谈App混合开发
http://bxbxbai.github.io/2015/08/16/talk-about-bybird-app/

Android业务组件化之URL Schema使用
http://www.itdadao.com/articles/c15a327723p0.html

Android基础入门教程——7.5.2 WebView和JavaScrip交互基础
http://www.voidcn.com/article/p-gddujpel-gg.html

【Android】Scheme详解
http://www.voidcn.com/article/p-pafqakyk-oc.html

iOS使用schema协议调起APP
http://js8.in/2013/12/16/ios%E4%BD%BF%E7%94%A8schema%E5%8D%8F%E8%AE%AE%E8%B0%83%E8%B5%B7app/

Android WebView 优化之路
http://www.voidcn.com/article/p-cfytgjzf-bnx.html

解耦—Hybrid H5跨平台性思考
http://www.iliunian.cn/14684843677084.html

深入浅出话iOS URL Scheme(一)理论篇
http://jobyang.com/2016/08/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA%E8%AF%9DiOS-URL-Scheme-%E4%B8%80-%E7%90%86%E8%AE%BA%E7%AF%87/

【杂谈1月】H5与APP交互错疑
http://www.sabria.me/2016/01/22/ZTN01_5/

相关文章

HTML5和CSS3实现3D展示商品信息的代码
利用HTML5中的Canvas绘制笑脸的代码
Html5剪切板功能的实现
如何通过HTML5触摸事件实现移动端简易进度条
Html5移动端获奖无缝滚动动画实现
关于HTML5和CSS3实现机器猫的代码