Timepicker with seconds

家住魔仙堡 提交于 2019-11-30 16:48:57

I finally got a working implementation. Here's the result:

The only downside is that the labels "hr", "min", and "sec" are in their own column. As such, there is a bounce effect if the user tries to interact with them. I'll try to see if I can improve it later. Suggestions welcome!

And, of course, the code (if it can be of use to someone):

The renderer

[assembly: ExportRendererAttribute(typeof(MyTimePicker), typeof(MyTimePickerRenderer))]
namespace MyProject.iOS.Renderers
{
    public class MyTimePickerRenderer : PickerRenderer
    {
        internal static  IDevice Device;

        internal const int ComponentCount = 6;

        private const int _labelSize = 30;

        private MyTimePicker _myTimePicker;

        public MyTimePickerRenderer()
        {
            // This is dependent on XForms (see Update note)
            Device = Resolver.Resolve<IDevice>();
        }

        protected override void OnElementChanged (ElementChangedEventArgs<Picker> e)
        {
            base.OnElementChanged (e);

            if (Control != null)
            {
                Control.BorderStyle = UITextBorderStyle.None;

                _myTimePicker = e.NewElement as MyTimePicker;

                var customModelPickerView = new UIPickerView
                    {
                        Model = new MyTimePickerView(_myTimePicker)
                    };

                SelectPickerValue(customModelPickerView, _myTimePicker);

                CreatePickerLabels(customModelPickerView);

                Control.InputView = customModelPickerView;
            }
        }

    private void SelectPickerValue(UIPickerView customModelPickerView, MyTimePicker myTimePicker)
    { 
        customModelPickerView.Select(new nint(myTimePicker.SelectedTime.Hours), 0, false);
        customModelPickerView.Select(new nint(myTimePicker.SelectedTime.Minutes), 2, false);
        customModelPickerView.Select(new nint(myTimePicker.SelectedTime.Seconds), 4, false);
    }

    private void CreatePickerLabels(UIPickerView customModelPickerView)
    {
        nfloat verticalPosition = (customModelPickerView.Frame.Size.Height / 2) - (_labelSize / 2);
        nfloat componentWidth = new nfloat(Device.Display.Width / ComponentCount / Device.Display.Scale);

        var hoursLabel = new UILabel(new CGRect(componentWidth, verticalPosition, _labelSize, _labelSize));
        hoursLabel.Text = "hrs";

        var minutesLabel = new UILabel(new CGRect((componentWidth * 3) + (componentWidth / 2), verticalPosition, _labelSize, _labelSize));
        minutesLabel.Text = "mins";

        var secondsLabel = new UILabel(new CGRect((componentWidth * 5) + (componentWidth / 2), verticalPosition, _labelSize, _labelSize));
        secondsLabel.Text = "secs";

        customModelPickerView.AddSubview(hoursLabel);
        customModelPickerView.AddSubview(minutesLabel);
        customModelPickerView.AddSubview(secondsLabel);
    }

    protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        base.OnElementPropertyChanged(sender, e);

        if (Control == null)
        {
            return;
        }

        if (e.PropertyName == "SelectedIndex")
        {
            var customModelPickerView = (UIPickerView)Control.InputView;

            SelectPickerValue(customModelPickerView, _myTimePicker);
        }
    }

    public class MyTimePickerView : UIPickerViewModel
    {
        private readonly MyTimePicker _myTimePicker;

        public MyTimePickerView(MyTimePicker picker)
        {
            _myTimePicker = picker;
        }

        public override nint GetComponentCount(UIPickerView pickerView)
        {
            return new nint(MyTimePickerRenderer.ComponentCount);
        }

        public override nint GetRowsInComponent(UIPickerView pickerView, nint component)
        {
            if (component == 0)
            {
                // Hours
                return new nint(24);
            }

            if (component % 2 != 0)
            {
                // Odd components are labels for hrs, mins and secs
                return new nint(1);
            }

            // Minutes & seconds
            return new nint(60);
        }

        public override string GetTitle(UIPickerView pickerView, nint row, nint component)
        {
            if (component == 0)
            {
                return row.ToString();
            }
            else if (component == 1)
            {
                return null;
            }
            else if (component == 3)
            {
                return null;
            }
            else if (component == 5)
            {
                return null;
            }
            return row.ToString("00");
        }

        public override void Selected(UIPickerView pickerView, nint row, nint component)
        {
            var selectedHours = pickerView.SelectedRowInComponent(0);
            var selectedMinutes = pickerView.SelectedRowInComponent(2);
            var selectedSeconds = pickerView.SelectedRowInComponent(4);

            var time = new TimeSpan((int)selectedHours, (int)selectedMinutes, (int)selectedSeconds);

            _myTimePicker.SelectedTime = time;
        }

        public override nfloat GetComponentWidth(UIPickerView pickerView, nint component)
        {
            var screenWidth = MyTimePickerRenderer.Device.Display.Width;

            var componentWidth = screenWidth /
                MyTimePickerRenderer.ComponentCount /
                MyTimePickerRenderer.Device.Display.Scale;

            return new nfloat(componentWidth);
        }
    }
}

Custom bindable picker class

public class MyTimePicker : Picker
{
    public static readonly BindableProperty SelectedTimeProperty =
    BindableProperty.Create<MyTimePicker, TimeSpan>(p => p.SelectedTime, TimeSpan.MinValue, BindingMode.TwoWay,propertyChanged: OnSelectedTimePropertyPropertyChanged);

    public MyTimePicker()
    {
        // Ugly hack since Xamarin Forms' Picker uses only one component internally
        // This is a list of all possible timespan from 0:00:00 to 23:59:59
        for (int hour = 0; hour < 24; hour++)
        {
            for (int minute = 0; minute < 60; minute ++)
            {
                for (int second = 0; second < 60; second++)
                {
                    Items.Add(string.Format("{0:D2}:{1:D2}:{2:D2}", hour, minute, second));
                }
            }
        }

        base.SelectedIndexChanged += (o, e) =>
        {
            if (base.SelectedIndex < 0)
            {
                SelectedTime = TimeSpan.MinValue;
                return;
            }

            int index = 0;
            foreach (var item in Items)
            {
                if (index == SelectedIndex)
                {
                    SelectedTime = TimeSpan.Parse(item);
                    break;
                }
                index++;
            }
        };
    }

    public TimeSpan SelectedTime
    {
        get { return (TimeSpan)GetValue(SelectedTimeProperty); }
        set { SetValue(SelectedTimeProperty, value); }
    }

    private static void OnSelectedTimePropertyPropertyChanged(BindableObject bindable, TimeSpan value, TimeSpan newValue)
    {
        var picker = (MyTimePicker)bindable;

        var itemMatch = picker.Items.FirstOrDefault(x => x == newValue.ToString());
        var index = picker.Items.IndexOf(itemMatch);

        picker.SelectedIndex = index;
    }
}

XAML usage

<myControls:MyTimePicker SelectedTime="{Binding SelectedTime}" />

Where SelectedTime is your ViewModel property used for binding. Must be of type TimeSpan.

I'm sure it can be improved, so comment away if you have suggestions.


UPDATE

I've updated the renderer code.

  • The "hrs", "mins" and "secs" are now label and no longer columns of their own. That means that the user no longer can interact with them (no more bounce effect)
  • I've also fixed a bug where, if the picker value was set from the ViewModel, upon opening the keyboard the selection was still at 00:00:00
  • Better support for iPhone Plus resolution

Note that this code is dependent on XForms (https://github.com/XLabs/Xamarin-Forms-Labs) to get the device screen size, because that's the framework that I'm using for my project, but it can easily be modified.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!