笔记 | Xamarin

引言

文件读写

参考:

外部读写

应用可以在外部存储上保留两种不同类型的文件:

  • 专用 文件 – 专用文件是特定于应用程序的文件(但仍然全局可读且全局可写)。 Android 期望专用文件存储在外部存储上的特定目录中。 尽管这些文件称为“专用”,但它们仍然可见,并且可由设备上的其他应用访问,Android 并没有对它们提供任何特殊保护。

    1
    2
    3
    // 专用外部存储目录
    // /storage/emulated/0/Android/data/com.companyname.app/files/
    Android.Content.Context.GetExternalFilesDir(string type)
  • 公共 文件 – 这些文件不被视为特定于应用程序,可自由共享。

    1
    2
    3
    // 主外部存储目录
    // /storage/emulated/0/
    Android.OS.Environment.ExternalStorageDirectory

Android 将外部存储视为危险权限,这通常要求用户授予其访问资源的权限。 用户可以随时撤销此权限。 这意味着在进行任何文件访问之前都应执行运行时权限请求。 应用会被自动授予读取和写入其自己的专用文件的权限。 在用户授予了权限之后,应用可以读取和写入属于其他应用的专用文件。

1
2
3
4
5
6
//global::Android.OS.Environment.ExternalStorageDirectory.AbsolutePath :得到安卓的根目录
//Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)::得到安卓data目录
var path = global::Android.OS.Environment.ExternalStorageDirectory.AbsolutePath + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);

// 创建文件
System.IO.Directory.CreateDirectory(path);

外部存储权限

所有 Android 应用都必须在 AndroidManifest.xml 中为外部存储声明两个权限之一。

若要标识权限,必须将以下两个 uses-permission 元素之一添加到 AndroidManifest.xml:

AndroidManifest.xml
1
2
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

注意:下面有误

如上,在安卓项目里有个Properties的文件下有个AndroidManifest.xml的文件。在

1
<application android:label="cardionNet2.Android"></application> 

下加

这个目测有误,直接加进去就是了,不需要放在这个;里

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.demoapp" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
<application android:label="DemoApp.Android" android:theme="@style/MainTheme"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
</manifest>

写入外部存储之前的第一步是检查它是可读或可写。

Android.OS.Environment.ExternalStorageState 属性保存标识外部存储状态的字符串。 此属性会返回表示状态的字符串。

1
2
bool isReadonly = Environment.MediaMountedReadOnly.Equals(Environment.ExternalStorageState);
bool isWriteable = Environment.MediaMounted.Equals(Environment.ExternalStorageState);

完全限定

1
2
bool isReadonly = Android.OS.Environment.MediaMountedReadOnly.Equals(Android.OS.Environment.ExternalStorageState);
bool isWriteable = Android.OS.Environment.MediaMounted.Equals(Android.OS.Environment.ExternalStorageState);

应用生命周期

参考:

Application 基类提供下列功能:

生命周期方法

Application 类包含三个虚拟方法,可以替代以响应生命周期更改:

  • OnStart - 在启动应用程序时调用它。
  • OnSleep - 每当应用程序转入后台时调用它。
  • OnResume - 应用程序发送到后台后恢复时调用。

布局

参考:

JarBinding Bugly

Xamarin.Forms Shell

参考:

视觉层次结构

Shell -> FlyoutItem / TabBar -> Tab -> ShellContent -> ContentPage

FloutItem: 浮出控件

TabBar: 底部选项卡栏

Tab: 分组内容

Tab 中存在多个 ShellContent,时,会在内部再次分布,

Tab 父级是 TabBar,则会在那个页面显示 顶部导航选项卡,以对应多个 ShellContent

Tab 父级是 FlyoutItem,则会在对应条下显示多个子条 (ShellContent)

若在 FloutItem / TabBar 中直接写 ShellContent,则会将每个ShellContent 隐式包裹在一个 Tab

补充:

TabBar 类是 ShellItem 类的别名,而 Tab 类是 ShellSection 类的别名。

因此,也可以

Shell -> FlyoutItem / ShellItem -> ShellSection -> ShellContent -> ContentPage

因此,在为 FlyoutItem 对象创建自定义呈现器时应重写 CreateShellItemRenderer 方法,在为 Tab 对象创建自定义呈现器时应重写 CreateShellSectionRenderer 方法。

1
2
3
<!-- When the Flyout is visible this will be a menu item you can tie a click behavior to  -->
<MenuItem Text="Logout" StyleClass="MenuItemLayoutStyle" Clicked="OnMenuItemClicked">
</MenuItem>

侧边浮出注销按钮

当 侧边 (Flyout) 浮出显示 时,MenItem 就会显示

MenuItem: 浮出控件的菜单项

浮出控件

参考:

可以通过图标或从屏幕的一侧轻扫来访问它。

浮出控件由可选标头、浮出控件项、可选菜单项和可选页脚组成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 
When the Flyout is visible this defines the content to display in the flyout.
FlyoutDisplayOptions="AsMultipleItems" will create a separate flyout item for each child element
https://docs.microsoft.com/dotnet/api/xamarin.forms.shellgroupitem.flyoutdisplayoptions?view=xamarin-forms
-->
<FlyoutItem Title="首页" Icon="icon_about.png">
<ShellContent Route="HomePage" ContentTemplate="{DataTemplate local:HomePage}" />
</FlyoutItem>
<FlyoutItem Title="列表" Icon="icon_feed.png">
<ShellContent Route="ItemsPage" ContentTemplate="{DataTemplate local:ItemsPage}" />
</FlyoutItem>
<FlyoutItem Title="设置" Icon="icon_setting.png">
<ShellContent Route="SettingPage" ContentTemplate="{DataTemplate local:SettingPage}" />
</FlyoutItem>

