XAML in Xamarin.Forms 基礎篇 電子書

XAML in Xamarin.Forms 基礎篇 電子書
XAML in Xamarin.Forms 基礎篇 電子書

Xamarin.Forms 快速入門 電子書

Xamarin.Forms 快速入門 電子書
Xamarin.Forms 快速入門 電子書

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 這個套件,已經可以正常使用。

2017/06/09

UML 表示說明

UML是一種開放的方法,用於說明、可視化、構建和編寫一個正在開發的、物件導向的、軟體密集系統的製品的開放方法。更多資訊可以參考 UML
底下的 UML 示意圖片使用 PowerPoint 製作出來,因為,在 PowerPoint 內無法做出空心箭頭的線條,因此,所有的空心箭頭,將會以實心箭頭來表示。

類別 / 抽象類別 / 介面

共需分三塊,上層,標示出 類別 / 抽象類別 / <> 介面的名稱。
中間區域,則是用來標示狀態,通常可以標示 Property 屬性。
最後一塊區域,則是說明其可以使用方法。

繼承

這裡是用來標示兩個實體間的 is-a 的關係
類別的繼承,使用實體線段與實心箭頭的線段將其連接起來。
而介面的實作部分,則是使用虛線與實心箭頭的線段將其連接起來。

組合

這裡是用來標示兩個實體間的 has-a 的關係
使用虛線與開放箭頭的線段將其連接起來。

程式碼片段

一個範例

請示著解讀底下的 UML 是在表示甚麼樣的關係呢?

2017/06/06

Xamarin.Android 各種封存屬性測試

在這篇 Xamarin 筆記中,將會針對 https://github.com/vulcanlee/XamarinHandsOnLab/tree/master/XFTask 專案,測試不同的專案選項,所封存的 apk 檔案大小。
在這裡,所有的測試都是在 Release 建置,並且使用專案屬性的這四種設定選項。而且,另種測試結果的 APK 檔案,將會可以從這裡取得 https://github.com/vulcanlee/XamarinHandsOnLabBuild
  • 連結器
  • 將組譯碼組合成機器碼
  • AOT 編譯
  • 使用 LLVM 最佳化編譯器
我們進行了六種組合模式的 Xamarin.Android APK 檔案的封存,得到這些模式下所產生的 APK 檔案大小。從底下的測試結果,我們看到了,只要當我們把可以產生原生碼的功能( 將組譯碼組合成機器碼 / 使用 LLVM 最佳化編譯器) 功能開啟之後,在這個測試專案下,我們可以看得出,所得到的 APK 檔案就會變得比較大。
模式APK大小
模式151.5 MB
模式223.3 MB
模式3126 MB
模式463.9 MB
模式5132 MB
模式664.3 MB
當我們沒有開啟 將組譯碼組合成機器碼 模式,這個專案內的所有組件將會存在於 APK 檔案內的 assemblies目錄下,底下將是模式1的 APK 檔案架構。
模式11
我們可以切換到 assemblies 目錄下,並且以組件檔案大小作排列,我們看到,最大的檔案那就是 Mono.Android.dll 這個組件,大小為 21.5 MB
模式12
可是,當我們查看模式2 (連結器:僅限 SDK 組譯碼) 的狀態下的 APK 檔案,切換到 assemblies 目錄下,並且以組件檔案大小作排列,我們看到,最大的檔案那就是 mscorlib.dll 這個組件,大小為 2.10 MB
所以,我們知道,當啟用了連結器模式,我們所產生的 APK 檔案大小將會明顯的小了許多,而且,就如同 Xamarin 官方文件對於連結器的說明文件描述一下,連結器會將這個專案內,沒有用到的類別、方法等等,從組件中移除,這樣將會使得您的 APK 檔案大幅瘦身下來。
模式21
至於要使用哪種專案屬性模式,來建置您的最終封存的檔案,就取決於您的決定了。

模式1

連結器:無
將組譯碼組合成機器碼:無選取
AOT 編譯:無選取
使用 LLVM 最佳化編譯器:無選取

模式2

連結器:僅限 SDK 組譯碼
將組譯碼組合成機器碼:無選取
AOT 編譯:無選取
使用 LLVM 最佳化編譯器:無選取

模式3

連結器:無
將組譯碼組合成機器碼:選取
AOT 編譯:選取
使用 LLVM 最佳化編譯器:選取

