2017-07-13

開啟舊的專案並且重新建置,得到錯誤訊息:java.lang.IllegalArgumentException: already added

今天,將一個月前的專案開啟,並且進行重新建置,想要看看執行結果,不過,卻得到底下的錯誤訊息:
java.lang.IllegalArgumentException: already added : Landroid/support/v4/accessibilityservice/AccessibilityServiceInfoCompat;
我非常確定在最後一次開啟那個專案的時候,確實是可以建置、除錯與執行的。
我現在的 Visual Studio 2017 已經更新到最新版本,而底下是我的 Visual Studio 2017 的版本資訊:
  • Visual Studio Enterprise 2017 版本 15.2 (26430.15)
  • Xamarin 4.5.0.486
  • Xamarin.Android SDK 7.3.1.2
  • Xamarin.ios and Xamarion.Mac SDK 10.10.0.37

如何解決一問題

我嘗試了許多不同方法,試圖來解決此一問題,不過,最終都失敗收場;最後,我解決的如下:
我這裡的解決方法
  • 在方案總管中,滑鼠右擊方案,選擇 管理方案的 NuGet 套件
  • 選擇 已安裝 標籤頁次,在文字搜尋輸入盒中,輸入 Xamarin.Android.Support.v4 查詢出這個套件。
  • 勾選右半部的 Android 專案
  • 選擇最新的版本,在這個時間點,最新的版本將會是 25.3.1
  • 點選安裝按鈕,進行升級到最新套件。
  • 當相關 NuGet 套件都升級完成之後,關閉 Visual Studio
  • 將該方案所在目錄下的 Packages 目錄刪除
  • 重新使用 Visual Studio 2017 開啟這個方案
  • 在 Visual Studio 的方案總管視窗內,將核心 PCL 專案與 Android 專案內的 bin & obj 目錄刪除
  • 分別重新建置核心 PCL 專案與 Android 專案
  • 應該就可以正常建置成功了

2017-07-05

從網路下載 APK 檔案,並且於 Xamarin.Forms 專案內進行升級動作

在這份筆記中,將會進行如何透過 Xamarin.Forms 的 App,從網路上下載最新的 APK 檔案到手機上,接著,進行這個 APK 檔案的安裝與升級動作。
在這樣的需求中,我們需要解決與處理底下這些需求:
  • 如何從網路上,透過指定的 URL ,取得最新的 APK 檔案。
  • 當取得 APK 檔案之後,要能夠儲存到手機記憶卡中。
  • 最後,要能夠執行這個 APK 檔案,便可以進行 App 的升級。

如何從網路上,透過指定的 URL ,取得最新的 APK 檔案

這當然需要使用 .NET 的 HttpClient 類別所產生出來的物件,使用這個物件的 GetStreamAsync 方法,便可以透過非同步的方式,取得遠端 Web Server 上的這個 APK 檔案。
由於這個方法 GetStreamAsync 會回傳一個 Stream 物件,我們便可以將這個物件,傳遞到原生 Android 專案內,就可以使用原生 Android SDK API,將這些檔案內容,寫入到 Android 專案內。
            DownloadCommand = new DelegateCommand(async () =>
            {
                HttpClientHandler handle = new HttpClientHandler();
                HttpClient client = new HttpClient(handle);
                Title = "正在下載中";
                using (var fooStream = await client.GetStreamAsync("https://github.com/vulcanlee/test/raw/master/com.vulcanlab.task.apk"))
                {
                    _APK.GenApkFile(fooStream);
                }

                Title = "已經下載完成";
            });

取得 APK 檔案之後,要能夠儲存到手機記憶卡中

由於 PCLStorage 僅能夠提供讀寫 App 的沙箱儲存體內,寫入到這些沙箱內的檔案,任何人與相關程式,是無法存取這個檔案的;因此,我們需要把 APK 檔案寫入到一個公開的目錄下,在這裡,我們將會寫入到 下載 資料夾內。
要做到這樣的工作,我們需要透過相依性服務注入功能,將 Android 原生平台下實作的介面物件,注入到 核心PCL 專案內來使用。
我們需要先在核心PCL專案內,建立一個介面:
在這個介面內,宣告了兩個方法需要在原生 Android 平台下來實作,一個是將剛剛從網路上取 APK 檔案的 Stream 物件內容,寫入到 Android 裝置記憶卡內;另外一個是將剛剛下載下來的檔案,進行安裝。
    public interface IAPK
    {
        void InstallAPK();

        void GenApkFile(Stream downloadStream);
    }
IAPK 介面在 Android 平台下實作的類別如下所示:
  • GenApkFile
    這裡使用了 Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDownloads).ToString(); 敘述,取得了手機中下載資料夾路徑,接著判斷該檔案是否存在,若不存在,則會產生出這個檔案,若已經存在,則會直接開啟,等候寫入這個檔案;最後,使用 CopyToAsnyc 方法,針對兩個 Stream 來複製,完成檔案寫入的需求。
[assembly: Xamarin.Forms.Dependency(typeof(SelfInstAPK.Droid.Infrastructures.APK_Droid))]
namespace SelfInstAPK.Droid.Infrastructures
{
    public class APK_Droid : IAPK
    {
        public string DownlaodPath = "";
        public string Filename = "new.apk";
        public string FullFilename = "";
        FileStream fooStream;

        public void GenApkFile(Stream downloadStream)
        {
            DownlaodPath = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDownloads).ToString();
            FullFilename = Path.Combine(DownlaodPath, Filename);

            if (File.Exists(FullFilename) == false)
            {
                Directory.CreateDirectory(DownlaodPath);
                fooStream= File.Create(FullFilename);
            }
            else
            {
                fooStream= File.OpenWrite(FullFilename);
            }


