12/08/2018, 15:41

[Xamarin Form] HOW TO CREATE DROPDOWN CONTROL?

Tổng quan. Đối với những ai đã từng sử dụng Xamarin Form trong dự án của mình, thì vấn đề gặp phải lớn nhất đó là số lượng các UI control được nó hỗ trợ khá là ít. Những ai đã quen code native thì việc dùng Xamarin Form cảm thấy khá bất tiện, đặc biệt đối với những giao diện có độ phức tạp tạo, ...

  1. Tổng quan. Đối với những ai đã từng sử dụng Xamarin Form trong dự án của mình, thì vấn đề gặp phải lớn nhất đó là số lượng các UI control được nó hỗ trợ khá là ít. Những ai đã quen code native thì việc dùng Xamarin Form cảm thấy khá bất tiện, đặc biệt đối với những giao diện có độ phức tạp tạo, việc code trên Form quả là không dễ dàng. Bài này mình sẽ hướng các bạn tạo một DropDown Control trên Xamarin Form. Trước khi bắt đâu, chúng ta nói qua về khái niệm Custom RenderRender. Bắt đầu bằng ví dụ phía dưới cho dễ hiểu, đây là cách thức Xamarin Form tạo tạo ra một UI control tên là Entry. Ta thấy có 3 tầng code:
    • "Entry" class ở tầng Form.
    • "EntryEnderer" class ở tầng render, ứng với mỗi nền tảng sẽ có một "EntryRender" riêng cho iOS,Android,Windows Phone.
    • Native UI control, tương ứng với mỗi UI control tầng form qua tầng render sẽ tạo ra một native control tương ứng. Xamarin Form biết được hạn chế của nó, nên cho phép người dùng có thể tuỳ biến lại tầng Form và tầng Render, cho phép tạo ra những UI control phức tạp. Ở tầng Render bạn phải tự việt code cho tất cả các nền tảng mà bạn muốn hỗ trợ. Quay trợ lại bài toán ban đầu, tạo ra một dropdown control, ở ví dụ này mình sẽ chỉ hỗ trợ 2 nền tảng phổ biến là iOS và Android. Chúng ta sẽ có kiến trúc như sau. Như đã nói ở trên chúng ta chỉ quan tâm đến tầng Form và tầng Render, do đó chỉ cần viết code cho 3 class DropDownMenuView.cs, DropDownMenuRender_iOS, DropDownMenuRender_Android. Giờ bắt tay vào code.
  2. DropDownMenuView
using Xamarin.Forms;
using System.Collections.Generic;
namespace Demo.Views.Customs
{
    public class DropDownMenuView : View
    {
        public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create("ItemsSource", typeof(List<string>), typeof(DropDownMenuView));
        public static readonly BindableProperty ItemSelecetedEventProperty = BindableProperty.Create("ItemSelecetedEvent", typeof(Action<int>), typeof(DropDownMenuView));
        public static readonly BindableProperty SetItemSelectionProperty = BindableProperty.Create("SetItemSelection", typeof(Action<int>), typeof(DropDownMenuView));

        public List<string> ItemsSource
        {
            get { return (List<string>)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        public Action<int> ItemSelecetedEvent
        {
            get { return (Action<int>)GetValue(ItemSelecetedEventProperty); }
            set { SetValue(ItemSelecetedEventProperty, value); }
        }
          public Action<int> SetItemSelection
        {
            get { return (Action<int>)GetValue(SetItemSelectionProperty); }
            set { SetValue(SetItemSelectionProperty, value); }
        }
    }
}

Hãy nói qua một chút về đoạn code trên, mình tạo ra một DropDownMenuView kế thừa từ class View, chưa có phần Render, DropDownMenuView khi sử dụng chỉ là một view trắng, không có gì cả. Class này có 3 trường:

  • ItemsSource: list các item hiển thị.
  • ItemSelecetedEvent: được gọi, khi người dùng chọn một item nào đó trong dropdown control.
  • SetItemSelection : item nào sẽ được focus sau khi khởi tạo.
  1. DropDownMenuRender_iOS
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
using FJKApp3.iOS.Views.Custom;
using FJKApp3.Views.Customs;
using System;
using UIKit;
using Foundation;
using CoreGraphics;

[assembly: ExportRenderer(typeof(DropDownMenuView), typeof(DropDownMenuRender_iOS))]

namespace FJKApp3.iOS.Views.Custom
{
    public class DropDownMenuRender_iOS : ViewRenderer<DropDownMenuView, UIView>
    {
        DropDownMenuView _dropDownView;
        UITableView tableView;
        UIView wrapper;
        UILabel label;
        UIImageView arrow;
        int selectedIndex = 0;

