我终于得到了一个有效的实现。结果如下:
唯一的缺点是标签“hr”、“min”和“sec”位于其自己的列中。因此,如果用户尝试与它们交互,就会产生反弹效果。稍后我会尝试看看是否可以改进。
欢迎提出建议!
当然,还有代码(如果它对某人有用的话):
渲染器
[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);
}
}
}
自定义可绑定选择器类
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 用法
<myControls:MyTimePicker SelectedTime="{Binding SelectedTime}" />
其中 SelectedTime 是用于绑定的 ViewModel 属性。必须是 TimeSpan 类型。
我确信它可以改进,所以如果您有建议,请发表评论。
UPDATE
我已经更新了渲染器代码。
- “小时”、“分钟”和“秒”现在是标签,不再是它们自己的列。这意味着用户不再可以与它们交互(不再有反弹效果)
- 我还修复了一个错误,如果选择器值是从 ViewModel 设置的,则在打开键盘时,选择仍为 00:00:00
- 更好地支持 iPhone Plus 分辨率
请注意,此代码依赖于 XForms (https://github.com/XLabs/Xamarin-Forms-Labs)来获取设备屏幕尺寸,因为这是我用于我的项目的框架,但它可以轻松修改。