            using (FileStream fs = fooStream)
            {
                downloadStream.CopyTo(fs);
            }
        }

        public void InstallAPK()
        {
            Intent setupIntent = new Intent(Intent.ActionView);
            setupIntent.SetDataAndType(Android.Net.Uri.FromFile(new Java.IO.File(FullFilename)), "application/vnd.android.package-archive");
            setupIntent.AddFlags(ActivityFlags.NewTask);

            var context = Android.App.Application.Context;
            context.StartActivity(setupIntent);
        }
    }
}

最後,要能夠執行這個 APK 檔案,便可以進行 App 的升級

這個實作介面的另外一個方法就是,InstallAPK,我們建立一個 Intent 物件,設定好相關參數,最後,使用 StartActivity 啟動安裝這個 APK 需求。

使用 Prism 自動注入這個介面實作物件

我們不需要使用 Xamarin.Forms 內建的 DependencyService 提供的靜態方法來注入實作物件,我們僅需要在 ViewModel 內的建構式,填入這個介面參數,Prism 便會自動幫您注入實作好的物件,這樣,您就可以在 PCL 專案內,將資料寫到 Android 的目錄下。
        IAPK _APK;

        public MainPageViewModel(IAPK apk)
        {
            _APK = apk;
            ...
        }

其他說明

若您沒有將 APK 檔案寫入到空開可以存取的目錄下,則當進行安裝 APK 檔案的時候,會出現如下圖錯誤訊息畫面。
Parse Error

There was a problem parsing the package.
而正常的情況下,會出現底下的畫面,讓您可以安裝這個 APK 檔案。

2017-07-01

圖片裁減與儲存和讀取 (學習將 Code Behind 範例,轉換成為 MVVM 模式)

在這份筆記中,我將會記錄如何使用 Xam.Plugins.ImageCropper 這個套件,利用 Xamarin.Froms 設計出可以根據所選取的照片,自行調剪裁區域,以辨惑得到想要大小的圖片範圍;當然,您也可以將擷取出來的圖片儲存到手機中或者告訴您如何從手機中讀取出這個圖片,並且顯示在 Xamarin.Forms 的畫面上。
 由於 Xam.Plugins.ImageCropper 這個套件中的範例程式碼都是使用 Code Behind 的方式來設計,對於我們經常在使用 MVVM 開發的開發者而言,遇到一個頭大的問題,那就是要如何把這些網路上所查詢、看到、讀到的 Code Behind 程式碼,轉換在我們 Prims 的 MVVM 架構下來使用。
您可以先觀看 Xam.Plugins.ImageCropper 所附的範例專案,接者,跟著本篇文章實作一次,就會知道箇中巧妙了。
在這裡我必須說明,在使用 MVVM 模式開發的時候,並沒有說不能夠使用 Code Behind 方式來開發,只是希望您儘量採用 MVVM 方式開發,在某些情況或者情境下,您一樣可以使用 Code Behind 的方式開發;當然,想要完全把 Code Behind 程式碼都轉換成為 MVVM 方式,有些時候,您可能會需要學會 XAML Behaviors 的觀念的開發技巧,便可以將更多的 Code Behind 包裝起來。
在這個練習中,我們需要使用底下三個套件:
在 Xam.Plugins.ImageCropper 套件內,其中,對於圖片裁切功能方面,在 Android 版本會使用到 https://github.com/ArthurHub/Android-Image-Cropper 套件,而 iOS 版本會使用到 https://github.com/TimOliver/TOCropViewController 這個套件

disableplanetxamarin

範例專案來源

建立練習專案

  1. 首先,開啟您的 Visual Studio 2017
  2. 接著透過 Visual Studio 2017 功能表,選擇這些項目 檔案 > 新增 > 專案 準備新增一個專案。
  3. 接著,Visual Studio 2017 會顯示 新增專案 對話窗,請在這個對話窗上,進行選擇 Visual C# > Prism > Prism Unity App (Xamarin.Forms)
  4. 接著,在最下方的 名稱 文字輸入盒處,輸入 XFImgCrop 這個名稱,最後使用滑鼠右擊右下方的 確定 按鈕。
  5. 當出現 PRISM PROJECT WIZARD 對話窗之後,請勾選 ANDROIDiOS 這2個平台選項,並且點選 CREATE PROJECT 按鈕。
  6. 接著會看到 新的通用 Windows 專案 對話視窗,此時,您只需要按下 確定 按鈕即可,此時,專案精靈會繼續完成相關平台的專案建立工作。
  7. 最後,整個新的 Xamarin.Forms 專案就建立完成了。

 確認 Android 專案可以建置與執行

  • 當 Visual Studio 2017 完成方案建立之後,會在方案總管視窗中,看到2個原生專案 XFImgCrop.Droid (Android 專案) / XFImgCrop.iOS (iOS 專案),這2個專案將會用來產生三個行動平台所用到的行動 App 與散佈 (Distribution) 檔案;和一個 XFImgCrop (核心PCL專案),這個專案將是用來設計 Xamarin.Forms 的相關檔案,也就是每個平台的共用 UI 與 共用商業邏輯,都會寫在這個專案內。
  • 滑鼠右擊 XFImgCrop.Droid 專案,選擇 設定為起始專案
  • 滑鼠右擊 XFImgCrop.Droid 專案,選擇 建置
  • 成功建置完成後,在螢幕的左下角,會出現, 訊息
