Xamarin.Forms读取并展示Android和iOS通讯录 - TerminalMACS客户端
本文同步更新地址:
https://dotnet9.com/11520.html
https://terminalmacs.com/861.html
阅读导航:
一、功能说明
二、代码实现
三、源码获取
四、参考资料
五、后面计划
一、功能说明
完整思维导图:https://github.com/dotnet9/TerminalMACS/blob/master/docs/TerminalMACS.xmind
本文介绍图中右侧画红圈处的功能,即使用Xamarin.Forms获取和展示Android和iOS的通讯录信息,下面是最终效果,由于使用的是真实手机,所以联系人姓名及电话号码打码显示。
并简单的进行了搜索功能处理,之所以说简单,是因为通讯录列表是全部读取出来了,搜索是直接从此列表进行过滤的。
下图来自:https://www.xamboy.com/2019/10/10/getting-phone-contacts-in-xamarin-forms/,本功能是参考此文所写,所以直接引用文中的图片。
二、代码实现
1、共享库工程创建联系人实体类:Contacts.cs
namespace TerminalMACS.Clients.App.Models
{
/// <summary>
/// 通讯录
/// </summary>
public class Contact
{
/// <summary>
/// 获取或者设置名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 获取或者设置 头像
/// </summary>
public string Image { get; set; }
/// <summary>
/// 获取或者设置 邮箱地址
/// </summary>
public string[] Emails { get; set; }
/// <summary>
/// 获取或者设置 手机号码
/// </summary>
public string[] PhoneNumbers { get; set; }
}
}
2、共享库创建通讯录服务接口:IContactsService.cs
包括:
一个通讯录获取请求接口:RetrieveContactsAsync
一个读取一条通讯结果通知事件:OnContactLoaded
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using TerminalMACS.Clients.App.Models;
namespace TerminalMACS.Clients.App.Services
{
/// <summary>
/// 通讯录事件参数
/// </summary>
public class ContactEventArgs:EventArgs
{
public Contact Contact { get; }
public ContactEventArgs(Contact contact)
{
Contact = contact;
}
}
/// <summary>
/// 通讯录服务接口,android和iOS终端具体的通讯录获取服务需要继承此接口
/// </summary>
public interface IContactsService
{
/// <summary>
/// 读取一条数据通知
/// </summary>
event EventHandler<ContactEventArgs> OnContactLoaded;
/// <summary>
/// 是否正在加载
/// </summary>
bool IsLoading { get; }
/// <summary>
/// 尝试获取所有通讯录
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
Task<IList<Contact>> RetrieveContactsAsync(CancellationToken? token = null);
}
}
3、iOS工程中添加通讯录服务,实现IContactsService接口:
using Contacts;
using Foundation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using TerminalMACS.Clients.App.Models;
using TerminalMACS.Clients.App.Services;
namespace TerminalMACS.Clients.App.iOS.Services
{
/// <summary>
/// 通讯录获取服务
/// </summary>
public class ContactsService : NSObject, IContactsService
{
const string ThumbnailPrefix = "thumb";
bool requestStop = false;
public event EventHandler<ContactEventArgs> OnContactLoaded;
bool _isLoading = false;
public bool IsLoading => _isLoading;
/// <summary>
/// 异步请求权限
/// </summary>
/// <returns></returns>
public async Task<bool> RequestPermissionAsync()
{
var status = CNContactStore.GetAuthorizationStatus(CNEntityType.Contacts);
Tuple<bool, NSError> authotization = new Tuple<bool, NSError>(status == CNAuthorizationStatus.Authorized, null);
if (status == CNAuthorizationStatus.NotDetermined)
{
using (var store = new CNContactStore())
{
authotization = await store.RequestAccessAsync(CNEntityType.Contacts);
}
}
return authotization.Item1;
}
/// <summary>
/// 异步请求通讯录,此方法由界面真正调用
/// </summary>
/// <param name="cancelToken"></param>
/// <returns></returns>
public async Task<IList<Contact>> RetrieveContactsAsync(CancellationToken? cancelToken = null)
{
requestStop = false;
if (!cancelToken.HasValue)
cancelToken = CancellationToken.None;
// 我们创建了一个十进制的TaskCompletionSource
var taskCompletionSource = new TaskCompletionSource<IList<Contact>>();
// 在cancellationToken中注册lambda
cancelToken.Value.Register(() =>
{
// 我们收到一条取消消息,取消TaskCompletionSource.Task
requestStop = true;
taskCompletionSource.TrySetCanceled();
});
_isLoading = true;
var task = LoadContactsAsync();
// 等待两个任务中的第一个任务完成
var completedTask = await Task.WhenAny(task, taskCompletionSource.Task);
_isLoading = false;
return await completedTask;
}
/// <summary>
/// 异步加载通讯录,具体的通讯录读取方法
/// </summary>
/// <returns></returns>
async Task<IList<Contact>> LoadContactsAsync()
{
IList<Contact> contacts = new List<Contact>();
var hasPermission = await RequestPermissionAsync();
if (hasPermission)
{
NSError error = null;
var keysToFetch = new[] { CNContactKey.PhoneNumbers, CNContactKey.GivenName, CNContactKey.FamilyName, CNContactKey.EmailAddresses, CNContactKey.ImageDataAvailable, CNContactKey.ThumbnailImageData };
var request = new CNContactFetchRequest(keysToFetch: keysToFetch);
request.SortOrder = CNContactSortOrder.GivenName;
using (var store = new CNContactStore())
{
var result = store.EnumerateContacts(request, out error, new CNContactStoreListContactsHandler((CNContact c, ref bool stop) =>
{
string path = null;
if (c.ImageDataAvailable)
{
path = path = Path.Combine(Path.GetTempPath(), $"{ThumbnailPrefix}-{Guid.NewGuid()}");
if (!File.Exists(path))
{
var imageData = c.ThumbnailImageData;
imageData?.Save(path, true);
}
}
var contact = new Contact()
{
Name = string.IsNullOrEmpty(c.FamilyName) ? c.GivenName : $"{c.GivenName} {c.FamilyName}",
Image = path,
PhoneNumbers = c.PhoneNumbers?.Select(p => p?.Value?.StringValue).ToArray(),
Emails = c.EmailAddresses?.Select(p => p?.Value?.ToString()).ToArray(),
};
if (!string.IsNullOrWhiteSpace(contact.Name))
{
OnContactLoaded?.Invoke(this, new ContactEventArgs(contact));
contacts.Add(contact);
}
stop = requestStop;
}));
}
}
return contacts;
}
}
}
4、在iOS工程中的Info.plist文件添加通讯录权限使用说明
5、在Android工程中添加读取通讯录权限配置:AndroidManifest.xml
<uses-permission android:name="android.permission.READ_CONTACTS"/>
完整权限配置如下
<?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.terminalmacs.clients.app">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
<application android:label="TerminalMACS.Clients.App.Android"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>
6、在Android工程中添加通讯录服务,实现IContactServer接口:ContactsService.cs
using Acr.UserDialogs;
using Android;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Database;
using Android.Provider;
using Android.Runtime;
using Android.Support.V4.App;
using Plugin.CurrentActivity;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using TerminalMACS.Clients.App.Models;
using TerminalMACS.Clients.App.Services;
namespace TerminalMACS.Clients.App.Droid.Services
{
/// <summary>
/// 通讯录获取服务
/// </summary>
public class ContactsService : IContactsService
{
const string ThumbnailPrefix = "thumb";
bool stopLoad = false;
static TaskCompletionSource<bool> contactPermissionTcs;
public string TAG
{
get
{
return "MainActivity";
}
}
bool _isLoading = false;
public bool IsLoading => _isLoading;
//权限请求状态码
public const int RequestContacts = 1239;
/// <summary>
/// 获取通讯录需要的请求权限
/// </summary>
static string[] PermissionsContact = {
Manifest.Permission.ReadContacts
};
public event EventHandler<ContactEventArgs> OnContactLoaded;
/// <summary>
/// 异步请求通讯录权限
/// </summary>
async void RequestContactsPermissions()
{
//检查是否可以弹出申请读、写通讯录权限
if (ActivityCompat.ShouldShowRequestPermissionRationale(CrossCurrentActivity.Current.Activity, Manifest.Permission.ReadContacts)
|| ActivityCompat.ShouldShowRequestPermissionRationale(CrossCurrentActivity.Current.Activity, Manifest.Permission.WriteContacts))
{
// 如果未授予许可,请向用户提供其他理由用户将从使用权限的附加上下文中受益。
// 例如,如果请求先前被拒绝。
await UserDialogs.Instance.AlertAsync("通讯录权限", "此操作需要“通讯录”权限", "确定");
}
else
{
// 尚未授予通讯录权限。直接请求这些权限。
ActivityCompat.RequestPermissions(CrossCurrentActivity.Current.Activity, PermissionsContact, RequestContacts);
}
}
/// <summary>
/// 收到用户响应请求权限操作后的结果
/// </summary>
/// <param name="requestCode"></param>
/// <param name="permissions"></param>
/// <param name="grantResults"></param>
public static void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{
if (requestCode == RequestContacts)
{
// 我们请求了多个通讯录权限,因此需要检查相关的所有权限
if (PermissionUtil.VerifyPermissions(grantResults))
{
// 已授予所有必需的权限,显示联系人片段。
contactPermissionTcs.TrySetResult(true);
}
else
{
contactPermissionTcs.TrySetResult(false);
}
}
}
/// <summary>
/// 异步请求权限
/// </summary>
/// <returns></returns>
public async Task<bool> RequestPermissionAsync()
{
contactPermissionTcs = new TaskCompletionSource<bool>();
// 验证是否已授予所有必需的通讯录权限。
if (Android.Support.V4.Content.ContextCompat.CheckSelfPermission(CrossCurrentActivity.Current.Activity, Manifest.Permission.ReadContacts) != (int)Permission.Granted
|| Android.Support.V4.Content.ContextCompat.CheckSelfPermission(CrossCurrentActivity.Current.Activity, Manifest.Permission.WriteContacts) != (int)Permission.Granted)
{
// 尚未授予通讯录权限。
RequestContactsPermissions();
}
else
{
// 已授予通讯录权限。
contactPermissionTcs.TrySetResult(true);
}
return await contactPermissionTcs.Task;
}
/// <summary>
/// 异步请求通讯录,此方法由界面真正调用
/// </summary>
/// <param name="cancelToken"></param>
/// <returns></returns>
public async Task<IList<Contact>> RetrieveContactsAsync(CancellationToken? cancelToken = null)
{
stopLoad = false;
if (!cancelToken.HasValue)
cancelToken = CancellationToken.None;
// 我们创建了一个十进制的TaskCompletionSource
var taskCompletionSource = new TaskCompletionSource<IList<Contact>>();
// 在cancellationToken中注册lambda
cancelToken.Value.Register(() =>
{
// 我们收到一条取消消息,取消TaskCompletionSource.Task
stopLoad = true;
taskCompletionSource.TrySetCanceled();
});
_isLoading = true;
var task = LoadContactsAsync();
// 等待两个任务中的第一个任务完成
var completedTask = await Task.WhenAny(task, taskCompletionSource.Task);
_isLoading = false;
return await completedTask;
}
/// <summary>
/// 异步加载通讯录,具体的通讯录读取方法
/// </summary>
/// <returns></returns>
async Task<IList<Contact>> LoadContactsAsync()
{
IList<Contact> contacts = new List<Contact>();
var hasPermission = await RequestPermissionAsync();
if (!hasPermission)
{
return contacts;
}
var uri = ContactsContract.Contacts.ContentUri;
var ctx = Application.Context;
await Task.Run(() =>
{
// 暂时只请求通讯录Id、DisplayName、PhotoThumbnailUri,可以扩展
var cursor = ctx.ApplicationContext.ContentResolver.Query(uri, new string[]
{
ContactsContract.Contacts.InterfaceConsts.Id,
ContactsContract.Contacts.InterfaceConsts.DisplayName,
ContactsContract.Contacts.InterfaceConsts.PhotoThumbnailUri
}, null, null, $"{ContactsContract.Contacts.InterfaceConsts.DisplayName} ASC");
if (cursor.Count > 0)
{
while (cursor.MoveToNext())
{
var contact = CreateContact(cursor, ctx);
if (!string.IsNullOrWhiteSpace(contact.Name))
{
// 读取出一条,即通知界面展示
OnContactLoaded?.Invoke(this, new ContactEventArgs(contact));
contacts.Add(contact);
}
if (stopLoad)
break;
}
}
});
return contacts;
}
/// <summary>
/// 读取一条通讯录数据
/// </summary>
/// <param name="cursor"></param>
/// <param name="ctx"></param>
/// <returns></returns>
Contact CreateContact(ICursor cursor, Context ctx)
{
var contactId = GetString(cursor, ContactsContract.Contacts.InterfaceConsts.Id);
var numbers = GetNumbers(ctx, contactId);
var emails = GetEmails(ctx, contactId);
var uri = GetString(cursor, ContactsContract.Contacts.InterfaceConsts.PhotoThumbnailUri);
string path = null;
if (!string.IsNullOrEmpty(uri))
{
try
{
using (var stream = Android.App.Application.Context.ContentResolver.OpenInputStream(Android.Net.Uri.Parse(uri)))
{
path = Path.Combine(Path.GetTempPath(), $"{ThumbnailPrefix}-{Guid.NewGuid()}");
using (var fstream = new FileStream(path, FileMode.Create))
{
stream.CopyTo(fstream);
fstream.Close();
}
stream.Close();
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
}
var contact = new Contact
{
Name = GetString(cursor, ContactsContract.Contacts.InterfaceConsts.DisplayName),
Emails = emails,
Image = path,
PhoneNumbers = numbers,
};
return contact;
}
/// <summary>
/// 读取联系人电话号码
/// </summary>
/// <param name="ctx"></param>
/// <param name="contactId"></param>
/// <returns></returns>
string[] GetNumbers(Context ctx, string contactId)
{
var key = ContactsContract.CommonDataKinds.Phone.Number;
var cursor = ctx.ApplicationContext.ContentResolver.Query(
ContactsContract.CommonDataKinds.Phone.ContentUri,
null,
ContactsContract.CommonDataKinds.Phone.InterfaceConsts.ContactId + " = ?",
new[] { contactId },
null
);
return ReadCursorItems(cursor, key)?.ToArray();
}
/// <summary>
/// 读取联系人邮箱地址
/// </summary>
/// <param name="ctx"></param>
/// <param name="contactId"></param>
/// <returns></returns>
string[] GetEmails(Context ctx, string contactId)
{
var key = ContactsContract.CommonDataKinds.Email.InterfaceConsts.Data;
var cursor = ctx.ApplicationContext.ContentResolver.Query(
ContactsContract.CommonDataKinds.Email.ContentUri,
null,
ContactsContract.CommonDataKinds.Email.InterfaceConsts.ContactId + " = ?",
new[] { contactId },
null);
return ReadCursorItems(cursor, key)?.ToArray();
}
IEnumerable<string> ReadCursorItems(ICursor cursor, string key)
{
while (cursor.MoveToNext())
{
var value = GetString(cursor, key);
yield return value;
}
cursor.Close();
}
string GetString(ICursor cursor, string key)
{
return cursor.GetString(cursor.GetColumnIndex(key));
}
}
}
需要添加 Plugin.CurrentActivity 和 Acr.UserDialogs 包。
7、Android工程添加权限处理判断类
Permission.Util.cs
using Android.Content.PM;
namespace TerminalMACS.Clients.App.Droid
{
public static class PermissionUtil
{
/**
* 通过验证给定数组中的每个条目的值是否为Permission.Granted,检查是否已授予所有给定权限。
*
* See Activity#onRequestPermissionsResult (int, String[], int[])
*/
public static bool VerifyPermissions(Permission[] grantResults)
{
// 必须至少检查一个结果.
if (grantResults.Length < 1)
return false;
// 验证是否已授予每个必需的权限,否则返回false.
foreach (Permission result in grantResults)
{
if (result != Permission.Granted)
{
return false;
}
}
return true;
}
}
}
MainActivity.OnRequestPermissionResult是权限申请结果处理函数,在此函数中调用ContactsService.OnRequestPermissionsResult通知通讯录服务权限处理结果。
MainActivity.cs
using Acr.UserDialogs;
using Android.App;
using Android.Content.PM;
using Android.OS;
using Android.Runtime;
using TerminalMACS.Clients.App.Droid.Services;
using TerminalMACS.Clients.App.Services;
namespace TerminalMACS.Clients.App.Droid
{
[Activity(Label = "TerminalMACS.Clients.App", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
IContactsService contactsService = new ContactsService();
protected override void OnCreate(Bundle savedInstanceState)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
base.OnCreate(savedInstanceState);
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
UserDialogs.Init(() => this);
// 将通讯录服务实例传递给共享库,由共享库使用读取通讯录接口
LoadApplication(new App(contactsService));
}
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{
Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
// 通讯录服务处理权限请求结果
ContactsService.OnRequestPermissionsResult(requestCode, permissions, grantResults);
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
}
8、创建通讯录ViewModel,并使用通讯录服务
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using TerminalMACS.Clients.App.Models;
using TerminalMACS.Clients.App.Services;
using Xamarin.Forms;
namespace TerminalMACS.Clients.App.ViewModels
{
/// <summary>
/// 通讯录ViewModel
/// </summary>
public class ContactViewModel : BaseViewModel
{
/// <summary>
/// 通讯录服务接口
/// </summary>
IContactsService _contactService;
/// <summary>
/// 标题
/// </summary>
public new string Title => "通讯录";
private string _SearchText;
/// <summary>
/// 搜索关键字
/// </summary>
public string SearchText
{
get { return _SearchText; }
set
{
SetProperty(ref _SearchText, value);
}
}
/// <summary>
/// 通讯录搜索命令
/// </summary>
public ICommand RaiseSearchCommand { get; }
/// <summary>
/// 通讯录列表
/// </summary>
public ObservableCollection<Contact> Contacts { get; set; }
private List<Contact> _FilteredContacts;
/// <summary>
/// 通讯录过滤列表
/// </summary>
public List<Contact> FilteredContacts
{
get { return _FilteredContacts; }
set
{
SetProperty(ref _FilteredContacts, value);
}
}
public ContactViewModel(IContactsService contactService)
{
_contactService = contactService;
Contacts = new ObservableCollection<Contact>();
Xamarin.Forms.BindingBase.EnableCollectionSynchronization(Contacts, null, ObservableCollectionCallback);
_contactService.OnContactLoaded += OnContactLoaded;
LoadContacts();
RaiseSearchCommand = new Command(RaiseSearchHandle);
}
/// <summary>
/// 过滤通讯录
/// </summary>
void RaiseSearchHandle()
{
if (string.IsNullOrEmpty(SearchText))
{
FilteredContacts = Contacts.ToList();
return;
}
Func<Contact, bool> checkContact = (s) =>
{
if (!string.IsNullOrWhiteSpace(s.Name) && s.Name.ToLower().Contains(SearchText.ToLower()))
{
return true;
}
else if (s.PhoneNumbers.Length > 0 && s.PhoneNumbers.ToList().Exists(cu => cu.ToString().Contains(SearchText)))
{
return true;
}
return false;
};
FilteredContacts = Contacts.ToList().Where(checkContact).ToList();
}
/// <summary>
/// BindingBase.EnableCollectionSynchronization 为集合启用跨线程更新
/// </summary>
/// <param name="collection"></param>
/// <param name="context"></param>
/// <param name="accessMethod"></param>
/// <param name="writeAccess"></param>
void ObservableCollectionCallback(IEnumerable collection, object context, Action accessMethod, bool writeAccess)
{
// `lock` ensures that only one thread access the collection at a time
lock (collection)
{
accessMethod?.Invoke();
}
}
/// <summary>
/// 收到事件通知,读取一条通讯录信息
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnContactLoaded(object sender, ContactEventArgs e)
{
Contacts.Add(e.Contact);
RaiseSearchHandle();
}
/// <summary>
/// 异步读取终端通讯录
/// </summary>
/// <returns></returns>
async Task LoadContacts()
{
try
{
await _contactService.RetrieveContactsAsync();
}
catch (TaskCanceledException)
{
Console.WriteLine("任务已经取消");
}
}
}
}
9、添加通讯录页面展示通讯录数据
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
mc:Ignorable="d"
Title="{Binding Title}"
x:Class="TerminalMACS.Clients.App.Views.ContactPage"
ios:Page.UseSafeArea="true">
<ContentPage.Content>
<StackLayout>
<SearchBar x:Name="filterText"
HeightRequest="40"
Text="{Binding SearchText}"
SearchCommand="{Binding RaiseSearchCommand}"/>
<ListView ItemsSource="{Binding FilteredContacts}"
HasUnevenRows="True">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Padding="10"
Orientation="Horizontal">
<Image Source="{Binding Image}"
VerticalOptions="Center"
x:Name="image"
Aspect="AspectFit"
HeightRequest="60"/>
<StackLayout VerticalOptions="Center">
<Label Text="{Binding Name}"
FontAttributes="Bold"/>
<Label Text="{Binding PhoneNumbers[0]}"/>
<Label Text="{Binding Emails[0]}"/>
</StackLayout>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
三、源码获取
1.完整源码:https://github.com/dotnet9/TerminalMACS
2.Android客户端可成功取得通讯录数据,并可查询;
已编译的Android客户端:https://terminalmacs.com/terminalmacs-clients-app-android
3.iOS读取通讯录功能代码也已添加,但由于本人没有iOS测试环境,所以未验证,有条件的朋友可以测试下iOS的通讯录读取功能,如果代码不起作用,可参考本文参考的文章检查iOS代码。
四、参考资料
Getting phone contacts in Xamarin Forms:https://www.xamboy.com/2019/10/10/getting-phone-contacts-in-xamarin-forms/
参考文章末尾有源代码链接。
五、后面计划
Xamarin.Forms客户端基本信息获取,比如IMEI、IMSI、本机号码、Mac地址等。
Xamarin.Forms读取并展示Android和iOS通讯录 - TerminalMACS客户端相关推荐
- android 获取通讯录全选反选_Xamarin.Forms读取并展示Android和iOS通讯录 TerminalMACS客户端...
本文同步更新地址: https://dotnet9.com/11520.html https://terminalmacs.com/861.html 阅读导航: 一.功能说明 二.代码实现 三.源码获 ...
- 张高兴的 Xamarin.Forms 开发笔记:Android 快捷方式 Shortcut 应用
一.Shortcut 简介 Shortcut 是 Android 7.1 (API Level 25) 的新特性,类似于苹果的 3D Touch ,但并不是压力感应,只是一种长按菜单.Shortcut ...
- xamarin.forms_重构:从Xamarin Native到Xamarin.Forms
xamarin.forms 介绍 (Introduction) Before you actually start developing a mobile app, you have to make ...
- 张高兴的 Xamarin.Forms 开发笔记:为 Android 与 iOS 引入 UWP 风格的汉堡菜单 ( MasterDetailPage )...
所谓 UWP 样式的汉堡菜单,我曾在"张高兴的 UWP 开发笔记:汉堡菜单进阶"里说过,也就是使用 Segoe MDL2 Assets 字体作为左侧 Icon,并且左侧使用填充颜色 ...
- xamarin android密码,Xamarin.Forms学习历程(七)——用户偏好设置存储
经常会遇到要存储一下用户账号密码之类的,让用户下次登录时不需要重新输入账号密码,直接进入主界面.Xamarin.Forms里没有自己的解决方案,还是得调用iOS和Android原生的API才可以实现. ...
- Xamarin.Forms教程下载安装Xamarin.iOS
Xamarin.Forms教程下载安装Xamarin.iOS 下载安装Xamarin.iOS Xamarin.iOS可以为Mac上iOS应用程序在Windows计算机上编写和测试网络提供构建和部署服务 ...
- Xamarin.Forms教程Android SDK工具下载安装
Xamarin.Form的Android SDK工具下载安装 本节将讲解如何下载Xamarin.Form的Android SDK工具,并使用其中的工具管理Android SDK,如何创建模拟器等内容. ...
- 从零开始学Xamarin.Forms(四) Android 准备步骤(添加第三方Xamarin.Forms.Labs库)
从零开始学Xamarin.Forms(四) Android 准备步骤(添加第三方Xamarin.Forms.Labs库) 原文:从零开始学Xamarin.Forms(四) Android 准备步骤(添 ...
- Xamarin.Forms 中iOS通过URL Scheme判断应用是否安装
Xamarin.Forms 中iOS通过URL Scheme判断应用是否安装 在移动应用开发中,经常需要判断一个app是否安装,iOS中有什么方式可以判断app是否安装呢? 这里介绍通过Url Sch ...
最新文章
- centos 7安装 navicat
- 常见排序算法效率比较
- Error in configuration process解决方法
- 破解前端面试系列(3):如何搞定纸上代码环节?
- 【DVWA(五)】XXS存储型跨站攻击
- Android中贝塞尔曲线的绘制方法
- 小程序开发(13)-location定位
- 利用 assistant_如何使用Dialogflow对Google Assistant操作实施本地履行
- 大部分人其实根本就不上进,他们只是表现的很努力
- PHP获取当前页面的完整URL
- unix下source的使用
- 处理vue项目中使用es6模板字符串中\n换行问题
- mybatis中sql写法技巧小总结
- 早间简评:黄金亚盘快速下跌 1300关口岌岌可危?
- JavaScript:实现PigeonHoleSort鸽巢排序算法(附完整源码)
- Android闹钟制作过程图,小学闹钟手工制作步骤详解(配图)
- 2021图机器学习有哪些新突破?麦吉尔大学博士后一文梳理展望领域趋势
- linux中shell的循环
- 【转发】微信小程序详细图文教程
- 央视网商城app_传播中国文化 央视网商城重磅打造中国好物产