[Xamarin Form] How to create PageView
1. Tổng quan Như các bạn đã biết, Xamarin Form có một sức mạnh lớn khi giúp chúng ta có thể code một lần, nhưng chaỵ được trên nhiều nền tảng. Tuy nhiên, chỉ dựa vào những UI Control default mà nó cung cấp thì không đủ để làm, ví dụ như trường hợp này, mình muốn tạo ra môt màn hình mà có nhiều ...
1. Tổng quan
- Như các bạn đã biết, Xamarin Form có một sức mạnh lớn khi giúp chúng ta có thể code một lần, nhưng chaỵ được trên nhiều nền tảng. Tuy nhiên, chỉ dựa vào những UI Control default mà nó cung cấp thì không đủ để làm, ví dụ như trường hợp này, mình muốn tạo ra môt màn hình mà có nhiều page và có thể vuốt sang trái phải. Điều này thì không có một UI Control của Xamarin Form cung cấp. Vậy nếu trong dự án gặp phải một yêu cầu như trên, bạn sẽ làm như nào? Bài này mình sẽ hướng dẫn bạn tạo giải quyết vấn đề trên.
- Ý tưởng để giải quyết bài toán này vẫn là: nhưng thứ mình không làm được trên tầng Form thì chúng ta sẽ đẩy về tầng Native để giải quyết nó. Trên tầng Form chúng ta chỉ cần tạo ra một view trống, và sẽ overrride lại tầng render của view đó cho nhưng platform mà mình cần (cụ thể trong trường hợp này iOS, Android). Tại tầng "Render" của mỗi platform chúng ta sử dụng các Native UI Control một cách thoải mái, giống như code native bình thường. Vậy để tạo một màn hình mà có nhiều page, có thể vuốt qua lại giữ các page thì ở iOS chúng ta sẽ dùng UIPageViewController và ở Android sẽ dùng ViewPager . Cấu trúc sẽ như sau:
2. PagerView
Đầu tiên trên tầng Xamarin Form, chúng ta sẽ tạo ra một class có têng là PagerView, kế thừa từ class View.
public class PagerView : View { public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create("ItemsSource", typeof(List<PagerTabModel>), typeof(PagerView)); public static readonly BindableProperty PageSelecetedEventProperty = BindableProperty.Create("PageSelecetedEvent", typeof(Action<int>), typeof(PagerView)); public List<PagerTabModel> ItemsSource { get { return (List<PagerTabModel>)GetValue(ItemsSourceProperty); } set { SetValue(ItemsSourceProperty, value); } } public Action<int> PageSelecetedEvent { get { return (Action<int>)GetValue(PageSelecetedEventProperty); } set { SetValue(PageSelecetedEventProperty, value); } } }
Class này chúng ta sẽ đi cụ thể cho từng đoạn code:
- List<PagerTabModel> ItemsSource: số lượng page mà chúng ta muốn hiển thị sẽ phụ thuộc vào giá trị của thuộc tính này. Ta có thể thấy rằng, hoàn toàn có thể sử dụng cơ chế binding cho nó thông qua ItemsSourceProperty.
- PageSelecetedEvent: tại tầng Form chúng ta có thể lắng nghe sự kiện, mỗi khi có một page (ở tầng native được hiển thị), PageSelecetedEvent sẽ được invoke. Dự vào đó chúng ta có những xử lý như mông muốn. Hiện tại mình thấy chỉ cần 2 thuộc tính trên, có thể làm được rất nhiều việc rồi, để phù hợp với từng bài toán thì các bạn có thể thêm hoặc bỏ bớt thuộc tính như các bạn mong muốn.
- Ngoài ra để lưu thông tin của mỗi page mình có tạo ra một class PagerTabModel, class này sẽ chứa toàn bộ thông tin của page. Dưới là ví dụ các thông tin mình muốn hiển thị, các bạn có thể tuỳ biến theo yêu cầu của mình.
public class PagerTabModel { public string Title { get; set; } public string TextSession1 { get; set; } public string TextSession2 { get; set; } public string Image { get; set; } }
OK, vậy là đã xong tầng Form, chúng ta sẽ đi vào từng tầng render của từng platform một.
3. PagerView Android Render
Như mình đã nói ở trên, chúng ta sẽ override lại tầng render của từng platform, ở Android có một UI Control là ViewPager có thể giúp giải quyết bài toán trên. Dưới đây là code mà mình viết, chúng ta sẽ đi cụ thể từng đoạn sau.
[assembly: ExportRenderer(typeof(PagerView), typeof(ViewPagerRender_Android))] namespace Demo.Droid.Views.Customs { public class ViewPagerRender_Android : ViewRenderer<PagerView, Android.Views.View> { Android.Support.V4.View.ViewPager _nativeView; ViewPager.PageScrollStateChangedEventArgs _lastState; PagerView _pageView; (1) protected override void OnElementChanged(ElementChangedEventArgs<PagerView> e) { base.OnElementChanged(e); if (e.NewElement != null) { _pageView = (PagerView)e.NewElement; if (Control == null) { _nativeView = new Android.Support.V4.View.ViewPager(Android.App.Application.Context); SetNativeControl(_nativeView); } if (_pageView.ItemsSource != null) SetApdater(); } } (2) private TutorialPagerAdapter _tutorialAdapter; private void SetApdater() { if(_pageView.ItemsSource!=null) { if(_tutorialAdapter==null) { _tutorialAdapter = new TutorialPagerAdapter(Android.App.Application.Context, _pageView.ItemsSource); _nativeView.Adapter = _tutorialAdapter; _nativeView.PageSelected += PageSelectedHandle; _pageView.SetPageSelection = (i) => { _nativeView.SetCurrentItem(i, true); }; }else { _tutorialAdapter.SetPageDatas(_pageView.ItemsSource); } } } (3) protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) { base.OnElementPropertyChanged(sender, e); if (e.PropertyName.Equals(PagerView.ItemsSourceProperty.PropertyName)) SetApdater(); } (4) private void PageSelectedHandle(object sender, ViewPager.PageSelectedEventArgs e) { if(_pageView.PageSelecetedEvent!=null) { _pageView.PageSelecetedEvent.Invoke(e.Position); } } } }
- Đoạn code (1) chúng ta sẽ overrride lại hàm OnElementChanged tại đó tạo ra một đối tượng có kiểu là ViewPager ( cụ thể mình dùng Android.Support.V4.View.ViewPager), rổi set chính ViewPager làm NativeControl thông qua hàm SetNativeControl. Vậy là ViewPager sẽ được hiện thị đè lên View trông ban đầu chúng ta tạo ra.
- Đoạn code (2) chúng ta xác định xem cái gì mà chúng ta muốn hiển thị lên màn hình thuông qua TutorialPagerAdapter
- Đoạn cdoe (3): Chúng ta lắng nghe sự thay đổi của thuộc tính ItemsSource , mỗi khí nó thay đổi chúng ta update lại hiển thị thông việc set lại Adapter.
- Đoạn code (4): Lắng nghe mỗi khi một page được hiển thi, lúc này PageSelecetedEvent được invoke để thông báo cho tầng Form biết. Như vậy là đã xong cho tầng Android, tiếp theo sẽ xử lý tiếp cho tầng iOS.
4. PagerView iOS Render
- Ở iOS chúng ta sẽ sử dụng UIPageViewController để giải quyết bài toán, mỗi một page sẽ được thể hiện một ViewController con, ở đây mình tạo ra một ViewController là TutorialPageViewController để hiển hiển thị thông tin cho từng page. Dễ thấy là vai trò của TutorialPageViewController cũng giống với TutorialPagerAdapter bên phía Android. Đưới đây là code demo.
[assembly: ExportRenderer(typeof(PagerView), typeof(ViewPagerRender_iOS))] namespace Demo.iOS.Views.Customs { public class ViewPagerRender_iOS : ViewRenderer<PagerView, UIKit.UIView> { PagerView _pageView; UIPageViewController _pageViewController; List<UIViewController> _listPage = null; int _currentIdx = 0; protected override void OnElementChanged(ElementChangedEventArgs<PagerView> e) { base.OnElementChanged(e); if (e.NewElement != null) { _pageView = (PagerView)e.NewElement; if (Control == null) { this._pageViewController = new UIPageViewController(UIPageViewControllerTransitionStyle.Scroll, UIPageViewControllerNavigationOrientation.Horizontal, UIPageViewControllerSpineLocation.Min); this._pageViewController.WeakDataSource = this; this._pageViewController.WeakDelegate = this; foreach (var subView in this._pageViewController.View.Subviews) { if ((subView as UIScrollView) != null) { (subView as UIScrollView).WeakDelegate = this; (subView as UIScrollView).ShowsHorizontalScrollIndicator = false; } } _pageView.SetPageSelection = (i) => { SetVisiblePage(i); }; (1) this._pageViewController.View.Frame = new CGRect(0, 0, this.Bounds.Width, this.Bounds.Height); SetNativeControl(this._pageViewController.View); } } } (2) void UpdatePagesSource() { if (_pageView.ItemsSource != null && _pageView.ItemsSource.Count() > 0) { var listTabModel = _pageView.ItemsSource.ToList(); _listPage = new List<UIViewController>(listTabModel.Count); for (int i = 0; i < listTabModel.Count; i++) { _listPage.Add(new TutorialPageViewController(listTabModel[i], i)); } SetVisiblePage(0, false); } } void PageSelectedHandle(int pos) { if (_pageView.PageSelecetedEvent != null) { _pageView.PageSelecetedEvent.Invoke(pos); } } (2) protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { base.OnElementPropertyChanged(sender, e); if (_pageViewController != null && _pageView != null && e.PropertyName == nameof(_pageView.ItemsSource)) { UpdatePagesSource(); } } void SetVisiblePage(int pageIndex, bool animate = true) { bool forward = pageIndex > _currentIdx; UIPageViewControllerNavigationDirection direction = forward ? UIPageViewControllerNavigationDirection.Forward : UIPageViewControllerNavigationDirection.Reverse; if (_listPage != null && pageIndex < _listPage.Count) this._pageViewController.SetViewControllers(new UIViewController[] { _listPage[pageIndex] }, direction, animate, s => { _currentIdx = pageIndex; }); } int CurrentScrollIndex(UIPageViewController pageViewController) { if (pageViewController.ViewControllers != null && pageViewController.ViewControllers.Count() > 0) { if (pageViewController.ViewControllers[0] as TutorialPageViewController != null) { TutorialPageViewController page = (pageViewController.ViewControllers[0] as TutorialPageViewController); return page.PageIndex; } } return -1; } #region PageViewController Datasource & Delegate [Export("presentationCountForPageViewController:")] public virtual int GetPresentationCount(UIPageViewController pageViewController) { return _listPage != null ? _listPage.Count : 0; } [Export("pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted:")] public virtual void DidFinishAnimating(UIPageViewController pageViewController, bool finished, UIViewController[] previousViewControllers, bool completed) { _currentIdx = CurrentScrollIndex(pageViewController); if (_currentIdx > 0 && _currentIdx < _listPage.Count) PageSelectedHandle(_currentIdx); } (3) [Export("pageViewController:viewControllerBeforeViewController:")] [Preserve(Conditional = true)] public UIViewController GetPreviousViewController(UIPageViewController pageViewController, UIViewController referenceViewController) { TutorialPageViewController page = referenceViewController as TutorialPageViewController; if (page == null || page.PageIndex == 0) return null; int previousIdx = page.PageIndex - 1; if (previousIdx >= 0 && previousIdx < _listPage.Count) return _listPage[previousIdx]; return null; } [Export("pageViewController:viewControllerAfterViewController:")] [Preserve(Conditional = true)] public UIViewController GetNextViewController(UIPageViewController pageViewController, UIViewController referenceViewController) { TutorialPageViewController page = referenceViewController as TutorialPageViewController; if (_listPage == null || page == null || page.PageIndex == _listPage.Count - 1) return null; int nextIdx = page.PageIndex + 1; if (nextIdx >= 0 && nextIdx < _listPage.Count) return _listPage[nextIdx]; return null; } #endregion } }
- Đoạn code (1): ý tưởng cũng đơn giản giống với Android, chúng ta ném hết page vào trong một UIPageViewController, và chỉ cần set native control là thuộc tính View của UIPageViewController đó thông qua hàm SetNativeControl(this._pageViewController.View) . Để ý rằng chúng ta phải set lại frame của this._pageViewController.View.Frame, đảm bảo nó sẽ nó bao phủ toàn bộ view trống hiện tại.
- Đoạn code (2): Mỗi khi thuộc tính ItemsSource của PagerView thay đổi, chúng ta cần update lại hiện thị, việc này được thược hiện thông qua hàm UpdatePagesSource, tại hàm đó chúng ta sẽ tính toán cần tạo bao nhiêu đối tượng TutorialPageViewController tương ứng với số lượng page cần hiện thị.
- Đonaj code (3): tất cả các hàm ở đoạn code này để làm hàm cần thiết khi phải implement khi sử dụng UIPageViewController, trong đó các bạn có thể để ý đến hàm public virtual void DidFinishAnimating(UIPageViewController pageViewController, bool finished, UIViewController[] previousViewControllers, bool completed), mỗi khi một page được hiện (ví dụ ngừoi dùng vuốt từ page#1 sang page#2, để hiện thị page#2), thì hàm này sẽ được gọi. Chúng ta có thể thông báo cho tầng form biết sự kiện này bằng cách invoke PageSelecetedEvent.
Trên đây mình đã hướng dẫn cách tạo một Page View bằng Xamarin Form, hi vọng bài viết sẽ có ích cho các bạn. Thanks.