現在,還不用特別來執行這個專案,因為,我們還沒有設計這個應用程式要出現的畫面。
接下來,我們將開始安裝兩個 NuGet 套件,並且確認您的應用程式可以使用裝置的鏡頭進行拍照與選取相簿的功能都可以正常運行。

 安裝需要的 NuGet 套件

 PropertyChanged.Fody

 這個 PropertyChanged.Fody 套件是用來簡化資料綁定的程式碼開發,因為,這個套件會自動於編譯時期,幫您自動產生相關資料綁定會用到的程式碼。
  • 滑鼠右擊 方案 XFImgCrop,選擇 管理方案的 NuGet 套件
  • 在 NuGet - 解決方案 視窗中,點選 瀏覽 標籤頁次
  • 在搜尋文字輸入盒中輸入 PropertyChanged.Fody,搜尋出這個套件
  • 在右半部,只需要勾選核心PCL專案,設定要安裝到最新版本的套件
  • 點選 安裝 按鈕,進行安裝這個套件
  • 在下圖中,顯示了當要安裝 PropertyChanged.Fody 這個套件,還須連帶安裝其他相依 NuGet 套件清單。
    確認無誤後,請點選 確定 按鈕
  • 若出現 接受授權 對話窗,請點選 我接受 按鈕

 Xam.Plugin.Media

 這個 Xam.Plugin.Media 套件是用來讓您可以在核心PCL專案內,呼叫裝置的拍照與選取相簿中某個圖片的功能。
  • 滑鼠右擊 方案 XFImgCrop,選擇 管理方案的 NuGet 套件
  • 在 NuGet - 解決方案 視窗中,點選 瀏覽 標籤頁次
  • 在搜尋文字輸入盒中輸入 Xam.Plugin.Media,搜尋出這個套件
  • 在右半部,需要勾選所有的專案,設定要安裝到最新版本的套件
  • 點選 安裝 按鈕,進行安裝這個套件
  • 在下圖中,顯示了當要安裝 Xam.Plugin.Media 這個套件,還須連帶安裝其他相依 NuGet 套件清單。
    確認無誤後,請點選 確定 按鈕
  • 若出現 接受授權 對話窗,請點選 我接受 按鈕
當您安裝完成這個 Xam.Plugin.Media 套件,Visual Studio 2017 中將會顯示出一個 readme.txt 視窗,請您務必要詳細閱讀這份文件,因為,上面會有說明再使用這個套件的時候,該注意的事項:套件初始化、原生平台的權限設定需求、基本使用方法等等。

 編譯/建置 確認沒有錯誤

  • 滑鼠右擊 XFImgCrop (可攜式) 專案,選擇 建置
  • 成功建置完成後,在螢幕的左下角,會出現, 訊息
  • 滑鼠右擊 XFImgCrop.Droid 專案,選擇 建置
  • 成功建置完成後,在螢幕的左下角,會出現, 訊息

建立專案資料夾

為了有效分類與管理專案內的程式碼,我們將要建立底下目錄

核心PCL專案

  • 滑鼠右擊 XFImgCrop (可攜式) 核心PCL專案,選擇 加入 > 新增資料夾
  • 在新產生的資料夾名稱中,輸入 CustomControls
  • 滑鼠右擊 XFImgCrop (可攜式) 核心PCL專案,選擇 加入 > 新增資料夾
  • 在新產生的資料夾名稱中,輸入 Infrastructures

原生 Android 專案

  • 滑鼠右擊 XFImgCrop.Droid 核心PCL專案,選擇 加入 > 新增資料夾
  • 在新產生的資料夾名稱中,輸入 Renderers
  • 滑鼠右擊 XFImgCrop.Droid 核心PCL專案,選擇 加入 > 新增資料夾
  • 在新產生的資料夾名稱中,輸入 Infrastructures

原生 iOS 專案

  • 滑鼠右擊 XFImgCrop.iOS 核心PCL專案,選擇 加入 > 新增資料夾
  • 在新產生的資料夾名稱中,輸入 Renderers
  • 滑鼠右擊 XFImgCrop.iOS 核心PCL專案,選擇 加入 > 新增資料夾
  • 在新產生的資料夾名稱中,輸入 Infrastructures

加入全域物件宣告

  • 在核心PCL專案下 ,開啟這個 App.xaml.cs 檔案
  • 將這個 public static byte[] CroppedImage; C# 程式碼,加入到 class App 類別宣告之後
    public partial class App : PrismApplication
    {
        public static byte[] CroppedImage;
        ...
    }

修改首頁頁面 (View)

  • 在核心PCL專案的 Views 資料夾,開啟這個 MainPage.xaml 檔案
  • 將底下的 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:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="XFImgCrop.Views.MainPage"
             Title="MainPage">

    <StackLayout HorizontalOptions="Center" VerticalOptions="Center">
        <Image Source="{Binding fooImageSource}"/>
        <Button Text="選取圖片" Command="{Binding TakePictureCommand}"/>
        <Button Text="儲存圖片" Command="{Binding SavePictureCommand}"/>
        <Button Text="讀取圖片" Command="{Binding LoadPictureCommand}"/>
        <Image Source="{Binding fooLoadImageSource}"/>
    </StackLayout>

</ContentPage>

