基于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);
}));
}
});
}
复制代码
经过这次更新,就可以扫描二维码了……