隐式转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Xaminals.Controls"
xmlns:views="clr-namespace:Xaminals.Views"
x:Class="Xaminals.AppShell">
<FlyoutItem Title="Cats"
Icon="cat.png">
<Tab>
<ShellContent ContentTemplate="{DataTemplate views:CatsPage}" />
</Tab>
</FlyoutItem>
<FlyoutItem Title="Dogs"
Icon="dog.png">
<Tab>
<ShellContent ContentTemplate="{DataTemplate views:DogsPage}" />
</Tab>
</FlyoutItem>
</Shell>

等同于下方:

1
2
3
4
5
6
7
8
9
10
11
12
<Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Xaminals.Controls"
xmlns:views="clr-namespace:Xaminals.Views"
x:Class="Xaminals.AppShell">
<ShellContent Title="Cats"
Icon="cat.png"
ContentTemplate="{DataTemplate views:CatsPage}" />
<ShellContent Title="Dogs"
Icon="dog.png"
ContentTemplate="{DataTemplate views:DogsPage}" />
</Shell>

此隐式转换自动将每个 ShellContent 对象包装在 Tab 对象中,而 Tab 则包装在 FlyoutItem 对象中。

Shell 中默认 FlyoutItem,

FlyoutItem / TabBar 中默认 Tab

备注

子类化的 Shell 对象中的所有 FlyoutItem 对象都会自动添加到 Shell.FlyoutItems 集合,

该集合定义将在浮出控件中显示的项的列表。

定义 FlyoutItem 外观

通过将 Shell.ItemTemplate 附加属性设置为 DataTemplate 可自定义每个 FlyoutItem 的外观:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<Shell ...>
...
<Shell.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="0.2*,0.8*">
<Image Source="{Binding FlyoutIcon}"
Margin="5"
HeightRequest="45" />
<Label Grid.Column="1"
Text="{Binding Title}"
FontAttributes="Italic"
VerticalTextAlignment="Center" />
</Grid>
</DataTemplate>
</Shell.ItemTemplate>
</Shell>

FontAttributes="Italic"

此示例以斜体显示每个 FlyoutItem 对象的标题:

Shell.ItemTemplate 是一个附加属性,因此可将不同的模板附加到特定的 FlyoutItem 对象。

替换浮出控件内容

浮出项表示浮出控件内容,可以选择将其替换为你自己的内容,方法是将 Shell.FlyoutContent 可绑定属性设置为 object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<Shell ...
x:Name="shell">
...
<Shell.FlyoutContent>
<CollectionView BindingContext="{x:Reference shell}"
IsGrouped="True"
ItemsSource="{Binding FlyoutItems}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Label Text="{Binding Title}"
TextColor="White"
FontSize="Large" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Shell.FlyoutContent>
</Shell>

在此示例中,将浮出控件内容替换为 CollectionView,它显示了 FlyoutItems 集合中每个项的标题。

此外,可以通过将 Shell.FlyoutContentTemplate 可绑定属性设置为 DataTemplate 来定义浮出控件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<Shell ...
x:Name="shell">
...
<Shell.FlyoutContentTemplate>
<DataTemplate>
<CollectionView BindingContext="{x:Reference shell}"
IsGrouped="True"
ItemsSource="{Binding FlyoutItems}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Label Text="{Binding Title}"
TextColor="White"
FontSize="Large" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</DataTemplate>
</Shell.FlyoutContentTemplate>
</Shell>

FlyoutItem 选择

场景: 有时候,默认并不需要显示第一个

首次运行使用浮出控件的 Shell 应用程序时,Shell.CurrentItem 属性将设置为子类化的 Shell 对象中的第一个 FlyoutItem 对象。 但是,此属性可以设置为另一个 FlyoutItem,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
<Shell ...
CurrentItem="{x:Reference aboutItem}">
<FlyoutItem FlyoutDisplayOptions="AsMultipleItems">
...
</FlyoutItem>
<ShellContent x:Name="aboutItem"
Title="About"
Icon="info.png"
ContentTemplate="{DataTemplate views:AboutPage}" />
</Shell>

此示例将 CurrentItem 属性设置为名为 aboutItemShellContent 对象,这将导致选中并显示该对象。

在此示例中,隐式转换用于将 ShellContent 对象包装在 Tab 对象中,后者包装在 FlyoutItem 对象中。

假设有一个名为 aboutItemShellContent 对象,则等效的 C# 代码为:

1
CurrentItem = aboutItem;

在此示例中,CurrentItem 属性是在子类化的 Shell 类中设置的。 或者,可通过 Shell.Current 静态属性在任何类中设置 CurrentItem 属性:

1
Shell.Current.CurrentItem = aboutItem;

FlyoutItem 可见性