修改首頁檢視模型 (ViewModel)

  • 在核心PCL專案的 ViewModels 資料夾,開啟這個 MainPageViewModel.cs 檔案
  • 將底下的 C# 程式碼,完全替換掉這個檔案中的類別 MainPageViewModel 定義內容。
    public class MainPageViewModel : INotifyPropertyChanged, INavigationAware
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public ImageSource fooImageSource { get; set; }
        public ImageSource fooLoadImageSource { get; set; }

        ImageSource _imageSource;
        private IMedia _mediaPicker;
        Image image;

        // 要執行 Code Behind 的委派方法
        public delegate Task TakePictureDelegate(byte[] image);
        public TakePictureDelegate NavigationImageCropping;

        // 命令物件欄位
        public DelegateCommand TakePictureCommand { get; set; }
        public DelegateCommand LoadPictureCommand { get; set; }
        public DelegateCommand SavePictureCommand { get; set; }

        // 注入物件欄位
        public readonly IPageDialogService _dialogService;
        private readonly INavigationService _navigationService;
        private readonly IEventAggregator _eventAggregator;
        //private readonly IPicture _picture;

        public MainPageViewModel(INavigationService navigationService, IEventAggregator eventAggregator,
            IPageDialogService dialogService //, IPicture picture
            )
        {
            //IUnityContainer myContainer = (App.Current as PrismApplication).Container;
            //_picture= myContainer.Resolve<IPicture>();
            // 相依性服務注入的物件
            //_picture = picture;
            _dialogService = dialogService;
            _eventAggregator = eventAggregator;
            _navigationService = navigationService;

            // 頁面中綁定的命令
            TakePictureCommand = new DelegateCommand(async () =>
            {
                var fooAction = await _dialogService.DisplayActionSheetAsync("請選擇圖片來源", "取消", null, "相簿", "拍照");
                if (fooAction == "相簿")
                {
                    App.CroppedImage = null;
                    await SelectPicture();
                }
                else if (fooAction == "拍照")
                {
                    App.CroppedImage = null;
                    await TakePicture();
                }
                else if (fooAction == "取消")
                {

                }
            });

            SavePictureCommand = new DelegateCommand(() =>
            {
                if (App.CroppedImage != null)
                {
                    //_picture.SavePictureToDisk("MyCrop", App.CroppedImage);
                }
            });

            LoadPictureCommand = new DelegateCommand(() =>
            {
                //App.CroppedImage = _picture.LoadPictureFromDisk("MyCrop");
                Stream stream = new MemoryStream(App.CroppedImage);
                fooLoadImageSource = ImageSource.FromStream(() => stream);
            });
        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {

        }

        public void OnNavigatingTo(NavigationParameters parameters)
        {

        }

        public void OnNavigatedTo(NavigationParameters parameters)
        {
        }


        // 其他方法
        public void Refresh()
        {
            try
            {
                if (App.CroppedImage != null)
                {
                    Stream stream = new MemoryStream(App.CroppedImage);
                    fooImageSource = ImageSource.FromStream(() => stream);
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
        }

        private async void Setup()
        {
            if (_mediaPicker != null)
            {
                return;
            }

            ////RM: hack for working on windows phone? 
            await CrossMedia.Current.Initialize();
            _mediaPicker = CrossMedia.Current;
        }

        private async Task SelectPicture()
        {
            Setup();

            _imageSource = null;

            try
            {

                var mediaFile = await CrossMedia.Current.PickPhotoAsync();

                _imageSource = ImageSource.FromStream(mediaFile.GetStream);

                var memoryStream = new MemoryStream();
                await mediaFile.GetStream().CopyToAsync(memoryStream);
                byte[] imageAsByte = memoryStream.ToArray();

                //await NavigationImageCropping(imageAsByte);
                //await Navigation.PushModalAsync(new CropView(imageAsByte, Refresh));

            }
            catch (System.Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
        }

        private async Task TakePicture()
        {
            Setup();

            _imageSource = null;

            try
            {
                var mediaFile = await CrossMedia.Current.TakePhotoAsync(new StoreCameraMediaOptions
                {
                    //DefaultCamera = CameraDevice.Front
                });

                _imageSource = ImageSource.FromStream(mediaFile.GetStream);

                var memoryStream = new MemoryStream();
                await mediaFile.GetStream().CopyToAsync(memoryStream);
                byte[] imageAsByte = memoryStream.ToArray();

                //await NavigationImageCropping(imageAsByte);
                //await Navigation.PushModalAsync(new CropView(imageAsByte, Refresh));
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
        }
    }
  • 在這個檔案前面,加入底下 using 敘述
using Plugin.Media;
using Plugin.Media.Abstractions;
using Prism.Events;
using Prism.Services;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Xamarin.Forms;

針對 Xam.Plugin.Media 套件所需要進行修正

關於所要做的各平台要修正的動作,詳細的說明文件內容,可以參考上面提到的 readme.txt 檔案或者這個網址 https://github.com/jamesmontemagno/MediaPlugin

原生 Android 專案

  • 在原生 Android 專案 (XFImgCrop.Droid) 下,開啟這個 MainActivity.cs 檔案
  • 在 MainActivity 類別內,加入底下的方法定義
        public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
        {
            Plugin.Permissions.PermissionsImplementation.Current.OnRequestPermissionsResult(requestCode, permissions, grantResults);
        }

原生 iOS 專案

  • 在原生 iOS 專案 (XFImgCrop.iOS) 下,開啟這個 Info.plist 檔案
    您也可以使用 Visual Studio Code 打開這個檔案,直接修改這個檔案內的 XML 內容
  • 在這個 XML 檔案內容裡面,在檔案最後,找到 </dict> 這個關鍵字,請在這個關鍵字之前,加入底下的方法定義
       <key>NSCameraUsageDescription</key>
      <string>This app needs access to the camera to take photos.</string>
      <key>NSPhotoLibraryUsageDescription</key>
      <string>This app needs access to photos.</string>
      <key>NSMicrophoneUsageDescription</key>
      <string>This app needs access to microphone.</string>
  • 修改完成之後,會如同底下螢幕截圖

 編譯/建置 確認沒有錯誤

  • 滑鼠右擊 XFImgCrop (可攜式) 專案,選擇 建置
  • 成功建置完成後,在螢幕的左下角,會出現, 訊息
  • 滑鼠右擊 XFImgCrop.Droid 專案,選擇 建置
  • 成功建置完成後,在螢幕的左下角,會出現, 訊息
  • 滑鼠右擊 XFImgCrop.iOS 專案,選擇 建置
  • 成功建置完成後,在螢幕的左下角,會出現, 訊息

在  平台下來進行測試

  • 滑鼠右擊 XFImgCrop.Droid 專案,選擇 設定為起始專案
  • 在工具列上,確認選取到適當的 Android 模擬器 (這裡選取的是 Visual Studio for Android Emulator 的 Android 6.0 - API 23 的模擬器),接著點選綠色三角形,準備在 Android 平台下執行這個專案。
    您也可以依據您的電腦環境,選擇適合您的 Android 模擬器或者實體手機
  • 若您在模擬器上看到下圖畫面,那表示您可以正常的建置、執行與得到正確的結果。
  • 請點選 選取圖片 按鈕,當出現如下圖的對話窗畫面,請點選 相簿 或者 拍照 按鈕,看看是否可以可以出現透過手機拍照的功能與看到可以選取項目內的圖片
  • 若一切都可以正常操作,那就表示 Xam.Plugin.Media 這個套件,已經可以正常使用,而且相關權限也都已經開啟了。

在  平台下來進行測試

由於 iOS 的模擬器,並不支援手機拍照功能,所以,我們需要使用實際的 iOS 裝置來進行測試;因此,您需要為這個專案建立一個 Provisioning Profile,這樣,才能夠在這台裝置上進行除錯。
  • 滑鼠右擊 XFImgCrop.iOS 專案,選擇 設定為起始專案
  • 在工具列上,切換 方案平台 下拉選單到 iPhone,接著選取您所要測試的實際硬體裝置,在這裡,我將會使用 iPad 來做為測試對象。
  • 若在您的 iOS 實際裝置上看到下圖畫面,那表示您可以正常的建置、執行與得到正確的結果。
  • 請點選 選取圖片 按鈕,當出現如下圖的對話窗畫面,請點選 相簿 或者 拍照 按鈕,看看是否可以可以出現透過手機拍照的功能與看到可以選取項目內的圖片
  • 若一切都可以正常操作,那就表示 Xam.Plugin.Media 這個套件,已經可以正常使用,而且相關權限也都已經開啟了。

 安裝需要的 NuGet 套件

 Xam.Plugins.ImageCropper

 這個 Xam.Plugins.ImageCropper 套件是用來提供使用者一個可以透過手勢操作,選取出某個照片中的一部分內容,這個套件,便會將所選取的區域擷取出來,變成 一個圖片資源,方便我們日後操作。
  • 滑鼠右擊 方案 XFImgCrop,選擇 管理方案的 NuGet 套件
  • 在 NuGet - 解決方案 視窗中,點選 瀏覽 標籤頁次
  • 在搜尋文字輸入盒中輸入 Xam.Plugins.ImageCropper,搜尋出這個套件
  • 在右半部,不需要勾選核心PCL專案,而是要勾選所有的原生專案,設定要安裝到最新版本的套件
  • 點選 安裝 按鈕,進行安裝這個套件
  • 在下圖中,顯示了當要安裝 Xam.Plugins.ImageCropper 這個套件,還須連帶安裝其他相依 NuGet 套件清單。
    確認無誤後,請點選 確定 按鈕
  • 若出現 接受授權 對話窗,請點選 我接受 按鈕

設計可以讓使用者選取部分圖片與存檔和讀取之功能

建立一個圖片存檔與讀取的介面與原生平台實作

建立 圖片存檔與讀取的介面 IPicture

  • 滑鼠右擊核心PCL專案下的 Infrastructures 資料夾
  • 從彈出功能表中,選擇 加入 > 類別
  • 在 加入新項目 - XFImgCorp 對話窗中,點選 Visual C# > 介面
  • 在 加入新項目 - XFImgCorp 對話窗下方的名稱欄位,輸入 IPicture
  • 最後,點選 新增 按鈕
  • 將底下關於 IPicture 介面定義 C# 程式碼,覆蓋掉剛剛產生的 IPicture 介面定義
    // http://www.goxuni.com/668633-how-to-save-an-image-to-a-device-using-xuni-and-xamarin-forms/
    public interface IPicture
    {
        void SavePictureToDisk(string filename, byte[] imageData);
        byte[] LoadPictureFromDisk(string filename);
    }

建立 Android 平台下的 IPicture 實作

  • 滑鼠右擊核心PCL專案下的 Infrastructures 資料夾
  • 從彈出功能表中,選擇 加入 > 類別
  • 在 加入新項目 - XFImgCorp.Droid 對話窗中,點選 Visual C# > 類別
  • 在 加入新項目 - XFImgCorp.Droid 對話窗下方的名稱欄位,輸入 Picture_Droid
  • 最後,點選 新增 按鈕
  • 將底下關於 Picture_Droid 介面實作 C# 程式碼,覆蓋掉剛剛產生的 Picture_Droid 類別定義
    public class Picture_Droid : XFImgCrop.Infrastructures.IPicture
    {
        public byte[] LoadPictureFromDisk(string filename)
        {
            byte[] fooResult = new byte[10];
            var dir = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDcim);
            var pictures = dir.AbsolutePath;
            string name = filename + ".jpg";
            string filePath = System.IO.Path.Combine(pictures, name);

            try
            {
                fooResult = System.IO.File.ReadAllBytes(filePath);
            }
            catch (System.Exception e)
            {
                System.Console.WriteLine(e.ToString());
            }

            return fooResult;
        }

        public void SavePictureToDisk(string filename, byte[] imageData)
        {
            var dir = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDcim);
            var pictures = dir.AbsolutePath;
            string name = filename + ".jpg";
            string filePath = System.IO.Path.Combine(pictures, name);
            try
            {
                System.IO.File.WriteAllBytes(filePath, imageData);
                //mediascan adds the saved image into the gallery
                var mediaScanIntent = new Intent(Intent.ActionMediaScannerScanFile);
                mediaScanIntent.SetData(Android.Net.Uri.FromFile(new File(filePath)));
                Xamarin.Forms.Forms.Context.SendBroadcast(mediaScanIntent);
            }
            catch (System.Exception e)
            {
                System.Console.WriteLine(e.ToString());
            }

        }
    }
  • 在這個檔案前面,加入底下 using 敘述
using Java.IO;
using XFImgCrop.Droid.Infrastructures;
  • 在這個檔案的 namespace XFImgCrop.Droid.Infrastructures 敘述之前,加入底下這行程式碼。
[assembly: Xamarin.Forms.Dependency(typeof(Picture_Droid))]

 編譯/建置 確認沒有錯誤

  • 滑鼠右擊 XFImgCrop (可攜式) 專案,選擇 建置
  • 成功建置完成後,在螢幕的左下角,會出現, 訊息
  • 滑鼠右擊 XFImgCrop.Droid 專案,選擇 建置
  • 成功建置完成後,在螢幕的左下角,會出現, 訊息

建立 iOS 平台下的 IPicture 實作

  • 滑鼠右擊核心PCL專案下的 Infrastructures 資料夾
  • 從彈出功能表中,選擇 加入 > 類別
  • 在 加入新項目 - XFImgCorp.iOS 對話窗中,點選 Visual C# > 類別
  • 在 加入新項目 - XFImgCorp.iOS 對話窗下方的名稱欄位,輸入 Picture_iOS
  • 最後,點選 新增 按鈕
  • 將底下關於 Picture_iOS 介面實作 C# 程式碼,覆蓋掉剛剛產生的 Picture_iOS 類別定義
    public class Picture_iOS : XFImgCrop.Infrastructures.IPicture
    {
        public byte[] LoadPictureFromDisk(string filename)
        {
            byte[] fooResult = new byte[10];

            var documentsDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
            string filePath = System.IO.Path.Combine(documentsDirectory, filename);

            try
            {
                fooResult = System.IO.File.ReadAllBytes(filePath);
            }
            catch (System.Exception e)
            {
                System.Console.WriteLine(e.ToString());
            }

            return fooResult;
        }

        public void SavePictureToDisk(string filename, byte[] imageData)
        {
            var documentsDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
            string filePath = System.IO.Path.Combine(documentsDirectory, filename);

            try
            {
                System.IO.File.WriteAllBytes(filePath, imageData);
            }
            catch (System.Exception e)
            {
                System.Console.WriteLine(e.ToString());
            }
        }
    }
  • 在這個檔案的 namespace XFImgCrop.iOS.Infrastructures 敘述之前,加入底下這行程式碼。
[assembly: Xamarin.Forms.Dependency(typeof(XFImgCrop.iOS.Infrastructures.Picture_iOS))]

 編譯/建置 確認沒有錯誤

  • 滑鼠右擊 XFImgCrop (可攜式) 專案,選擇 建置
  • 成功建置完成後,在螢幕的左下角,會出現, 訊息
  • 滑鼠右擊 XFImgCrop.iOS 專案,選擇 建置
  • 成功建置完成後,在螢幕的左下角,會出現, 訊息
在練習過程之中,若發生了莫名奇怪的現象,建議您將 Visual Studio 2017 關閉之後,重新開啟這個專案,應該就可以解決這些莫名奇怪的現象。

建立一個新的客製頁面與各平台 Renderer

建立 客製頁面控制項 CropView

  • 滑鼠右擊核心PCL專案下的 CustomControls 資料夾
  • 從彈出功能表中,選擇 加入 > 類別
  • 在 加入新項目 - XFImgCorp 對話窗中,點選 Visual C# > 類別
  • 在 加入新項目 - XFImgCorp 對話窗下方的名稱欄位,輸入 CropView
  • 最後,點選 新增 按鈕
  • 將底下關於 CropView 類別定義 C# 程式碼,覆蓋掉剛剛產生的 CropView 類別定義
    public class CropView : ContentPage
    {
        public byte[] Image;
        public Action RefreshAction;
        public bool DidCrop = false;

        public CropView(byte[] imageAsByte, Action refreshAction)
        {

            NavigationPage.SetHasNavigationBar(this, false);
            BackgroundColor = Color.Black;
            Image = imageAsByte;

            RefreshAction = refreshAction;
        }

        protected override void OnDisappearing()
        {
            base.OnDisappearing();

            if (DidCrop)
                RefreshAction.Invoke();
        }
    }
  • 在這個檔案前面,加入底下 using 敘述
using Xamarin.Forms;

建立 客製頁面控制項 CropView的 Android 原生 Renderer

  • 滑鼠右擊 XFImgCrop.Droid 專案下的 CustomControls 資料夾
  • 從彈出功能表中,選擇 加入 > 類別
  • 在 加入新項目 - XFImgCrop.Droid 對話窗中,點選 Visual C# > 類別
  • 在 加入新項目 - XFImgCrop.Droid 對話窗下方的名稱欄位,輸入 CropViewRenderer
  • 最後,點選 新增 按鈕
  • 將底下關於 CropViewRenderer 類別定義 C# 程式碼,覆蓋掉剛剛產生的 CropViewRenderer 類別定義
    public class CropViewRenderer : PageRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
        {
            base.OnElementChanged(e);
            var page = Element as CropView;
            if (page != null)
            {
                var cropImageView = new CropImageView(Context);
                cropImageView.AutoZoomEnabled = false;
                //cropImageView.SetMinCropResultSize(200, 200);
                //cropImageView.SetMaxCropResultSize(300, 300);
                cropImageView.SetAspectRatio(1, 1);
                cropImageView.LayoutParameters = new LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent);
                Bitmap bitmp = BitmapFactory.DecodeByteArray(page.Image, 0, page.Image.Length);
                cropImageView.SetImageBitmap(bitmp);

                var stackLayout = new StackLayout { Children = { cropImageView } };

                var rotateButton = new Xamarin.Forms.Button { Text = "旋轉" };

                rotateButton.Clicked += (sender, ex) =>
                {
                    cropImageView.RotateImage(90);
                };
                stackLayout.Children.Add(rotateButton);

                var finishButton = new Xamarin.Forms.Button { Text = "完成" };
                finishButton.Clicked += (sender, ex) =>
                {
                    Bitmap cropped = cropImageView.CroppedImage;
                    using (MemoryStream memory = new MemoryStream())
                    {
                        cropped.Compress(Bitmap.CompressFormat.Png, 100, memory);
                        XFImgCrop.App.CroppedImage = memory.ToArray();
                    }
                    page.DidCrop = true;
                    page.Navigation.PopModalAsync();
                };

                stackLayout.Children.Add(finishButton);
                page.Content = stackLayout;
            }
        }
    }
  • 在這個檔案前面,加入底下 using 敘述