		protected override void OnElementChanged(ElementChangedEventArgs<DropDownMenuView> e)
        {
            base.OnElementChanged(e);
            if (e.NewElement != null)
            {
                _dropDownView = (DropDownMenuView)e.NewElement;
                if (Control == null)
                {
                    wrapper = new UIView();
                    wrapper.Layer.BorderColor = ColorExtensions.ToCGColor(Color.FromHex(Define.DISABLE_CONTENT_COLOR));
                    wrapper.Layer.BorderWidth = 1;
                    wrapper.Frame = this.Frame;
                    //wrapper.BackgroundColor = UIColor.Red;
                    wrapper.AddGestureRecognizer( new UITapGestureRecognizer(() =>{
                        ButtonClickHanlde();
                    }));

                    arrow = new UIImageView();
                    arrow.Image = new UIImage("ic_arrow_drop_down_black.png");
                        arrow.Frame = new CGRect(10, 10, 50, 30);
					wrapper.AddSubview(arrow);


                    label = new UILabel();
					label.TextColor = UIColor.Black;
                    label.Frame = new CGRect(10, 10, 50, 30);
                        if (_dropDownView != null && _dropDownView.ItemsSource != null)
						label.Text = _dropDownView.ItemsSource[0];
					wrapper.AddSubview(label);
                    tableView = new UITableView();
                    //tableView
                    tableView.AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
                    tableView.Frame = wrapper.Frame;
                    tableView.Layer.BorderWidth = 1;
                    tableView.Layer.BorderColor = ColorExtensions.ToCGColor(Color.FromHex(Define.DISABLE_CONTENT_COLOR));
                    tableView.WeakDelegate = this;
                    tableView.WeakDataSource = this;
                    _dropDownView.SetItemSelection += (obj) => {
                        //if(label!=null)
                        selectedIndex = obj;
                        label.Text = _dropDownView.ItemsSource[obj];
						if (_dropDownView.ItemSelecetedEvent != null)
							_dropDownView.ItemSelecetedEvent.Invoke(obj);
                    };
                    SetNativeControl(wrapper);
                }
            }

        }

        protected override void Dispose(bool disposing)
        {
            if (disposing && tableView != null)
                tableView.RemoveFromSuperview();
        
            base.Dispose(disposing);
        }

        public override CGRect Frame
        {
            get
            {
                return base.Frame;
            }
            set
            {
                base.Frame = value;
                if  ( label!=null&& wrapper != null && value != CGRect.Empty)
                {
                    //label.BackgroundColor = UIColor.Green;
                    label.Frame = new CGRect(8, 2, value.Size.Width - 10, value.Size.Height - 4);
                    this.LayoutSubviews();

                }
                if (arrow != null && wrapper != null && value != CGRect.Empty)
				{
					//arrow.BackgroundColor = UIColor.Green;
                    arrow.Frame = new CGRect(value.Size.Width-20, (value.Size.Height-10)/2, 10, 10);
					this.LayoutSubviews();
				}


            }
        }

        public void UpdateButton(string name)
        {
            
        }

        bool isShowDialog = false;
        private void ButtonClickHanlde()
        {
            if (wrapper == null)
                return;
            isShowDialog = !isShowDialog;
            if(isShowDialog)
            {
                var rect = wrapper.ConvertRectToView(wrapper.Frame, AppDelegate.AppWindow);
                nfloat height = AppDelegate.AppWindow.Bounds.Height - rect.Y - 10; 
                CGRect r = new CGRect(rect.X, rect.Y, rect.Width, height);
                int listCount= _dropDownView == null ? 0 : _dropDownView.ItemsSource.Count;

                AppDelegate.Instance.ShowSubviewAt(r, tableView,()=>{
					if (selectedIndex < listCount)
                        tableView.ScrollToRow(NSIndexPath.FromRowSection(selectedIndex, 0), UITableViewScrollPosition.Top, false);
                });
				
            }else
            {
                tableView.RemoveFromSuperview();
            }

        }

      
        [Export("tableView:numberOfRowsInSection:")]
        public nint RowsInSection(UITableView tableView, nint section)
        {
            return _dropDownView == null ? 0 : _dropDownView.ItemsSource.Count;
        }