浮出项在浮出控件中默认可见。 但是,可以使用 FlyoutItemIsVisible 属性将项隐藏在浮出控件中,并使用 IsVisible 属性将其从浮出控件中删除:

  • 类型为 boolFlyoutItemIsVisible 指示项是否已隐藏在浮出控件中但仍可以通过 GoToAsync 导航方法进行访问。 此属性的默认值为 true
  • 类型为 boolIsVisible 指示是否应从可视化树中移除项,从而不在浮出控件中显示。 它的默认值为 true

备注

还有一个 Shell.FlyoutItemIsVisible 附加属性,可在 FlyoutItemMenuItemTabShellContent 对象上设置该属性。

以编程方式打开和关闭浮出控件

1
2
3
<Shell ...
FlyoutIsPresented="{Binding IsFlyoutOpen}">
</Shell>
1
Shell.Current.FlyoutIsPresented = false;

底部导航栏

参考:

1
2
3
4
<TabBar>
<ShellContent Title="About" Icon="icon_about.png" Route="AboutPage" ContentTemplate="{DataTemplate local:AboutPage}" />
<ShellContent Title="Browse" Icon="icon_feed.png" ContentTemplate="{DataTemplate local:ItemsPage}" />
</TabBar>

目测,不加 Title, Icon 就会隐藏起来,那么这个时候就只能通过代码导航到这里了。

1
2
3
4
5
6
7
<!--
If you would like to navigate to this content you can do so by calling
await Shell.Current.GoToAsync("//LoginPage");
-->
<TabBar>
<ShellContent Route="LoginPage" ContentTemplate="{DataTemplate local:LoginPage}" />
</TabBar>

单页

TabBar 中只有 一个 ShellContent,就不会显示底部选项卡导航栏

1
2
3
4
5
6
7
8
9
10
<Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:Xaminals.Views"
x:Class="Xaminals.AppShell">
<TabBar>
<Tab>
<ShellContent ContentTemplate="{DataTemplate views:CatsPage}" />
</Tab>
</TabBar>
</Shell>

底部选项卡

倘若单个 TabBar 对象中有多个 Tab 对象,则 Tab 对象呈现为底部选项卡:

类型为 stringTitle 属性,可定义选项卡标题。 类型为 ImageSourceIcon 属性,可定义选项卡图标:

如果 TabBar 上有五个以上的选项卡,则显示“更多”选项卡,可用于访问其他选项卡:

底部和顶部选项卡

如果一个 Tab 对象中存在多个 ShellContent 对象时,则将在底部选项卡中添加一个顶部选项卡栏,通过该选项卡栏可以导航 ContentPage 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:Xaminals.Views"
x:Class="Xaminals.AppShell">
<TabBar>
<Tab Title="Domestic"
Icon="paw.png">
<ShellContent Title="Cats"
ContentTemplate="{DataTemplate views:CatsPage}" />
<ShellContent Title="Dogs"
ContentTemplate="{DataTemplate views:DogsPage}" />
</Tab>
<Tab Title="Monkeys"
Icon="monkey.png">
<ShellContent ContentTemplate="{DataTemplate views:MonkeysPage}" />
</Tab>
</TabBar>
</Shell>

选项卡选择

首次运行使用选项卡栏的 Shell 应用程序时,Shell.CurrentItem 属性将设置为子类化的 Shell 对象中的第一个 Tab 对象。 但是,此属性可以设置为另一个 Tab,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
<Shell ...
CurrentItem="{x:Reference dogsItem}">
<TabBar>
<ShellContent Title="Cats"
Icon="cat.png"
ContentTemplate="{DataTemplate views:CatsPage}" />
<ShellContent x:Name="dogsItem"
Title="Dogs"
Icon="dog.png"
ContentTemplate="{DataTemplate views:DogsPage}" />
</TabBar>
</Shell>

补充

同时显示 浮出、底部导航栏

参考:

没办法直接在Shell中,同时显式定义 FlyoutItemTabBar

只能通过 FlyoutItem 隐式达到效果

注意: 并没有在 FlyoutItem 上使用 FlyoutDisplayOptions="AsMultipleItems"

这会导致 首页、游戏、频道、动态也显示在侧边浮出栏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!-- 显示在底部导航栏 -->
<FlyoutItem Title="首页" Icon="icon_about.png">
<Tab Title="首页" Icon="icon_about.png">
<ShellContent x:Name="HotPageItem"
Title="热门"
ContentTemplate="{DataTemplate local:HotPage}" />
<ShellContent x:Name="RecomPageItem"
Title="推荐"
ContentTemplate="{DataTemplate local:RecomPage}" />
<ShellContent x:Name="LastPageItem"
Title="最新"
ContentTemplate="{DataTemplate local:LastPage}" />
</Tab>
<Tab Title="游戏" Icon="icon_feed.png">
<ShellContent ContentTemplate="{DataTemplate local:ItemsPage}" />
</Tab>
<Tab Title="频道" Icon="icon_feed.png">
<ShellContent ContentTemplate="{DataTemplate local:ItemsPage}" />
</Tab>
<Tab Title="动态" Icon="icon_feed.png">
<ShellContent ContentTemplate="{DataTemplate local:ItemsPage}" />
</Tab>
</FlyoutItem>
<!-- 显示在侧边浮出栏 -->
<FlyoutItem Title="关于" Icon="icon_about.png">
<ShellContent ContentTemplate="{DataTemplate local:HomePage}" />
</FlyoutItem>
<FlyoutItem Title="设置" Icon="icon_setting.png">
<ShellContent ContentTemplate="{DataTemplate local:SettingPage}" />
</FlyoutItem>

