Xamarin.Forms学习之路——Media整理(多种库的用法以及原生写法)、.Android和.Forms的通信
项目效果
banner展示
音乐播放器以及文件选择
音乐播放器
文件选择
相册展览
瀑布流展示
拍照
相册
录音功能
学习目标
- 瀑布流图片布局;
- FFimageLoading的用法;
- 基于CrossMedia的照相机获取图片及本地图片的获取;
- 基于MediaMannager的播放器功能;
- 基于安卓系统的文件选择功能;
- 基于Android原生和AudioRecorder的录音功能;
- 展示RefreshView的用法;
- 基于CarouselView的Banner制作方法;
- 沉浸式布局的方法;
项目结构(iOS不做展示)
项目准备
- Xamarin.Forms
- Xamarin.Android 添加CurrentActivity包就行
- Android Properties设置
步骤
全局构建
根据VS2019自动生成Shell页,可以从中知道一些MVVM编码范式,从而可以构建我们的整个项目
在Services文件夹里新建IDataStore接口
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Conclusion.Services
{
public interface IDataStore<T>
{
Task<bool> AddObjAsync(T _object);
Task<bool> UpdateObjAsync(T _object);
Task<bool> DeleteObjAsync(string id);
Task<T> GetObjAsync(string id);
Task<IEnumerable<T>> GetObjsAsync(bool foreceRefreshing = false);
}
}
建立IDataStore接口的目的是为了方便以后的网络版本。现在我只是用的MockDataStore作为项目数据。
建立MockDataStore.cs类,拓展IDataStore接口
using Conclusion.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace Conclusion.Services
{
public class MockDataStore : IDataStore<Item>
{
public readonly List<Item> items;
public MockDataStore(Type _type)
{
if(_type == typeof(Player))
{
items = new List<Item>
{
};
}
else if(_type == typeof(Photo))
{
var imageUrls = new List<Photo>();
for(int i = 1; i <= 17; i++)
{
imageUrls.Add(new Photo {
PhotoUrl = "img" + i + ".jpg"
});
}
items = new List<Item>(imageUrls);
}
else if(_type == typeof(Refresh))
{
items = new List<Item>
{
new Refresh
{
BannerUrl="banner1.jpg",
BannerColor=Color.AliceBlue
},
new Refresh
{
BannerUrl="banner2.jpg",
BannerColor=Color.PaleVioletRed
},
new Refresh
{
BannerUrl="banner3.jpg",
BannerColor=Color.DarkKhaki
}
};
System.Diagnostics.Debug.WriteLine("count:"+items.Count);
}
}
#region 对数据的一系列操作
public async Task<bool> AddObjAsync(Item _item)
{
items.Add(_item);
return await Task.FromResult(true);
}
public async Task<bool> DeleteObjAsync(string id)
{
Item oldItem = items.Where((Item arg) => arg.Id == id).FirstOrDefault();
items.Remove(oldItem);
return await Task.FromResult(true);
}
public async Task<Item> GetObjAsync(string id)
{
return await Task.FromResult(items.FirstOrDefault((Item arg) => arg.Id == id));
}
public async Task<IEnumerable<Item>> GetObjsAsync(bool foreceRefreshing = false)
{
return await Task.FromResult(items);
}
public async Task<bool> UpdateObjAsync(Item _item)
{
var oldItem = items.Where((Item arg) => arg.Id == _item.Id).FirstOrDefault();
items.Remove(oldItem);
items.Add(_item);
return await Task.FromResult(true);
}
#endregion
}
}
下面是介绍,大佬可直接跳过
这里我用到了比较骚的操作,可以看到,我的Model中有4个类,其中Item类是一个基类
Item类的定义很简单:
using System;
using System.Collections.Generic;
using System.Text;
namespace Conclusion.Models
{
public class Item
{
public string Id { get; set; }
}
}
其他三个类均继承于Item类。
这么做的目的是为了增强代码的可重用性。大家可以再看一遍MockDataStore.cs代码:
这样一来,只需要对当前数据类型进行判断,从而抓取不同的数据,从而就不需要写三个DataStore了。
在App.xaml.cs 中实现Store的全局初始化
using System;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using Conclusion.Views;
using Conclusion.Services;
using Conclusion.Models;
namespace Conclusion
{
public partial class App : Application
{
public static MockDataStore PhotoStore{get;set;}
public static MockDataStore PlayerStore{get;set;}
public static MockDataStore RefreshStore{get;set;}
public App()
{
InitializeComponent();
PhotoStore = new MockDataStore(typeof(Photo));
PlayerStore = new MockDataStore(typeof(Player));
RefreshStore = new MockDataStore(typeof(Refresh));
MainPage = new AppShell();
}
protected override void OnStart()
{
}
protected override void OnSleep()
{
}
protected override void OnResume()
{
}
}
}
.Android.MainActivity.cs
- 重写Oncreate方法
protected override void OnCreate(Bundle savedInstanceState)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
base.OnCreate(savedInstanceState);
//包的初始化
NugetInit(savedInstanceState);
//布局的初始化
LayOutInit();
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
LoadApplication(new App());
//写一个函数来提供权限
CheckPermisson();
}
- LayOutInit()函数
private void LayOutInit()
{
var newUiOptions = (int)SystemUiFlags.LayoutStable;
newUiOptions |= (int)SystemUiFlags.LayoutFullscreen;
//newUiOptions |= (int)SystemUiFlags.HideNavigation;
Window.SetStatusBarColor(Android.Graphics.Color.Argb(0, 0, 0, 0));
newUiOptions |= (int)SystemUiFlags.ImmersiveSticky;
Window.DecorView.SystemUiVisibility = (StatusBarVisibility)newUiOptions;
}
- NuGetInit()函数
private void NugetInit(Bundle savedInstanceState)
{
//Nuget包的初始化,这些在文档中可以参考借鉴,以后编码把要装的包的初始化写在这个函数中即可
Forms.SetFlags("CollectionView_Experimental");
CrossCurrentActivity.Current.Init(this, savedInstanceState);
ButtonCircleRenderer.Init();
CachedImageRenderer.Init(enableFastRenderer: true);
CachedImageRenderer.InitImageViewHandler();
CrossMediaManager.Current.Init(this);
}
- CheckPermisson()函数
private void CheckPermisson()
{
//检查录音权限
if (ContextCompat.CheckSelfPermission(this, Manifest.Permission.RecordAudio) != Permission.Granted)
{
ActivityCompat.RequestPermissions(this, new string[] { Manifest.Permission.RecordAudio }, 1);
}
//检查外部存储 读写权限
if (ContextCompat.CheckSelfPermission(this, Manifest.Permission.ReadExternalStorage) != Permission.Granted ||
ContextCompat.CheckSelfPermission(this, Manifest.Permission.WriteExternalStorage) != Permission.Granted)
{
ActivityCompat.RequestPermissions(this, new string[] {Manifest.Permission.ReadExternalStorage,
Manifest.Permission.WriteExternalStorage}, 1);
}
}
ViewModels.BaseViewModel.cs
using System;
using System.Collections.Generic;
using System.Text;
using Conclusion.Services;
using Xamarin.Forms;
namespace Conclusion.ViewModels
{
class BaseViewModel:BindableObject
{
private bool _isRefresh;
public bool IsRefresh
{
get { return _isRefresh; }
set
{
if (_isRefresh == value)
return;
_isRefresh = value;
OnPropertyChanged();
}
}
}
}
这里写BassViewModel类的目的主要是更好让“RefreshView”重用,其他ViewModel继承BaseViewModel就好了
Banner页
为什么先从Banner开始,因为他最简单
Models.Refresh.cs
using System;
using System.Collections.Generic;
using System.Text;
using Xamarin.Forms;
namespace Conclusion.Models
{
class Refresh:Item
{
public string BannerUrl { get; set; }
public Color BannerColor { get; set; }
}
}
为什么取名叫Refresh呢?因为当时脑子抽了,大家大可以修改成Banner。
ViewModels.RefreshViewModel.cs
using System;
using System.Collections.Generic;
using System.Text;
using Conclusion.Services;
using Conclusion.Models;
using Xamarin.Forms;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Diagnostics;
namespace Conclusion.ViewModels
{
class RefreshViewModel : BaseViewModel
{
private readonly MockDataStore refreshService;
private ObservableCollection<Refresh> _banners;
public Command RefreshViewCommand { get; set; }
public ObservableCollection<Refresh> Banners
{
get { return _banners; }
set
{
if (_banners == value)
return;
_banners = value;
OnPropertyChanged();
}
}
public RefreshViewModel()
{
//初始化refreshService
refreshService = App.RefreshStore;
//为Refresh绑定命令
RefreshViewCommand = new Command(async () => await ExecuteRefreshViewCommand());
//初始化Banners
if(Banners == null)
{
Banners = new ObservableCollection<Refresh>();
RefreshViewCommand.Execute(null);
}
}
private async Task ExecuteRefreshViewCommand()
{
IsRefresh = true;
try
{
Banners.Clear();
var banners = await refreshService.GetObjsAsync(true);
foreach(Refresh banner in banners)
{
Banners.Add(banner);
}
Debug.WriteLine("banners:"+Banners.Count);
}
catch(Exception ex)
{
Debug.WriteLine(ex);
}
finally
{
IsRefresh = false;
}
}
}
}
Views.RefreshView.xaml
<?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:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="Conclusion.Views.RefreshView">
<!--这一页展示RefreshView的用法,以及基于CarouselView的Banner制作方法-->
<RefreshView
IsRefreshing="{Binding IsRefresh}"
Command="{Binding RefreshViewCommand}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="250"></RowDefinition>
<RowDefinition ></RowDefinition>
</Grid.RowDefinitions>
<!--#region Banner区域-->
<CarouselView
Grid.Row="0"
PeekAreaInsets="0"
ItemsSource="{Binding Banners}"
CurrentItemChangedCommand="{Binding CurrentItemChanged}"
x:Name="myBanner"
Margin="0,20,0,0">
<CarouselView.ItemsLayout>
<LinearItemsLayout
SnapPointsType="Mandatory"
SnapPointsAlignment="Center"
Orientation="Horizontal"
ItemSpacing="20"></LinearItemsLayout>
</CarouselView.ItemsLayout>
<CarouselView.ItemTemplate>
<DataTemplate>
<Frame
VerticalOptions="Start"
HorizontalOptions="Start"
HeightRequest="200"
WidthRequest="300"
Padding="0"
HasShadow="True"
IsClippedToBounds="true"
CornerRadius="10"
BackgroundColor="{Binding BannerColor}">
<Image Source="{Binding BannerUrl}"
Aspect="AspectFill"></Image>
</Frame>
</DataTemplate>
</CarouselView.ItemTemplate>
</CarouselView>
<!--#endregion-->
<Label
Grid.Row="1"
Text="这里是Banner展示"
HorizontalOptions="Center"
VerticalOptions="Center"
FontAttributes="Bold"></Label>
</Grid>
</RefreshView>
</ContentPage>
Views.RefreshView.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Conclusion.ViewModels;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using Conclusion.Models;
namespace Conclusion.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class RefreshView : ContentPage
{
private RefreshViewModel vm;
private bool isPageOn;//控制页面计时器Timer,当进入Page时,Timer打开,离开Page时,Timer关闭
public Command CurrentItemChanged { get; set; }
Queue<Refresh> bannerQueue;
public RefreshView()
{
InitializeComponent();
BindingContext = vm = new RefreshViewModel();
//创建banner队列
bannerQueue = new Queue<Refresh>(vm.Banners);
//绑定命令,当用户滑动后,更新队列
CurrentItemChanged = new Command(() => UpdateBannerQueue());
}
#region 处理banner自动换页效果
private bool ScrollBanner()
{
//获取要展示的banner
Refresh exhibitionBanner = bannerQueue.Dequeue();
//此banner入队列
bannerQueue.Enqueue(exhibitionBanner);
//滚动到要展示的banner
myBanner.ScrollTo(vm.Banners.IndexOf(exhibitionBanner));
return isPageOn;
}
private void UpdateBannerQueue()
{
//在mybanner里展示的应该是bannerQueue的最后一个banner,因此当 当前的banner不是应该正在显示的banner时,更新我们的队列
while (myBanner.CurrentItem as Refresh != bannerQueue.Last())
{
bannerQueue.Enqueue(bannerQueue.Dequeue());
}
return;
}
#endregion
/// <summary>
/// 离开页面关闭Timer
/// </summary>
protected override void OnDisappearing()
{
base.OnDisappearing();
isPageOn = false;
}
/// <summary>
/// 进入页面开启timer
/// </summary>
protected override void OnAppearing()
{
base.OnAppearing();
Device.StartTimer(TimeSpan.FromSeconds(10), ScrollBanner);
isPageOn = true;
}
}
}
录音页
录音页有一点复杂,我会做些讲解。
值得一提的是,在RecorderView中,我并没有适用RecorderViewModel,因为感觉没必要,不过大家完全可以写一个RecorderViewModel,将按键逻辑绑定到Commad上
Interfaces.IRecorder.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace Conclusion.Interfaces
{
public interface IRecorder
{
void GetRecord();
void StopRecord();
}
}
建立IRecorder接口,让Xamarin.Android能够拓展IRecoder
.Android. RecorderForAndroid.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using Android.App;
using Android.Content;
using Android.Media;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Conclusion.Interfaces;
using Java.IO;
using MediaDemo.Droid;
using Xamarin.Forms;
using Conclusion.Droid;
[assembly:Dependency(typeof(RecorderForAndroid))]
namespace Conclusion.Droid
{
#region 用安卓原生Media实现录音功能
class RecorderForAndroid : IRecorder
{
private AudioRecord recorder = null;
private ThreadStart recordingThreadStart = null;
private Thread recordingThread = null;
private int SampleRate = 8000;
private bool isRecording = false;
private TimeSpan maxRecordTime = TimeSpan.FromSeconds(15);
private DateTime startTime;
public void GetRecord()
{
//Mono:单声道采集样本,8000HZ
int minBufferSize = AudioRecord.GetMinBufferSize(SampleRate, ChannelIn.Mono, Android.Media.Encoding.Pcm16bit);
recorder = new AudioRecord(AudioSource.Mic, SampleRate, ChannelIn.Mono,
Android.Media.Encoding.Pcm16bit, minBufferSize);
recorder.StartRecording();
System.Diagnostics.Debug.WriteLine("开始录制");
recordingThreadStart = new ThreadStart(StartRecord);
recordingThread = new Thread(recordingThreadStart);
isRecording = true;
recordingThread.Start();
startTime = DateTime.Now;
System.Diagnostics.Debug.WriteLine("录音线程开始");
}
public void StopRecord()
{
if (recorder != null)
{
isRecording = false;
recorder.Stop();
recorder.Release();
recorder = null;
recordingThread = null;
System.Diagnostics.Debug.WriteLine("已停止录音");
#region 将录制的PCM文件转换成Wav文件
string musicDir = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryMusic).AbsolutePath;
string inFilePath = musicDir + "/voice8K16bitmono.pcm";
string outFilePath = musicDir + "/Demo.Wav";
PcmToWav pcm2WavTool = new PcmToWav(SampleRate, ChannelIn.Mono, Android.Media.Encoding.Pcm16bit);
pcm2WavTool.transferPcmToWav(inFilePath, outFilePath);
#endregion
}
}
int bufferElementsToRec = 512; // want to play 1024 since 2 bytes we use only 512;
int bytesPerElement = 2; //2 bytes in 16bits format; 用short采集的数据,一个short占两个字节
private void StartRecord()
{
//强制停止,这里应该发送消息给UI,表明录音结束,从而更改图标,不过我太懒了,没有实现,就注释掉了
/*if(startTime + maxRecordTime >= DateTime.Now)
{
StopRecord();
}*/
//获取Android Music文件根目录
string musicDir = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryMusic).AbsolutePath;
//PCM录音文件路径
string myPath = musicDir + "/voice8K16bitmono.pcm";
//初始化shortData Buffer
short[] sData = new short[bufferElementsToRec];
FileOutputStream os = null;
try
{
os = new FileOutputStream(myPath);
}
catch (FileNotFoundException e)
{
e.PrintStackTrace();
}
while (isRecording)
{
recorder.Read(sData, 0, bufferElementsToRec);
System.Diagnostics.Debug.WriteLine("将麦克风采集到的short类型数据读入Buffer" + sData);
try
{
byte[] bData = ShortToByte(sData);
//向PCM文件写入字节流
os.Write(bData, 0, bufferElementsToRec * bytesPerElement);
}
catch (IOException e)
{
e.PrintStackTrace();
}
}
try
{
os.Close();
}
catch (IOException e)
{
e.PrintStackTrace();
}
}
/// <summary>
/// 将short类型数据转换成字节类型
/// </summary>
/// <param name="sData"></param>
/// <returns></returns>
private byte[] ShortToByte(short[] sData)
{
int shortArrsize = sData.Length;
byte[] bytes = new byte[shortArrsize * 2];
for (int i = 0; i < shortArrsize; i++)
{
//低8位
bytes[i * 2] = (byte)(sData[i] & 0x00FF);
//高8位
bytes[i * 2 + 1] = (byte)(sData[i] >> 8);
sData[i] = 0;
}
return bytes;
}
}
#endregion
}
以这种方式录制的声音是.PCM文件,在安卓手机上并不能播放,因此,参考别人的做法将PCM转成了WAV文件。最后千万不要忘记添加
[assembly:Dependency(typeof(RecorderForAndroid))]
.Android.PcmToWav.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.Media;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Java.IO;
namespace MediaDemo.Droid
{
#region PCM转Wav类
public class PcmToWav
{
/// <summary>
/// 缓存的音频大小
/// </summary>
private int _bufferSize;
/// <summary>
/// 采样率
/// </summary>
private int _sampleRate;
/// <summary>
/// 声道数
/// </summary>
private ChannelIn _channel;
public PcmToWav(int SampleRate, ChannelIn Channel, Android.Media.Encoding Encoding)
{
_sampleRate = SampleRate;
_channel = Channel;
_bufferSize = AudioRecord.GetMinBufferSize(_sampleRate, _channel, Encoding);
}
public void transferPcmToWav(string inFileName, string outFileName)
{
FileInputStream inFile;
FileOutputStream outFile;
long totalAudioLen;
long totalDataLen;
long longSampleRate = _sampleRate;
int channels = _channel == ChannelIn.Mono ? 1 : 2;
long byteRate = 16 * _sampleRate * channels / 8;
byte[] data = new byte[_bufferSize];
try
{
inFile = new FileInputStream(inFileName);
outFile = new FileOutputStream(outFileName);
totalAudioLen = inFile.Channel.Size();
totalDataLen = totalAudioLen + 36;
WriteWaveFileHeader(outFile, totalAudioLen, totalDataLen, longSampleRate, channels, byteRate);
while (inFile.Read(data) != -1)
{
outFile.Write(data);
}
inFile.Close();
outFile.Close();
}
catch (IOException e)
{
e.PrintStackTrace();
}
}
private void WriteWaveFileHeader(FileOutputStream outFile, long totalAudioLen, long totalDataLen, long longSampleRate, int channels, long byteRate)
{
byte[] header = new byte[44];
//RIFF header
header[0] = (byte)'R';
header[1] = (byte)'I';
header[2] = (byte)'F';
header[3] = (byte)'F';
header[4] = (byte)(totalDataLen & 0xff);
header[5] = (byte)((totalDataLen >> 8) & 0xff);
header[6] = (byte)((totalDataLen >> 16) & 0xff);
header[7] = (byte)((totalDataLen >> 24) & 0xff);
//WAVE
header[8] = (byte)'W';
header[9] = (byte)'A';
header[10] = (byte)'V';
header[11] = (byte)'E';
// 'fmt ' chunk
header[12] = (byte)'f';
header[13] = (byte)'m';
header[14] = (byte)'t';
header[15] = (byte)' ';
// 4 bytes: size of 'fmt ' chunk
header[16] = 16;
header[17] = 0;
header[18] = 0;
header[19] = 0;
// format = 1
header[20] = 1;
header[21] = 0;
header[22] = (byte)channels;
header[23] = 0;
header[24] = (byte)(longSampleRate & 0xff);
header[25] = (byte)((longSampleRate >> 8) & 0xff);
header[26] = (byte)((longSampleRate >> 16) & 0xff);
header[27] = (byte)((longSampleRate >> 24) & 0xff);
header[28] = (byte)(byteRate & 0xff);
header[29] = (byte)((byteRate >> 8) & 0xff);
header[30] = (byte)((byteRate >> 16) & 0xff);
header[31] = (byte)((byteRate >> 24) & 0xff);
// block align
header[32] = (byte)(2 * 16 / 8);
header[33] = 0;
// bits per sample
header[34] = 16;
header[35] = 0;
//data
header[36] = (byte)'d';
header[37] = (byte)'a';
header[38] = (byte)'t';
header[39] = (byte)'a';
header[40] = (byte)(totalAudioLen & 0xff);
header[41] = (byte)((totalAudioLen >> 8) & 0xff);
header[42] = (byte)((totalAudioLen >> 16) & 0xff);
header[43] = (byte)((totalAudioLen >> 24) & 0xff);
outFile.Write(header, 0, 44);
}
}
#endregion
}
这个类就可以当做工具以后要用直接拿来使用了。
RecorderView.xaml
<?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:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:circlebutton="clr-namespace:ButtonCircle.FormsPlugin.Abstractions;assembly=ButtonCircle.FormsPlugin.Abstractions"
mc:Ignorable="d"
x:Class="Conclusion.Views.RecorderView">
<!--这一页展示基于Android原生和AudioRecorder的录音功能-->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Label
Margin="0,10,0,0"
Grid.Row="0"
Text="这里是基于安卓原生的录音"
HorizontalOptions="Center"></Label>
<Frame
Grid.Row="1"
CornerRadius="10"
HasShadow="True"
Margin="10">
<!--md-mic-->
<Grid>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<circlebutton:CircleButton
FontIcon="Material"
Icon="md-mic-none"
FontSize="50" TextColor="Black"
HeightRequest="70" WidthRequest="70"
BorderThickness="1" BorderColor="Black"
BackgroundColor="WhiteSmoke"
Clicked="RecordMovement1"
HorizontalOptions="Center"
VerticalOptions="Center"
Grid.Row="0">
</circlebutton:CircleButton>
<Label
x:Name="pathOne"
Grid.Row="1"
Text="保存在emulate/0/Music/Demo.wav中"
HorizontalOptions="Center"
Opacity="0"></Label>
</Grid>
</Frame>
<Label
Grid.Row="2"
Text="这里是基于AudioRecorder的录音"
HorizontalOptions="Center"></Label>
<Frame
Grid.Row="3"
CornerRadius="10"
HasShadow="True"
Margin="10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<circlebutton:CircleButton
FontIcon="Material"
Icon="md-mic-none"
FontSize="50" TextColor="Black"
HeightRequest="70" WidthRequest="70"
BorderThickness="1" BorderColor="Black"
BackgroundColor="WhiteSmoke"
Clicked="RecordMovement2"
HorizontalOptions="Center"
VerticalOptions="Center"
Grid.Row="0">
</circlebutton:CircleButton>
<Label
x:Name="pathTwo"
Grid.Row="1"
Text="保存在emulate/0/Music/Myfolder/Demo.wav中"
HorizontalOptions="Center"
Opacity="0"></Label>
</Grid>
</Frame>
</Grid>
</ContentPage>
RecorderView.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Conclusion.Interfaces;
using Xamarin.Forms;
using Plugin.AudioRecorder;
using Xamarin.Forms.Xaml;
using ButtonCircle;
using Conclusion.Utils;
namespace Conclusion.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class RecorderView : ContentPage
{
private AudioRecorderNuGet audioRecorderService;
public RecorderView()
{
InitializeComponent();
audioRecorderService = new AudioRecorderNuGet();
}
#region 安卓原生录音示例
private void RecordMovement1(object sender, EventArgs e)
{
ButtonCircle.FormsPlugin.Abstractions.CircleButton recordImage = sender as ButtonCircle.FormsPlugin.Abstractions.CircleButton;
if(recordImage.Icon.ToString().Contains("md-mic-none"))
{
DependencyService.Get<IRecorder>().GetRecord();
recordImage.Icon = "md-mic";
pathOne.Opacity = 0;
}
else
{
DependencyService.Get<IRecorder>().StopRecord();
recordImage.Icon = "md-mic-none";
pathOne.Opacity = 1;
}
}
#endregion
#region AudioRecorder示例
private async void RecordMovement2(object sender, EventArgs e)
{
ButtonCircle.FormsPlugin.Abstractions.CircleButton recordImage = sender as ButtonCircle.FormsPlugin.Abstractions.CircleButton;
if (recordImage.Icon.ToString().Contains("md-mic-none"))
{
recordImage.Icon = "md-mic";
pathTwo.Opacity = 0;
string filePath = await audioRecorderService.GetRecord();
DependencyService.Get<IFileModifier>().ModifyFileLocation(filePath, "/Music/MyFolder", "/Music/MyFolder/Demo.wav");
}
else
{
recordImage.Icon = "md-mic-none";
pathTwo.Opacity = 1;
string filePath = await audioRecorderService.StopRecord();
DependencyService.Get<IFileModifier>().ModifyFileLocation(filePath, "/Music/MyFolder", "/Music/MyFolder/Demo.wav");
}
}
#endregion
}
}
好了,大家可以看到这里有个RecorderMovement2,它的功能就是基于AudioRecorder实现录音,这里我将AudioRecorder的使用封装成了一个类,方便使用
Utils.AudioRecorderNuget.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Plugin.AudioRecorder;
namespace Conclusion.Utils
{
#region AudioRecorder用法
public class AudioRecorderNuGet
{
readonly AudioRecorderService recorder;
public AudioRecorderNuGet()
{
recorder = new AudioRecorderService
{
StopRecordingAfterTimeout = false,
TotalAudioTimeout = TimeSpan.FromSeconds(15),
AudioSilenceTimeout = TimeSpan.FromSeconds(2)
};
}
public async Task<string> GetRecord()
{
var audioRecordTask = await recorder.StartRecording();
var audioFile = await audioRecordTask;
return audioFile;
}
public async Task<string> StopRecord()
{
await recorder.StopRecording();
return recorder.GetAudioFilePath();
}
}
#endregion
}
不过AudioRecorder将文件存进0/User/Data中,很多安卓手机无法访问(至少我的VivoNEX S无法访问),因此,我写了一个IFileModifier.cs接口,从而帮助我修改文件路径
Interfaces.IFileModifier.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace Conclusion.Interfaces
{
public interface IFileModifier
{
void ModifyFileLocation(string fromPath, string folderPath, string toPath);
}
}
.Android.FileModifierForAndroid.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Android;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using Android.Runtime;
using Android.Support.V4.Content;
using Android.Views;
using Android.Widget;
using Conclusion.Interfaces;
using Xamarin.Forms;
using Conclusion.Droid;
[assembly: Dependency(typeof(FileModifierForAndroid))]
namespace Conclusion.Droid
{
#region 修改文件路径
class FileModifierForAndroid : IFileModifier
{
/// <summary>
/// 修改文件路径
/// </summary>
/// <param name="fromPath">原本文件路径</param>
/// <param name="folderPath">文件夹路径</param>
/// <param name="toPath">现在文件路径</param>
public void ModifyFileLocation(string fromPath, string folderPath, string toPath)
{
string androidExternalStorageRootPath = Android.OS.Environment.ExternalStorageDirectory.AbsolutePath;
System.Diagnostics.Debug.WriteLine("path:" + androidExternalStorageRootPath);
folderPath = androidExternalStorageRootPath + folderPath;
toPath = androidExternalStorageRootPath + toPath;
//创建文件
if (!File.Exists(folderPath))
{
Directory.CreateDirectory(folderPath);
File.Create(toPath).Dispose();
}
//写入文件
if (ContextCompat.CheckSelfPermission(Android.App.Application.Context, Manifest.Permission.WriteExternalStorage) == (int)Permission.Granted)
{
try
{
if (File.Exists(toPath))
{
File.Delete(toPath);
System.Diagnostics.Debug.WriteLine("删除成功");
}
File.Copy(fromPath, toPath);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.Message);
}
}
else
{
System.Diagnostics.Debug.WriteLine("不准写入");
}
}
}
#endregion
}
总结一下,.Android和.Forms基于Dependency进行互动(这是我的理解),方法如下:
- .Forms新建ISomeInterface接口 ;
- .Android新建SomeClassForAndroid.cs拓展ISomeInterface接口;
- 在SomeClassForAndroid文件前加上[assembly:Dependency(typeof(SomeClassForAndroid))];
- .Forms用Dependency.get< ISomeInterface >().SomeMethod()调用相应方法;
音乐播放器页
音乐播放器用到了MediaManager库,这里可以参考我的上上篇文章
音乐播放器要实现基本的播放器功能,这里用循环队列实现;以及音乐的选取功能
Views.PlayerView.xaml
<?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:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:circlebutton="clr-namespace:ButtonCircle.FormsPlugin.Abstractions;assembly=ButtonCircle.FormsPlugin.Abstractions"
mc:Ignorable="d"
Shell.NavBarIsVisible="False"
x:Class="Conclusion.Views.PlayerView">
<!--这一页展示基于MediaMannager的播放器功能,以及基于安卓系统的文件选择功能-->
<Grid BackgroundColor="#81C3D7">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Frame
Grid.Row="0"
Margin="10"
CornerRadius="10"
HasShadow="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<!--#region 上方选择歌曲按钮-->
<StackLayout
Orientation="Horizontal"
HorizontalOptions="End"
Grid.Row="0">
<circlebutton:CircleButton
FontIcon="Material"
Icon="md-playlist-add"
FontSize="30" TextColor="Black"
HeightRequest="40" WidthRequest="40"
BorderThickness="0" BorderColor="Black"
BackgroundColor="WhiteSmoke"
Clicked="ChooseMusic">
</circlebutton:CircleButton>
</StackLayout>
<!--#endregion-->
<!--#region 中间转盘-->
<Grid
Margin="10"
Grid.Row="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Grid
Grid.RowSpan="3">
<Image Source="{Binding SongCover}"></Image>
</Grid>
<Label
Grid.Row="0"
Margin="5"
HorizontalOptions="Center"
Text="{Binding SongName}"></Label>
<Label
Grid.Row="0"
Margin="5"
HorizontalOptions="Center"
Text="{Binding ArtistName}"></Label>
<Image
Grid.Row="1"
Source="CD.png"></Image>
</Grid>
</Grid>
<!--#endregion-->
</Grid>
</Frame>
<Frame
Grid.Row="1"
Margin="10"
CornerRadius="10"
HasShadow="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Grid
Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<!--#region 进度条-->
<!--Label一定要写宽度,不然闪烁-->
<Label
Grid.Column="0"
WidthRequest="40"
HorizontalOptions="Start"
VerticalOptions="Center"
Text="{Binding NowDuration, StringFormat='\{0:mm\\:ss}'}"></Label>
<Slider
Grid.Column="1"
Value="{Binding TimeValue}"
Maximum="{Binding Duration.TotalMinutes}"
Minimum="0"
Margin="10"
VerticalOptions="Center"
ThumbColor="Black"
MinimumTrackColor="DarkRed"
MaximumTrackColor="DarkCyan"></Slider>
<Label
Grid.Column="2"
WidthRequest="40"
HorizontalOptions="Start"
VerticalOptions="Center"
Text="{Binding Duration, StringFormat='\{0:mm\\:ss}'}"></Label>
<!--#endregion-->
</Grid>
<!--#region 下方操作按钮-->
<Grid
Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<circlebutton:CircleButton
Grid.Column="0"
FontIcon="Material"
Icon="md-skip-previous"
FontSize="30" TextColor="Black"
HeightRequest="40" WidthRequest="40"
BorderThickness="0" BorderColor="Black"
BackgroundColor="WhiteSmoke"
HorizontalOptions="Start"
Command="{Binding PreviousCommand}">
</circlebutton:CircleButton>
<circlebutton:CircleButton
FontIcon="Material"
Grid.Column="1"
Icon="md-play-arrow"
FontSize="60" TextColor="Black"
HeightRequest="80" WidthRequest="80"
BorderThickness="0" BorderColor="Black"
BackgroundColor="WhiteSmoke"
Command="{Binding PlayCommand}"
x:Name="PlayControlBtn">
</circlebutton:CircleButton>
<circlebutton:CircleButton
FontIcon="Material"
Grid.Column="2"
Icon="md-skip-next"
FontSize="30" TextColor="Black"
HeightRequest="40" WidthRequest="40"
BorderThickness="0" BorderColor="Black"
BackgroundColor="WhiteSmoke"
Command="{Binding NextCommand}">
</circlebutton:CircleButton>
</Grid>
<!--#endregion-->
</Grid>
</Frame>
</Grid>
</ContentPage>
界面先放在这里了,接着我们分两步进行,先实现文件选择,下面定义IFilePick接口
Interfaces.IFilePicker.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace Conclusion.Interfaces
{
public interface IFilePicker
{
void PickFile();
}
}
.Android.FilePickerForAndroid.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Conclusion.Interfaces;
using Plugin.CurrentActivity;
using Xamarin.Forms;
using Conclusion.Droid;
[assembly:Dependency(typeof(FilePickerForAndroid))]
namespace Conclusion.Droid
{
class FilePickerForAndroid : IFilePicker
{
public void PickFile()
{
Intent intent = new Intent();
intent.SetType("file/*");
intent.SetAction(Intent.ActionGetContent);
Activity activity = CrossCurrentActivity.Current.Activity as Activity;
var chooser = Intent.CreateChooser(intent, "Select File");
activity.StartActivityForResult(chooser, 0);
return;
}
}
}
这里便用到了CurrentActivity Nuget包,用于获取当前安卓的活动页,然后调用StartActivityForResult方法,在MainActivity中重写OnActivityResult方法:
.Android.MainActivity.cs
protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
if (resultCode != Result.Ok)
{
return;
}
else
{
if (data == null)
{
return;
}
else
{
var uri = data.Data as Android.Net.Uri;
System.Diagnostics.Debug.WriteLine("uri:" + uri);
//发送AddMusic消息
MessagingCenter.Send(new MessageCenter(), "AddMusic", uri.ToString());
}
}
}
当选择文件后,我们将“AddMusic”这个信息通过MessagingCenter发送给MessageCenter()类,发送的参数是选取的文件的Uri。对于MessageCenter(),我把他设定为我的消息中心,页面与页面,平台与平台的信息交互都可以通过MessagingCenter来实现。下面是MessageCenter的定义
Utils.MessageCenter.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace Conclusion.Utils
{
public class MessageCenter
{
}
}
对,没错,他什么都没有,他就像个工具人,我只将它作为信息中心。好,接下来,就可以为我们的PlayerView写ViewModel了
ViewModels.PlayerViewModel.cs
using System;
using System.Collections.Generic;
using System.Text;
using Conclusion.Models;
using Conclusion.Services;
using Xamarin.Forms;
using MediaManager;
using Conclusion.Views;
using System.Threading.Tasks;
using Conclusion.Utils;
namespace Conclusion.ViewModels
{
class PlayerViewModel:BaseViewModel
{
#region 变量区
private readonly MockDataStore playerService;
public Command PlayCommand { get; set; }
public Command NextCommand { get; set; }
public Command PreviousCommand { get; set; }
private TimeSpan _duration;
private TimeSpan _nowDuration;
private double _timeValue;
private string _songName;
private string _artistName;
private string _songCover;
public TimeSpan Duration
{
get => _duration;
set
{
if (_duration == value)
return;
_duration = value;
OnPropertyChanged();
}
}
public TimeSpan NowDuration
{
get => _nowDuration;
set
{
if (_nowDuration == value)
return;
_nowDuration = value;
OnPropertyChanged();
}
}
public double TimeValue
{
get => _timeValue;
set
{
if (_timeValue == value)
return;
_timeValue = value;
OnPropertyChanged();
}
}
public string SongName
{
get => _songName;
set
{
if (_songName == value)
return;
_songName = value;
OnPropertyChanged();
}
}
public string ArtistName
{
get => _artistName;
set
{
if (_artistName == value)
return;
_artistName = value;
OnPropertyChanged();
}
}
public string SongCover
{
get => _songCover;
set
{
if (_songCover == value)
return;
_songCover = value;
OnPropertyChanged();
}
}
#endregion
private readonly int InitPointerCode = 0;
private List<Player> playQueue = new List<Player>();
private Player currentMusic;
private int currentMusicPointer;
private bool _pauseFlag;
private bool nextFlag;
private bool previousFlag;
private bool allowReload;
//PauseFlag通知器
public bool PauseFlag
{
get => _pauseFlag;
set
{
_pauseFlag = value;
MessagingCenter.Send(new MessageCenter(), "pauseFlagChanged", PauseFlag);
}
}
public PlayerViewModel()
{
playerService = App.PlayerStore;
Duration = TimeSpan.FromMinutes(100);
ButtonHandle();
LoadPlayList();
//接受来自MessageCenter的信息——信息标题为“AddMusic”
MessagingCenter.Subscribe<MessageCenter, string>(this, "AddMusic", AddMusic);
CrossMediaManager.Current.PositionChanged += CurrentPositionChanged;
CrossMediaManager.Current.MediaItemFinished += CurrentMediaItemFinished;
}
#region 加载音乐以及设置开始位置区
/// <summary>
/// 加载音乐,从playerService获取
/// </summary>
private async void LoadPlayList()
{
try
{
//尝试暂停播放器
await CrossMediaManager.Current.Stop();
PauseFlag = true;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("LoadPlayList:" + ex);
}
finally
{
playQueue.Clear();
var players = await playerService.GetObjsAsync(true);
foreach (Player player in players)
{
playQueue.Add(player);
}
SongName = "您还未播放音乐";
SetMusicStater(playQueue.Count == 0 ? InitPointerCode : playQueue.Count - 1);
}
}
private void SetMusicStater(int musicIndex)
{
currentMusicPointer = musicIndex;
}
/// <summary>
/// 只需要添加音乐的URL就好了,因为CrossMediaManager可以获得音乐的所有信息
/// </summary>
/// <param name="arg1"></param>
/// <param name="arg2"></param>
private async void AddMusic(MessageCenter arg1, string arg2)
{
Player newMusic = new Player
{
MusicUrl = arg2
};
await playerService.AddObjAsync(newMusic);
LoadPlayList();
}
#endregion
#region 处理前端按键逻辑区
//状态转换机以及状态处理机
private void ButtonHandle()
{
//初始状态暂停
PauseFlag = true;
PlayCommand = new Command(() => ExecutePlayCommand());
//不向前
nextFlag = false;
NextCommand = new Command(() => ExecuteNextCommand());
//不向后
previousFlag = false;
PreviousCommand = new Command(() => ExecutePreviousCommand());
//允许重新加载歌曲
allowReload = true;
}
private async void CurrentMediaItemFinished(object sender, MediaManager.Media.MediaItemEventArgs e)
{
nextFlag = true;
await PlayerStateHandler();
}
private async void ExecutePreviousCommand()
{
previousFlag = true;
await PlayerStateHandler();
}
private async void ExecuteNextCommand()
{
nextFlag = true;
await PlayerStateHandler();
}
private async void ExecutePlayCommand()
{
if (CrossMediaManager.Current.IsPlaying() == false)
{
PauseFlag = false;
}
else
{
PauseFlag = true;
}
await PlayerStateHandler();
}
private async Task PlayerStateHandler()
{
//如果PlayQueue没有歌,则不做处理
if (playQueue.Count == 0)
{
PauseFlag = true;
return;
}
System.Diagnostics.Debug.WriteLine(nextFlag);
System.Diagnostics.Debug.WriteLine(previousFlag);
//利用循环队列实现播放器
//按下 上/下首歌优先级最高
if (nextFlag)
{
currentMusicPointer++;
currentMusicPointer %= playQueue.Count;
nextFlag = false;
PauseFlag = false;//切歌后,继续播放
allowReload = true;//切歌后,允许重新加载本首歌
}
else if (previousFlag)
{
currentMusicPointer--;
currentMusicPointer = (currentMusicPointer + playQueue.Count) % playQueue.Count;
previousFlag = false;
PauseFlag = false;
allowReload = true;
}
System.Diagnostics.Debug.WriteLine(currentMusicPointer);
System.Diagnostics.Debug.WriteLine(PauseFlag);
System.Diagnostics.Debug.WriteLine(allowReload);
if (PauseFlag == false)
{
if (allowReload)
{
currentMusic = playQueue[currentMusicPointer];
var currentMusicInfo = await CrossMediaManager.Current.Play(currentMusic.MusicUrl);
//拉取歌曲信息
/*
* 事实上,因为有了currentMusic对象,这个对象是网络传给我们的,因此,只需要服务器上有图像就可以了
*/
SongName = currentMusicInfo.FileName.Length >= 20 ? currentMusicInfo.FileName.Substring(0, 17) + "..." : currentMusicInfo.FileName;
}
else await CrossMediaManager.Current.Play();
}
else
{
await CrossMediaManager.Current.Pause();
allowReload = false;
}
}
#endregion
#region 更新音乐信息区
/// <summary>
/// 更新歌曲时间
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void CurrentPositionChanged(object sender, MediaManager.Playback.PositionChangedEventArgs e)
{
NowDuration = e.Position;
//为0:0:0的话,SLider最小值和最大值相等,报错,这么操作就好了
Duration = CrossMediaManager.Current.Duration == TimeSpan.FromMinutes(0)?TimeSpan.FromMinutes(100):CrossMediaManager.Current.Duration;
TimeValue = e.Position.TotalMinutes;
}
#endregion
}
}
这样就实现了音乐播放器的功能。接下来,完成整个PlayerView
Views.PlayerView.xaml.cs
using Conclusion.ViewModels;
using MediaManager;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Conclusion.Interfaces;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using Conclusion.Utils;
namespace Conclusion.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class PlayerView : ContentPage
{
public PlayerView()
{
InitializeComponent();
BindingContext = new PlayerViewModel();
MessagingCenter.Subscribe<MessageCenter, bool>(this, "pauseFlagChanged", HandleIcon);
}
private void HandleIcon(MessageCenter arg1, bool arg2)
{
if(arg2 == true)
{
PlayControlBtn.Icon = "md-play-arrow";
}
else
{
PlayControlBtn.Icon = "md-pause";
}
}
//文件选择
private void ChooseMusic(object sender, EventArgs e)
{
DependencyService.Get<IFilePicker>().PickFile();
}
}
}
个人认为音乐播放器这一页可以很好地为大家展示前后逻辑的分离。
图片展览页
这里主要展示瀑布流布局以及FFimageLoading和CrossMedia的用法
CustomLayout.FlowLayout.cs
(感谢 https://www.cnblogs.com/cjw1115/p/6544544.html 作者)
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
//https://www.cnblogs.com/cjw1115/p/6544544.html
namespace Conclusion.CustomLayout
{
public class FlowLayout : Layout<View>
{
public static readonly BindableProperty ColumnProperty = BindableProperty.Create("Column", typeof(int), typeof(FlowLayout), 1);
public int Column
{
get { return (int)this.GetValue(ColumnProperty); }
set { SetValue(ColumnProperty, value); }
}
private double columnWidth = 0;
public static readonly BindableProperty RowSpacingProerpty = BindableProperty.Create("RowSpacing", typeof(double), typeof(FlowLayout), 0.0);
public double RowSpacing
{
get { return (double)this.GetValue(RowSpacingProerpty); }
set { SetValue(RowSpacingProerpty, value); }
}
public static readonly BindableProperty ColumnSpacingProerpty = BindableProperty.Create("ColumnSpacing", typeof(double), typeof(FlowLayout), 0.0);
public double ColumnSpacing
{
get { return (double)this.GetValue(ColumnSpacingProerpty); }
set { SetValue(ColumnSpacingProerpty, value); }
}
protected override void LayoutChildren(double x, double y, double width, double height)
{
double[] colHeights = new double[Column];
double allColumnSpacing = ColumnSpacing * (Column - 1);
columnWidth = (width - allColumnSpacing) / Column;
foreach (var item in this.Children)
{
var measuredSize = item.Measure(columnWidth, height, MeasureFlags.IncludeMargins);
int col = 0;
for (int i = 1; i < Column; i++)
{
if (colHeights[i] < colHeights[col])
{
col = i;
}
}
item.Layout(new Rectangle(col * (columnWidth + ColumnSpacing), colHeights[col], columnWidth, measuredSize.Request.Height));
colHeights[col] += measuredSize.Request.Height + RowSpacing;
}
}
private double _maxHeight;
/// <summary>
/// 计算父元素需要的空间大小
/// </summary>
/// <param name="widthConstraint">可供布局的宽度</param>
/// <param name="heightConstraint">可供布局的高度</param>
/// <returns>实际需要的布局大小</returns>
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{
double[] colHeights = new double[Column];
double allColumnSpacing = ColumnSpacing * (Column - 1);
columnWidth = (widthConstraint - allColumnSpacing) / Column;
foreach (var item in this.Children)
{
var measuredSize = item.Measure(columnWidth, heightConstraint, MeasureFlags.IncludeMargins);
int col = 0;
for (int i = 1; i < Column; i++)
{
if (colHeights[i] < colHeights[col])
{
col = i;
}
}
colHeights[col] += measuredSize.Request.Height + RowSpacing;
}
_maxHeight = colHeights.OrderByDescending(m => m).First();
return new SizeRequest(new Size(widthConstraint, _maxHeight));
}
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create("ItemTemplate", typeof(DataTemplate), typeof(FlowLayout), null);
public DataTemplate ItemTemplate
{
get { return (DataTemplate)this.GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create("ItemsSource", typeof(IList), typeof(FlowLayout), null, propertyChanged: ItemsSource_PropertyChanged);
public IList ItemsSource
{
get { return (IList)this.GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
private static void ItemsSource_PropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var flowLayout = (FlowLayout)bindable;
var newItems = newValue as IList;
var oldItems = oldValue as IList;
var oldCollection = oldValue as INotifyCollectionChanged;
if (oldCollection != null)
{
oldCollection.CollectionChanged -= flowLayout.OnCollectionChanged;
}
if (newValue == null)
{
return;
}
if (newItems == null)
return;
if (oldItems == null || newItems.Count != oldItems.Count)
{
flowLayout.Children.Clear();
for (int i = 0; i < newItems.Count; i++)
{
var child = flowLayout.ItemTemplate.CreateContent();
((BindableObject)child).BindingContext = newItems[i];
flowLayout.Children.Add((View)child);
}
}
var newCollection = newValue as INotifyCollectionChanged;
if (newCollection != null)
{
newCollection.CollectionChanged += flowLayout.OnCollectionChanged;
}
flowLayout.UpdateChildrenLayout();
flowLayout.InvalidateLayout();
}
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
{
this.Children.RemoveAt(e.OldStartingIndex);
this.UpdateChildrenLayout();
this.InvalidateLayout();
}
if (e.NewItems == null)
{
return;
}
//System.Diagnostics.Debug.WriteLine("OncollectionChanged:"+e.NewItems.Count);
for (int i = 0; i < e.NewItems.Count; i++)
{
var child = this.ItemTemplate.CreateContent();
((BindableObject)child).BindingContext = e.NewItems[i];
this.Children.Add((View)child);
}
this.UpdateChildrenLayout();
this.InvalidateLayout();
}
}
}
首先自定义我们的FlowLayout页,接下来,初始化我们的FFimageloading Nuget
.Android.MainActivity.cs
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{
Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
//请求权限,添加下面一行即可
Plugin.Permissions.PermissionsImplementation.Current.OnRequestPermissionsResult(requestCode, permissions, grantResults);
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
接着初始化CrossMedia以实现照片获取
初始化CrossMedia
在Android.Resources中新建xml文件夹
file_paths.xml
<?xml version="1.0" encoding="utf-8" ?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path name="my_images" path="Pictures" />
<external-files-path name="my_movies" path="Movies" />
</paths>
修改Android.Properties.Menifest
<?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.conclusion" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
<application android:label="Conclusion.Android">
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
</manifest>
准备工作做足后,写我们的Model
Models.Photo
using System;
using System.Collections.Generic;
using System.Text;
namespace Conclusion.Models
{
public class Photo:Item
{
public string PhotoUrl { get; set; }
}
}
ViewModels.PhotoExhibithonViewModel.cs
using System;
using System.Collections.Generic;
using System.Text;
using Xamarin.Forms;
using System.Collections.ObjectModel;
using Conclusion.Models;
using System.Threading.Tasks;
using Conclusion.Services;
using System.Diagnostics;
using Conclusion.Utils;
using Plugin.Media;
namespace Conclusion.ViewModels
{
class PhotoExhibitionViewModel : BaseViewModel
{
private MockDataStore photoService;
private ObservableCollection<Photo> _photos;
public Command PhotoExhibitionViewCommand { get; set; }
public ObservableCollection<Photo> Photos
{
get
{
return _photos;
}
set
{
_photos = value;
OnPropertyChanged();
}
}
public PhotoExhibitionViewModel()
{
photoService = App.PhotoStore;
PhotoExhibitionViewCommand = new Command(async () => await ExecutePhotoExhibitionViewCommand());
if (Photos == null)
{
Photos = new ObservableCollection<Photo>();
PhotoExhibitionViewCommand.Execute(null);
}
MessagingCenter.Subscribe<MessageCenter,string>(this, "AddPhoto", AddPhoto);
}
private async void AddPhoto(MessageCenter arg1, string arg2)
{
System.Diagnostics.Debug.WriteLine("收到信息");
if (arg2.Equals("Album"))
{
if (!CrossMedia.Current.IsPickPhotoSupported)
{
MessagingCenter.Send(new MessageCenter(), "MediaNotAvailable","PickPhoto");
return;
}
var imageFile = await Plugin.Media.CrossMedia.Current.PickPhotoAsync(new Plugin.Media.Abstractions.PickMediaOptions
{
PhotoSize = Plugin.Media.Abstractions.PhotoSize.Medium
});
if (imageFile == null)
return;
//Debug.WriteLine("路径" +imageFile.AlbumPath);
//Debug.WriteLine("路径" +imageFile.Path);
await photoService.AddObjAsync(new Photo
{
PhotoUrl = imageFile.Path
});
}
else
{
if(!CrossMedia.Current.IsCameraAvailable || !CrossMedia.Current.IsTakePhotoSupported)
{
MessagingCenter.Send(new MessageCenter(), "MediaNotAvailable","TakePhoto");
return;
}
var imageFile = await CrossMedia.Current.TakePhotoAsync(new Plugin.Media.Abstractions.StoreCameraMediaOptions
{
Directory = "MyImage",
SaveToAlbum = true,
CompressionQuality = 5,
CustomPhotoSize = 100,
DefaultCamera = Plugin.Media.Abstractions.CameraDevice.Front
});
if (imageFile == null)
return;
await photoService.AddObjAsync(new Photo
{
PhotoUrl = imageFile.AlbumPath
});
}
}
private async Task ExecutePhotoExhibitionViewCommand()
{
Debug.WriteLine("哈哈哈");
IsRefresh = true;
try
{
await Task.Delay(520);
Photos.Clear();
var photos = await photoService.GetObjsAsync(true);
//因为用的Add方法
ObservableCollection<Photo> CachePhotos = new ObservableCollection<Photo>();
foreach(Photo photo in photos)
{
CachePhotos.Add(photo);
}
Photos = CachePhotos;
//Debug.WriteLine(Photos.Count);
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
finally
{
IsRefresh = false;
}
}
}
}
下面开始吐槽
值得注意的是,我这里新建了一个CachePhotos
这是因为,FlowLayout的作者是这样更新ItemSource的
但是,我们每Add一个元素,都会引起FlowLayout的改变,而新元素又只有1个,因此就算我们Add了很多,最终FlowLayout也只能显示一个元素
下面看看我们的界面
Views.PhotoExhibitionView.xaml
<?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:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:customlayout="clr-namespace:Conclusion.CustomLayout"
xmlns:circlebutton="clr-namespace:ButtonCircle.FormsPlugin.Abstractions;assembly=ButtonCircle.FormsPlugin.Abstractions"
mc:Ignorable="d"
xmlns:ffimageloading="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
x:Class="Conclusion.Views.PhotoExhibitionView"
Shell.NavBarIsVisible="False">
<!--这一页展示瀑布流图片布局以及FFimageLoading的用法,用照相机获取图片及本地图片的获取-->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<!--#region 顶部照片添加区-->
<Frame
Grid.Row="0"
Margin="10"
CornerRadius="10"
HasShadow="True">
<circlebutton:CircleButton
FontIcon="Material"
Icon="md-add-a-photo"
FontSize="30" TextColor="Black"
HeightRequest="40" WidthRequest="40"
BorderThickness="0" BorderColor="Black"
BackgroundColor="WhiteSmoke"
Clicked="ChoosePhoto">
</circlebutton:CircleButton>
</Frame>
<!--#endregion-->
<!--#region 照片展示区-->
<RefreshView
Grid.Row="1"
Margin="10"
IsRefreshing="{Binding IsRefresh}"
Command="{Binding PhotoExhibitionViewCommand}">
<ScrollView>
<customlayout:FlowLayout
ItemsSource="{Binding Photos}"
Column="2"
RowSpacing="10"
ColumnSpacing="10">
<customlayout:FlowLayout.ItemTemplate>
<DataTemplate>
<Grid
BackgroundColor="Black"
Padding="5">
<ffimageloading:CachedImage
Source="{Binding PhotoUrl}"
LoadingPlaceholder="notFound.jpg"
ErrorPlaceholder="notFound.jpg">
</ffimageloading:CachedImage>
</Grid>
</DataTemplate>
</customlayout:FlowLayout.ItemTemplate>
</customlayout:FlowLayout>
</ScrollView>
</RefreshView>
<!--#endregion-->
</Grid>
</ContentPage>
这里说一下为什么用FFimageLoading。
FFimageLoading可以加快图片加载速率,有效地管理内存,让图片展示得更加完美,当然,大家可以试试单纯的Image标签(手动滑稽)
Views.PhotoExhibitionView.xaml.cs
using Conclusion.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Conclusion.ViewModels;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Conclusion.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class PhotoExhibitionView : ContentPage
{
public PhotoExhibitionView()
{
InitializeComponent();
BindingContext = new PhotoExhibitionViewModel();
MessagingCenter.Subscribe<MessageCenter,string>(this, "MediaNotAvailable", HandleError);
}
private void HandleError(MessageCenter arg1, string arg2)
{
if (arg2.Equals("PickPhoto"))
{
DisplayAlert("失败", "相册不可用", "确认");
}
else
{
DisplayAlert("失败","相机不可用","确认");
}
}
private async void ChoosePhoto(object sender, EventArgs e)
{
string choice = await DisplayActionSheet("添加照片", "取消", null, "相册", "相机");
System.Diagnostics.Debug.WriteLine(choice);
if(choice == "取消")
{
return;
}
choice = choice == "相册" ? "Album" : "Camera";
MessagingCenter.Send(new MessageCenter(), "AddPhoto", choice);
}
}
}
.Android.MainActivity.cs完整版
using System;
using Android.App;
using Android.Content.PM;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Android.OS;
using Xamarin.Forms;
using Android.Support.V4.Content;
using Android;
using Android.Support.V4.App;
using ButtonCircle;
using ButtonCircle.FormsPlugin.Droid;
using MediaManager;
using Plugin.CurrentActivity;
using Android.Content;
using Conclusion.Utils;
using Xamarin.Forms.Platform.Android;
using FFImageLoading.Forms.Platform;
namespace Conclusion.Droid
{
[Activity(Label = "Conclusion", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
protected override void OnCreate(Bundle savedInstanceState)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
base.OnCreate(savedInstanceState);
//包的初始化
NugetInit(savedInstanceState);
//布局的初始化
LayOutInit();
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
LoadApplication(new App());
//写一个函数来提供权限
CheckPermisson();
}
private void LayOutInit()
{
var newUiOptions = (int)SystemUiFlags.LayoutStable;
newUiOptions |= (int)SystemUiFlags.LayoutFullscreen;
//newUiOptions |= (int)SystemUiFlags.HideNavigation;
Window.SetStatusBarColor(Android.Graphics.Color.Argb(0, 0, 0, 0));
newUiOptions |= (int)SystemUiFlags.ImmersiveSticky;
Window.DecorView.SystemUiVisibility = (StatusBarVisibility)newUiOptions;
}
private void NugetInit(Bundle savedInstanceState)
{
Forms.SetFlags("CollectionView_Experimental");
CrossCurrentActivity.Current.Init(this, savedInstanceState);
ButtonCircleRenderer.Init();
CachedImageRenderer.Init(enableFastRenderer: true);
CachedImageRenderer.InitImageViewHandler();
CrossMediaManager.Current.Init(this);
}
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{
Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
//请求权限
Plugin.Permissions.PermissionsImplementation.Current.OnRequestPermissionsResult(requestCode, permissions, grantResults);
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
private void CheckPermisson()
{
//检查录音权限
if (ContextCompat.CheckSelfPermission(this, Manifest.Permission.RecordAudio) != Permission.Granted)
{
ActivityCompat.RequestPermissions(this, new string[] { Manifest.Permission.RecordAudio }, 1);
}
//检查外部存储 读写权限
if (ContextCompat.CheckSelfPermission(this, Manifest.Permission.ReadExternalStorage) != Permission.Granted ||
ContextCompat.CheckSelfPermission(this, Manifest.Permission.WriteExternalStorage) != Permission.Granted)
{
ActivityCompat.RequestPermissions(this, new string[] {Manifest.Permission.ReadExternalStorage,
Manifest.Permission.WriteExternalStorage}, 1);
}
}
protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
if (resultCode != Result.Ok)
{
return;
}
else
{
if (data == null)
{
return;
}
else
{
var uri = data.Data as Android.Net.Uri;
System.Diagnostics.Debug.WriteLine("uri:" + uri);
//发送AddMusic消息
MessagingCenter.Send(new MessageCenter(), "AddMusic", uri.ToString());
}
}
}
}
}
欢迎交流
Xamarin开发群:906556968
QQ:1972353869