        [Export("tableView:cellForRowAtIndexPath:")]
        public  UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
        {
            var simpleTableIdentifier = @"SimpleTableItem";

            var cell = tableView.DequeueReusableCell(simpleTableIdentifier);

            if (cell == null)

            {
                cell = new UITableViewCell(UITableViewCellStyle.Default, simpleTableIdentifier);
            }

            cell.TextLabel.Text = _dropDownView.ItemsSource[indexPath.Row];
            return cell;
        }

        [Export("numberOfSectionsInTableView:")]
        public virtual nint NumberOfSections(UITableView tableView)
        {
            return 1;
        }


        [Export("tableView:didSelectRowAtIndexPath:")]
        public void RowSelected(UITableView tableView, NSIndexPath indexPath)
        {
            selectedIndex = indexPath.Row;
            ButtonClickHanlde();
            tableView.DeselectRow(indexPath,true);
            label.Text = _dropDownView.ItemsSource[indexPath.Row];
            if (_dropDownView.ItemSelecetedEvent != null)
                _dropDownView.ItemSelecetedEvent.Invoke(indexPath.Row);
        }

        [Export("tableView:heightForRowAtIndexPath:")]
        public virtual nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath)
        {
            return 50;
        }

    }
}

Đoạn code trên có một đoạn khá hay, đó là khởi tạo một UITableview mà không gắn vào một view chả nào cả, tableView này chính là nơi hiển thị list các item của dropdown control,đồng thời sử lý các sự kiện thông qua các hàm delegate.

                    tableView.AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
                    tableView.Frame = wrapper.Frame;
                    tableView.Layer.BorderWidth = 1;
                    tableView.Layer.BorderColor = ColorExtensions.ToCGColor(Color.FromHex(Define.DISABLE_CONTENT_COLOR));
                    tableView.WeakDelegate = this;
                    tableView.WeakDataSource = this;
                    
                    // các hàm delegate, datasource
                      [Export("tableView:cellForRowAtIndexPath:")]
                     [Export("tableView:heightForRowAtIndexPath:")]
                     [Export("tableView:didSelectRowAtIndexPath:")]

tiếp đến chúng ta sẽ add tableview này vào Window của ứng dụng, mở AppDelegate.cs ra thêm đoạn code sau

public void ShowSubviewAt(CGRect rect, UIView subView, Action didFinishAnimation)
        {
            UIView cover = new UIView();
            cover.Frame = new CGRect(0, 0, AppWindow.Bounds.Width, AppWindow.Bounds.Height);
            //cover.AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
            //cover.Opaque = true;
            cover.BackgroundColor = UIColor.Clear;
            cover.AddGestureRecognizer(new UITapGestureRecognizer(() =>
            {

                cover.RemoveFromSuperview();
                if (subView != null)
                    subView.RemoveFromSuperview();

            }));
            AppWindow.AddSubview(cover);

            subView.Frame = new CGRect(rect.X, rect.Y, rect.Width, 0);
            UIView.Animate(0.2, () =>
            {
                subView.Frame = new CGRect(rect.X, rect.Y, rect.Width, rect.Height);
                AppWindow.AddSubview(subView);
            }, didFinishAnimation);

        }

Tuy nhiên muốn xác định đụng vị trị của tabview trên windown của app ta để ý đoạn code sau:

// covert toạ độ của một view sang toạ độ của windows
var rect = wrapper.ConvertRectToView(wrapper.Frame, AppDelegate.AppWindow);

Vậy là xong, khởi chạy chúng ta được kết quả sau:

4.DropDownMenuRender_Android
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 Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using FJKApp3.Droid.Views.Customs;

using FJKApp3.Views.Customs;
using FJKApp3.ViewModels;
using Android.Support.V4.View;
using System.ComponentModel;
using Java.Lang;

[assembly: ExportRenderer(typeof(DropDownMenuView), typeof(DropDownMenuRender_Android))]
namespace FJKApp3.Droid.Views.Customs
{
    public class DropDownMenuRender_Android : ViewRenderer<DropDownMenuView, Android.Views.View>, Spinner.IOnItemSelectedListener
    {
        Spinner _nativeView;
        Spinner.IOnItemSelectedListener _itemSelected;
        DropDownMenuView _dropDownView;
        SpinnerAdapter _adapter;