模式4

連結器:僅限 SDK 組譯碼
將組譯碼組合成機器碼:選取
AOT 編譯:選取
使用 LLVM 最佳化編譯器:選取

模式5

連結器:無
將組譯碼組合成機器碼:無選取
AOT 編譯:選取
使用 LLVM 最佳化編譯器:選取

模式6

連結器:僅限 SDK 組譯碼
將組譯碼組合成機器碼:無選取
AOT 編譯:選取
使用 LLVM 最佳化編譯器:選取

2017/06/02

升級到 Windows 10 Creators Update後,Visual Studio for Android Emulator 無法上網了

今天中午,我的電腦終於收到了 Windows 10 Creators Update 的升級推播通知,並且要求我要立即重新開機,這個時候,當然是要進行升級更新;當系統重新開機,花費了一些時間將 Windows 10 Creators Update 安裝完成後,當然是要繼續進行手頭上的工作。
可是,悲慘的問題來了,當我準備要進行剛剛開發到一半的 Xamarin.Forms 專案,繼續進行除錯的時候,此時模擬器啟動之後,出現了底下對話窗:
The emulator requires an Internet connection to start. Do you want to configure the emulator to connect to the Internet?
Your computer might lose network connectivity while these changes are applied. This might affect existing network operations.
詢問是否要上網
我立馬反應的點下了  這個按鈕,可是,當模擬器啟動之後,卻發現到所啟動的 Android 模擬器,無法連接上網路。
詢問是否要上網
若您也發生了這樣的問題,可以參考底下步驟來解決此一問題。
  • 請先關閉所有開啟的 Visual Studio for Android Emulator 模擬器
  • 開啟 Hyper-V 管理員
  • 從虛擬機器的清單中,找到您的 Android 模擬器
    在這裡,我選擇的是 VS Emulator 5.2-inch Marshmallow XXHDPI Phone 這個虛擬機器
    虛擬機器設定
  • 我們從這個模擬器的虛擬機器設定內容上,看到了一個問題,那就是這個虛擬機器原本是要有兩張網卡連接到這個虛擬機器上的,可是,現在只剩下一張網卡,而且,這張網卡卻是連接到內部網路之用的網卡;也就是說,要連接到 Internet 上的那張網卡不見了。
    Android虛擬機器設定內容
  • 請您參考上圖,點選數字1 的標示處:新增硬體
  • 在右半部區域,點選 網路介面卡
  • 接著點選 新增 按鈕
  • 接著,這台虛擬機器多了一個新硬體,就是剛剛加入的 網路介面卡
  • 請點選這張新的 網路介面卡,然後,在右半部的 虛擬交換器 下拉選單中,選擇您可以上 Inetnet 的網卡。
  • 完成後,請點選 確定 按鈕
    加入一張新的虛擬網卡
  • 最後,請重新啟動您的 Visual Studio for Android Emulator 模擬器
  • 一樣會出現系圖對話窗,請點選  按鈕
    詢問是否要上網
  • 當模擬器出現的時候,您會在右上角狀態列區域,看到這個模擬器已經連接上網路了,而且打開瀏覽器,確認是可以讀取到網路上的網站網頁內容。
詢問是否要上網

Hyper-V 升級更新

2017/05/20

Pull 所有目錄的 Git Repository

因為工作需要,所以,使用了兩台電腦,可是,之前經常要把這兩台電腦上的 Git Repository 做同步,都是使用手動方式,一個目錄一個目錄的下 Pull 命令。
今天正好有空,根據網路上找到的參考資訊,修改成我自己需要的同步 Script。
我的狀況是,我都會把 Repositories 都放在同一個目錄下,透過底下的 PowerShell Script,便可以逐一針對每個目錄,執行 Pull 命令,對我來說,真的節省了太多時間了。
$git = $env:programfiles+"\Git\cmd\git.exe";
Write-Host $git
if (!$git) {
  Write-Host "Something went wrong. Please enter the path to git.cmd or git.exe:";
  $git = Read-Host;
  Set-Variable -Name git -Value $git;
}
ls -name -Exclude *.* | foreach {Write-Host $_;cd .\$_ ; & $git pull origin master  2> $null ; cd ..}
Write-Host "Done.";
cmd /c pause | out-null