using Xamarin.Forms.Platform.Android;
using Xamarin.Forms;
using XFImgCrop.CustomControls;
using Com.Theartofdev.Edmodo.Cropper;
using Android.Graphics;
using System.IO;
  • 在這個檔案的 namespace XFImgCrop.Droid.Renderers 敘述之前,加入底下這行程式碼。
[assembly: ExportRenderer(typeof(CropView), typeof(XFImgCrop.Droid.Renderers.CropViewRenderer))]

 編譯/建置 確認沒有錯誤

  • 滑鼠右擊 XFImgCrop (可攜式) 專案,選擇 建置
  • 成功建置完成後,在螢幕的左下角,會出現, 訊息
  • 滑鼠右擊 XFImgCrop.Droid 專案,選擇 建置
  • 成功建置完成後,在螢幕的左下角,會出現, 訊息

建立 客製頁面控制項 CropView的 iOS 原生 Renderer

  • 滑鼠右擊 XFImgCrop.Droid 專案下的 CustomControls 資料夾
  • 從彈出功能表中,選擇 加入 > 類別
  • 在 加入新項目 - XFImgCrop.iOS 對話窗中,點選 Visual C# > 類別
  • 在 加入新項目 - XFImgCrop.iOS 對話窗下方的名稱欄位,輸入 CropViewRenderer
  • 最後,點選 新增 按鈕
  • 將底下關於 CropViewRenderer 類別定義 C# 程式碼,覆蓋掉剛剛產生的 CropViewRenderer 類別定義
    public class CropViewRenderer : PageRenderer
    {
        CropViewDelegate selector;
        byte[] Image;
        bool IsShown;
        public bool DidCrop;

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();

            var page = base.Element as CropView;
            Image = page.Image;
            DidCrop = page.DidCrop;
        }

        public override void ViewDidAppear(bool animated)
        {
            base.ViewDidAppear(animated);

            try
            {
                if (!IsShown)
                {

                    IsShown = true;

                    UIImage image = new UIImage(NSData.FromArray(Image));
                    Image = null;

                    selector = new CropViewDelegate(this);

                    // https://github.com/TimOliver/TOCropViewController
                    TOCropViewController picker = new TOCropViewController(image);
                    // Demo for Circular Cropped Image
                    //TOCropViewController picker = new TOCropViewController(TOCropViewCroppingStyle.Circular, image);
                    picker.Delegate = selector;

                    PresentViewController(picker, false, null);

                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
        }

        public override void ViewWillDisappear(bool animated)
        {
            base.ViewWillDisappear(animated);

            try
            {
                var page = base.Element as CropView;
                page.DidCrop = selector.DidCrop;
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }

        }
    }


    public class CropViewDelegate : TOCropViewControllerDelegate
    {
        readonly UIViewController parent;
        public bool DidCrop;

        public CropViewDelegate(UIViewController parent)
        {
            this.parent = parent;
        }

        public override void DidCropToImage(TOCropViewController cropViewController, UIImage image, CoreGraphics.CGRect cropRect, nint angle)
        {
            DidCrop = true;

            try
            {
                if (image != null)
                    App.CroppedImage = image.AsPNG().ToArray();

            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
            finally
            {
                if (image != null)
                {
                    image.Dispose();
                    image = null;
                }
            }

            parent.DismissViewController(true, () => { App.Current.MainPage.Navigation.PopModalAsync(); });
        }

        public override void DidFinishCancelled(TOCropViewController cropViewController, bool cancelled)
        {
            parent.DismissViewController(true, () => { App.Current.MainPage.Navigation.PopModalAsync(); });
        }
    }
  • 在這個檔案前面,加入底下 using 敘述
using Foundation;
using System.Diagnostics;
using UIKit;
using Xam.Plugins.ImageCropper.iOS;
using Xamarin.Forms.Platform.iOS;
using XFImgCrop.CustomControls;
using Xamarin.Forms;
  • 在這個檔案的 namespace XFImgCrop.iOS.Renderers 敘述之前,加入底下這行程式碼。
[assembly: ExportRenderer(typeof(XFImgCrop.CustomControls.CropView), typeof(XFImgCrop.iOS.Renderers.CropViewRenderer))]

 編譯/建置 確認沒有錯誤

  • 滑鼠右擊 XFImgCrop (可攜式) 專案,選擇 建置
  • 成功建置完成後,在螢幕的左下角,會出現, 訊息
  • 滑鼠右擊 XFImgCrop.Droid 專案,選擇 建置
  • 成功建置完成後,在螢幕的左下角,會出現, 訊息

修正核心PCL專案可以使用 Xam.Plugins.ImageCropper 套件功能

修正 MainPageViewModel.cs

  • 在核心PCL專案內,展開 ViewModels 資料夾,打開這個 MainPageViewModel.cs 檔案
  • 在這個檔案前面,加入底下 using 敘述
using XFImgCrop.Infrastructures;
  • 找到這行程式碼 private readonly IPicture _picture; 將其註解解除
  • 在建構式中,將 IPicture 注入的參數註解解除,修正完後的建構式定義如下
        public MainPageViewModel(INavigationService navigationService, IEventAggregator eventAggregator,
            IPageDialogService dialogService , IPicture picture
            )
        {
  • 找到這行程式碼 _picture = picture; 將其註解解除
  • 找到這行程式碼 _picture.SavePictureToDisk("MyCrop", App.CroppedImage); 將其註解解除
  • 找到這行程式碼 App.CroppedImage = _picture.LoadPictureFromDisk("MyCrop"); 將其註解解除
  • 在 SelectPict 方法內,找到這行程式碼 await NavigationImageCropping(imageAsByte); 將其註解解除
  • 在 TakePicture 方法內,找到這行程式碼 await NavigationImageCropping(imageAsByte); 將其註解解除

修正 MainPage.xaml.cs 的 Code Behind 程式碼

  • 在核心PCL專案內,展開 Views 資料夾,打開這個 MainPage.xaml.cs 檔案
  • 在這個檔案前面,加入底下 using 敘述
using System.Threading.Tasks;
using XFImgCrop.CustomControls;
using XFImgCrop.ViewModels;
  • 將底下關於 MainPage 類別定義 C# 程式碼,覆蓋掉剛剛產生的 MainPage 類別定義
    public partial class MainPage : ContentPage
    {
        MainPageViewModel MainPageViewModel;

        public MainPage()
        {
            InitializeComponent();

            MainPageViewModel = this.BindingContext as MainPageViewModel;
            MainPageViewModel.NavigationImageCropping = TakePicture;
        }

        public async Task TakePicture(byte[] imageAsByte)
        {
            await Navigation.PushModalAsync(new CropView(imageAsByte, MainPageViewModel.Refresh));
        }
    }

 編譯/建置 確認沒有錯誤

  • 滑鼠右擊 XFImgCrop (可攜式) 專案,選擇 建置
  • 成功建置完成後,在螢幕的左下角,會出現, 訊息
  • 滑鼠右擊 XFImgCrop.Droid 專案,選擇 建置
  • 成功建置完成後,在螢幕的左下角,會出現, 訊息
  • 滑鼠右擊 XFImgCrop.iOS 專案,選擇 建置
  • 成功建置完成後,在螢幕的左下角,會出現, 訊息

在  平台下來進行測試

  • 滑鼠右擊 XFImgCrop.Droid 專案,選擇 設定為起始專案
  • 在工具列上,確認選取到適當的 Android 模擬器 (這裡選取的是 Visual Studio for Android Emulator 的 Android 6.0 - API 23 的模擬器),接著點選綠色三角形,準備在 Android 平台下執行這個專案。
    您也可以依據您的電腦環境,選擇適合您的 Android 模擬器或者實體手機
  • 請點選 選取圖片 按鈕,當出現如下圖的對話窗畫面,請點選 相簿 或者 拍照 按鈕,看看是否可以可以出現透過手機拍照的功能與看到可以選取項目內的圖片
  • 接著,將從相機或者相簿中取得的照片,自行設定要擷取的區域,完成之後,請點選如下圖的 完成 按鈕
  • 此時,您剛剛擷取區域的圖片就會出現在下圖的上半部。
  • 接著,您可以點選 儲存圖片 按鈕,將剛剛擷取下來的圖片,儲存到您的手機中,然後,就可以點選 讀取圖片 按鈕,便可以從手機檔案中,將這個圖片讀取出來,接著,顯示在首頁的最下方。
  • 若一切都可以正常操作,那就表示 Xam.Plugins.ImageCropper 這個套件,已經可以正常使用。

在  平台下來進行測試

由於 iOS 的模擬器,並不支援手機拍照功能,所以,我們需要使用實際的 iOS 裝置來進行測試;因此,您需要為這個專案建立一個 Provisioning Profile,這樣,才能夠在這台裝置上進行除錯。
  • 滑鼠右擊 XFImgCrop.iOS 專案,選擇 設定為起始專案
  • 在工具列上,切換 方案平台 下拉選單到 iPhone,接著選取您所要測試的實際硬體裝置,在這裡,我將會使用 iPad 來做為測試對象。
  • 請點選 選取圖片 按鈕,當出現如下圖的對話窗畫面,請點選 相簿 或者 拍照 按鈕,看看是否可以可以出現透過手機拍照的功能與看到可以選取項目內的圖片;下面螢幕截圖,表示正在使用 iPad 上的鏡頭進行拍照。
  • 接著,將從相機或者相簿中取得的照片,自行設定要擷取的區域,完成之後,請點選如下圖的 完成 按鈕
  • 此時,您剛剛擷取區域的圖片就會出現在下圖的上半部。
  • 接著,您可以點選 儲存圖片 按鈕,將剛剛擷取下來的圖片,儲存到您的手機中,然後,就可以點選 讀取圖片 按鈕,便可以從手機檔案中,將這個圖片讀取出來,接著,顯示在首頁的最下方。
  • 若一切都可以正常操作,那就表示 Xam.Plugins.ImageCropper 這個套件,已經可以正常使用。