补充

让首页默认选中 第二个 推荐,在 首页 项使用 CurrentItem

1
2
3
4
5
6
7
8
9
10
11
<Tab Title="首页" Icon="icon_about.png" CurrentItem="{x:Reference RecomPageItem}">
<ShellContent x:Name="HotPageItem"
Title="热门"
ContentTemplate="{DataTemplate local:HotPage}" />
<ShellContent x:Name="RecomPageItem"
Title="推荐"
ContentTemplate="{DataTemplate local:RecomPage}" />
<ShellContent x:Name="LastPageItem"
Title="最新"
ContentTemplate="{DataTemplate local:LastPage}" />
</Tab>

Shell 添加手势、滑动

参考:

官方没有实现 底部选项导航栏(包括子项顶部导航栏) 滑动动画切换页面

[Feature] Swipe left/right to navigate between upper/bottom tabs of Shell · Issue #12435 · xamarin/Xamarin.Forms

Xamarin.Forms 滑动、手势

参考:

滚动视图 ScrollView

在Xamarin.Forms中,滚动视图ScrollView用来实现长内容的滚动显示。虽然ScrollView的Content属性只能设置一个值,即ScrollView只能包含一个子元素,但它实际是一个布局控件,一个特殊的布局元素。

在使用的时候,ScrollView要求父容器给它分配固定的大小,同时子元素并且有固定的大小。这样,ScrollView才能根据各自大小计算滚动量。ScrollView不仅提供了当前滚动量ScrollX和ScrollY,还提供内容总量ContentSize。这样,开发者就可以计算滚动进度,显示给用户。同时,利用ScrollView提供的滚动结束事件Scrolled,可以提示用户,或者加载新的内容。

Q&A

补充

解析 Markdown

参考:

VPN

参考:

内网穿透

参考:

自动升级

参考:

Xamarin.Android获取当前版本号

Android

1
2
3
4
5
6
7
public string GetVersion()
{
// https://stackoverflow.com/questions/47353986/xamarin-forms-forms-context-is-obsolete
var context = Android.App.Application.Context;

return context.PackageManager.GetPackageInfo(context.PackageName, 0).VersionName;
}

下方错误,Activity 继承 Context,这样 拿到的 activity=null

1
2
3
4
5
public string GetVersion()
{
var activity=Xamarin.Forms.Forms.Context as Activity;
return activity.PackageManager.GetPackageInfo(activity.PackageName, 0).VersionName;
}

iOS

1
2
3
4
public string GetVersion()
{
return NSBundle.MainBundle.InfoDictionary["CFBundleShortVersionString"].ToString();
}

安装本地apk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public void InstallAPK(string filePath)
{
try
{
Java.IO.File apkFile = new Java.IO.File(filePath);

Intent intent = new Intent(Intent.ActionView);
intent.SetFlags(ActivityFlags.NewTask);

// 注意: 直接从文件中 安装apk 和 从下载管理器中安装 不一样
// 获取下载文件的Uri
if (Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.N)
{
// Android 7.0+
Android.Net.Uri apkFileUri = Android.Support.V4.Content.FileProvider.GetUriForFile(_mContext, "github.yiyungent.onetree.fileprovider", apkFile);
intent.AddFlags(ActivityFlags.GrantReadUriPermission);
intent.SetDataAndType(apkFileUri, "application/vnd.android.package-archive");
}
else
{
Android.Net.Uri apkFileUri = Android.Net.Uri.FromFile(apkFile);
intent.SetDataAndType(apkFileUri, "application/vnd.android.package-archive");
}
_mContext.StartActivity(intent);
}
catch (Exception ex)
{

}
}
> AndroidManifest.xml
AndroidManifest.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="0.4.2" package="github.yiyungent.onetree" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
<application android:label="OneTree" android:theme="@style/MainTheme">
<provider android:name="android.support.v4.content.FileProvider" android:authorities="github.yiyungent.onetree.fileprovider" android:exported="false" android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
</provider>
</application>
<!-- 访问网络状态权限 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 读写外部存储权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 访问网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 在SDCard中创建与删除文件权限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<!-- 允许安装未知来源安装包 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
</manifest>

Resources/xml/file_paths.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!--Context.getFilesDir() 位于/data/data/安装目录-->
<files-path name="internalPath" path="file" />
<!--Context.getCacheDir()-->
<cache-path name="cachePath" path="file" />
<!--Environment.getExternalStorageDirectory()-->
<external-path name="externalPath" path="file" />
<!--Context.getExternalFilesDir(null)-->
<external-files-path name="externalFPath" path="file" />
<root-path
name="root-path"
path="." />
</paths>

参考:

  • https://stackoverflow.com/questions/54421242/how-to-make-xamarin-forms-app-auto-updated
  • https://stackoverflow.com/questions/58409164/xamarin-display-notification-in-notification-bar-once-the-file-is-downloaded

android8.0以上权限变更,若apk内下载安装包后安装,首先需要确认是否有安装未知来源应用程序的权限。

首先,需要在清单文件内加入以下权限:

1
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

安卓8以上,代码Intent调用打开apk,未唤起安装界面

