編輯:關於Android編程
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頁面獲取登陸用戶的信息。
webview相關頁面配置
WebSettings webSettings = mWebview.getSettings(); //①設置WebView允許調用js webSettings.setJavaScriptEnabled(true); webSettings.setDefaultTextEncodingName("UTF-8"); //②將object對象暴露給Js,調用addjavascriptInterface mWebview.addJavascriptInterface(new MyObject(TestWebViewActivity.this), "myObj");
靜態頁面
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(); } }
這個講解的最為具體
點擊網頁裡的超鏈接,跳轉到原生頁面
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 scheme
配置信息
頁面
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) { } }
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), matcher.group(2) ); intent.putExtra("mJumpParam", 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, MaopaoDetailActivity_.class); MaopaoDetailActivity.ClickParam param = new MaopaoDetailActivity.ClickParam( matcher.group(1), matcher.group(2), 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, SubjectDetailActivity_.class); intent.putExtra("topicId", 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(2), 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, ImagePagerActivity_.class); intent.putExtra("mSingleUri", 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類似。
總體上Music App分為UI界面、服務兩個模塊,其中關於音樂文件的播放都由服務負責,服務配合AIDL使用的,界面綁定服務後可以拿到服務裡所有參數及狀態進行UI刷新。A
首先貼出實現的效果圖:gif的效果可能有點過快,在真機上運行的效果會更好一些。我們主要的思路就是利用屬性動畫來動態地畫出選中狀態以及對勾的繪制過程。看到上面的效果圖,相信
在之前的Android超精准計步器開發-Dylan計步中的首頁用到了一個自定義控件,和QQ運動的界面有點類似,還有動畫效果,下面就來講一下這個View是如何繪制的。1.先
上一篇博客我們講到了ViewRoot中與UI相關的三個重要步驟:performMeasure(測量)、performLayout(布局)和performDraw(繪制),