基於Xamarin Android實現的簡單的瀏覽器
最近做了一個Android瀏覽器,當然功能比較簡單,主要實現了自己想要的一些功能……現在有好多瀏覽器爲什麼還要自己寫?當你使用的時候總有那麼一些地方不如意,於是就想自己寫一個。
開發環境:Xamarin Android(非Forms)+聯想機子(5.0)+榮耀機子(8.0)
【開發目標】
1、瀏覽器的基本功能,關聯Http和Https(在另一個APP中打開網頁時,可以彈出本應用)
2、創建應用目錄,用來存放離線網頁文件
3、可以離線保存網頁(格式爲mht)
4、關聯mht和mhtml格式的文件
【涉及到的技術點】
1、重寫Activity中的OnBackPressed方法,實現webview回退和再按一次退出程序的功能
2、重寫Activity中的OnConfigurationChanged方法,實現橫豎屏功能
【webview相關技術點】
1、開啓一些常用的設置:JavaScriptEnabled、DomStorageEnabled(如果DomStorageEnabled不啓用,網頁中的下拉刷新和加載更多將不起作用;例子:百度首頁加載新聞)
2、重寫WebViewClient中的ShouldOverrideUrlLoading方法,在點擊打開網頁中的鏈接時,用自己的webview中打開連接,而不是打開其他的瀏覽器
3、重寫WebChromeClient中的OnReceivedTitle和OnProgressChanged方法,分別獲取頁面標題(作爲離線文件的名稱)和加載進度
4、採用事件的方式,通知主Activity關於頁面加載開始、加載結束、標題、加載進度等的一些事情,進而更新UI(這裏和Java的寫法有些不同)
5、頁面加載進度條
【懸浮按鈕】1、全屏(退出)按鈕 2、保存網頁 3、掃描二維碼(版本兼容問題尚未實現)
【網址輸入框】
1、輸入正確的網址之後點擊輸入法中的“前往”調轉
2、隱藏輸入法
以上列到的功能基本實現,最後在榮耀V10上測試時,其他的功能還好,就是在打開離線文件時也不報錯,就是打不開……鬱悶啊!最後查了一下也沒有找到原因。這裏說一下場景,以方便大神發現問題,希望大神不吝賜教。在我的聯想手機上測試時發現本地文件路徑是這樣的:file:///storage/emulated/0/DDZMyBrowser/SavePages/1.mht 此時可以正常瀏覽,而V10中得到的路徑是這樣的,內部存儲:content://com.huawei.hidisk.fileprovider/root/storage/emulated/0/DDZMyBrowser/SavePages/1.mht SD卡:content://com.huawei.hidisk.fileprovider/root/storage/0ABF-6213/1.mht 這兩個都打不開。我查詢的結果,這路徑應該是利用FileProvider生成的(7.0以上),哎,並非真正的android開發,並不太懂,一臉懵逼,不知道是不是因爲這個原因……開始我還寄希望於將content://轉爲file:///格式的,但是都失敗了,最後想想網上說的webview支持content://開頭的啊,自己在輸入框中手動修改爲:file:///storage/emulated/0/DDZMyBrowser/SavePages/1.mht 發現是可以征程瀏覽的……
上一下截圖:
1、應用首頁
2、再按一次退出程序
3、橫屏
4、豎屏
5、網頁中的下拉刷新
6、加載更多
7、用自己的webview中打開連接,而不是打開其他的瀏覽器;進度條
8、全屏
9、離線保存
10、關聯MHT
11、關聯HTTP和HTTPS
12、actionGo
13、最後再來一張V10加載異常的圖片
去去去,傳上去之後發現圖片太大了,全是百度的圖片……這事兒弄得
最後在貼一下代碼,記錄一下
CS代碼:
複製代碼
1 using Android.App;
2 using Android.Widget;
3 using Android.OS;
4 using Android.Webkit;
5 using System;
6 using Android.Support.Design.Widget;
7 using Android.Content;
8 using Android.Views;
9 using Java.IO;
10 using Android.Views.InputMethods;
11 using Android.Content.PM;
12 using Android.Content.Res;
13 using Android.Provider;
14 using Android.Database;
15
16 namespace DDZ.MyBrowser
17 {
18 /// <summary>
19 /// 獲取網頁Title
20 /// </summary>
21 /// <param name="title"></param>
22 public delegate void MyWebViewTitleDelegate(string title);
23
24 /// <summary>
25 /// 獲取網頁加載進度
26 /// </summary>
27 public delegate void MyWebViewProgressChangedDelegate(int newProgress);
28
29 /// <summary>
30 /// 網頁加載完成事件
31 /// </summary>
32 public delegate void MyWebViewPageFinishedDelegate();
33
34 [IntentFilter(
35 new[] { Intent.ActionView },
36 Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
37 DataSchemes = new[] { "http", "https" })]
38 [IntentFilter(
39 new[] { Intent.ActionView },
40 Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
41 DataSchemes = new[] { "file", "content" }, DataMimeType = "*/*", DataHost = "*", DataPathPattern = ".*\\\\.mhtml")]
42 [IntentFilter(
43 new[] { Intent.ActionView },
44 Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
45 DataMimeType = "*/*", DataSchemes = new[] { "file", "content" }, DataHost = "*", DataPathPattern = ".*\\\\.mht")]
46 [Activity(Label = "@string/app_name", Theme = "@style/AppTheme", MainLauncher = true,
47 ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.KeyboardHidden)]
48 public class MainActivity : Activity
49 {
50 WebView myBrowser;
51 EditText edtTxtUrl;
52 FloatingActionButton fabMain;
53 FloatingActionButton fabSubQRcodeScan;
54 FloatingActionButton fabSubToggleFullScreen;
55 FloatingActionButton fabSubSaveMHT;
56 ProgressBar myBrowserPBar;
57
58 private static bool isFabOpen;
59 private static bool isFullScreen;
60 private static DateTime lastClickGoBack = DateTime.Now;
61
62 private static string currentPageTitle;
63 private readonly string externalStorageDirPath = Android.OS.Environment.ExternalStorageDirectory.AbsolutePath;
64 private readonly string selfFolderName = "DDZMyBrowser";
65 private static string selfApplicationDirPath;
66 protected override void OnCreate(Bundle savedInstanceState)
67 {
68 // https://blog.csdn.net/niunan/article/details/71774292
69 base.OnCreate(savedInstanceState);
70 // Set our view from the "main" layout resource
71 SetContentView(Resource.Layout.activity_main);
72
73 // 1、瀏覽器控件相關
74 myBrowser = FindViewById<WebView>(Resource.Id.myBrowser);
75 // 要與Javascript交互,則webview必須設置支持Javascript
76 myBrowser.Settings.JavaScriptEnabled = true;
77 // 支持通過JS打開新窗口
78 myBrowser.Settings.JavaScriptCanOpenWindowsAutomatically = true;
79 myBrowser.Settings.DomStorageEnabled = true;
80 myBrowser.Settings.AllowFileAccessFromFileURLs = true;
81
82 var myWebViewClient = new MyWebViewClient();
83 myWebViewClient.GetWebViewPageFinishedDelegate += MyWebViewClient_GetWebViewPageFinishedDelegate;
84 myBrowser.SetWebViewClient(myWebViewClient);
85 var myWebChromeClient = new MyWebChromeClient();
86 myWebChromeClient.GetWebViewTitleDelegate += MyWebChromeClient_GetWebViewTitleDelegate;
87 myWebChromeClient.GetWebViewProgressChangedDelegate += MyWebChromeClient_GetWebViewProgressChangedDelegate;
88 myBrowser.SetWebChromeClient(myWebChromeClient);
89 edtTxtUrl = FindViewById<EditText>(Resource.Id.edtTxtUrl);
90 edtTxtUrl.EditorAction += EdtTxtUrl_EditorAction;
91 myBrowserPBar = FindViewById<ProgressBar>(Resource.Id.myBrowserPBar);
92
93 // 2、右下方懸浮控件
94 fabMain = FindViewById<FloatingActionButton>(Resource.Id.fabMain);
95 fabSubQRcodeScan = FindViewById<FloatingActionButton>(Resource.Id.fabSubQRcodeScan);
96 fabSubToggleFullScreen = FindViewById<FloatingActionButton>(Resource.Id.fabSubToggleFullScreen);
97 fabSubSaveMHT = FindViewById<FloatingActionButton>(Resource.Id.fabSubSaveMHT);
98 fabMain.Click += FabMain_Click;
99 fabSubQRcodeScan.Click += FabSubQRcodeScan_Click;
100 fabSubToggleFullScreen.Click += FabSubToggleFullScreen_Click; ;
101 fabSubSaveMHT.Click += FabSubSaveMHT_Click;
102
103 // 3、第三方應用使用該應用打開網頁時,"this.Intent.DataString" 獲取需要打開的網址
104 // 自己打開時,"this.Intent.DataString" 的值爲空
105 String url = this.Intent.DataString;
106 if (!String.IsNullOrEmpty(url))
107 {
108 if (this.Intent.Data.Scheme == "content")
109 {
110 // DocumentsContract.IsDocumentUri(this, this.Intent.Data):false
111
112 // this.Intent.Data.Authority:com.huawei.hidisk.fileprovider
113 // this.Intent.Data.Host:com.huawei.hidisk.fileprovider
114 // this.Intent.Data.Path:/root/storage/0ABF-6213/xxx.mht
115 // this.Intent.Data.PathSegments:this.Intent.Data.Path的數組形式
116
117 // Android.Support.V4.Content.FileProvider.GetUriForFile()
118 // this.Intent.SetFlags(ActivityFlags.GrantReadUriPermission).SetFlags(ActivityFlags.GrantWriteUriPermission);
119 }
120 }
121 edtTxtUrl.Text = url;
122 LoadOnePage(url);
123
124 // 4、創建應用目錄
125 CreateSelfApplicationFolder();
126 }
127
128 private void EdtTxtUrl_EditorAction(object sender, TextView.EditorActionEventArgs e)
129 {
130 string inputUrl = edtTxtUrl.Text.Trim();
131 if (e.ActionId == ImeAction.Go)
132 {
133 HideSoftInputFn();
134 LoadOnePage(inputUrl);
135 }
136 }
137
138 #region 獲取WebView加載頁面相關信息的一些自定義事件
139 private void MyWebViewClient_GetWebViewPageFinishedDelegate()
140 {
141 Toast.MakeText(this, "加載完成", ToastLength.Long).Show();
142 }
143
144 private void MyWebChromeClient_GetWebViewProgressChangedDelegate(int newProgress)
145 {
146 myBrowserPBar.Visibility = ViewStates.Visible;
147 myBrowserPBar.Progress = newProgress;
148 if (newProgress == 100)
149 {
150 myBrowserPBar.Visibility = ViewStates.Gone;
151 }
152 }
153
154 private void MyWebChromeClient_GetWebViewTitleDelegate(string title)
155 {
156 currentPageTitle = title;
157 }
158 #endregion
159
160 #region 懸浮按鈕
161 private void FabMain_Click(object sender, EventArgs e)
162 {
163 if (!isFabOpen)
164 {
165 HideSoftInputFn();
166 ShowFabMenu();
167 }
168 else
169 {
170 CloseFabMenu();
171 }
172 SetToggleFullScreenBtnImg();
173 }
174
175 private void FabSubQRcodeScan_Click(object sender, EventArgs e)
176 {
177 Toast.MakeText(this, "掃描二維碼", ToastLength.Long).Show();
178 }
179
180 private void FabSubSaveMHT_Click(object sender, EventArgs e)
181 {
182 string savePageDirPath = $"{selfApplicationDirPath}{File.Separator}SavePages";
183 File dir = new File(savePageDirPath);
184 if (!dir.Exists())
185 {
186 bool retBool = dir.Mkdir();
187 }
188 myBrowser.SaveWebArchive($"{savePageDirPath}{File.Separator}{currentPageTitle}.mht");
189 }
190
191 private void FabSubToggleFullScreen_Click(object sender, EventArgs e)
192 {
193 if (isFullScreen)
194 { // 目前爲全屏狀態,修改爲非全屏
195 edtTxtUrl.Visibility = ViewStates.Visible;
196 this.Window.ClearFlags(WindowManagerFlags.Fullscreen);
197 }
198 else
199 { // 目前爲非全屏狀態,修改爲全屏
200 edtTxtUrl.Visibility = ViewStates.Gone;
201 this.Window.SetFlags(WindowManagerFlags.Fullscreen, WindowManagerFlags.Fullscreen);
202 }
203 isFullScreen = !isFullScreen;
204 SetToggleFullScreenBtnImg();
205 }
206
207 private void ShowFabMenu()
208 {
209 isFabOpen = true;
210 fabSubQRcodeScan.Visibility = ViewStates.Visible;
211 fabSubToggleFullScreen.Visibility = ViewStates.Visible;
212 fabSubSaveMHT.Visibility = ViewStates.Visible;
213
214 fabMain.Animate().Rotation(135f);
215 fabSubQRcodeScan.Animate()
216 .TranslationY(-600f)
217 .Rotation(0f);
218 fabSubToggleFullScreen.Animate()
219 .TranslationY(-410f)
220 .Rotation(0f);
221 fabSubSaveMHT.Animate()
222 .TranslationY(-220f)
223 .Rotation(0f);
224 }
225
226 private void CloseFabMenu()
227 {
228 isFabOpen = false;
229
230 fabMain.Animate().Rotation(0f);
231 fabSubQRcodeScan.Animate()
232 .TranslationY(0f)
233 .Rotation(90f);
234 fabSubToggleFullScreen.Animate()
235 .TranslationY(0f)
236 .Rotation(90f);
237 fabSubSaveMHT.Animate()
238 .TranslationY(0f)
239 .Rotation(90f);
240
241 fabSubQRcodeScan.Visibility = ViewStates.Gone;
242 fabSubToggleFullScreen.Visibility = ViewStates.Gone;
243 fabSubSaveMHT.Visibility = ViewStates.Gone;
244 }
245
246 private void SetToggleFullScreenBtnImg()
247 {
248 if (isFullScreen)
249 {
250 fabSubToggleFullScreen.SetImageResource(Resource.Drawable.fullscreenExit);
251 }
252 else
253 {
254 fabSubToggleFullScreen.SetImageResource(Resource.Drawable.fullscreen);
255 }
256 }
257 #endregion
258
259 #region 重寫基類 Activity 方法
260 public override void OnConfigurationChanged(Configuration newConfig)
261 {
262 base.OnConfigurationChanged(newConfig);
263 if (newConfig.Orientation == Android.Content.Res.Orientation.Portrait)
264 {
265 edtTxtUrl.Visibility = ViewStates.Visible;
266 fabMain.Visibility = ViewStates.Visible;
267 this.Window.ClearFlags(WindowManagerFlags.Fullscreen);
268 isFullScreen = false;
269 Toast.MakeText(Application.Context, "豎屏模式!", ToastLength.Long).Show();
270 }
271 else
272 {
273 CloseFabMenu();
274 edtTxtUrl.Visibility = ViewStates.Gone;
275 fabMain.Visibility = ViewStates.Gone;
276 this.Window.SetFlags(WindowManagerFlags.Fullscreen, WindowManagerFlags.Fullscreen);
277 isFullScreen = true;
278 Toast.MakeText(Application.Context, "橫屏模式!", ToastLength.Long).Show();
279 }
280 }
281 public override void OnBackPressed()
282 {
283 if (myBrowser.CanGoBack())
284 {
285 myBrowser.GoBack();
286 }
287 else
288 {
289 if ((DateTime.Now - lastClickGoBack).Seconds > 2)
290 {
291 Toast.MakeText(this, $"再按一次退出程序", ToastLength.Long).Show();
292 lastClickGoBack = DateTime.Now;
293 }
294 else
295 {
296 this.Finish();
297 }
298 }
299 }
300 #endregion
301
302 private void LoadOnePage(String url = "")
303 {
304 currentPageTitle = null;
305 if (String.IsNullOrEmpty(url)) url = "https://www.baidu.com/";
306 myBrowser.LoadUrl(url);
307 }
308
309 private void HideSoftInputFn()
310 {
311 // 隱藏鍵盤
312 InputMethodManager imm = (InputMethodManager)this.GetSystemService(Context.InputMethodService);
313 imm.HideSoftInputFromWindow(edtTxtUrl.WindowToken, 0);
314 }
315
316 private void CreateSelfApplicationFolder()
317 {
318 selfApplicationDirPath = $"{externalStorageDirPath}{File.Separator}{selfFolderName}";
319 File dir = new File(selfApplicationDirPath);
320 if (!dir.Exists())
321 {
322 bool retBool = dir.Mkdir();
323 }
324 }
325
326 private String GetRealPathFromURI(Context context, Android.Net.Uri uri)
327 {
328 String retPath = null;
329 if (context == null || uri == null) return retPath;
330 Boolean isKitKat = Build.VERSION.SdkInt >= BuildVersionCodes.Kitkat;
331 if (isKitKat && DocumentsContract.IsDocumentUri(context, uri))
332 {
333 if (uri.Authority.Equals("com.android.externalstorage.documents", StringComparison.OrdinalIgnoreCase))
334 {
335 String docId = DocumentsContract.GetDocumentId(uri);
336 String[] split = docId.Split(':');
337 if (split[0].Equals("primary", StringComparison.OrdinalIgnoreCase))
338 {
339 retPath = Android.OS.Environment.ExternalStorageDirectory + "/" + split[1];
340 }
341 }
342 }
343 return retPath;
344 }
345
346 private String GetFilePathFromContentUri(Context context,Android.Net.Uri url)
347 {
348 String filePath = null;
349 String[] filePathColumn = { MediaStore.MediaColumns.Data };
350 using (ICursor cursor = context.ContentResolver.Query(url, filePathColumn, null, null, null))
351 {
352 if (cursor != null && cursor.MoveToFirst())
353 {
354 int columnIndex = cursor.GetColumnIndexOrThrow(filePathColumn[0]);
355 if (columnIndex > -1)
356 {
357 filePath = cursor.GetString(columnIndex);
358 }
359 }
360 }
361 return filePath;
362 }
363 }
364
365 public class MyWebViewClient : WebViewClient
366 {
367 public event MyWebViewPageFinishedDelegate GetWebViewPageFinishedDelegate;
368 public override bool ShouldOverrideUrlLoading(WebView view, IWebResourceRequest request)
369 {
370 view.LoadUrl(request.Url.ToString());
371 return true;
372 }
373
374 public override void OnPageFinished(WebView view, string url)
375 {
376 base.OnPageFinished(view, url);
377 GetWebViewPageFinishedDelegate();
378 }
379 }
380
381 public class MyWebChromeClient : WebChromeClient
382 {
383 public event MyWebViewTitleDelegate GetWebViewTitleDelegate;//聲明一個事件
384
385 public event MyWebViewProgressChangedDelegate GetWebViewProgressChangedDelegate;
386 public override void OnReceivedTitle(WebView view, string title)
387 {
388 base.OnReceivedTitle(view, title);
389 GetWebViewTitleDelegate(title);
390 }
391
392 public override void OnProgressChanged(WebView view, int newProgress)
393 {
394 base.OnProgressChanged(view, newProgress);
395 GetWebViewProgressChangedDelegate(newProgress);
396 }
397 }
398 }
複製代碼
佈局代碼:
複製代碼
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="3dip"
android:max="100"
android:progress="0"
android:visibility="gone"
android:id="@+id/myBrowserPBar" />
<android.webkit.WebView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:id="@+id/myBrowser" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/edtTxtUrl"
android:inputType="text"
android:singleLine="true"
android:imeOptions="actionGo"
android:hint="請輸入網址" />
</LinearLayout>
</LinearLayout>
<android.support.design.widget.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_margin="18dp"
android:visibility="gone"
android:rotation="90"
android:src="@drawable/qrcodeScan"
app:fabSize="mini"
android:id="@+id/fabSubQRcodeScan" />
<android.support.design.widget.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_margin="18dp"
android:visibility="gone"
android:rotation="90"
android:src="@drawable/saveMht"
app:fabSize="mini"
android:id="@+id/fabSubSaveMHT" />
<android.support.design.widget.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_margin="18dp"
android:visibility="gone"
android:rotation="90"
app:fabSize="mini"
android:id="@+id/fabSubToggleFullScreen" />
<android.support.design.widget.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_margin="10dp"
android:src="@drawable/plus"
app:fabSize="normal"
android:id="@+id/fabMain" />
</RelativeLayout>
複製代碼
用到的一些權限
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.BIND_INPUT_METHOD" />
到此就結束了,webview在8.0下打開content://文件失敗的問題,如果有大神看到,希望幫忙解答,十分感謝!以後有機會在慢慢完善他的功能。
【2018-06-26更新】
昨天在小米note3測試本應用,本來是想測試一下看看能不能打開離線文件的問題,但是連關聯MHT都不行,打開的時候,應用列表沒有該應用……鬱悶了,越淌水越深啊!
【2018-11-08更新】
開始因爲版本兼容的問題,掃描二維碼未處理,一直想着過後看看官方能不能解決,但是今天看看,還是不行……但是VS給出瞭解決方案:
VS會依次提示單獨安裝上述的幾個包,開始這個項目只安裝了一個包,現在的依賴如下:
這次添加的相機權限
<uses-permission android:name="android.permission.CAMERA" />
相機相關代碼
1、初始化
複製代碼
protected override void OnCreate(Bundle savedInstanceState)
{
………………
………………
// 5、初始化
MobileBarcodeScanner.Initialize(Application);
}
複製代碼
2、點擊掃描二維碼執行的代碼
複製代碼
private void FabSubQRcodeScan_Click(object sender, EventArgs e)
{
//Toast.MakeText(this, "掃描二維碼", ToastLength.Long).Show();
Task.Run(() =>
{
var scanner = new MobileBarcodeScanner();
var result = scanner.Scan();
if (result != null)
{
string scanResult = result.Result.Text;
this.RunOnUiThread(new Java.Lang.Runnable(() =>
{
edtTxtUrl.Text = scanResult;
LoadOnePage(scanResult);
}));
}
});
}
複製代碼
經過這次更新,就可以掃描二維碼了……