解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//下载到本地后执行安装
private void InstallAPK()
{
// 获取下载文件的Uri
Android.Net.Uri downloadFileUri = _downloadManager.GetUriForDownloadedFile(_downloadId);
if (downloadFileUri != null)
{
Intent intent = new Intent(Intent.ActionView);
intent.SetDataAndType(downloadFileUri, "application/vnd.android.package-archive");
//intent.AddFlags(ActivityFlags.NewTask);

intent.SetFlags(ActivityFlags.NewTask);
intent.AddFlags(ActivityFlags.GrantReadUriPermission);

_mContext.StartActivity(intent);
}
}

注意:下方两句都要有

1
2
intent.SetFlags(ActivityFlags.NewTask);
intent.AddFlags(ActivityFlags.GrantReadUriPermission);

权限注意:

1
2
<!-- 允许安装未知来源安装包 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

同时,记得在代码中请求此权限

常用包

参考:

Firebase

参考:

界面设计

参考:

设置 Button IsEnabled="False",将会使 Button 变回默认样式

参考:

Android 11 访问 Android/data 目录

参考:

JarBinding 极光推送

参考:

Android 初始屏幕 Splash

参考:

注意:

splash_screen.xml 文件默认为 TransformFile,这样会导致 Rebuild 找不到文件

解决:

改为: AndroidResource

即,OneTree.Android.csproj,其中如下:

OneTree.Android.csproj
1
2
3
<ItemGroup>                                                          
<AndroidResource Include="Resources\drawable\splash_screen.xml" />
</ItemGroup>

选择文件

参考:

上传文件

参考:

Android 9.0 必须使用 HTTPS

参考:

AndroidManifest.xml

1
2
3
4
5
6
<application 
android:label="@string/ApplicationName"
android:theme="@style/MainTheme"
android:icon="@mipmap/icon"
android:networkSecurityConfig="@xml/network_security_config">
</application>

xml/network_security_config.xml

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Android 9.0+ 必须使用HTTPS -->
<base-config cleartextTrafficPermitted="true" />
</network-security-config>

下拉刷新,上拉加载

参考:

自定义控件

参考:

HTTP Listener

参考:

UI

参考:

分割线

参考:

App.xaml

App.xaml
1
2
3
4
5
6
7
<Style x:Key="Separator" TargetType="BoxView">
<Setter Property="HeightRequest" Value="1" />
<Setter Property="HorizontalOptions" Value="FillAndExpand" />
<Setter Property="Color" Value="Gray" />
<Setter Property="Margin" Value="0, 5, 0, 5" />
<Setter Property="Opacity" Value="0.5" />
</Style>
1
<BoxView Style="{StaticResource Separator}" />

发布到酷安

参考:

Visual Stuido 2019 找签名文件 yiyun.keystore

1.右键进入 查看归档

如果之前没有生成过 apk(Archive),请先执行一次 Archive

archive.xml

archive.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<Archive>
<Name>OneTree</Name>
<PackageName>github.yiyungent.onetree</PackageName>
<PackageVersionCode>1</PackageVersionCode>
<PackageVersionName>0.4.0</PackageVersionName>
<PackageFormat>apk</PackageFormat>
<CreationDate>637619313170861804</CreationDate>
<SolutionName>OneTree.App</SolutionName>
<SolutionPath>F:\Com\me\Repos\OneTree.App\OneTree.App.sln</SolutionPath>
<Status></Status>
<Configuration>
<DebugMode>false</DebugMode>
</Configuration>
<Comment />
<LastUsedKeystore>C:\Users\yiyun\AppData\Local\Xamarin\Mono for Android\Keystore\yiyun\yiyun.keystore</LastUsedKeystore>
<TimeStampingAuthority />
<LastInsightsUploadDate>0</LastInsightsUploadDate>
</Archive>

其中 LastUsedKeystore 即为签名文件路径

使用此签名文件,对酷安给的未签名apk ( CoolApkDevVerify_no_sign.apk )签名,生成 签名的 signed.apk

1
jarsigner -verbose -keystore yiyun.keystore -signedjar signed.apk CoolApkDevVerify_no_sign.apk yiyun
1
jarsigner -verbose -keystore [Your signature storage path] -signedjar [signed filename] [unsigned filename] [Your alias key]

补充:

查看 alias key,其实就是你当时创建秘钥时的用户名

1
keytool -keystore yiyun.keystore -list  -v
1
keytool -keystore [your key store] -list -v

yiyun.keystore:代表你的项目签名文件

signed.apk:代表你apk的签名包

CoolApkDevVerify_no_sign.apk:代表酷安提供给你的未签名包

输入上面的命令后你桌面要上传到酷安的apk会变成已签名(并且和酷安提供的未签名安装包差不多大)

其实就是将 酷安给你的 CoolApkDevVerify_no_sign.apk ,用你给你自己的apk签名的秘钥,再给这个验证apk 签名一下

其实就是下面这个,我没设置,所以没有

WebView

参考:

WebView 与 JavaScript 交互

参考:

1.js -> WebView: console.log

1
2
// Javascript 代码
console.log('{"width": "750"}');
1
2
3
4
5
6
7
8
9
10
11
// C#
public class TorchWebChromeClient : Android.Webkit.WebChromeClient
{

public override void OnConsoleMessage(string message, int lineNumber, string sourceID)
{
// message 即为 JS 传过来的消息,判断消息来决定调用方法
base.OnConsoleMessage(message, lineNumber, sourceID);
}

}

