我有一个像月份选择器这样的要求,它只显示月份和年份,而不显示日期。我们如何使用 Xamarin 表单在 IOS 和 Android 平台上实现相同的效果?
您可以使用自定义渲染器来实现它
创建自定义视图
using System;
using System.Collections.Generic;
using System.Text;
using Xamarin.Forms;
namespace App20
{
public class MonthYearPickerView :View
{
public static readonly BindableProperty FontSizeProperty = BindableProperty.Create(
propertyName: nameof(FontSize),
returnType: typeof(double),
declaringType: typeof(MonthYearPickerView),
defaultValue: (double)24,
defaultBindingMode: BindingMode.TwoWay);
[TypeConverter(typeof(FontSizeConverter))]
public double FontSize
{
get => (double)GetValue(FontSizeProperty);
set => SetValue(FontSizeProperty, value);
}
public static readonly BindableProperty TextColorProperty = BindableProperty.Create(
propertyName: nameof(TextColor),
returnType: typeof(Color),
declaringType: typeof(MonthYearPickerView),
defaultValue: Color.White,
defaultBindingMode: BindingMode.TwoWay);
public Color TextColor
{
get => (Color)GetValue(TextColorProperty);
set => SetValue(TextColorProperty, value);
}
public static readonly BindableProperty InfiniteScrollProperty = BindableProperty.Create(
propertyName: nameof(InfiniteScroll),
returnType: typeof(bool),
declaringType: typeof(MonthYearPickerView),
defaultValue: true,
defaultBindingMode: BindingMode.TwoWay);
public bool InfiniteScroll
{
get => (bool)GetValue(InfiniteScrollProperty);
set => SetValue(InfiniteScrollProperty, value);
}
public static readonly BindableProperty DateProperty = BindableProperty.Create(
propertyName: nameof(Date),
returnType: typeof(DateTime),
declaringType: typeof(MonthYearPickerView),
defaultValue: default,
defaultBindingMode: BindingMode.TwoWay);
public DateTime Date
{
get => (DateTime)GetValue(DateProperty);
set => SetValue(DateProperty, value);
}
public static readonly BindableProperty MaxDateProperty = BindableProperty.Create(
propertyName: nameof(MaxDate),
returnType: typeof(DateTime?),
declaringType: typeof(MonthYearPickerView),
defaultValue: default,
defaultBindingMode: BindingMode.TwoWay);
public DateTime? MaxDate
{
get => (DateTime?)GetValue(MaxDateProperty);
set => SetValue(MaxDateProperty, value);
}
public static readonly BindableProperty MinDateProperty = BindableProperty.Create(
propertyName: nameof(MinDate),
returnType: typeof(DateTime?),
declaringType: typeof(MonthYearPickerView),
defaultValue: default,
defaultBindingMode: BindingMode.TwoWay);
public DateTime? MinDate
{
get => (DateTime?)GetValue(MinDateProperty);
set => SetValue(MinDateProperty, value);
}
}
}
using System;
using App20;
using App20.iOS;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(MonthYearPickerView), typeof(MonthYearPickerRenderer))]
namespace App20.iOS
{
public class MonthYearPickerRenderer : ViewRenderer<MonthYearPickerView, UITextField>
{
private DateTime _selectedDate;
private UITextField _dateLabel;
private PickerDateModel _pickerModel;
protected override void OnElementChanged(ElementChangedEventArgs<MonthYearPickerView> e)
{
base.OnElementChanged(e);
_dateLabel = new UITextField();
_dateLabel.TextAlignment = UITextAlignment.Center;
var dateToday = DateTime.Today;
SetupPicker(new DateTime(dateToday.Year, dateToday.Month, 1));
SetNativeControl(_dateLabel);
Control.EditingChanged += ControlOnEditingChanged;
Element.PropertyChanged += Element_PropertyChanged;
}
private void ControlOnEditingChanged(object sender, EventArgs e)
{
var currentDate = $"{Element.Date.Month:D2} | {Element.Date.Year}";
if (_dateLabel.Text != currentDate)
{
_dateLabel.Text = currentDate;
}
}
protected override void Dispose(bool disposing)
{
Element.PropertyChanged -= Element_PropertyChanged;
base.Dispose(disposing);
}
private void SetupPicker(DateTime date)
{
var datePicker = new UIPickerView();
_pickerModel = new PickerDateModel(datePicker, date, Element.MaxDate, Element.MinDate);
datePicker.ShowSelectionIndicator = true;
_selectedDate = date;
_pickerModel.PickerChanged += (sender, e) =>
{
_selectedDate = e;
};
datePicker.Model = _pickerModel;
_pickerModel.MaxDate = Element.MaxDate ?? DateTime.MaxValue;
_pickerModel.MinDate = Element.MinDate ?? DateTime.MinValue;
var toolbar = new UIToolbar
{
BarStyle = UIBarStyle.Default,
Translucent = true
};
toolbar.SizeToFit();
var doneButton = new UIBarButtonItem("Done", UIBarButtonItemStyle.Done,
(s, e) =>
{
Element.Date = _selectedDate;
_dateLabel.Text = $"{Element.Date.Month:D2} | {Element.Date.Year}";
_dateLabel.ResignFirstResponder();
});
toolbar.SetItems(new[] { new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace), doneButton }, true);
_dateLabel.InputView = datePicker;
_dateLabel.Text = $"{Element.Date.Month:D2} | {Element.Date.Year}";
_dateLabel.InputAccessoryView = toolbar;
_dateLabel.TextColor = Element.TextColor.ToUIColor();
}
private void Element_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == MonthYearPickerView.MaxDateProperty.PropertyName)
{
_pickerModel.MaxDate = Element.MaxDate ?? DateTime.MinValue;
}
else if (e.PropertyName == MonthYearPickerView.MinDateProperty.PropertyName)
{
_pickerModel.MinDate = Element.MinDate ?? DateTime.MaxValue;
}
}
}
}
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using UIKit;
namespace App20.iOS
{
public class PickerDateModel : UIPickerViewModel
{
public event EventHandler<DateTime> PickerChanged;
#region Fields
private readonly List<string> _mainNamesOfMonthSource;
private readonly List<int> _mainYearsSource;
private readonly UIPickerView _picker;
private readonly int _numberOfComponents;
private readonly int _minYear;
private readonly int _maxYear;
private List<int> _years;
private List<string> _namesOfMonth;
private DateTime _selectedDate;
private DateTime _maxDate;
private DateTime _minDate;
#endregion Fields
#region Constructors
public PickerDateModel(UIPickerView datePicker, DateTime selectedDate, DateTime? maxDate, DateTime? minDate)
{
_mainNamesOfMonthSource = DateTimeFormatInfo.CurrentInfo?.MonthNames
.Where(x => !string.IsNullOrWhiteSpace(x))
.ToList();
_maxDate = maxDate ?? DateTime.MaxValue;
_minDate = minDate ?? DateTime.MinValue;
_maxYear = _maxDate.Year;
_minYear = _minDate.Year;
_years = new List<int>();
_picker = datePicker;
_namesOfMonth = _mainNamesOfMonthSource;
_numberOfComponents = 2;
SelectedDate = selectedDate;
}
#endregion Constructors
#region Properties
public DateTime SelectedDate
{
get => _selectedDate;
set
{
_selectedDate = value;
ReloadSections();
PickerChanged?.Invoke(this, value);
}
}
public DateTime MaxDate
{
get => _maxDate;
set
{
_maxDate = value;
ReloadSections();
}
}
public DateTime MinDate
{
get => _minDate;
set
{
_minDate = value;
ReloadSections();
}
}
#endregion Properties
#region Private Methods
private void ReloadSections()
{
var selectedDate = SelectedDate == DateTime.MinValue
? DateTime.Today
: SelectedDate;
_years.Clear();
for (int i = _minYear; i <= _maxYear; i++)
{
_years.Add(i);
}
_namesOfMonth = _mainNamesOfMonthSource;
if (SelectedDate.Year == MinDate.Year)
{
_namesOfMonth = _mainNamesOfMonthSource.Skip(MinDate.Month - 1).ToList();
}
if (SelectedDate.Year == MaxDate.Year)
{
_namesOfMonth = _mainNamesOfMonthSource.Take(MaxDate.Month).ToList();
}
SetCarousels(selectedDate);
}
#endregion Private Methods
#region Public Methods
public void SetCarousels(DateTime dateTime)
{
if (_picker.NumberOfComponents != _numberOfComponents) return;
var y = DateTimeFormatInfo.CurrentInfo?.GetMonthName(dateTime.Month);
var x = _namesOfMonth.IndexOf(y);
_picker.Select(x, 0, false);
_picker.Select(_years.IndexOf(dateTime.Year), 1, false);
_picker.ReloadComponent(0);
_picker.ReloadComponent(1);
}
public override nint GetComponentCount(UIPickerView pickerView)
{
return _numberOfComponents;
}
public override nint GetRowsInComponent(UIPickerView pickerView, nint component)
{
if (component == 0)
{
return _namesOfMonth.Count;
}
else if (component == 1)
{
return _years.Count;
}
else
{
return 0;
}
}
public override string GetTitle(UIPickerView pickerView, nint row, nint component)
{
if (component == 0)
{
return _namesOfMonth.Count==0 ? _namesOfMonth.First() : _namesOfMonth[(int)row];
}
else if (component == 1)
{
var list = _years;
return _years.Count==0? _years.First().ToString() : _years[(int)row].ToString();
}
else
{
return row.ToString();
}
}
public override void Selected(UIPickerView pickerView, nint row, nint component)
{
var month = GetMonthNumberByName(_namesOfMonth[(int)pickerView.SelectedRowInComponent(0)]);
var year = _years[(int)pickerView.SelectedRowInComponent(1)];
if (year == MinDate.Year)
{
month = month >= MinDate.Month ? month : MinDate.Month;
}
if (year == MaxDate.Year)
{
month = month <= MaxDate.Month ? month : MaxDate.Month;
}
SelectedDate = new DateTime(year, month, 1);
ReloadSections();
pickerView.ReloadAllComponents();
int GetMonthNumberByName(string monthName) =>
DateTime.ParseExact(monthName, "MMMM", CultureInfo.CurrentCulture).Month;
}
#endregion Public Methods
}
}
在主活动中
public static MainActivity Instance { get; private set; }
protected override void OnCreate(Bundle savedInstanceState)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
Instance = this;
base.OnCreate(savedInstanceState);
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
LoadApplication(new App());
}
using Android.Content;
using Android.Support.V7.App;
using Android.Widget;
using App20;
using App20.Droid;
using System;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(MonthYearPickerView), typeof(MonthYearPickerRenderer))]
namespace App20.Droid
{
public class MonthYearPickerRenderer : ViewRenderer<MonthYearPickerView, EditText>
{
private readonly Context _context;
private MonthYearPickerDialog _monthYearPickerDialog;
public MonthYearPickerRenderer(Context context) : base(context)
{
_context = context;
}
protected override void OnElementChanged(ElementChangedEventArgs<MonthYearPickerView> e)
{
base.OnElementChanged(e);
CreateAndSetNativeControl();
Control.KeyListener = null;
Element.Focused += Element_Focused;
}
protected override void Dispose(bool disposing)
{
if (Control == null) return;
Element.Focused -= Element_Focused;
if (_monthYearPickerDialog != null)
{
_monthYearPickerDialog.OnDateTimeChanged -= OnDateTimeChanged;
_monthYearPickerDialog.OnClosed -= OnClosed;
_monthYearPickerDialog.Hide();
_monthYearPickerDialog.Dispose();
_monthYearPickerDialog = null;
}
base.Dispose(disposing);
}
#region Private Methods
private void ShowDatePicker()
{
if (_monthYearPickerDialog == null)
{
_monthYearPickerDialog = new MonthYearPickerDialog();
_monthYearPickerDialog.OnDateTimeChanged += OnDateTimeChanged;
_monthYearPickerDialog.OnClosed += OnClosed;
}
_monthYearPickerDialog.Date = Element.Date;
_monthYearPickerDialog.MinDate = FormatDateToMonthYear(Element.MinDate);
_monthYearPickerDialog.MaxDate = FormatDateToMonthYear(Element.MaxDate);
_monthYearPickerDialog.InfiniteScroll = Element.InfiniteScroll;
var appcompatActivity = MainActivity.Instance;
var mFragManager = appcompatActivity?.SupportFragmentManager;
if (mFragManager != null)
{
_monthYearPickerDialog.Show(mFragManager, nameof(MonthYearPickerDialog));
}
}
private void ClearPickerFocus()
{
((IElementController)Element).SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
Control.ClearFocus();
}
private DateTime? FormatDateToMonthYear(DateTime? dateTime) =>
dateTime.HasValue ? (DateTime?) new DateTime(dateTime.Value.Year, dateTime.Value.Month, 1) : null;
private void CreateAndSetNativeControl()
{
var tv = new EditText(_context);
tv.SetTextColor(Element.TextColor.ToAndroid());
tv.TextSize = (float)Element.FontSize;
tv.Text = $"{Element.Date.Month:D2} | {Element.Date.Year}";
tv.Gravity = Android.Views.GravityFlags.Center;
tv.SetBackgroundColor(Element.BackgroundColor.ToAndroid());
SetNativeControl(tv);
}
#endregion
#region Event Handlers
private void Element_Focused(object sender, FocusEventArgs e)
{
if (e.IsFocused)
{
ShowDatePicker();
}
}
private void OnClosed(object sender, DateTime e)
{
ClearPickerFocus();
}
private void OnDateTimeChanged(object sender, DateTime e)
{
Element.Date = e;
Control.Text = $"{Element.Date.Month:D2} | {Element.Date.Year}";
ClearPickerFocus();
}
#endregion
}
}
using Android.App;
using Android.OS;
using Android.Views;
using Android.Widget;
using System;
using System.Linq;
namespace App20.Droid
{
public class MonthYearPickerDialog : Android.Support.V4.App.DialogFragment
{
public event EventHandler<DateTime> OnDateTimeChanged;
public event EventHandler<DateTime> OnClosed;
#region Private Fields
private const int DefaultDay = 1;
private const int MinNumberOfMonths = 1;
private const int MaxNumberOfMonths = 12;
private const int MinNumberOfYears = 1900;
private const int MaxNumberOfYears = 2100;
private NumberPicker _monthPicker;
private NumberPicker _yearPicker;
#endregion
#region Public Properties
public DateTime? MinDate { get; set; }
public DateTime? MaxDate { get; set; }
public DateTime? Date { get; set; }
public bool InfiniteScroll { get; set; }
#endregion
public void Hide() => base.Dialog?.Hide();
public override Dialog OnCreateDialog(Bundle savedInstanceState)
{
var builder = new AlertDialog.Builder(Activity);
var inflater = Activity.LayoutInflater;
var selectedDate = GetSelectedDate();
var dialog = inflater.Inflate(Resource.Layout.date_picker_dialog, null);
_monthPicker = (NumberPicker)dialog.FindViewById(Resource.Id.picker_month);
_yearPicker = (NumberPicker)dialog.FindViewById(Resource.Id.picker_year);
InitializeMonthPicker(selectedDate.Month);
InitializeYearPicker(selectedDate.Year);
SetMaxMinDate(MaxDate, MinDate);
builder.SetView(dialog)
.SetPositiveButton("Ok", (sender, e) =>
{
selectedDate = new DateTime(_yearPicker.Value, _monthPicker.Value, DefaultDay);
OnDateTimeChanged?.Invoke(dialog, selectedDate);
})
.SetNegativeButton("Cancel", (sender, e) =>
{
Dialog.Cancel();
OnClosed?.Invoke(dialog, selectedDate);
});
return builder.Create();
}
protected override void Dispose(bool disposing)
{
if (_yearPicker != null)
{
_yearPicker.ScrollChange -= YearPicker_ScrollChange;
_yearPicker.Dispose();
_yearPicker = null;
}
_monthPicker?.Dispose();
_monthPicker = null;
base.Dispose(disposing);
}
#region Private Methods
private DateTime GetSelectedDate() => Date ?? DateTime.Now;
private void InitializeYearPicker(int year)
{
_yearPicker.MinValue = MinNumberOfYears;
_yearPicker.MaxValue = MaxNumberOfYears;
_yearPicker.Value = year;
_yearPicker.ScrollChange += YearPicker_ScrollChange;
if (!InfiniteScroll)
{
_yearPicker.WrapSelectorWheel = false;
_yearPicker.DescendantFocusability = DescendantFocusability.BlockDescendants;
}
}
private void InitializeMonthPicker(int month)
{
_monthPicker.MinValue = MinNumberOfMonths;
_monthPicker.MaxValue = MaxNumberOfMonths;
_monthPicker.SetDisplayedValues(GetMonthNames());
_monthPicker.Value = month;
if (!InfiniteScroll)
{
_monthPicker.WrapSelectorWheel = false;
_monthPicker.DescendantFocusability = DescendantFocusability.BlockDescendants;
}
}
private void YearPicker_ScrollChange(object sender, View.ScrollChangeEventArgs e)
{
SetMaxMinDate(MaxDate, MinDate);
}
private void SetMaxMinDate(DateTime? maxDate, DateTime? minDate)
{
try
{
if (maxDate.HasValue)
{
var maxYear = maxDate.Value.Year;
var maxMonth = maxDate.Value.Month;
if (_yearPicker.Value == maxYear)
{
_monthPicker.MaxValue = maxMonth;
}
else if (_monthPicker.MaxValue != MaxNumberOfMonths)
{
_monthPicker.MaxValue = MaxNumberOfMonths;
}
_yearPicker.MaxValue = maxYear;
}
if (minDate.HasValue)
{
var minYear = minDate.Value.Year;
var minMonth = minDate.Value.Month;
if (_yearPicker.Value == minYear)
{
_monthPicker.MinValue = minMonth;
}
else if (_monthPicker.MinValue != MinNumberOfMonths)
{
_monthPicker.MinValue = MinNumberOfMonths;
}
_yearPicker.MinValue = minYear;
}
_monthPicker.SetDisplayedValues(GetMonthNames(_monthPicker.MinValue));
}
catch (Exception e)
{
}
}
private string[] GetMonthNames(int start = 1) =>
System.Globalization.DateTimeFormatInfo.CurrentInfo?.MonthNames.Skip(start - 1).ToArray();
#endregion
}
}
在 Resource ->layout
中创建 date_picker_dialog.xml<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="horizontal">
<NumberPicker
android:id="@+id/picker_month"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:layout_marginRight="20dp">
</NumberPicker>
<NumberPicker
android:id="@+id/picker_year"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</NumberPicker>
</LinearLayout>
</LinearLayout>
现在你可以在Xaml中引用它了
<StackLayout VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand">
<local:MonthYearPickerView
Date="06.15.2020"
BackgroundColor="LightBlue"
WidthRequest="150"
MinDate="01.01.2020"
MaxDate="12.31.2050"
HorizontalOptions="CenterAndExpand"
VerticalOptions="Center" />
</StackLayout>
如何在MAUI中实现上面的自定义渲染?