        protected override void OnElementChanged(ElementChangedEventArgs<DropDownMenuView> e)
        {
            base.OnElementChanged(e);
            if (e.NewElement != null)
            {
                _dropDownView = (DropDownMenuView)e.NewElement;

                if (Control == null)
                {
                    Android.Widget.RelativeLayout wraper = new Android.Widget.RelativeLayout(Android.App.Application.Context);
                    ContextThemeWrapper theme = new ContextThemeWrapper(Android.App.Application.Context, FJKApp3.Droid.Resource.Style.SpinnerAsEditText);
                    _nativeView = new Spinner(theme);
                    _nativeView.OnItemSelectedListener = this;
                    if (Android.OS.Build.VERSION.SdkInt < Android.OS.BuildVersionCodes.M)
                        _nativeView.SetBackgroundResource(FJKApp3.Droid.Resource.Drawable.border);
                    else
                        wraper.SetBackgroundResource(FJKApp3.Droid.Resource.Drawable.border);
                    _nativeView.LayoutParameters = new Android.Widget.RelativeLayout.LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent);
                    _dropDownView.SetItemSelection = (i) => {
                        _nativeView.SetSelection(i);
                    };

                    ImageView downIcon = new ImageView(Android.App.Application.Context);
                    var param = new Android.Widget.RelativeLayout.LayoutParams(20, 20);
                    param.AddRule(LayoutRules.AlignParentRight);
                    param.AddRule(LayoutRules.CenterInParent);
                    param.SetMargins(0, 0, 14, 0);
                    downIcon.LayoutParameters = param;
                    downIcon.SetImageResource(FJKApp3.Droid.Resource.Drawable.ic_arrow_drop_down_black);
                    
                    wraper.AddView(_nativeView);
                    wraper.AddView(downIcon);
                    SetNativeControl(wraper);
                }

                if (_dropDownView.ItemsSource != null)
                    SetApdater();
            }
        }

        private void SetApdater()
        {
            if(_dropDownView.ItemsSource!=null)
            {
                //ArrayAdapter<String> adapter = new ArrayAdapter<String>(Android.App.Application.Context,
                //        Android.Resource.Layout.SimpleSpinnerItem, _dropDownView.ItemsSource);
                //adapter.SetDropDownViewResource(Android.Resource.Layout.SimpleListItemSingleChoice);
                if (_adapter == null)
                {
                    _adapter = new SpinnerAdapter(_dropDownView.ItemsSource);
                    _nativeView.Adapter = _adapter;
                }
                else
                    _adapter.SetData(_dropDownView.ItemsSource);
            }
        }

        class SpinnerAdapter : BaseAdapter
        {
            private List<string> _datas;
            public SpinnerAdapter(List<string> datas)
            {
                _datas = datas;
            }

            public override int Count
            {
                get
                {
                    return _datas != null ? _datas.Count : 0;
                }
            }

            public void SetData(List<string> datas)
            {
                _datas = datas;
                NotifyDataSetChanged();
            }

            public override Java.Lang.Object GetItem(int position)
            {
                return _datas[position];
            }

            public override long GetItemId(int position)
            {
                return 0;
            }

            public override Android.Views.View GetView(int position, Android.Views.View convertView, ViewGroup parent)
            {
                var item = (Android.Views.View)Android.Views.View.Inflate(Android.App.Application.Context, Resource.Layout.spinner_item, null);
                TextView text = item.FindViewById<TextView>(FJKApp3.Droid.Resource.Id.txtSpinnerText1);
                text.Text = _datas[position];
                if (Android.OS.Build.VERSION.SdkInt < Android.OS.BuildVersionCodes.M)
                {
                    item.SetBackgroundResource(FJKApp3.Droid.Resource.Drawable.border);
                    item.FindViewById(FJKApp3.Droid.Resource.Id.line).Visibility = ViewStates.Gone;
                }
                return item;
            }
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);
            SetApdater();
        }

        public void OnItemSelected(AdapterView parent, Android.Views.View view, int position, long id)
        {
            if(_dropDownView.ItemSelecetedEvent!=null)
            {
                _dropDownView.ItemSelecetedEvent.Invoke(position);
            }
        }

        public void OnNothingSelected(AdapterView parent)
        {
            if (_dropDownView.ItemSelecetedEvent != null)
            {
                _dropDownView.ItemSelecetedEvent.Invoke(-1);
            }
        }
    }
}

Rất may là Android có sẵn một UI control giống như dropdown menu, đó là Spinner, chúng ta chỉ cần tạo một object của class Spinner, tạo ra một class SpinnerAdapter kế thừa từ BaseAdapter, gán adapter cho object của Spinner vừa tạo được. Vậy là OK

0