2.js -> WebView: JSBridge

最普遍方法,方便简洁,但是唯一的不足是在 4.2 系统以下存在漏洞问题

通过 addJavascriptInterface 方法进行添加对象映射

这种方法实际是向 js 环境上下文 ( Window ) 注入,以供 js 调用

实际上,下面向 window 中注入了 jsBridge.invokeActioninvokeCSharpAction,后者是前者的封装,实际上你也可以直接使用 jsBridge.invokeAction,不过一定要保证在 OnPageFinished

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class JSBridge : Java.Lang.Object
{
readonly WeakReference<HybridWebViewRenderer> hybridWebViewRenderer;

public JSBridge(HybridWebViewRenderer hybridRenderer)
{
hybridWebViewRenderer = new WeakReference<HybridWebViewRenderer>(hybridRenderer);
}

// 暴露方法名: invokeAction
[JavascriptInterface]
[Export("invokeAction")]
public void InvokeAction(string data)
{
HybridWebViewRenderer hybridRenderer;

if (hybridWebViewRenderer != null && hybridWebViewRenderer.TryGetTarget(out hybridRenderer))
{
// 调用 WebView
((HybridWebView)hybridRenderer.Element).InvokeAction(data);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[assembly: ExportRenderer(typeof(HybridWebView), typeof(HybridWebViewRenderer))]
namespace TorchView4Droid.Components
{
public class HybridWebViewRenderer : WebViewRenderer
{
const string JavascriptFunction = "function invokeCSharpAction(data){jsBridge.invokeAction(data);}";

protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
{
base.OnElementChanged(e);

if (e.OldElement != null)
{
// Unsubscribe from event handlers

Control.RemoveJavascriptInterface("jsBridge");
((HybridWebView)Element).Cleanup();
}
if (e.NewElement != null)
{
// 1.WebViewClient
var webViewClient = new JavascriptWebViewClient(this, $"javascript: {JavascriptFunction}");
Control.SetWebViewClient(webViewClient);
// 暴露在 jsBridge 对象上
// 于是最终: jsBridge.invokeAction
Control.AddJavascriptInterface(new JSBridge(this), "jsBridge");
}
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class JavascriptWebViewClient : FormsWebViewClient
{
string _javascript;

public JavascriptWebViewClient(HybridWebViewRenderer renderer, string javascript) : base(renderer)
{
_javascript = javascript;
}

public override void OnPageFinished(WebView view, string url)
{
base.OnPageFinished(view, url);
// 封装的函数被在此处 保证执行了一次,以注入
view.EvaluateJavascript(_javascript, null);
}
}

js 调用C#

1
2
3
4
5
6
7
8
function invokeCSCode(data) {
try {
log("Sending Data:" + data);
invokeCSharpAction(data);
} catch (err) {
log(err);
}
}

3. js -> WebView: WebViewClient 拦截 url

缺点: 协议的约束需要记录一个规范的文档,并且 js 无法立即获取 C# 的返回值,需要 C# 再次主动调用 js 来传递返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class JavascriptWebViewClient : FormsWebViewClient
{
public override bool ShouldOverrideUrlLoading(WebView view, IWebResourceRequest request)
{
// 拦截url, 检查 Url.Scheme 是否为你为js调用定义的 Scheme
if (request.Url != null && request.Url.Scheme != null && request.Url.Scheme.ToLower() == "js")
{
// 调用 C#
// 可以从 Query 中解析传过来的数据
var queryPars = request.Url.QueryParameterNames;

// 举例: 打开本地页面
// url = "js://openActivity?arg1=111&arg2=222"
/ ...
}

return base.ShouldOverrideUrlLoading(view, request);
}
}
1
2
3
4
// JavaScript
function openActivity(){
document.location = "js://openActivity?arg1=111&arg2=222";
}

缺点: 不能拿到 C# 的返回值,

若 js 想拿到方法的返回值,只能通过 WebView 的 loadUrl 方法去执行 js 方法把返回值传递回去,相关的代码如下:

1
webView.LoadUrl("javascript:returnResult(" + result + ")");
1
2
3
4
// JavaScript
function returnResult(result){
alert("result is" + result);
}

4.js -> WebView: WebChromeClient 三方法拦截消息

prompt 对话框方法可以返回字符串类型的返回值,

缺点: 协议的制定比较麻烦,需要记录详细的文档,但是不会存在漏洞问题

拦截 js 中的几个提示方法,也就是几种样式的对话框,在 js 中有三个常用的对话框方法:

  • OnJsAlert 方法是弹出警告框,一般情况下在 Android 中为 Toast,在文本里面加入;
  • OnJsConfirm 弹出确认框,会返回布尔值,通过这个值可以判断点击时确认还是取消,true表示点击了确认,false表示点击了取消;
  • OnJsPrompt 弹出输入框,点击确认返回输入框中的值,点击取消返回 null。
1
2
3
4
5
6
// JavaScript
function clickPrompt(){
// 优点: 可以拿到 C# 方法返回值
var result = prompt("js://openActivity?arg1=111&arg2=222");
alert("open activity " + result);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class TorchWebChromeClient : Android.Webkit.WebChromeClient
{
#region js 三对话框

public override bool OnJsAlert(WebView view, string url, string message, JsResult result)
{
return base.OnJsAlert(view, url, message, result);
}

public override bool OnJsConfirm(WebView view, string url, string message, JsResult result)
{
return base.OnJsConfirm(view, url, message, result);
}

public override bool OnJsPrompt(WebView view, string url, string message, string defaultValue, JsPromptResult result)
{
// 注意: js 传过来的数据在 message
// message = "js://openActivity?arg1=111&arg2=222"
Android.Net.Uri uri = Android.Net.Uri.Parse(message);
string scheme = uri.Scheme;
if (scheme != null && scheme.ToLower() == "js")
{
IList<string> queryPars = uri.QueryParameterNames?.ToList();


// 代表应用内部处理完成
result.Confirm("success");

return true;
}

return base.OnJsPrompt(view, url, message, defaultValue, result);
}

#endregion
}

只有 OnJsPrompt 方法可以返回字符串类型的值,放在 result (JsPromptResult) 中,所以选择拦截它

5.WebView -> js: webView.LoadUrl

缺点: C# 调用 js ,无法立即获取 js的返回值,只能通过 js再次调用 C# 来传入返回值,

loadUrl 的执行会造成页面刷新一次

1
2
// C#
mWebView.LoadUrl("javascript:show(" + result + ")");
1
2
3
4
5
// JavaScript
function show(result){
alert("result"=result);
return "success";
}

注意

方法名字对应,

还有,js 的调用一定要在 WebViewClient.OnPageFinished 函数回调之后才能调用,要不然也会失败。

6. WebView -> js: webView.EvaluateJavascript

Google 在 Android4.4 为我们新增加了一个新方法,这个方法比 loadUrl 方法更加方便简洁,而且比 loadUrl 效率更高,因为 loadUrl 的执行会造成页面刷新一次,这个方法不会,因为这个方法是在 4.4 版本才引入的,所以我们使用的时候需要添加版本的判断

1
2
3
4
5
6
7
8
9
10
string jsFuncStr = "";
if ((int)Build.VERSION.SdkInt < 18)
{
webView.LoadUrl(jsFuncStr);
}
else
{
var jsCallback = new JsFuncValueCallback();
webView.EvaluateJavascript(jsFuncStr, jsCallback);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#region JsFuncValueCallback
public class JsFuncValueCallback : Android.Webkit.IValueCallback
{
public void OnReceiveValue(Object? value)
{
// value 为 js 返回的结果

// 转换为 string 写法来自:Xamarin.Forms.Platform.Android.JavascriptResult
string data = ((Java.Lang.String)value)?.ToString();

// TODO: js 返回值处理
}

// ...
}
#endregion

一般最常使用的就是第一种方法,但是第一种方法获取返回的值比较麻烦,而第二种方法由于是在 4.4 版本引入的,所以局限性比较大。

WebView 加载本地 html

方案1:

1
file://xxxx/index.html

强烈不推荐

方案2:

在本地启动一个 WebServer,监听某个端口,url使用 http://localhost:12531

方案3:

参考:

自定义url前缀,或是 HTTP Url.Scheme, Url.Host,再通过 override WebViewClient ,拦截url请求

1
2
public override WebResourceResponse ShouldInterceptRequest( WebView view, IWebResourceRequest request ) {
}

Failed to decode downloaded font: <URL>

参考:

文本方式读写二进制文件,可能导致损坏内容

二进制方式很简单,读文件时,会原封不动的读出文件的全部內容,写的時候,也是把內存缓冲区的內容原封不动的写到文件中。

  而文本方式就不一样了,在写文件时,会将换行符号CRLF(0x0D 0x0A)全部转换成单个的0x0A,并且当遇到结束符CTRLZ(0x1A)时,就认为文件已经结束。相应的,写文件时,会将所有的0x0A换成0x0D0x0A。

所以,若使用文本方式打开二进制文件时,就很容易出现文件读不完整,或內容不对的错误。即使是用文本方式打开文本文件,也要谨慎使用,比如复制文件,就不应该使用文本方式。

Resource interpreted as Stylesheet but transferred with MIME type text/plain: "<URL>".

Java.Lang.IllegalStateException: AssetInputStream is closed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{Java.Lang.IllegalStateException: AssetInputStream is closed
at Java.Interop.JniEnvironment+InstanceMethods.CallNonvirtualIntMethod (Java.Interop.JniObjectReference instance, Java.Interop.JniObjectReference type, Java.Interop.JniMethodInfo method, Java.Interop.JniArgumentValue* args) [0x0008e] in <8b3b636835d84984ba4604c1f57b1983>:0
at Java.Interop.JniPeerMembers+JniInstanceMethods.InvokeNonvirtualInt32Method (System.String encodedMember, Java.Interop.IJavaPeerable self, Java.Interop.JniArgumentValue* parameters) [0x0001f] in <8b3b636835d84984ba4604c1f57b1983>:0
at Android.Content.Res.AssetManager+AssetInputStream.Read (System.Byte[] b, System.Int32 off, System.Int32 len) [0x00052] in <44e54a86dea24313a2bdb807df77c27a>:0
at Android.Runtime.InputStreamInvoker.Read (System.Byte[] buffer, System.Int32 offset, System.Int32 count) [0x00006] in <44e54a86dea24313a2bdb807df77c27a>:0
at System.IO.Stream.CopyTo (System.IO.Stream destination, System.Int32 bufferSize) [0x0001f] in /Users/builder/jenkins/workspace/archive-mono/2020-02/android/release/external/corert/src/System.Private.CoreLib/shared/System/IO/Stream.cs:179
at System.IO.Stream.CopyTo (System.IO.Stream destination) [0x00007] in /Users/builder/jenkins/workspace/archive-mono/2020-02/android/release/external/corert/src/System.Private.CoreLib/shared/System/IO/Stream.cs:168
at (wrapper remoting-invoke-with-check) System.IO.Stream.CopyTo(System.IO.Stream)
at TorchView.WebServer.TaskProc (System.Object obj) [0x000ac] in F:\Com\me\Repos\TorchView\src\TorchView\WebServer.cs:144
--- End of managed Java.Lang.IllegalStateException stack trace ---
java.lang.IllegalStateException: AssetInputStream is closed
at android.content.res.AssetManager$AssetInputStream.ensureOpen(AssetManager.java:1364)
at android.content.res.AssetManager$AssetInputStream.read(AssetManager.java:1303)
}

打包 apk

参考:

指定图标

ProGuard

自定义 ProGuard

或者,可添加自定义 ProGuard 配置文件,实现对 ProGuard 工具的更多掌控。 例如,你可能想就要保留的类显式通知 ProGuard。 为此,请新建 .cfg 文件,并在 解决方案资源管理器 的“属性”窗格中应用 ProGuardConfiguration 生成操作:

例如,使用了 腾讯 Bugly,则

  • 请避免混淆Bugly,在Proguard混淆文件中增加以下配置:
Proguard.cfg
1
2
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}

对于大多数 Xamarin.Android 应用,Xamarin.Android 提供的默认 ProGuard 配置文件足以删除所有(仅)未使用的代码。 若要查看默认 ProGuard 配置,请打开 **obj_xamarin.cfg** 处的文件。

请记住,该配置文件不会替换 Xamarin.Android proguard_xamarin.cfg 文件,因为 ProGuard 将使用这两者。

对应 OneTree.Android.csproj

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>portable</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release</OutputPath>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AndroidManagedSymbols>true</AndroidManagedSymbols>
<AndroidUseSharedRuntime>false</AndroidUseSharedRuntime>
<AotAssemblies>false</AotAssemblies>
<EnableLLVM>false</EnableLLVM>
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
<BundleAssemblies>true</BundleAssemblies>
<AndroidLinkTool>proguard</AndroidLinkTool>
</PropertyGroup>
1
2
3
<ItemGroup>
<ProguardConfiguration Include="ProGuard.cfg" />
</ItemGroup>

保护应用程序

禁用调试

在 Android 应用程序开发期间,将使用 Java 调试线路协议 (JDWP) 执行调试。 这是一种技术,它允许 adb 等工具出于调试目的与 JVM 通信。 默认对 Xamarin.Android 应用程序的调试版本启用 JDWP。 虽然 JDWP 在开发过程中很重要,但它会对已发布的应用程序造成安全问题。

重要

请始终禁用已发布应用程序中的调试状态,因为如果不禁用此状态,则可能(通过 JDWP)获得 Java 进程的完全访问权限并在应用程序的上下文中执行任意代码。

Android 清单包含 android:debuggable 属性,该属性控制是否可以调试应用程序。 将 android:debuggable 属性设置为 false 被视为一种很好的做法。 执行此操作最简单的方法是在 AssemblyInfo.cs 中添加条件编译语句:

1
2
3
4
5
#if DEBUG
[assembly: Application(Debuggable=true)]
#else
[assembly: Application(Debuggable=false)]
#endif

将程序集捆绑到本机代码

此选项启用时,程序集会捆绑到本机共享库中。 这样便可以对程序集进行压缩,减小 .apk 文件的大小。 程序集压缩还提供最小形式的模糊处理;此类模糊处理不应作为依据。

此选项需要 Enterprise 许可证,仅当“使用快速部署”禁用时才可用。 “将程序集捆绑到本机代码”在默认情况下处于禁用状态。

请注意,“捆绑到本机代码”选项执行不意味着程序集会编译到本机代码中。 无法使用 AOT 编译将程序集编译为本机代码。

对应 OneTree.Android.csproj

1
2
3
<PropertyGroup>
<BundleAssemblies>true</BundleAssemblies>
</PropertyGroup>

注意:

本人试用后,apk体积从 14MB 增加到 25MB,不知道为何,并没有缩小,反而增大

注意:

发现,同一套代码,同一个打包配置,

Visual Studio 2019 Professional 打包体积 13.5 MB

Visual Studio 2019 Enterprise 打包体积 19.2 MB,

居然企业版打包体积还要大些,而只有企业版有 into Native Code

配置链接器

报错: Using ProGuard with the D8 DEX compiler is no longer supported

1
Using ProGuard with the D8 DEX compiler is no longer supported. Please set the code shrinker to 'r8' in the Visual Studio project property pages or edit the project file in a text editor and set the 'AndroidLinkTool' MSBuild property to 'r8'.

解决:

ProGuard 不能与 d8 一起使用,要么 使用 ProGuard,就只能换 d8dx

或者不用 ProGuard,而是 使用 r8d8

keystore 密码修改

参考:

Visual Studio AppCenter

参考:

Xamarin 结合 GitHub Actions

参考:

参考

感谢帮助!