基于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代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 }
布局代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<?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>
用到的一些权限
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<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); })); } }); }
经过这次更新,就可以扫描二维码了……