diff options
Diffstat (limited to 'Xamarin.Forms.Platform.Android')
148 files changed, 17655 insertions, 0 deletions
diff --git a/Xamarin.Forms.Platform.Android/AndroidActivity.cs b/Xamarin.Forms.Platform.Android/AndroidActivity.cs new file mode 100644 index 00000000..d675926f --- /dev/null +++ b/Xamarin.Forms.Platform.Android/AndroidActivity.cs @@ -0,0 +1,9 @@ +using System; + +namespace Xamarin.Forms.Platform.Android +{ + [Obsolete("AndroidActivity is obsolete as of version 1.3, please use FormsApplicationActivity")] + public class AndroidActivity : FormsApplicationActivity + { + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/AndroidApplicationLifecycleState.cs b/Xamarin.Forms.Platform.Android/AndroidApplicationLifecycleState.cs new file mode 100644 index 00000000..b0adf0de --- /dev/null +++ b/Xamarin.Forms.Platform.Android/AndroidApplicationLifecycleState.cs @@ -0,0 +1,14 @@ +namespace Xamarin.Forms.Platform.Android +{ + internal enum AndroidApplicationLifecycleState + { + Uninitialized, + OnCreate, + OnStart, + OnResume, + OnPause, + OnStop, + OnRestart, + OnDestroy + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/AndroidTicker.cs b/Xamarin.Forms.Platform.Android/AndroidTicker.cs new file mode 100644 index 00000000..2f5104e3 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/AndroidTicker.cs @@ -0,0 +1,43 @@ +using System; +using Android.Animation; + +namespace Xamarin.Forms.Platform.Android +{ + internal class AndroidTicker : Ticker, IDisposable + { + ValueAnimator _val; + + public AndroidTicker() + { + _val = new ValueAnimator(); + _val.SetIntValues(0, 100); // avoid crash + _val.RepeatCount = ValueAnimator.Infinite; + _val.Update += OnValOnUpdate; + } + + public void Dispose() + { + if (_val != null) + { + _val.Update -= OnValOnUpdate; + _val.Dispose(); + } + _val = null; + } + + protected override void DisableTimer() + { + _val?.Cancel(); + } + + protected override void EnableTimer() + { + _val?.Start(); + } + + void OnValOnUpdate(object sender, ValueAnimator.AnimatorUpdateEventArgs e) + { + SendSignals(); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/AndroidTitleBarVisibility.cs b/Xamarin.Forms.Platform.Android/AndroidTitleBarVisibility.cs new file mode 100644 index 00000000..b80f54fd --- /dev/null +++ b/Xamarin.Forms.Platform.Android/AndroidTitleBarVisibility.cs @@ -0,0 +1,8 @@ +namespace Xamarin.Forms +{ + public enum AndroidTitleBarVisibility + { + Default = 0, + Never = 1 + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/AppCompat/ButtonRenderer.cs b/Xamarin.Forms.Platform.Android/AppCompat/ButtonRenderer.cs new file mode 100644 index 00000000..c6e31e18 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/AppCompat/ButtonRenderer.cs @@ -0,0 +1,243 @@ +using System; +using System.ComponentModel; +using Android.Content; +using Android.Content.Res; +using Android.Graphics; +using Android.Graphics.Drawables; +using Android.Support.V7.Widget; +using Android.Util; +using GlobalResource = Android.Resource; +using Object = Java.Lang.Object; + +namespace Xamarin.Forms.Platform.Android.AppCompat +{ + public class ButtonRenderer : ViewRenderer<Button, AppCompatButton>, global::Android.Views.View.IOnAttachStateChangeListener + { + static readonly int[][] States = { new[] { GlobalResource.Attribute.StateEnabled }, new[] { -GlobalResource.Attribute.StateEnabled } }; + + ColorStateList _buttonDefaulTextColors; + Color _currentTextColor; + float _defaultFontSize; + Typeface _defaultTypeface; + bool _isDisposed; + + public ButtonRenderer() + { + AutoPackage = false; + } + + global::Android.Widget.Button NativeButton => Control; + + void IOnAttachStateChangeListener.OnViewAttachedToWindow(global::Android.Views.View attachedView) + { + UpdateText(); + } + + void IOnAttachStateChangeListener.OnViewDetachedFromWindow(global::Android.Views.View detachedView) + { + } + + public override SizeRequest GetDesiredSize(int widthConstraint, int heightConstraint) + { + UpdateText(); + return base.GetDesiredSize(widthConstraint, heightConstraint); + } + + protected override AppCompatButton CreateNativeControl() + { + return new AppCompatButton(Context); + } + + protected override void Dispose(bool disposing) + { + if (_isDisposed) + return; + + _isDisposed = true; + + if (disposing) + { + } + + base.Dispose(disposing); + } + + protected override void OnElementChanged(ElementChangedEventArgs<Button> e) + { + base.OnElementChanged(e); + + if (e.OldElement != null) + { + } + + if (e.NewElement != null) + { + if (Control == null) + { + AppCompatButton button = CreateNativeControl(); + + button.SetOnClickListener(ButtonClickListener.Instance.Value); + button.Tag = this; + _buttonDefaulTextColors = button.TextColors; + SetNativeControl(button); + + button.AddOnAttachStateChangeListener(this); + } + + UpdateAll(); + UpdateBackgroundColor(); + } + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == Button.TextProperty.PropertyName) + UpdateText(); + else if (e.PropertyName == Button.TextColorProperty.PropertyName) + UpdateTextColor(); + else if (e.PropertyName == VisualElement.IsEnabledProperty.PropertyName) + UpdateEnabled(); + else if (e.PropertyName == Button.FontProperty.PropertyName) + UpdateFont(); + else if (e.PropertyName == Button.ImageProperty.PropertyName) + UpdateBitmap(); + else if (e.PropertyName == VisualElement.IsVisibleProperty.PropertyName) + UpdateText(); + + base.OnElementPropertyChanged(sender, e); + } + + protected override void UpdateBackgroundColor() + { + if (Element == null || Control == null) + return; + + Color backgroundColor = Element.BackgroundColor; + if (backgroundColor.IsDefault) + { + if (Control.SupportBackgroundTintList != null) + { + Context context = Context; + int id = GlobalResource.Attribute.ButtonTint; + unchecked + { + using(var value = new TypedValue()) + { + try + { + Resources.Theme theme = context.Theme; + if (theme != null && theme.ResolveAttribute(id, value, true)) + Control.SupportBackgroundTintList = Resources.GetColorStateList(value.Data); + else + Control.SupportBackgroundTintList = new ColorStateList(States, new[] { (int)0xffd7d6d6, 0x7fd7d6d6 }); + } + catch (Exception ex) + { + Control.SupportBackgroundTintList = new ColorStateList(States, new[] { (int)0xffd7d6d6, 0x7fd7d6d6 }); + } + } + } + } + } + else + { + int intColor = backgroundColor.ToAndroid().ToArgb(); + int disableColor = backgroundColor.MultiplyAlpha(0.5).ToAndroid().ToArgb(); + Control.SupportBackgroundTintList = new ColorStateList(States, new[] { intColor, disableColor }); + } + } + + void UpdateAll() + { + UpdateFont(); + UpdateText(); + UpdateBitmap(); + UpdateTextColor(); + UpdateEnabled(); + } + + void UpdateBitmap() + { + FileImageSource elementImage = Element.Image; + string imageFile = elementImage?.File; + if (elementImage != null && !string.IsNullOrEmpty(imageFile)) + { + Drawable image = Context.Resources.GetDrawable(imageFile); + Control.SetCompoundDrawablesWithIntrinsicBounds(image, null, null, null); + image?.Dispose(); + } + else + Control.SetCompoundDrawablesWithIntrinsicBounds(null, null, null, null); + } + + void UpdateEnabled() + { + Control.Enabled = Element.IsEnabled; + } + + void UpdateFont() + { + Button button = Element; + Font font = button.Font; + + if (font == Font.Default && _defaultFontSize == 0f) + return; + + if (_defaultFontSize == 0f) + { + _defaultTypeface = NativeButton.Typeface; + _defaultFontSize = NativeButton.TextSize; + } + + if (font == Font.Default) + { + NativeButton.Typeface = _defaultTypeface; + NativeButton.SetTextSize(ComplexUnitType.Px, _defaultFontSize); + } + else + { + NativeButton.Typeface = font.ToTypeface(); + NativeButton.SetTextSize(ComplexUnitType.Sp, font.ToScaledPixel()); + } + } + + void UpdateText() + { + NativeButton.Text = Element.Text; + } + + void UpdateTextColor() + { + Color color = Element.TextColor; + if (color == _currentTextColor) + return; + + _currentTextColor = color; + + if (color.IsDefault) + NativeButton.SetTextColor(_buttonDefaulTextColors); + else + { + // Set the new enabled state color, preserving the default disabled state color + int defaultDisabledColor = _buttonDefaulTextColors.GetColorForState(States[1], color.ToAndroid()); + + NativeButton.SetTextColor(new ColorStateList(States, new[] { color.ToAndroid().ToArgb(), defaultDisabledColor })); + } + } + + class ButtonClickListener : Object, IOnClickListener + { + #region Statics + + public static readonly Lazy<ButtonClickListener> Instance = new Lazy<ButtonClickListener>(() => new ButtonClickListener()); + + #endregion + + public void OnClick(global::Android.Views.View v) + { + var renderer = v.Tag as ButtonRenderer; + ((IButtonController)renderer?.Element)?.SendClicked(); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/AppCompat/CarouselPageRenderer.cs b/Xamarin.Forms.Platform.Android/AppCompat/CarouselPageRenderer.cs new file mode 100644 index 00000000..5e8a1b8a --- /dev/null +++ b/Xamarin.Forms.Platform.Android/AppCompat/CarouselPageRenderer.cs @@ -0,0 +1,155 @@ +using System.Collections.Specialized; +using System.ComponentModel; +using Android.Content; +using Android.Support.V4.View; +using Android.Views; + +namespace Xamarin.Forms.Platform.Android.AppCompat +{ + public class CarouselPageRenderer : VisualElementRenderer<CarouselPage>, ViewPager.IOnPageChangeListener + { + bool _disposed; + FormsViewPager _viewPager; + + public CarouselPageRenderer() + { + AutoPackage = false; + } + + void ViewPager.IOnPageChangeListener.OnPageScrolled(int position, float positionOffset, int positionOffsetPixels) + { + } + + void ViewPager.IOnPageChangeListener.OnPageScrollStateChanged(int state) + { + } + + void ViewPager.IOnPageChangeListener.OnPageSelected(int position) + { + Element.CurrentPage = Element.Children[position]; + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + RemoveAllViews(); + foreach (ContentPage pageToRemove in Element.Children) + { + IVisualElementRenderer pageRenderer = Android.Platform.GetRenderer(pageToRemove); + if (pageRenderer != null) + { + pageRenderer.ViewGroup.RemoveFromParent(); + pageRenderer.Dispose(); + } + pageToRemove.ClearValue(Android.Platform.RendererProperty); + } + + if (_viewPager != null) + { + _viewPager.Adapter.Dispose(); + _viewPager.Dispose(); + _viewPager = null; + } + + if (Element != null) + Element.InternalChildren.CollectionChanged -= OnChildrenCollectionChanged; + } + + base.Dispose(disposing); + } + + protected override void OnAttachedToWindow() + { + base.OnAttachedToWindow(); + Element.SendAppearing(); + } + + protected override void OnDetachedFromWindow() + { + base.OnDetachedFromWindow(); + Element.SendDisappearing(); + } + + protected override void OnElementChanged(ElementChangedEventArgs<CarouselPage> e) + { + base.OnElementChanged(e); + + var activity = (FormsAppCompatActivity)Context; + + if (e.OldElement != null) + e.OldElement.InternalChildren.CollectionChanged -= OnChildrenCollectionChanged; + + if (e.NewElement != null) + { + FormsViewPager pager = + _viewPager = + new FormsViewPager(activity) + { + OverScrollMode = OverScrollMode.Never, + EnableGesture = true, + LayoutParameters = new LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent), + Adapter = new FormsFragmentPagerAdapter<ContentPage>(e.NewElement, activity.SupportFragmentManager) { CountOverride = e.NewElement.Children.Count } + }; + pager.Id = FormsAppCompatActivity.GetUniqueId(); + pager.AddOnPageChangeListener(this); + + AddView(pager); + CarouselPage carouselPage = e.NewElement; + if (carouselPage.CurrentPage != null) + ScrollToCurrentPage(); + + UpdateIgnoreContainerAreas(); + carouselPage.InternalChildren.CollectionChanged += OnChildrenCollectionChanged; + } + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == "CurrentPage") + ScrollToCurrentPage(); + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + FormsViewPager pager = _viewPager; + Context context = Context; + int width = r - l; + int height = b - t; + + pager.Measure(MeasureSpecFactory.MakeMeasureSpec(width, MeasureSpecMode.AtMost), MeasureSpecFactory.MakeMeasureSpec(height, MeasureSpecMode.AtMost)); + + if (width > 0 && height > 0) + { + Element.ContainerArea = new Rectangle(0, 0, context.FromPixels(width), context.FromPixels(height)); + pager.Layout(0, 0, width, b); + } + + base.OnLayout(changed, l, t, r, b); + } + + void OnChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + FormsViewPager pager = _viewPager; + + ((FormsFragmentPagerAdapter<ContentPage>)pager.Adapter).CountOverride = Element.Children.Count; + pager.Adapter.NotifyDataSetChanged(); + + UpdateIgnoreContainerAreas(); + } + + void ScrollToCurrentPage() + { + _viewPager.SetCurrentItem(Element.Children.IndexOf(Element.CurrentPage), true); + } + + void UpdateIgnoreContainerAreas() + { + foreach (ContentPage child in Element.Children) + child.IgnoresContainerArea = child is NavigationPage; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/AppCompat/FormsAppCompatActivity.cs b/Xamarin.Forms.Platform.Android/AppCompat/FormsAppCompatActivity.cs new file mode 100644 index 00000000..916f1859 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/AppCompat/FormsAppCompatActivity.cs @@ -0,0 +1,452 @@ +#region + +using System; +using System.ComponentModel; +using System.Linq; +using Android.App; +using Android.Content; +using Android.Content.Res; +using Android.OS; +using Android.Runtime; +using Android.Support.V7.App; +using Android.Util; +using Android.Views; +using Android.Widget; +using Xamarin.Forms.Platform.Android.AppCompat; +using AToolbar = Android.Support.V7.Widget.Toolbar; +using AColor = Android.Graphics.Color; +using AlertDialog = Android.Support.V7.App.AlertDialog; +using ARelativeLayout = Android.Widget.RelativeLayout; + +#endregion + +namespace Xamarin.Forms.Platform.Android +{ + public class FormsAppCompatActivity : AppCompatActivity, IDeviceInfoProvider, IStartActivityForResult + { + public delegate bool BackButtonPressedEventHandler(object sender, EventArgs e); + + readonly ConcurrentDictionary<int, Action<Result, Intent>> _activityResultCallbacks = new ConcurrentDictionary<int, Action<Result, Intent>>(); + + Application _application; + int _busyCount; + AndroidApplicationLifecycleState _currentState; + ARelativeLayout _layout; + + int _nextActivityResultCallbackKey; + + AppCompat.Platform _platform; + + AndroidApplicationLifecycleState _previousState; + + bool _renderersAdded; + int _statusBarHeight = -1; + global::Android.Views.View _statusBarUnderlay; + + protected FormsAppCompatActivity() + { + _previousState = AndroidApplicationLifecycleState.Uninitialized; + _currentState = AndroidApplicationLifecycleState.Uninitialized; + } + + public event EventHandler ConfigurationChanged; + + int IStartActivityForResult.RegisterActivityResultCallback(Action<Result, Intent> callback) + { + int requestCode = _nextActivityResultCallbackKey; + + while (!_activityResultCallbacks.TryAdd(requestCode, callback)) + { + _nextActivityResultCallbackKey += 1; + requestCode = _nextActivityResultCallbackKey; + } + + _nextActivityResultCallbackKey += 1; + + return requestCode; + } + + void IStartActivityForResult.UnregisterActivityResultCallback(int requestCode) + { + Action<Result, Intent> callback; + _activityResultCallbacks.TryRemove(requestCode, out callback); + } + + public override void OnBackPressed() + { + if (BackPressed != null && BackPressed(this, EventArgs.Empty)) + return; + base.OnBackPressed(); + } + + public override void OnConfigurationChanged(Configuration newConfig) + { + base.OnConfigurationChanged(newConfig); + ConfigurationChanged?.Invoke(this, new EventArgs()); + } + + public override bool OnOptionsItemSelected(IMenuItem item) + { + if (item.ItemId == global::Android.Resource.Id.Home) + BackPressed?.Invoke(this, EventArgs.Empty); + + return base.OnOptionsItemSelected(item); + } + + public void SetStatusBarColor(AColor color) + { + _statusBarUnderlay.SetBackgroundColor(color); + } + + protected void LoadApplication(Application application) + { + if (!_renderersAdded) + { + RegisterHandlerForDefaultRenderer(typeof(NavigationPage), typeof(NavigationPageRenderer), typeof(NavigationRenderer)); + RegisterHandlerForDefaultRenderer(typeof(TabbedPage), typeof(TabbedPageRenderer), typeof(TabbedRenderer)); + RegisterHandlerForDefaultRenderer(typeof(MasterDetailPage), typeof(MasterDetailPageRenderer), typeof(MasterDetailRenderer)); + RegisterHandlerForDefaultRenderer(typeof(Button), typeof(AppCompat.ButtonRenderer), typeof(ButtonRenderer)); + RegisterHandlerForDefaultRenderer(typeof(Switch), typeof(AppCompat.SwitchRenderer), typeof(SwitchRenderer)); + RegisterHandlerForDefaultRenderer(typeof(Picker), typeof(AppCompat.PickerRenderer), typeof(PickerRenderer)); + RegisterHandlerForDefaultRenderer(typeof(Frame), typeof(AppCompat.FrameRenderer), typeof(FrameRenderer)); + RegisterHandlerForDefaultRenderer(typeof(CarouselPage), typeof(AppCompat.CarouselPageRenderer), typeof(CarouselPageRenderer)); + } + + if (application == null) + throw new ArgumentNullException("application"); + + _application = application; + Xamarin.Forms.Application.Current = application; + + application.PropertyChanged += AppOnPropertyChanged; + + SetMainPage(); + } + + protected override void OnActivityResult(int requestCode, Result resultCode, Intent data) + { + base.OnActivityResult(requestCode, resultCode, data); + + Action<Result, Intent> callback; + + if (_activityResultCallbacks.TryGetValue(requestCode, out callback)) + callback(resultCode, data); + } + + protected override void OnCreate(Bundle savedInstanceState) + { + base.OnCreate(savedInstanceState); + + AToolbar bar; + if (ToolbarResource != 0) + { + bar = LayoutInflater.Inflate(ToolbarResource, null).JavaCast<AToolbar>(); + if (bar == null) + throw new InvalidOperationException("ToolbarResource must be set to a Android.Support.V7.Widget.Toolbar"); + } + else + bar = new AToolbar(this); + + SetSupportActionBar(bar); + + Window.SetSoftInputMode(SoftInput.AdjustPan); + + _layout = new ARelativeLayout(BaseContext); + SetContentView(_layout); + + Xamarin.Forms.Application.ClearCurrent(); + + _previousState = _currentState; + _currentState = AndroidApplicationLifecycleState.OnCreate; + + OnStateChanged(); + + _statusBarUnderlay = new global::Android.Views.View(this); + var layoutParameters = new ARelativeLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, GetStatusBarHeight()) { AlignWithParent = true }; + layoutParameters.AddRule(LayoutRules.AlignTop); + _statusBarUnderlay.LayoutParameters = layoutParameters; + _layout.AddView(_statusBarUnderlay); + + if (Forms.IsLollipopOrNewer) + { + Window.DecorView.SystemUiVisibility = (StatusBarVisibility)(SystemUiFlags.LayoutFullscreen | SystemUiFlags.LayoutStable); + Window.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds); + Window.SetStatusBarColor(AColor.Transparent); + + int primaryColorDark = GetColorPrimaryDark(); + + if (primaryColorDark != 0) + { + int r = AColor.GetRedComponent(primaryColorDark); + int g = AColor.GetGreenComponent(primaryColorDark); + int b = AColor.GetBlueComponent(primaryColorDark); + int a = AColor.GetAlphaComponent(primaryColorDark); + SetStatusBarColor(AColor.Argb(a, r, g, b)); + } + } + } + + protected override void OnDestroy() + { + // may never be called + base.OnDestroy(); + + MessagingCenter.Unsubscribe<Page, AlertArguments>(this, Page.AlertSignalName); + MessagingCenter.Unsubscribe<Page, bool>(this, Page.BusySetSignalName); + MessagingCenter.Unsubscribe<Page, ActionSheetArguments>(this, Page.ActionSheetSignalName); + + if (_platform != null) + _platform.Dispose(); + } + + protected override void OnNewIntent(Intent intent) + { + base.OnNewIntent(intent); + } + + protected override void OnPause() + { + _layout.HideKeyboard(true); + + // Stop animations or other ongoing actions that could consume CPU + // Commit unsaved changes, build only if users expect such changes to be permanently saved when thy leave such as a draft email + // Release system resources, such as broadcast receivers, handles to sensors (like GPS), or any resources that may affect battery life when your activity is paused. + // Avoid writing to permanent storage and CPU intensive tasks + base.OnPause(); + + _previousState = _currentState; + _currentState = AndroidApplicationLifecycleState.OnPause; + + OnStateChanged(); + } + + protected override void OnRestart() + { + base.OnRestart(); + + _previousState = _currentState; + _currentState = AndroidApplicationLifecycleState.OnRestart; + + OnStateChanged(); + } + + protected override void OnResume() + { + // counterpart to OnPause + base.OnResume(); + + _previousState = _currentState; + _currentState = AndroidApplicationLifecycleState.OnResume; + + OnStateChanged(); + } + + protected override void OnStart() + { + base.OnStart(); + + _previousState = _currentState; + _currentState = AndroidApplicationLifecycleState.OnStart; + + OnStateChanged(); + } + + // Scenarios that stop and restart your app + // -- Switches from your app to another app, activity restarts when clicking on the app again. + // -- Action in your app that starts a new Activity, the current activity is stopped and the second is created, pressing back restarts the activity + // -- The user receives a phone call while using your app on his or her phone + protected override void OnStop() + { + // writing to storage happens here! + // full UI obstruction + // users focus in another activity + // perform heavy load shutdown operations + // clean up resources + // clean up everything that may leak memory + base.OnStop(); + + _previousState = _currentState; + _currentState = AndroidApplicationLifecycleState.OnStop; + + OnStateChanged(); + } + + internal int GetStatusBarHeight() + { + if (_statusBarHeight >= 0) + return _statusBarHeight; + + var result = 0; + int resourceId = Resources.GetIdentifier("status_bar_height", "dimen", "android"); + if (resourceId > 0) + result = Resources.GetDimensionPixelSize(resourceId); + return _statusBarHeight = result; + } + + void AppOnPropertyChanged(object sender, PropertyChangedEventArgs args) + { + if (args.PropertyName == "MainPage") + InternalSetPage(_application.MainPage); + } + + int GetColorPrimaryDark() + { + FormsAppCompatActivity context = this; + int id = global::Android.Resource.Attribute.ColorPrimaryDark; + using(var value = new TypedValue()) + { + try + { + Resources.Theme theme = context.Theme; + if (theme != null && theme.ResolveAttribute(id, value, true)) + { + if (value.Type >= DataType.FirstInt && value.Type <= DataType.LastInt) + return value.Data; + if (value.Type == DataType.String) + return context.Resources.GetColor(value.ResourceId); + } + } + catch (Exception ex) + { + } + + return -1; + } + } + + void InternalSetPage(Page page) + { + if (!Forms.IsInitialized) + throw new InvalidOperationException("Call Forms.Init (Activity, Bundle) before this"); + + if (_platform != null) + { + _platform.SetPage(page); + return; + } + + _busyCount = 0; + MessagingCenter.Subscribe<Page, bool>(this, Page.BusySetSignalName, OnPageBusy); + MessagingCenter.Subscribe<Page, AlertArguments>(this, Page.AlertSignalName, OnAlertRequested); + MessagingCenter.Subscribe<Page, ActionSheetArguments>(this, Page.ActionSheetSignalName, OnActionSheetRequested); + + _platform = new AppCompat.Platform(this); + if (_application != null) + _application.Platform = _platform; + _platform.SetPage(page); + _layout.AddView(_platform); + _layout.BringToFront(); + } + + void OnActionSheetRequested(Page sender, ActionSheetArguments arguments) + { + var builder = new AlertDialog.Builder(this); + builder.SetTitle(arguments.Title); + string[] items = arguments.Buttons.ToArray(); + builder.SetItems(items, (o, args) => arguments.Result.TrySetResult(items[args.Which])); + + if (arguments.Cancel != null) + builder.SetPositiveButton(arguments.Cancel, (o, args) => arguments.Result.TrySetResult(arguments.Cancel)); + + if (arguments.Destruction != null) + builder.SetNegativeButton(arguments.Destruction, (o, args) => arguments.Result.TrySetResult(arguments.Destruction)); + + AlertDialog dialog = builder.Create(); + builder.Dispose(); + //to match current functionality of renderer we set cancelable on outside + //and return null + dialog.SetCanceledOnTouchOutside(true); + dialog.CancelEvent += (o, e) => arguments.SetResult(null); + dialog.Show(); + } + + void OnAlertRequested(Page sender, AlertArguments arguments) + { + AlertDialog alert = new AlertDialog.Builder(this).Create(); + alert.SetTitle(arguments.Title); + alert.SetMessage(arguments.Message); + if (arguments.Accept != null) + alert.SetButton((int)DialogButtonType.Positive, arguments.Accept, (o, args) => arguments.SetResult(true)); + alert.SetButton((int)DialogButtonType.Negative, arguments.Cancel, (o, args) => arguments.SetResult(false)); + alert.CancelEvent += (o, args) => { arguments.SetResult(false); }; + alert.Show(); + } + + void OnPageBusy(Page sender, bool enabled) + { + _busyCount = Math.Max(0, enabled ? _busyCount + 1 : _busyCount - 1); + + if (!Forms.SupportsProgress) + return; + + SetProgressBarIndeterminate(true); + UpdateProgressBarVisibility(_busyCount > 0); + } + + async void OnStateChanged() + { + if (_application == null) + return; + + if (_previousState == AndroidApplicationLifecycleState.OnCreate && _currentState == AndroidApplicationLifecycleState.OnStart) + _application.SendStart(); + else if (_previousState == AndroidApplicationLifecycleState.OnStop && _currentState == AndroidApplicationLifecycleState.OnRestart) + _application.SendResume(); + else if (_previousState == AndroidApplicationLifecycleState.OnPause && _currentState == AndroidApplicationLifecycleState.OnStop) + await _application.SendSleepAsync(); + } + + void RegisterHandlerForDefaultRenderer(Type target, Type handler, Type filter) + { + Type current = Registrar.Registered.GetHandlerType(filter); + if (current == target) + return; + + Registrar.Registered.Register(target, handler); + } + + void SetMainPage() + { + InternalSetPage(_application.MainPage); + } + + void UpdateProgressBarVisibility(bool isBusy) + { + if (!Forms.SupportsProgress) + return; + + SetProgressBarIndeterminateVisibility(isBusy); + } + + internal class DefaultApplication : Application + { + } + + #region Statics + + public static event BackButtonPressedEventHandler BackPressed; + + public static int TabLayoutResource { get; set; } + + public static int ToolbarResource { get; set; } + + internal static int GetUniqueId() + { + // getting unique Id's is an art, and I consider myself the Jackson Pollock of the field + if ((int)Build.VERSION.SdkInt >= 17) + return global::Android.Views.View.GenerateViewId(); + + // Numbers higher than this range reserved for xml + // If we roll over, it can be exceptionally problematic for the user if they are still retaining things, android's internal implementation is + // basically identical to this except they do a lot of locking we don't have to because we know we only do this + // from the UI thread + if (s_id >= 0x00ffffff) + s_id = 0x00000400; + return s_id++; + } + + static int s_id = 0x00000400; + + #endregion + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/AppCompat/FormsFragmentPagerAdapter.cs b/Xamarin.Forms.Platform.Android/AppCompat/FormsFragmentPagerAdapter.cs new file mode 100644 index 00000000..99fbb396 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/AppCompat/FormsFragmentPagerAdapter.cs @@ -0,0 +1,59 @@ +using Android.OS; +using Android.Support.V4.App; +using Java.Lang; + +namespace Xamarin.Forms.Platform.Android.AppCompat +{ + internal class FormsFragmentPagerAdapter<T> : FragmentPagerAdapter where T : Page + { + MultiPage<T> _page; + + public FormsFragmentPagerAdapter(MultiPage<T> page, FragmentManager fragmentManager) : base(fragmentManager) + { + _page = page; + } + + public override int Count => CountOverride; + + public int CountOverride { get; set; } + + public override Fragment GetItem(int position) + { + return FragmentContainer.CreateInstance(_page.Children[position]); + } + + public override long GetItemId(int position) + { + return _page.Children[position].GetHashCode(); + } + + public override int GetItemPosition(Object objectValue) + { + var fragContainer = objectValue as FragmentContainer; + if (fragContainer != null && fragContainer.Page != null) + { + int index = _page.Children.IndexOf(fragContainer.Page); + if (index >= 0) + return index; + } + return PositionNone; + } + + public override ICharSequence GetPageTitleFormatted(int position) + { + return new String(_page.Children[position].Title); + } + + // http://stackoverflow.com/questions/18642890/fragmentstatepageradapter-with-childfragmentmanager-fragmentmanagerimpl-getfra/19099987#19099987 + public override void RestoreState(IParcelable state, ClassLoader loader) + { + } + + protected override void Dispose(bool disposing) + { + if (disposing) + _page = null; + base.Dispose(disposing); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/AppCompat/FormsViewPager.cs b/Xamarin.Forms.Platform.Android/AppCompat/FormsViewPager.cs new file mode 100644 index 00000000..2327bb4e --- /dev/null +++ b/Xamarin.Forms.Platform.Android/AppCompat/FormsViewPager.cs @@ -0,0 +1,34 @@ +using System; +using Android.Content; +using Android.Runtime; +using Android.Support.V4.View; +using Android.Views; + +namespace Xamarin.Forms.Platform.Android.AppCompat +{ + internal class FormsViewPager : ViewPager + { + public FormsViewPager(Context context) : base(context) + { + } + + protected FormsViewPager(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + + public bool EnableGesture { get; set; } = true; + + public override bool OnInterceptTouchEvent(MotionEvent ev) + { + // Same as: + // if (!EnableGesture) return false; + // However this is, at least in theory a tidge faster which in this particular area is good + return EnableGesture && base.OnInterceptTouchEvent(ev); + } + + public override bool OnTouchEvent(MotionEvent e) + { + return EnableGesture && base.OnTouchEvent(e); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/AppCompat/FragmentContainer.cs b/Xamarin.Forms.Platform.Android/AppCompat/FragmentContainer.cs new file mode 100644 index 00000000..39219188 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/AppCompat/FragmentContainer.cs @@ -0,0 +1,111 @@ +using System; +using Android.OS; +using Android.Runtime; +using Android.Support.V4.App; +using Android.Views; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android.AppCompat +{ + internal class FragmentContainer : Fragment + { + readonly WeakReference _pageReference; + + bool? _isVisible; + PageContainer _pageContainer; + IVisualElementRenderer _visualElementRenderer; + + public FragmentContainer() + { + } + + public FragmentContainer(Page page) : this() + { + _pageReference = new WeakReference(page); + } + + protected FragmentContainer(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + + public Page Page => (Page)_pageReference?.Target; + + public override bool UserVisibleHint + { + get { return base.UserVisibleHint; } + set + { + base.UserVisibleHint = value; + if (_isVisible == value) + return; + _isVisible = value; + if (_isVisible.Value) + Page?.SendAppearing(); + else + Page?.SendDisappearing(); + } + } + + public static Fragment CreateInstance(Page page) + { + return new FragmentContainer(page) { Arguments = new Bundle() }; + } + + public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + if (Page != null) + { + _visualElementRenderer = Android.Platform.CreateRenderer(Page, ChildFragmentManager); + Android.Platform.SetRenderer(Page, _visualElementRenderer); + + _pageContainer = new PageContainer(Forms.Context, _visualElementRenderer, true); + return _pageContainer; + } + + return null; + } + + public override void OnDestroyView() + { + if (Page != null) + { + IVisualElementRenderer renderer = _visualElementRenderer; + PageContainer container = _pageContainer; + + if (container.Handle != IntPtr.Zero && renderer.ViewGroup.Handle != IntPtr.Zero) + { + container.RemoveFromParent(); + renderer.ViewGroup.RemoveFromParent(); + Page.ClearValue(Android.Platform.RendererProperty); + + container.Dispose(); + renderer.Dispose(); + } + } + + _visualElementRenderer = null; + _pageContainer = null; + + base.OnDestroyView(); + } + + public override void OnHiddenChanged(bool hidden) + { + base.OnHiddenChanged(hidden); + + if (Page == null) + return; + + if (hidden) + Page.SendDisappearing(); + else + Page.SendAppearing(); + } + + public override void OnPause() + { + Page?.SendDisappearing(); + base.OnPause(); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/AppCompat/FrameRenderer.cs b/Xamarin.Forms.Platform.Android/AppCompat/FrameRenderer.cs new file mode 100644 index 00000000..7b544b25 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/AppCompat/FrameRenderer.cs @@ -0,0 +1,215 @@ +using System; +using System.ComponentModel; +using Android.Content; +using Android.Support.V4.View; +using Android.Support.V7.Widget; +using Android.Views; +using AColor = Android.Graphics.Color; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android.AppCompat +{ + public class FrameRenderer : CardView, IVisualElementRenderer, AView.IOnClickListener, AView.IOnTouchListener + { + readonly Lazy<GestureDetector> _gestureDetector; + readonly PanGestureHandler _panGestureHandler; + readonly PinchGestureHandler _pinchGestureHandler; + readonly Lazy<ScaleGestureDetector> _scaleDetector; + readonly TapGestureHandler _tapGestureHandler; + + float _defaultElevation = -1f; + + bool _disposed; + Frame _element; + InnerGestureListener _gestureListener; + VisualElementPackager _visualElementPackager; + VisualElementTracker _visualElementTracker; + + public FrameRenderer() : base(Forms.Context) + { + _tapGestureHandler = new TapGestureHandler(() => Element); + _panGestureHandler = new PanGestureHandler(() => Element, Context.FromPixels); + _pinchGestureHandler = new PinchGestureHandler(() => Element); + + _gestureDetector = + new Lazy<GestureDetector>( + () => + new GestureDetector( + _gestureListener = + new InnerGestureListener(_tapGestureHandler.OnTap, _tapGestureHandler.TapGestureRecognizers, _panGestureHandler.OnPan, _panGestureHandler.OnPanStarted, _panGestureHandler.OnPanComplete))); + + _scaleDetector = + new Lazy<ScaleGestureDetector>( + () => new ScaleGestureDetector(Context, new InnerScaleListener(_pinchGestureHandler.OnPinch, _pinchGestureHandler.OnPinchStarted, _pinchGestureHandler.OnPinchEnded), Handler)); + } + + protected CardView Control => this; + + protected Frame Element + { + get { return _element; } + set + { + if (_element == value) + return; + + Frame oldElement = _element; + _element = value; + + OnElementChanged(new ElementChangedEventArgs<Frame>(oldElement, _element)); + + if (_element != null) + _element.SendViewInitialized(Control); + } + } + + void IOnClickListener.OnClick(AView v) + { + _tapGestureHandler.OnSingleClick(); + } + + bool IOnTouchListener.OnTouch(AView v, MotionEvent e) + { + var handled = false; + if (_pinchGestureHandler.IsPinchSupported) + { + if (!_scaleDetector.IsValueCreated) + ScaleGestureDetectorCompat.SetQuickScaleEnabled(_scaleDetector.Value, true); + handled = _scaleDetector.Value.OnTouchEvent(e); + } + return _gestureDetector.Value.OnTouchEvent(e) || handled; + } + + VisualElement IVisualElementRenderer.Element => Element; + + public event EventHandler<VisualElementChangedEventArgs> ElementChanged; + + SizeRequest IVisualElementRenderer.GetDesiredSize(int widthConstraint, int heightConstraint) + { + Context context = Context; + return new SizeRequest(new Size(context.ToPixels(20), context.ToPixels(20))); + } + + void IVisualElementRenderer.SetElement(VisualElement element) + { + var frame = element as Frame; + if (frame == null) + throw new ArgumentException("Element must be of type Frame"); + Element = frame; + + if (!string.IsNullOrEmpty(Element.AutomationId)) + ContentDescription = Element.AutomationId; + } + + VisualElementTracker IVisualElementRenderer.Tracker => _visualElementTracker; + + void IVisualElementRenderer.UpdateLayout() + { + VisualElementTracker tracker = _visualElementTracker; + tracker?.UpdateLayout(); + } + + ViewGroup IVisualElementRenderer.ViewGroup => this; + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + + if (_gestureListener != null) + { + _gestureListener.Dispose(); + _gestureListener = null; + } + + if (_visualElementTracker != null) + { + _visualElementTracker.Dispose(); + _visualElementTracker = null; + } + + if (_visualElementPackager != null) + { + _visualElementPackager.Dispose(); + _visualElementPackager = null; + } + + if (Element != null) + Element.PropertyChanged -= OnElementPropertyChanged; + } + + base.Dispose(disposing); + } + + protected virtual void OnElementChanged(ElementChangedEventArgs<Frame> e) + { + ElementChanged?.Invoke(this, new VisualElementChangedEventArgs(e.OldElement, e.NewElement)); + + if (e.OldElement != null) + e.OldElement.PropertyChanged -= OnElementPropertyChanged; + else + { + SetOnClickListener(this); + SetOnTouchListener(this); + } + + if (e.NewElement != null) + { + if (_visualElementTracker == null) + { + _visualElementTracker = new VisualElementTracker(this); + _visualElementPackager = new VisualElementPackager(this); + _visualElementPackager.Load(); + } + + e.NewElement.PropertyChanged += OnElementPropertyChanged; + UpdateShadow(); + UpdateBackgroundColor(); + } + } + + protected override void OnLayout(bool changed, int left, int top, int right, int bottom) + { + if (Element == null) + return; + + var children = Element.LogicalChildren; + for (var i = 0; i < children.Count; i++) + { + var visualElement = children[i] as VisualElement; + if (visualElement == null) + continue; + IVisualElementRenderer renderer = Android.Platform.GetRenderer(visualElement); + renderer?.UpdateLayout(); + } + } + + void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == Frame.HasShadowProperty.PropertyName) + UpdateShadow(); + else if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName) + UpdateBackgroundColor(); + } + + void UpdateBackgroundColor() + { + Color bgColor = Element.BackgroundColor; + SetCardBackgroundColor(bgColor.IsDefault ? AColor.White : bgColor.ToAndroid()); + } + + void UpdateShadow() + { + float elevation = _defaultElevation; + + if (elevation == -1f) + _defaultElevation = elevation = CardElevation; + + if (Element.HasShadow) + CardElevation = elevation; + else + CardElevation = 0f; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/AppCompat/IManageFragments.cs b/Xamarin.Forms.Platform.Android/AppCompat/IManageFragments.cs new file mode 100644 index 00000000..5b6c4cb7 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/AppCompat/IManageFragments.cs @@ -0,0 +1,13 @@ +using Android.Support.V4.App; + +namespace Xamarin.Forms.Platform.Android.AppCompat +{ + /// <summary> + /// Allows the platform to inject child fragment managers for renderers + /// which do their own fragment management + /// </summary> + internal interface IManageFragments + { + void SetFragmentManager(FragmentManager fragmentManager); + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/AppCompat/MasterDetailPageRenderer.cs b/Xamarin.Forms.Platform.Android/AppCompat/MasterDetailPageRenderer.cs new file mode 100644 index 00000000..ec6b4395 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/AppCompat/MasterDetailPageRenderer.cs @@ -0,0 +1,355 @@ +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using Android.Support.V4.Widget; +using Android.Views; + +namespace Xamarin.Forms.Platform.Android.AppCompat +{ + public class MasterDetailPageRenderer : DrawerLayout, IVisualElementRenderer, DrawerLayout.IDrawerListener + { + #region Statics + + //from Android source code + const uint DefaultScrimColor = 0x99000000; + + #endregion + + int _currentLockMode = -1; + MasterDetailContainer _detailLayout; + + bool _disposed; + bool _isPresentingFromCore; + MasterDetailContainer _masterLayout; + bool _presented; + VisualElementTracker _tracker; + + public MasterDetailPageRenderer() : base(Forms.Context) + { + } + + MasterDetailPage Element { get; set; } + + bool Presented + { + get { return _presented; } + set + { + if (value == _presented) + return; + UpdateSplitViewLayout(); + _presented = value; + if (Element.MasterBehavior == MasterBehavior.Default && Element.ShouldShowSplitMode) + return; + if (_presented) + OpenDrawer(_masterLayout); + else + CloseDrawer(_masterLayout); + } + } + + void IDrawerListener.OnDrawerClosed(global::Android.Views.View drawerView) + { + } + + void IDrawerListener.OnDrawerOpened(global::Android.Views.View drawerView) + { + } + + void IDrawerListener.OnDrawerSlide(global::Android.Views.View drawerView, float slideOffset) + { + } + + void IDrawerListener.OnDrawerStateChanged(int newState) + { + _presented = IsDrawerVisible(_masterLayout); + UpdateIsPresented(); + } + + VisualElement IVisualElementRenderer.Element => Element; + + event EventHandler<VisualElementChangedEventArgs> IVisualElementRenderer.ElementChanged + { + add { ElementChanged += value; } + remove { ElementChanged -= value; } + } + + SizeRequest IVisualElementRenderer.GetDesiredSize(int widthConstraint, int heightConstraint) + { + Measure(widthConstraint, heightConstraint); + return new SizeRequest(new Size(MeasuredWidth, MeasuredHeight)); + } + + void IVisualElementRenderer.SetElement(VisualElement element) + { + MasterDetailPage oldElement = Element; + MasterDetailPage newElement = Element = element as MasterDetailPage; + + if (oldElement != null) + { + oldElement.BackButtonPressed -= OnBackButtonPressed; + oldElement.PropertyChanged -= HandlePropertyChanged; + oldElement.Appearing -= MasterDetailPageAppearing; + oldElement.Disappearing -= MasterDetailPageDisappearing; + } + + var statusBarHeight = 0; + if (Forms.IsLollipopOrNewer) + statusBarHeight = ((FormsAppCompatActivity)Context).GetStatusBarHeight(); + + if (newElement != null) + { + if (_detailLayout == null) + { + _detailLayout = new MasterDetailContainer(newElement, false, Context) + { + TopPadding = statusBarHeight, + LayoutParameters = new LayoutParams(ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.WrapContent) + }; + + _masterLayout = new MasterDetailContainer(newElement, true, Context) + { + LayoutParameters = new LayoutParams(ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.WrapContent) { Gravity = (int)GravityFlags.Start } + }; + + AddView(_detailLayout); + AddView(_masterLayout); + + Device.Info.PropertyChanged += DeviceInfoPropertyChanged; + + SetDrawerListener(this); + } + + UpdateBackgroundColor(newElement); + UpdateBackgroundImage(newElement); + + UpdateMaster(); + UpdateDetail(); + + newElement.BackButtonPressed += OnBackButtonPressed; + newElement.PropertyChanged += HandlePropertyChanged; + newElement.Appearing += MasterDetailPageAppearing; + newElement.Disappearing += MasterDetailPageDisappearing; + + SetGestureState(); + + Presented = newElement.IsPresented; + + newElement.SendViewInitialized(this); + } + + OnElementChanged(oldElement, newElement); + + // Make sure to initialize this AFTER event is fired + if (_tracker == null) + _tracker = new VisualElementTracker(this); + } + + VisualElementTracker IVisualElementRenderer.Tracker => _tracker; + + void IVisualElementRenderer.UpdateLayout() + { + _tracker?.UpdateLayout(); + } + + ViewGroup IVisualElementRenderer.ViewGroup => this; + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + + if (_tracker != null) + { + _tracker.Dispose(); + _tracker = null; + } + + if (_detailLayout != null) + { + _detailLayout.Dispose(); + _detailLayout = null; + } + + if (_masterLayout != null) + { + _masterLayout.Dispose(); + _masterLayout = null; + } + + Device.Info.PropertyChanged -= DeviceInfoPropertyChanged; + + if (Element != null) + { + Element.BackButtonPressed -= OnBackButtonPressed; + Element.PropertyChanged -= HandlePropertyChanged; + Element.Appearing -= MasterDetailPageAppearing; + Element.Disappearing -= MasterDetailPageDisappearing; + Element.ClearValue(Android.Platform.RendererProperty); + Element = null; + } + } + + base.Dispose(disposing); + } + + protected override void OnAttachedToWindow() + { + base.OnAttachedToWindow(); + Element.SendAppearing(); + } + + protected override void OnDetachedFromWindow() + { + base.OnDetachedFromWindow(); + Element.SendDisappearing(); + } + + protected virtual void OnElementChanged(VisualElement oldElement, VisualElement newElement) + { + ElementChanged?.Invoke(this, new VisualElementChangedEventArgs(oldElement, newElement)); + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + base.OnLayout(changed, l, t, r, b); + //hack to make the split layout handle touches the full width + if (Element.ShouldShowSplitMode && _masterLayout != null) + _masterLayout.Right = r; + } + + async void DeviceInfoPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (nameof(Device.Info.CurrentOrientation) == e.PropertyName) + { + if (!Element.ShouldShowSplitMode && Presented) + { + Element.CanChangeIsPresented = true; + //hack : when the orientation changes and we try to close the Master on Android + //sometimes Android picks the width of the screen previous to the rotation + //this leaves a little of the master visible, the hack is to delay for 50ms closing the drawer + await Task.Delay(100); + CloseDrawer(_masterLayout); + } + UpdateSplitViewLayout(); + } + } + + event EventHandler<VisualElementChangedEventArgs> ElementChanged; + + void HandleMasterPropertyChanged(object sender, PropertyChangedEventArgs e) + { + } + + void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == "Master") + UpdateMaster(); + else if (e.PropertyName == "Detail") + UpdateDetail(); + else if (e.PropertyName == "IsGestureEnabled") + SetGestureState(); + else if (e.PropertyName == MasterDetailPage.IsPresentedProperty.PropertyName) + { + _isPresentingFromCore = true; + Presented = Element.IsPresented; + _isPresentingFromCore = false; + } + else if (e.PropertyName == Page.BackgroundImageProperty.PropertyName) + UpdateBackgroundImage(Element); + else if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName) + UpdateBackgroundColor(Element); + } + + void MasterDetailPageAppearing(object sender, EventArgs e) + { + Element.Master?.SendAppearing(); + Element.Detail?.SendAppearing(); + } + + void MasterDetailPageDisappearing(object sender, EventArgs e) + { + Element.Master?.SendDisappearing(); + Element.Detail?.SendDisappearing(); + } + + void OnBackButtonPressed(object sender, BackButtonPressedEventArgs backButtonPressedEventArgs) + { + if (!IsDrawerOpen((int)GravityFlags.Start) || _currentLockMode == LockModeLockedOpen) + return; + + CloseDrawer((int)GravityFlags.Start); + backButtonPressedEventArgs.Handled = true; + } + + void SetGestureState() + { + SetDrawerLockMode(Element.IsGestureEnabled ? LockModeUnlocked : LockModeLockedClosed); + } + + void SetLockMode(int lockMode) + { + if (_currentLockMode != lockMode) + { + SetDrawerLockMode(lockMode); + _currentLockMode = lockMode; + } + } + + void UpdateBackgroundColor(Page view) + { + Color backgroundColor = view.BackgroundColor; + if (backgroundColor.IsDefault) + SetBackgroundColor(backgroundColor.ToAndroid()); + } + + void UpdateBackgroundImage(Page view) + { + string backgroundImage = view.BackgroundImage; + if (!string.IsNullOrEmpty(backgroundImage)) + this.SetBackground(Context.Resources.GetDrawable(backgroundImage)); + } + + void UpdateDetail() + { + Context.HideKeyboard(this); + _detailLayout.ChildView = Element.Detail; + } + + void UpdateIsPresented() + { + if (_isPresentingFromCore) + return; + if (Presented != Element.IsPresented) + ((IElementController)Element).SetValueFromRenderer(MasterDetailPage.IsPresentedProperty, Presented); + } + + void UpdateMaster() + { + MasterDetailContainer masterContainer = _masterLayout; + if (masterContainer == null) + return; + + if (masterContainer.ChildView != null) + masterContainer.ChildView.PropertyChanged -= HandleMasterPropertyChanged; + + masterContainer.ChildView = Element.Master; + if (Element.Master != null) + Element.Master.PropertyChanged += HandleMasterPropertyChanged; + } + + void UpdateSplitViewLayout() + { + if (Device.Idiom == TargetIdiom.Tablet) + { + bool isShowingSplit = Element.ShouldShowSplitMode || (Element.ShouldShowSplitMode && Element.MasterBehavior != MasterBehavior.Default && Element.IsPresented); + SetLockMode(isShowingSplit ? LockModeLockedOpen : LockModeUnlocked); + unchecked + { + SetScrimColor(isShowingSplit ? Color.Transparent.ToAndroid() : (int)DefaultScrimColor); + } + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/AppCompat/NavigationPageRenderer.cs b/Xamarin.Forms.Platform.Android/AppCompat/NavigationPageRenderer.cs new file mode 100644 index 00000000..fd4cb72b --- /dev/null +++ b/Xamarin.Forms.Platform.Android/AppCompat/NavigationPageRenderer.cs @@ -0,0 +1,789 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using Android.Animation; +using Android.App; +using Android.Content; +using Android.Content.Res; +using Android.Graphics; +using Android.Graphics.Drawables; +using Android.OS; +using Android.Runtime; +using Android.Support.V4.Widget; +using Android.Support.V7.Graphics.Drawable; +using Android.Util; +using Android.Views; +using ActionBarDrawerToggle = Android.Support.V7.App.ActionBarDrawerToggle; +using AView = Android.Views.View; +using AToolbar = Android.Support.V7.Widget.Toolbar; +using Fragment = Android.Support.V4.App.Fragment; +using FragmentManager = Android.Support.V4.App.FragmentManager; +using FragmentTransaction = Android.Support.V4.App.FragmentTransaction; +using Object = Java.Lang.Object; + +namespace Xamarin.Forms.Platform.Android.AppCompat +{ + public class NavigationPageRenderer : VisualElementRenderer<NavigationPage>, IManageFragments + { + #region Statics + + // All statics need to be made non-static/bound to platform + + static ViewPropertyAnimator s_currentAnimation; + + #endregion + + readonly List<Fragment> _fragmentStack = new List<Fragment>(); + + Drawable _backgroundDrawable; + Page _current; + + bool _disposed; + ActionBarDrawerToggle _drawerToggle; + FragmentManager _fragmentManager; + int _lastActionBarHeight = -1; + AToolbar _toolbar; + ToolbarTracker _toolbarTracker; + bool _toolbarVisible; + + public NavigationPageRenderer() + { + AutoPackage = false; + Id = FormsAppCompatActivity.GetUniqueId(); + Device.Info.PropertyChanged += DeviceInfoPropertyChanged; + } + + internal int ContainerPadding { get; set; } + + Page Current + { + get { return _current; } + set + { + if (_current == value) + return; + + if (_current != null) + _current.PropertyChanged -= CurrentOnPropertyChanged; + + _current = value; + + if (_current != null) + { + _current.PropertyChanged += CurrentOnPropertyChanged; + ToolbarVisible = NavigationPage.GetHasNavigationBar(_current); + } + } + } + + FragmentManager FragmentManager => _fragmentManager ?? (_fragmentManager = ((FormsAppCompatActivity)Context).SupportFragmentManager); + + bool ToolbarVisible + { + get { return _toolbarVisible; } + set + { + if (_toolbarVisible == value) + return; + _toolbarVisible = value; + RequestLayout(); + } + } + + public void SetFragmentManager(FragmentManager childFragmentManager) + { + if (_fragmentManager == null) + _fragmentManager = childFragmentManager; + } + + public Task<bool> PopToRootAsync(Page page, bool animated = true) + { + return OnPopToRootAsync(page, animated); + } + + public Task<bool> PopViewAsync(Page page, bool animated = true) + { + return OnPopViewAsync(page, animated); + } + + public Task<bool> PushViewAsync(Page page, bool animated = true) + { + return OnPushAsync(page, animated); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + + var activity = (FormsAppCompatActivity)Context; + + // API only exists on newer android YAY + if ((int)Build.VERSION.SdkInt >= 17) + { + if (!activity.IsDestroyed) + { + FragmentManager fm = FragmentManager; + FragmentTransaction trans = fm.BeginTransaction(); + foreach (Fragment fragment in _fragmentStack) + trans.Remove(fragment); + trans.CommitAllowingStateLoss(); + fm.ExecutePendingTransactions(); + } + } + + if (Element != null) + { + for (var i = 0; i < Element.InternalChildren.Count; i++) + { + var child = Element.InternalChildren[i] as VisualElement; + if (child == null) + continue; + IVisualElementRenderer renderer = Android.Platform.GetRenderer(child); + renderer?.Dispose(); + } + Element.PushRequested -= OnPushed; + Element.PopRequested -= OnPopped; + Element.PopToRootRequested -= OnPoppedToRoot; + Element.InsertPageBeforeRequested -= OnInsertPageBeforeRequested; + Element.RemovePageRequested -= OnRemovePageRequested; + Element.SendDisappearing(); + } + + if (_toolbarTracker != null) + { + _toolbarTracker.CollectionChanged -= ToolbarTrackerOnCollectionChanged; + _toolbarTracker.Target = null; + _toolbarTracker = null; + } + + if (_toolbar != null) + { + _toolbar.NavigationClick -= BarOnNavigationClick; + _toolbar.Dispose(); + _toolbar = null; + } + + Current = null; + + Device.Info.PropertyChanged -= DeviceInfoPropertyChanged; + } + + base.Dispose(disposing); + } + + protected override void OnAttachedToWindow() + { + base.OnAttachedToWindow(); + Element.SendAppearing(); + _fragmentStack.Last().UserVisibleHint = true; + RegisterToolbar(); + UpdateToolbar(); + } + + protected override void OnDetachedFromWindow() + { + base.OnDetachedFromWindow(); + Element.SendDisappearing(); + } + + protected override void OnElementChanged(ElementChangedEventArgs<NavigationPage> e) + { + base.OnElementChanged(e); + + if (e.OldElement != null) + { + e.OldElement.PushRequested -= OnPushed; + e.OldElement.PopRequested -= OnPopped; + e.OldElement.PopToRootRequested -= OnPoppedToRoot; + e.OldElement.InsertPageBeforeRequested -= OnInsertPageBeforeRequested; + e.OldElement.RemovePageRequested -= OnRemovePageRequested; + + RemoveAllViews(); + if (_toolbar != null) + AddView(_toolbar); + } + + if (e.NewElement != null) + { + if (_toolbarTracker == null) + { + SetupToolbar(); + _toolbarTracker = new ToolbarTracker(); + _toolbarTracker.CollectionChanged += ToolbarTrackerOnCollectionChanged; + } + + var parents = new List<Page>(); + Page root = Element; + while (!Application.IsApplicationOrNull(root.RealParent)) + { + root = (Page)root.RealParent; + parents.Add(root); + } + + _toolbarTracker.Target = e.NewElement; + _toolbarTracker.AdditionalTargets = parents; + UpdateMenu(); + + e.NewElement.PushRequested += OnPushed; + e.NewElement.PopRequested += OnPopped; + e.NewElement.PopToRootRequested += OnPoppedToRoot; + e.NewElement.InsertPageBeforeRequested += OnInsertPageBeforeRequested; + e.NewElement.RemovePageRequested += OnRemovePageRequested; + + // If there is already stuff on the stack we need to push it + e.NewElement.StackCopy.Reverse().ForEach(p => PushViewAsync(p, false)); + } + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == NavigationPage.BarBackgroundColorProperty.PropertyName) + UpdateToolbar(); + else if (e.PropertyName == NavigationPage.BarTextColorProperty.PropertyName) + UpdateToolbar(); + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + AToolbar bar = _toolbar; + // make sure bar stays on top of everything + bar.BringToFront(); + + base.OnLayout(changed, l, t, r, b); + + int barHeight = ActionBarHeight(); + + if (barHeight != _lastActionBarHeight && _lastActionBarHeight > 0) + { + ResetToolbar(); + bar = _toolbar; + } + _lastActionBarHeight = barHeight; + + bar.Measure(MeasureSpecFactory.MakeMeasureSpec(r - l, MeasureSpecMode.Exactly), MeasureSpecFactory.MakeMeasureSpec(barHeight, MeasureSpecMode.Exactly)); + + int internalHeight = b - t - barHeight; + int containerHeight = ToolbarVisible ? internalHeight : b - t; + containerHeight -= ContainerPadding; + + Element.ContainerArea = new Rectangle(0, 0, Context.FromPixels(r - l), Context.FromPixels(containerHeight)); + // Potential for optimization here, the exact conditions by which you don't need to do this are complex + // and the cost of doing when it's not needed is moderate to low since the layout will short circuit pretty fast + Element.ForceLayout(); + + for (var i = 0; i < ChildCount; i++) + { + AView child = GetChildAt(i); + bool isBar = JNIEnv.IsSameObject(child.Handle, bar.Handle); + + if (ToolbarVisible) + { + if (isBar) + bar.Layout(0, 0, r - l, barHeight); + else + child.Layout(0, barHeight + ContainerPadding, r, b); + } + else + { + if (isBar) + bar.Layout(0, -1000, r, barHeight - 1000); + else + child.Layout(0, ContainerPadding, r, b); + } + } + } + + protected virtual void SetupPageTransition(FragmentTransaction transaction, bool isPush) + { + if (isPush) + transaction.SetTransition((int)FragmentTransit.FragmentOpen); + else + transaction.SetTransition((int)FragmentTransit.FragmentClose); + } + + internal int GetNavBarHeight() + { + if (!ToolbarVisible) + return 0; + + return ActionBarHeight(); + } + + int ActionBarHeight() + { + int attr = Resource.Attribute.actionBarSize; + + int actionBarHeight; + using(var tv = new TypedValue()) + { + actionBarHeight = 0; + if (Context.Theme.ResolveAttribute(attr, tv, true)) + actionBarHeight = TypedValue.ComplexToDimensionPixelSize(tv.Data, Resources.DisplayMetrics); + } + + if (actionBarHeight <= 0) + return Device.Info.CurrentOrientation.IsPortrait() ? (int)Context.ToPixels(56) : (int)Context.ToPixels(48); + + return actionBarHeight; + } + + void AnimateArrowIn() + { + var icon = _toolbar.NavigationIcon as DrawerArrowDrawable; + if (icon == null) + return; + + ValueAnimator valueAnim = ValueAnimator.OfFloat(0, 1); + valueAnim.SetDuration(200); + valueAnim.Update += (s, a) => icon.Progress = (float)a.Animation.AnimatedValue; + valueAnim.Start(); + } + + void AnimateArrowOut() + { + var icon = _toolbar.NavigationIcon as DrawerArrowDrawable; + if (icon == null) + return; + + ValueAnimator valueAnim = ValueAnimator.OfFloat(1, 0); + valueAnim.SetDuration(200); + valueAnim.Update += (s, a) => icon.Progress = (float)a.Animation.AnimatedValue; + valueAnim.Start(); + } + + void BarOnNavigationClick(object sender, AToolbar.NavigationClickEventArgs navigationClickEventArgs) + { + Element?.PopAsync(); + } + + void CurrentOnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == NavigationPage.HasNavigationBarProperty.PropertyName) + ToolbarVisible = NavigationPage.GetHasNavigationBar(Current); + else if (e.PropertyName == Page.TitleProperty.PropertyName) + UpdateToolbar(); + } + + async void DeviceInfoPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (nameof(Device.Info.CurrentOrientation) == e.PropertyName) + ResetToolbar(); + } + + void FilterPageFragment(Page page) + { + _fragmentStack.RemoveAll(f => ((FragmentContainer)f).Page == page); + } + + void HandleToolbarItemPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == MenuItem.IsEnabledProperty.PropertyName || e.PropertyName == MenuItem.TextProperty.PropertyName || e.PropertyName == MenuItem.IconProperty.PropertyName) + UpdateMenu(); + } + + void InsertPageBefore(Page page, Page before) + { + UpdateToolbar(); + + int index = Element.InternalChildren.IndexOf(before); + if (index == -1) + throw new InvalidOperationException("This should never happen, please file a bug"); + + Fragment fragment = FragmentContainer.CreateInstance(page); + _fragmentStack.Insert(index, fragment); + } + + void OnInsertPageBeforeRequested(object sender, NavigationRequestedEventArgs e) + { + InsertPageBefore(e.Page, e.BeforePage); + } + + void OnPopped(object sender, NavigationRequestedEventArgs e) + { + e.Task = PopViewAsync(e.Page, e.Animated); + } + + void OnPoppedToRoot(object sender, NavigationRequestedEventArgs e) + { + e.Task = PopToRootAsync(e.Page, e.Animated); + } + + Task<bool> OnPopToRootAsync(Page page, bool animated) + { + return SwitchContentAsync(page, animated, true, true); + } + + Task<bool> OnPopViewAsync(Page page, bool animated) + { + Page pageToShow = Element.StackCopy.Skip(1).FirstOrDefault(); + if (pageToShow == null) + return Task.FromResult(false); + + return SwitchContentAsync(pageToShow, animated, true); + } + + Task<bool> OnPushAsync(Page view, bool animated) + { + return SwitchContentAsync(view, animated); + } + + void OnPushed(object sender, NavigationRequestedEventArgs e) + { + e.Task = PushViewAsync(e.Page, e.Animated); + } + + void OnRemovePageRequested(object sender, NavigationRequestedEventArgs e) + { + RemovePage(e.Page); + } + + void RegisterToolbar() + { + Context context = Context; + AToolbar bar = _toolbar; + Element page = Element.RealParent; + + MasterDetailPage masterDetailPage = null; + while (page != null) + { + if (page is MasterDetailPage) + { + masterDetailPage = page as MasterDetailPage; + break; + } + page = page.RealParent; + } + + if (masterDetailPage == null) + { + masterDetailPage = Element.InternalChildren[0] as MasterDetailPage; + if (masterDetailPage == null) + return; + } + + if (masterDetailPage.ShouldShowSplitMode) + return; + + var renderer = Android.Platform.GetRenderer(masterDetailPage) as MasterDetailPageRenderer; + if (renderer == null) + return; + + var drawerLayout = (DrawerLayout)renderer; + _drawerToggle = new ActionBarDrawerToggle((Activity)context, drawerLayout, bar, global::Android.Resource.String.Ok, global::Android.Resource.String.Ok) + { + ToolbarNavigationClickListener = new ClickListener(Element) + }; + + drawerLayout.SetDrawerListener(new DrawerMultiplexedListener { Listeners = { _drawerToggle, renderer } }); + _drawerToggle.DrawerIndicatorEnabled = true; + } + + void RemovePage(Page page) + { + IVisualElementRenderer rendererToRemove = Android.Platform.GetRenderer(page); + var containerToRemove = (PageContainer)rendererToRemove?.ViewGroup.Parent; + + // Also remove this page from the fragmentStack + FilterPageFragment(page); + + containerToRemove.RemoveFromParent(); + if (rendererToRemove != null) + { + rendererToRemove.ViewGroup.RemoveFromParent(); + rendererToRemove.Dispose(); + } + containerToRemove?.Dispose(); + + Device.StartTimer(TimeSpan.FromMilliseconds(10), () => + { + UpdateToolbar(); + return false; + }); + } + + void ResetToolbar() + { + _toolbar.RemoveFromParent(); + _toolbar.NavigationClick -= BarOnNavigationClick; + _toolbar = null; + + SetupToolbar(); + RegisterToolbar(); + UpdateToolbar(); + UpdateMenu(); + } + + void SetupToolbar() + { + Context context = Context; + var activity = (FormsAppCompatActivity)context; + + AToolbar bar; + if (FormsAppCompatActivity.ToolbarResource != 0) + bar = activity.LayoutInflater.Inflate(FormsAppCompatActivity.ToolbarResource, null).JavaCast<AToolbar>(); + else + bar = new AToolbar(context); + + bar.NavigationClick += BarOnNavigationClick; + + AddView(bar); + _toolbar = bar; + } + + Task<bool> SwitchContentAsync(Page view, bool animated, bool removed = false, bool popToRoot = false) + { + var activity = (FormsAppCompatActivity)Context; + var tcs = new TaskCompletionSource<bool>(); + Fragment fragment = FragmentContainer.CreateInstance(view); + FragmentManager fm = FragmentManager; + List<Fragment> fragments = _fragmentStack; + + Current = view; + + FragmentTransaction transaction = fm.BeginTransaction(); + + if (animated) + SetupPageTransition(transaction, !removed); + + transaction.DisallowAddToBackStack(); + + if (fragments.Count == 0) + { + transaction.Add(Id, fragment); + fragments.Add(fragment); + } + else + { + if (removed) + { + // pop only one page, or pop everything to the root + var popPage = true; + while (fragments.Count > 1 && popPage) + { + Fragment currentToRemove = fragments.Last(); + fragments.RemoveAt(fragments.Count - 1); + transaction.Remove(currentToRemove); + popPage = popToRoot; + } + + Fragment toShow = fragments.Last(); + // Execute pending transactions so that we can be sure the fragment list is accurate. + fm.ExecutePendingTransactions(); + if (fm.Fragments.Contains(toShow)) + transaction.Show(toShow); + else + transaction.Add(Id, toShow); + } + else + { + // push + Fragment currentToHide = fragments.Last(); + transaction.Hide(currentToHide); + transaction.Add(Id, fragment); + fragments.Add(fragment); + } + } + transaction.Commit(); + + // The fragment transitions don't really SUPPORT telling you when they end + // There are some hacks you can do, but they actually are worse than just doing this: + + if (animated) + { + if (!removed) + { + UpdateToolbar(); + if (_drawerToggle != null && Element.StackDepth == 2) + AnimateArrowIn(); + } + else if (_drawerToggle != null && Element.StackDepth == 2) + AnimateArrowOut(); + + Device.StartTimer(TimeSpan.FromMilliseconds(200), () => + { + tcs.TrySetResult(true); + fragment.UserVisibleHint = true; + if (removed) + UpdateToolbar(); + return false; + }); + } + else + { + Device.StartTimer(TimeSpan.FromMilliseconds(1), () => + { + tcs.TrySetResult(true); + fragment.UserVisibleHint = true; + UpdateToolbar(); + return false; + }); + } + + // 200ms is how long the animations are, and they are "reversible" in the sense that starting another one slightly before it's done is fine + + return tcs.Task; + } + + void ToolbarTrackerOnCollectionChanged(object sender, EventArgs eventArgs) + { + UpdateMenu(); + } + + void UpdateMenu() + { + AToolbar bar = _toolbar; + Context context = Context; + IMenu menu = bar.Menu; + + foreach (ToolbarItem item in _toolbarTracker.ToolbarItems) + item.PropertyChanged -= HandleToolbarItemPropertyChanged; + menu.Clear(); + + foreach (ToolbarItem item in _toolbarTracker.ToolbarItems) + { + item.PropertyChanged += HandleToolbarItemPropertyChanged; + if (item.Order == ToolbarItemOrder.Secondary) + { + IMenuItem menuItem = menu.Add(item.Text); + menuItem.SetEnabled(item.IsEnabled); + menuItem.SetOnMenuItemClickListener(new GenericMenuClickListener(item.Activate)); + } + else + { + IMenuItem menuItem = menu.Add(item.Text); + FileImageSource icon = item.Icon; + if (!string.IsNullOrEmpty(icon)) + { + Drawable iconBitmap = context.Resources.GetDrawable(icon); + if (iconBitmap != null) + menuItem.SetIcon(iconBitmap); + } + menuItem.SetEnabled(item.IsEnabled); + menuItem.SetShowAsAction(ShowAsAction.Always); + menuItem.SetOnMenuItemClickListener(new GenericMenuClickListener(item.Activate)); + } + } + } + + void UpdateToolbar() + { + if (_disposed) + return; + + Context context = Context; + var activity = (FormsAppCompatActivity)context; + AToolbar bar = _toolbar; + ActionBarDrawerToggle toggle = _drawerToggle; + + if (bar == null) + return; + + bool isNavigated = Element.StackDepth > 1; + bar.NavigationIcon = null; + + if (isNavigated) + { + if (toggle != null) + { + toggle.DrawerIndicatorEnabled = false; + toggle.SyncState(); + } + + if (NavigationPage.GetHasBackButton(Element.CurrentPage)) + { + var icon = new DrawerArrowDrawable(activity.SupportActionBar.ThemedContext); + icon.Progress = 1; + bar.NavigationIcon = icon; + } + } + else + { + if (toggle != null) + { + toggle.DrawerIndicatorEnabled = true; + toggle.SyncState(); + } + } + + Color tintColor = Element.BarBackgroundColor; + + if (Forms.IsLollipopOrNewer) + { + if (tintColor.IsDefault) + bar.BackgroundTintMode = null; + else + { + bar.BackgroundTintMode = PorterDuff.Mode.Src; + bar.BackgroundTintList = ColorStateList.ValueOf(tintColor.ToAndroid()); + } + } + else + { + if (tintColor.IsDefault && _backgroundDrawable != null) + bar.SetBackground(_backgroundDrawable); + else if (!tintColor.IsDefault) + { + if (_backgroundDrawable == null) + _backgroundDrawable = bar.Background; + bar.SetBackgroundColor(tintColor.ToAndroid()); + } + } + + Color textColor = Element.BarTextColor; + if (!textColor.IsDefault) + bar.SetTitleTextColor(textColor.ToAndroid().ToArgb()); + + bar.Title = Element.CurrentPage.Title ?? ""; + } + + class ClickListener : Object, IOnClickListener + { + readonly NavigationPage _element; + + public ClickListener(NavigationPage element) + { + _element = element; + } + + public void OnClick(AView v) + { + _element?.PopAsync(); + } + } + + class DrawerMultiplexedListener : Object, DrawerLayout.IDrawerListener + { + public List<DrawerLayout.IDrawerListener> Listeners { get; } = new List<DrawerLayout.IDrawerListener>(2); + + public void OnDrawerClosed(AView drawerView) + { + foreach (DrawerLayout.IDrawerListener listener in Listeners) + listener.OnDrawerClosed(drawerView); + } + + public void OnDrawerOpened(AView drawerView) + { + foreach (DrawerLayout.IDrawerListener listener in Listeners) + listener.OnDrawerOpened(drawerView); + } + + public void OnDrawerSlide(AView drawerView, float slideOffset) + { + foreach (DrawerLayout.IDrawerListener listener in Listeners) + listener.OnDrawerSlide(drawerView, slideOffset); + } + + public void OnDrawerStateChanged(int newState) + { + foreach (DrawerLayout.IDrawerListener listener in Listeners) + listener.OnDrawerStateChanged(newState); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/AppCompat/PickerRenderer.cs b/Xamarin.Forms.Platform.Android/AppCompat/PickerRenderer.cs new file mode 100644 index 00000000..d59d9f6e --- /dev/null +++ b/Xamarin.Forms.Platform.Android/AppCompat/PickerRenderer.cs @@ -0,0 +1,141 @@ +using System; +using System.ComponentModel; +using System.Linq; +using Android.App; +using Android.Text; +using Android.Widget; +using Object = Java.Lang.Object; + +namespace Xamarin.Forms.Platform.Android.AppCompat +{ + public class PickerRenderer : ViewRenderer<Picker, EditText> + { + AlertDialog _dialog; + bool _disposed; + + public PickerRenderer() + { + AutoPackage = false; + } + + protected override EditText CreateNativeControl() + { + return new EditText(Context); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + + ((ObservableList<string>)Element.Items).CollectionChanged -= RowsCollectionChanged; + } + + base.Dispose(disposing); + } + + protected override void OnElementChanged(ElementChangedEventArgs<Picker> e) + { + if (e.OldElement != null) + ((ObservableList<string>)e.OldElement.Items).CollectionChanged -= RowsCollectionChanged; + + if (e.NewElement != null) + { + ((ObservableList<string>)e.NewElement.Items).CollectionChanged += RowsCollectionChanged; + if (Control == null) + { + EditText textField = CreateNativeControl(); + textField.Focusable = false; + textField.Clickable = true; + textField.Tag = this; + textField.InputType = InputTypes.Null; + textField.SetOnClickListener(PickerListener.Instance); + SetNativeControl(textField); + } + UpdatePicker(); + } + + base.OnElementChanged(e); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == Picker.TitleProperty.PropertyName) + UpdatePicker(); + if (e.PropertyName == Picker.SelectedIndexProperty.PropertyName) + UpdatePicker(); + } + + internal override void OnFocusChangeRequested(object sender, VisualElement.FocusRequestArgs e) + { + base.OnFocusChangeRequested(sender, e); + + if (e.Focus) + OnClick(); + else if (_dialog != null) + { + _dialog.Hide(); + ((IElementController)Element).SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, false); + Control.ClearFocus(); + _dialog = null; + } + } + + void OnClick() + { + Picker model = Element; + using(var builder = new AlertDialog.Builder(Context)) + { + builder.SetTitle(model.Title ?? ""); + string[] items = model.Items.ToArray(); + builder.SetItems(items, (s, e) => ((IElementController)model).SetValueFromRenderer(Picker.SelectedIndexProperty, e.Which)); + + builder.SetNegativeButton(global::Android.Resource.String.Cancel, (o, args) => { }); + + _dialog = builder.Create(); + } + + _dialog.SetCanceledOnTouchOutside(true); + _dialog.DismissEvent += (sender, args) => + { + _dialog.Dispose(); + _dialog = null; + }; + + _dialog.Show(); + } + + void RowsCollectionChanged(object sender, EventArgs e) + { + UpdatePicker(); + } + + void UpdatePicker() + { + Control.Hint = Element.Title; + + if (Element.SelectedIndex == -1 || Element.Items == null) + Control.Text = null; + else + Control.Text = Element.Items[Element.SelectedIndex]; + } + + class PickerListener : Object, IOnClickListener + { + #region Statics + + public static readonly PickerListener Instance = new PickerListener(); + + #endregion + + public void OnClick(global::Android.Views.View v) + { + var renderer = v.Tag as PickerRenderer; + renderer?.OnClick(); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/AppCompat/Platform.cs b/Xamarin.Forms.Platform.Android/AppCompat/Platform.cs new file mode 100644 index 00000000..12c6a02a --- /dev/null +++ b/Xamarin.Forms.Platform.Android/AppCompat/Platform.cs @@ -0,0 +1,394 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Android.Content; +using Android.Views; +using Android.Views.Animations; +using ARelativeLayout = Android.Widget.RelativeLayout; + +namespace Xamarin.Forms.Platform.Android.AppCompat +{ + internal class Platform : BindableObject, IPlatform, IPlatformLayout, INavigation, IDisposable + { + readonly Context _context; + readonly PlatformRenderer _renderer; + bool _disposed; + bool _navAnimationInProgress; + NavigationModel _navModel = new NavigationModel(); + + public Platform(Context context) + { + _context = context; + + _renderer = new PlatformRenderer(context, this); + + FormsAppCompatActivity.BackPressed += HandleBackPressed; + } + + internal bool NavAnimationInProgress + { + get { return _navAnimationInProgress; } + set + { + if (_navAnimationInProgress == value) + return; + _navAnimationInProgress = value; + if (value) + MessagingCenter.Send(this, CloseContextActionsSignalName); + } + } + + Page Page { get; set; } + + public void Dispose() + { + if (_disposed) + return; + _disposed = true; + + SetPage(null); + + FormsAppCompatActivity.BackPressed -= HandleBackPressed; + } + + void INavigation.InsertPageBefore(Page page, Page before) + { + throw new InvalidOperationException("InsertPageBefore is not supported globally on Android, please use a NavigationPage."); + } + + IReadOnlyList<Page> INavigation.ModalStack => _navModel.Modals.ToList(); + + IReadOnlyList<Page> INavigation.NavigationStack => new List<Page>(); + + Task<Page> INavigation.PopAsync() + { + return ((INavigation)this).PopAsync(true); + } + + Task<Page> INavigation.PopAsync(bool animated) + { + throw new InvalidOperationException("PopAsync is not supported globally on Android, please use a NavigationPage."); + } + + Task<Page> INavigation.PopModalAsync() + { + return ((INavigation)this).PopModalAsync(true); + } + + Task<Page> INavigation.PopModalAsync(bool animated) + { + Page modal = _navModel.PopModal(); + modal.SendDisappearing(); + var source = new TaskCompletionSource<Page>(); + + IVisualElementRenderer modalRenderer = Android.Platform.GetRenderer(modal); + if (modalRenderer != null) + { + var modalContainer = modalRenderer.ViewGroup.Parent as ModalContainer; + if (animated) + { + modalContainer.Animate().TranslationY(_renderer.Height).SetInterpolator(new AccelerateInterpolator(1)).SetDuration(300).SetListener(new GenericAnimatorListener + { + OnEnd = a => + { + modalContainer.RemoveFromParent(); + modalContainer.Dispose(); + source.TrySetResult(modal); + _navModel.CurrentPage?.SendAppearing(); + modalContainer = null; + } + }); + } + else + { + modalContainer.RemoveFromParent(); + modalContainer.Dispose(); + source.TrySetResult(modal); + _navModel.CurrentPage?.SendAppearing(); + } + } + + return source.Task; + } + + Task INavigation.PopToRootAsync() + { + return ((INavigation)this).PopToRootAsync(true); + } + + Task INavigation.PopToRootAsync(bool animated) + { + throw new InvalidOperationException("PopToRootAsync is not supported globally on Android, please use a NavigationPage."); + } + + Task INavigation.PushAsync(Page root) + { + return ((INavigation)this).PushAsync(root, true); + } + + Task INavigation.PushAsync(Page root, bool animated) + { + throw new InvalidOperationException("PushAsync is not supported globally on Android, please use a NavigationPage."); + } + + Task INavigation.PushModalAsync(Page modal) + { + return ((INavigation)this).PushModalAsync(modal, true); + } + + async Task INavigation.PushModalAsync(Page modal, bool animated) + { + _navModel.CurrentPage?.SendDisappearing(); + + _navModel.PushModal(modal); + + modal.Platform = this; + + Task presentModal = PresentModal(modal, animated); + + await presentModal; + + // Verify that the modal is still on the stack + if (_navModel.CurrentPage == modal) + modal.SendAppearing(); + } + + void INavigation.RemovePage(Page page) + { + throw new InvalidOperationException("RemovePage is not supported globally on Android, please use a NavigationPage."); + } + + SizeRequest IPlatform.GetNativeSize(VisualElement view, double widthConstraint, double heightConstraint) + { + Performance.Start(); + + // FIXME: potential crash + IVisualElementRenderer viewRenderer = Android.Platform.GetRenderer(view); + + // negative numbers have special meanings to android they don't to us + widthConstraint = widthConstraint <= -1 ? double.PositiveInfinity : _context.ToPixels(widthConstraint); + heightConstraint = heightConstraint <= -1 ? double.PositiveInfinity : _context.ToPixels(heightConstraint); + + int width = !double.IsPositiveInfinity(widthConstraint) + ? MeasureSpecFactory.MakeMeasureSpec((int)widthConstraint, MeasureSpecMode.AtMost) + : MeasureSpecFactory.MakeMeasureSpec(0, MeasureSpecMode.Unspecified); + + int height = !double.IsPositiveInfinity(heightConstraint) + ? MeasureSpecFactory.MakeMeasureSpec((int)heightConstraint, MeasureSpecMode.AtMost) + : MeasureSpecFactory.MakeMeasureSpec(0, MeasureSpecMode.Unspecified); + + SizeRequest rawResult = viewRenderer.GetDesiredSize(width, height); + if (rawResult.Minimum == Size.Zero) + rawResult.Minimum = rawResult.Request; + var result = new SizeRequest(new Size(_context.FromPixels(rawResult.Request.Width), _context.FromPixels(rawResult.Request.Height)), + new Size(_context.FromPixels(rawResult.Minimum.Width), _context.FromPixels(rawResult.Minimum.Height))); + + Performance.Stop(); + return result; + } + + void IPlatformLayout.OnLayout(bool changed, int l, int t, int r, int b) + { + if (changed) + LayoutRootPage(Page, r - l, b - t); + + Android.Platform.GetRenderer(Page).UpdateLayout(); + + for (var i = 0; i < _renderer.ChildCount; i++) + { + global::Android.Views.View child = _renderer.GetChildAt(i); + if (child is ModalContainer) + { + child.Measure(MeasureSpecFactory.MakeMeasureSpec(r - l, MeasureSpecMode.Exactly), MeasureSpecFactory.MakeMeasureSpec(t - b, MeasureSpecMode.Exactly)); + child.Layout(l, t, r, b); + } + } + } + + protected override void OnBindingContextChanged() + { + SetInheritedBindingContext(Page, BindingContext); + + base.OnBindingContextChanged(); + } + + internal void SetPage(Page newRoot) + { + var layout = false; + if (Page != null) + { + _renderer.RemoveAllViews(); + + foreach (IVisualElementRenderer rootRenderer in _navModel.Roots.Select(Android.Platform.GetRenderer)) + rootRenderer.Dispose(); + _navModel = new NavigationModel(); + + layout = true; + } + + if (newRoot == null) + return; + + _navModel.Push(newRoot, null); + + Page = newRoot; + Page.Platform = this; + AddChild(Page, layout); + + ((Application)Page.RealParent).NavigationProxy.Inner = this; + } + + void AddChild(Page page, bool layout = false) + { + if (Android.Platform.GetRenderer(page) != null) + return; + + Android.Platform.SetPageContext(page, _context); + IVisualElementRenderer renderView = RendererFactory.GetRenderer(page); + Android.Platform.SetRenderer(page, renderView); + + if (layout) + LayoutRootPage(page, _renderer.Width, _renderer.Height); + + _renderer.AddView(renderView.ViewGroup); + } + + bool HandleBackPressed(object sender, EventArgs e) + { + if (NavAnimationInProgress) + return true; + + Page root = _navModel.Roots.Last(); + bool handled = root.SendBackButtonPressed(); + + return handled; + } + + void LayoutRootPage(Page page, int width, int height) + { + var activity = (FormsAppCompatActivity)_context; + int statusBarHeight = Forms.IsLollipopOrNewer ? activity.GetStatusBarHeight() : 0; + + if (page is MasterDetailPage) + page.Layout(new Rectangle(0, 0, _context.FromPixels(width), _context.FromPixels(height))); + else + { + page.Layout(new Rectangle(0, _context.FromPixels(statusBarHeight), _context.FromPixels(width), _context.FromPixels(height - statusBarHeight))); + } + } + + Task PresentModal(Page modal, bool animated) + { + var modalContainer = new ModalContainer(_context, modal); + + _renderer.AddView(modalContainer); + + var source = new TaskCompletionSource<bool>(); + NavAnimationInProgress = true; + if (animated) + { + modalContainer.TranslationY = _renderer.Height; + modalContainer.Animate().TranslationY(0).SetInterpolator(new DecelerateInterpolator(1)).SetDuration(300).SetListener(new GenericAnimatorListener + { + OnEnd = a => + { + source.TrySetResult(false); + NavAnimationInProgress = false; + modalContainer = null; + }, + OnCancel = a => + { + source.TrySetResult(true); + NavAnimationInProgress = false; + modalContainer = null; + } + }); + } + else + { + NavAnimationInProgress = false; + source.TrySetResult(true); + } + + return source.Task; + } + + sealed class ModalContainer : ViewGroup + { + global::Android.Views.View _backgroundView; + bool _disposed; + Page _modal; + IVisualElementRenderer _renderer; + + public ModalContainer(Context context, Page modal) : base(context) + { + _modal = modal; + + _backgroundView = new global::Android.Views.View(context); + _backgroundView.SetWindowBackground(); + AddView(_backgroundView); + + Android.Platform.SetPageContext(modal, context); + _renderer = RendererFactory.GetRenderer(modal); + Android.Platform.SetRenderer(modal, _renderer); + + AddView(_renderer.ViewGroup); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + RemoveAllViews(); + if (_renderer != null) + { + _renderer.Dispose(); + _renderer = null; + _modal.ClearValue(Android.Platform.RendererProperty); + _modal = null; + } + + if (_backgroundView != null) + { + _backgroundView.Dispose(); + _backgroundView = null; + } + } + + base.Dispose(disposing); + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + var activity = (FormsAppCompatActivity)Context; + int statusBarHeight = Forms.IsLollipopOrNewer ? activity.GetStatusBarHeight() : 0; + if (changed) + { + if (_modal is MasterDetailPage) + _modal.Layout(new Rectangle(0, 0, activity.FromPixels(r - l), activity.FromPixels(b - t))); + else + { + _modal.Layout(new Rectangle(0, activity.FromPixels(statusBarHeight), activity.FromPixels(r - l), activity.FromPixels(b - t - statusBarHeight))); + } + + _backgroundView.Layout(0, statusBarHeight, r - l, b - t); + } + + _renderer.UpdateLayout(); + } + } + + #region Statics + + public static implicit operator ViewGroup(Platform canvas) + { + return canvas._renderer; + } + + internal const string CloseContextActionsSignalName = "Xamarin.CloseContextActions"; + + #endregion + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/AppCompat/Resource.cs b/Xamarin.Forms.Platform.Android/AppCompat/Resource.cs new file mode 100644 index 00000000..08d3c5e4 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/AppCompat/Resource.cs @@ -0,0 +1,31 @@ +using Android.Runtime; + +[assembly: ResourceDesigner("Xamarin.Forms.Platform.Android.Resource", IsApplication = false)] + +namespace Xamarin.Forms.Platform.Android +{ + public class Resource + { + static Resource() + { + ResourceIdManager.UpdateIdValues(); + } + + public class Attribute + { + // aapt resource value: 0x7f0100a5 + // ReSharper disable once InconsistentNaming + // Android is pretty insistent about this casing + public static int actionBarSize = 2130772133; + + static Attribute() + { + ResourceIdManager.UpdateIdValues(); + } + + Attribute() + { + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/AppCompat/SwitchRenderer.cs b/Xamarin.Forms.Platform.Android/AppCompat/SwitchRenderer.cs new file mode 100644 index 00000000..f243c634 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/AppCompat/SwitchRenderer.cs @@ -0,0 +1,92 @@ +using System; +using Android.Support.V7.Widget; +using Android.Widget; + +namespace Xamarin.Forms.Platform.Android.AppCompat +{ + public class SwitchRenderer : ViewRenderer<Switch, SwitchCompat>, CompoundButton.IOnCheckedChangeListener + { + bool _disposed; + + public SwitchRenderer() + { + AutoPackage = false; + } + + void CompoundButton.IOnCheckedChangeListener.OnCheckedChanged(CompoundButton buttonView, bool isChecked) + { + ((IViewController)Element).SetValueFromRenderer(Switch.IsToggledProperty, isChecked); + } + + public override SizeRequest GetDesiredSize(int widthConstraint, int heightConstraint) + { + SizeRequest sizeConstraint = base.GetDesiredSize(widthConstraint, heightConstraint); + + if (sizeConstraint.Request.Width == 0) + { + int width = widthConstraint; + if (widthConstraint <= 0) + width = (int)Context.GetThemeAttributeDp(global::Android.Resource.Attribute.SwitchMinWidth); + else if (widthConstraint <= 0) + width = 100; + + sizeConstraint = new SizeRequest(new Size(width, sizeConstraint.Request.Height), new Size(width, sizeConstraint.Minimum.Height)); + } + + return sizeConstraint; + } + + protected override SwitchCompat CreateNativeControl() + { + return new SwitchCompat(Context); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + + if (Element != null) + Element.Toggled -= HandleToggled; + + Control.SetOnCheckedChangeListener(null); + } + + base.Dispose(disposing); + } + + protected override void OnElementChanged(ElementChangedEventArgs<Switch> e) + { + base.OnElementChanged(e); + + if (e.OldElement != null) + e.OldElement.Toggled -= HandleToggled; + + if (e.NewElement != null) + { + if (Control == null) + { + SwitchCompat aswitch = CreateNativeControl(); + aswitch.SetOnCheckedChangeListener(this); + SetNativeControl(aswitch); + } + else + UpdateEnabled(); // Normally set by SetNativeControl, but not when the Control is reused. + + e.NewElement.Toggled += HandleToggled; + Control.Checked = e.NewElement.IsToggled; + } + } + + void HandleToggled(object sender, EventArgs e) + { + Control.Checked = Element.IsToggled; + } + + void UpdateEnabled() + { + Control.Enabled = Element.IsEnabled; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/AppCompat/TabbedPageRenderer.cs b/Xamarin.Forms.Platform.Android/AppCompat/TabbedPageRenderer.cs new file mode 100644 index 00000000..f978028b --- /dev/null +++ b/Xamarin.Forms.Platform.Android/AppCompat/TabbedPageRenderer.cs @@ -0,0 +1,322 @@ +using System; +using System.Collections.Specialized; +using System.ComponentModel; +using Android.Content; +using Android.OS; +using Android.Runtime; +using Android.Support.Design.Widget; +using Android.Support.V4.App; +using Android.Support.V4.View; +using Android.Views; + +namespace Xamarin.Forms.Platform.Android.AppCompat +{ + public class TabbedPageRenderer : VisualElementRenderer<TabbedPage>, TabLayout.IOnTabSelectedListener, ViewPager.IOnPageChangeListener, IManageFragments + { + bool _disposed; + FragmentManager _fragmentManager; + TabLayout _tabLayout; + bool _useAnimations = true; + FormsViewPager _viewPager; + + public TabbedPageRenderer() + { + AutoPackage = false; + } + + public FragmentManager FragmentManager => _fragmentManager ?? (_fragmentManager = ((FormsAppCompatActivity)Context).SupportFragmentManager); + + internal bool UseAnimations + { + get { return _useAnimations; } + set + { + FormsViewPager pager = _viewPager; + + _useAnimations = value; + if (pager != null) + pager.EnableGesture = value; + } + } + + public void SetFragmentManager(FragmentManager childFragmentManager) + { + if (_fragmentManager == null) + _fragmentManager = childFragmentManager; + } + + void ViewPager.IOnPageChangeListener.OnPageScrolled(int position, float positionOffset, int positionOffsetPixels) + { + UpdateTabBarTranslation(position, positionOffset); + } + + void ViewPager.IOnPageChangeListener.OnPageScrollStateChanged(int state) + { + } + + void ViewPager.IOnPageChangeListener.OnPageSelected(int position) + { + Element.CurrentPage = Element.Children[position]; + } + + void TabLayout.IOnTabSelectedListener.OnTabReselected(TabLayout.Tab tab) + { + } + + void TabLayout.IOnTabSelectedListener.OnTabSelected(TabLayout.Tab tab) + { + if (Element == null) + return; + + int selectedIndex = tab.Position; + if (Element.Children.Count > selectedIndex && selectedIndex >= 0) + Element.CurrentPage = Element.Children[selectedIndex]; + } + + void TabLayout.IOnTabSelectedListener.OnTabUnselected(TabLayout.Tab tab) + { + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + RemoveAllViews(); + foreach (Page pageToRemove in Element.Children) + { + IVisualElementRenderer pageRenderer = Android.Platform.GetRenderer(pageToRemove); + if (pageRenderer != null) + { + pageRenderer.ViewGroup.RemoveFromParent(); + pageRenderer.Dispose(); + } + pageToRemove.ClearValue(Android.Platform.RendererProperty); + } + + if (_viewPager != null) + { + _viewPager.Adapter.Dispose(); + _viewPager.Dispose(); + _viewPager = null; + } + + if (_tabLayout != null) + { + _tabLayout.SetOnTabSelectedListener(null); + _tabLayout.Dispose(); + _tabLayout = null; + } + + if (Element != null) + Element.InternalChildren.CollectionChanged -= OnChildrenCollectionChanged; + } + + base.Dispose(disposing); + } + + protected override void OnAttachedToWindow() + { + base.OnAttachedToWindow(); + Element.SendAppearing(); + } + + protected override void OnDetachedFromWindow() + { + base.OnDetachedFromWindow(); + Element.SendDisappearing(); + } + + protected override void OnElementChanged(ElementChangedEventArgs<TabbedPage> e) + { + base.OnElementChanged(e); + + var activity = (FormsAppCompatActivity)Context; + + if (e.OldElement != null) + e.OldElement.InternalChildren.CollectionChanged -= OnChildrenCollectionChanged; + + if (e.NewElement != null) + { + if (_tabLayout == null) + { + TabLayout tabs; + if (FormsAppCompatActivity.TabLayoutResource > 0) + { + tabs = _tabLayout = activity.LayoutInflater.Inflate(FormsAppCompatActivity.TabLayoutResource, null).JavaCast<TabLayout>(); + } + else + tabs = _tabLayout = new TabLayout(activity) { TabMode = TabLayout.ModeFixed, TabGravity = TabLayout.GravityFill }; + FormsViewPager pager = + _viewPager = + new FormsViewPager(activity) + { + OverScrollMode = OverScrollMode.Never, + EnableGesture = UseAnimations, + LayoutParameters = new LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent), + Adapter = new FormsFragmentPagerAdapter<Page>(e.NewElement, FragmentManager) { CountOverride = e.NewElement.Children.Count } + }; + pager.Id = FormsAppCompatActivity.GetUniqueId(); + pager.AddOnPageChangeListener(this); + + tabs.SetupWithViewPager(pager); + UpdateTabIcons(); + tabs.SetOnTabSelectedListener(this); + + AddView(pager); + AddView(tabs); + } + + TabbedPage tabbedPage = e.NewElement; + if (tabbedPage.CurrentPage != null) + ScrollToCurrentPage(); + + UpdateIgnoreContainerAreas(); + tabbedPage.InternalChildren.CollectionChanged += OnChildrenCollectionChanged; + } + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == "CurrentPage") + ScrollToCurrentPage(); + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + TabLayout tabs = _tabLayout; + FormsViewPager pager = _viewPager; + Context context = Context; + int width = r - l; + int height = b - t; + + tabs.Measure(MeasureSpecFactory.MakeMeasureSpec(width, MeasureSpecMode.Exactly), MeasureSpecFactory.MakeMeasureSpec(height, MeasureSpecMode.AtMost)); + var tabsHeight = 0; + + //MinimumHeight is only available on API 16+ + if ((int)Build.VERSION.SdkInt >= 16) + tabsHeight = Math.Min(height, Math.Max(tabs.MeasuredHeight, tabs.MinimumHeight)); + else + tabsHeight = Math.Min(height, tabs.MeasuredHeight); + + pager.Measure(MeasureSpecFactory.MakeMeasureSpec(width, MeasureSpecMode.AtMost), MeasureSpecFactory.MakeMeasureSpec(height, MeasureSpecMode.AtMost)); + + if (width > 0 && height > 0) + { + Element.ContainerArea = new Rectangle(0, context.FromPixels(tabsHeight), context.FromPixels(width), context.FromPixels(height - tabsHeight)); + + for (var i = 0; i < Element.InternalChildren.Count; i++) + { + var child = Element.InternalChildren[i] as VisualElement; + if (child == null) + continue; + IVisualElementRenderer renderer = Android.Platform.GetRenderer(child); + var navigationRenderer = renderer as NavigationPageRenderer; + if (navigationRenderer != null) + navigationRenderer.ContainerPadding = tabsHeight; + } + + pager.Layout(0, 0, width, b); + // We need to measure again to ensure that the tabs show up + tabs.Measure(MeasureSpecFactory.MakeMeasureSpec(width, MeasureSpecMode.Exactly), MeasureSpecFactory.MakeMeasureSpec(tabsHeight, MeasureSpecMode.Exactly)); + tabs.Layout(0, 0, width, tabsHeight); + + UpdateTabBarTranslation(pager.CurrentItem, 0); + } + + base.OnLayout(changed, l, t, r, b); + } + + void OnChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + FormsViewPager pager = _viewPager; + TabLayout tabs = _tabLayout; + + ((FormsFragmentPagerAdapter<Page>)pager.Adapter).CountOverride = Element.Children.Count; + pager.Adapter.NotifyDataSetChanged(); + + if (Element.Children.Count == 0) + tabs.RemoveAllTabs(); + else + { + tabs.SetupWithViewPager(pager); + UpdateTabIcons(); + tabs.SetOnTabSelectedListener(this); + } + + UpdateIgnoreContainerAreas(); + } + + void ScrollToCurrentPage() + { + _viewPager.SetCurrentItem(Element.Children.IndexOf(Element.CurrentPage), UseAnimations); + } + + void UpdateIgnoreContainerAreas() + { + foreach (Page child in Element.Children) + child.IgnoresContainerArea = child is NavigationPage; + } + + void UpdateTabBarTranslation(int position, float offset) + { + TabLayout tabs = _tabLayout; + + if (position >= Element.InternalChildren.Count) + return; + + var leftPage = (Page)Element.InternalChildren[position]; + IVisualElementRenderer leftRenderer = Android.Platform.GetRenderer(leftPage); + + if (leftRenderer == null) + return; + + if (offset <= 0 || position >= Element.InternalChildren.Count - 1) + { + var leftNavRenderer = leftRenderer as NavigationPageRenderer; + if (leftNavRenderer != null) + tabs.TranslationY = leftNavRenderer.GetNavBarHeight(); + else + tabs.TranslationY = 0; + } + else + { + var rightPage = (Page)Element.InternalChildren[position + 1]; + IVisualElementRenderer rightRenderer = Android.Platform.GetRenderer(rightPage); + + var leftHeight = 0; + var leftNavRenderer = leftRenderer as NavigationPageRenderer; + if (leftNavRenderer != null) + leftHeight = leftNavRenderer.GetNavBarHeight(); + + var rightHeight = 0; + var rightNavRenderer = rightRenderer as NavigationPageRenderer; + if (rightNavRenderer != null) + rightHeight = rightNavRenderer.GetNavBarHeight(); + + tabs.TranslationY = leftHeight + (rightHeight - leftHeight) * offset; + } + } + + void UpdateTabIcons() + { + TabLayout tabs = _tabLayout; + + if (tabs.TabCount != Element.Children.Count) + return; + + for (var i = 0; i < Element.Children.Count; i++) + { + Page child = Element.Children[i]; + FileImageSource icon = child.Icon; + if (string.IsNullOrEmpty(icon)) + continue; + + TabLayout.Tab tab = tabs.GetTabAt(i); + tab.SetIcon(ResourceManager.IdFromTitle(icon, ResourceManager.DrawableClass)); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/AppCompat/ViewRenderer.cs b/Xamarin.Forms.Platform.Android/AppCompat/ViewRenderer.cs new file mode 100644 index 00000000..3c869216 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/AppCompat/ViewRenderer.cs @@ -0,0 +1,7 @@ +namespace Xamarin.Forms.Platform.Android.AppCompat +{ + public abstract class ViewRenderer<TView, TControl> : Android.ViewRenderer<TView, TControl> where TView : View where TControl : global::Android.Views.View + { + protected abstract TControl CreateNativeControl(); + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/CellAdapter.cs b/Xamarin.Forms.Platform.Android/CellAdapter.cs new file mode 100644 index 00000000..9f439157 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/CellAdapter.cs @@ -0,0 +1,327 @@ +using System; +using System.Collections.Specialized; +using System.ComponentModel; +using Android.App; +using Android.Content; +using Android.Util; +using Android.Views; +using Android.Widget; +using AView = Android.Views.View; +using AListView = Android.Widget.ListView; + +namespace Xamarin.Forms.Platform.Android +{ + public abstract class CellAdapter : BaseAdapter<object>, AdapterView.IOnItemLongClickListener, ActionMode.ICallback, AdapterView.IOnItemClickListener, + global::Android.Support.V7.View.ActionMode.ICallback + { + readonly Context _context; + ActionMode _actionMode; + Cell _actionModeContext; + + bool _actionModeNeedsUpdates; + AView _contextView; + global::Android.Support.V7.View.ActionMode _supportActionMode; + + protected CellAdapter(Context context) + { + if (context == null) + throw new ArgumentNullException("context"); + + _context = context; + } + + internal Cell ActionModeContext + { + get { return _actionModeContext; } + set + { + if (_actionModeContext == value) + return; + + if (_actionModeContext != null) + ((INotifyCollectionChanged)_actionModeContext.ContextActions).CollectionChanged -= OnContextItemsChanged; + + ActionModeObject = null; + _actionModeContext = value; + + if (_actionModeContext != null) + { + ((INotifyCollectionChanged)_actionModeContext.ContextActions).CollectionChanged += OnContextItemsChanged; + ActionModeObject = _actionModeContext.BindingContext; + } + } + } + + internal object ActionModeObject { get; set; } + + internal AView ContextView + { + get { return _contextView; } + set + { + if (_contextView == value) + return; + + if (_contextView != null) + { + var isSelected = (bool)ActionModeContext.GetValue(ListViewAdapter.IsSelectedProperty); + if (isSelected) + SetSelectedBackground(_contextView); + else + UnsetSelectedBackground(_contextView); + } + + _contextView = value; + + if (_contextView != null) + SetSelectedBackground(_contextView, true); + } + } + + public bool OnActionItemClicked(ActionMode mode, IMenuItem item) + { + OnActionItemClickedImpl(item); + if (mode != null && mode.Handle != IntPtr.Zero) + mode.Finish(); + return true; + } + + bool global::Android.Support.V7.View.ActionMode.ICallback.OnActionItemClicked(global::Android.Support.V7.View.ActionMode mode, IMenuItem item) + { + OnActionItemClickedImpl(item); + mode.Finish(); + return true; + } + + public bool OnCreateActionMode(ActionMode mode, IMenu menu) + { + CreateContextMenu(menu); + return true; + } + + bool global::Android.Support.V7.View.ActionMode.ICallback.OnCreateActionMode(global::Android.Support.V7.View.ActionMode mode, IMenu menu) + { + CreateContextMenu(menu); + return true; + } + + public void OnDestroyActionMode(ActionMode mode) + { + OnDestroyActionModeImpl(); + _actionMode.Dispose(); + _actionMode = null; + } + + void global::Android.Support.V7.View.ActionMode.ICallback.OnDestroyActionMode(global::Android.Support.V7.View.ActionMode mode) + { + OnDestroyActionModeImpl(); + _supportActionMode.Dispose(); + _supportActionMode = null; + } + + public bool OnPrepareActionMode(ActionMode mode, IMenu menu) + { + return OnPrepareActionModeImpl(menu); + } + + bool global::Android.Support.V7.View.ActionMode.ICallback.OnPrepareActionMode(global::Android.Support.V7.View.ActionMode mode, IMenu menu) + { + return OnPrepareActionModeImpl(menu); + } + + public void OnItemClick(AdapterView parent, AView view, int position, long id) + { + if (_actionMode != null || _supportActionMode != null) + { + var listView = parent as AListView; + if (listView != null) + position -= listView.HeaderViewsCount; + HandleContextMode(view, position); + } + else + HandleItemClick(parent, view, position, id); + } + + public bool OnItemLongClick(AdapterView parent, AView view, int position, long id) + { + var listView = parent as AListView; + if (listView != null) + position -= listView.HeaderViewsCount; + return HandleContextMode(view, position); + } + + protected abstract Cell GetCellForPosition(int position); + + protected virtual void HandleItemClick(AdapterView parent, AView view, int position, long id) + { + } + + protected void SetSelectedBackground(AView view, bool isContextTarget = false) + { + int attribute = isContextTarget ? global::Android.Resource.Attribute.ColorLongPressedHighlight : global::Android.Resource.Attribute.ColorActivatedHighlight; + using(var value = new TypedValue()) + { + if (_context.Theme.ResolveAttribute(attribute, value, true)) + view.SetBackgroundResource(value.ResourceId); + else + view.SetBackgroundResource(global::Android.Resource.Color.HoloBlueDark); + } + } + + protected void UnsetSelectedBackground(AView view) + { + view.SetBackgroundResource(0); + } + + internal void CloseContextAction() + { + if (_actionMode != null) + _actionMode.Finish(); + if (_supportActionMode != null) + _supportActionMode.Finish(); + } + + void CreateContextMenu(IMenu menu) + { + var changed = new PropertyChangedEventHandler(OnContextActionPropertyChanged); + var changing = new PropertyChangingEventHandler(OnContextActionPropertyChanging); + var commandChanged = new EventHandler(OnContextActionCommandCanExecuteChanged); + + for (var i = 0; i < ActionModeContext.ContextActions.Count; i++) + { + MenuItem action = ActionModeContext.ContextActions[i]; + + IMenuItem item = menu.Add(Menu.None, i, Menu.None, action.Text); + + if (action.Icon != null) + item.SetIcon(_context.Resources.GetDrawable(action.Icon)); + + action.PropertyChanged += changed; + action.PropertyChanging += changing; + + if (action.Command != null) + action.Command.CanExecuteChanged += commandChanged; + + if (!action.IsEnabled) + item.SetEnabled(false); + } + } + + bool HandleContextMode(AView view, int position) + { + Cell cell = GetCellForPosition(position); + + if (_actionMode != null || _supportActionMode != null) + { + if (!cell.HasContextActions) + { + _actionMode?.Finish(); + _supportActionMode?.Finish(); + return false; + } + + ActionModeContext = cell; + + _actionMode?.Invalidate(); + _supportActionMode?.Invalidate(); + } + else + { + if (!cell.HasContextActions) + return false; + + ActionModeContext = cell; + + var appCompatActivity = Forms.Context as FormsAppCompatActivity; + if (appCompatActivity == null) + _actionMode = ((Activity)Forms.Context).StartActionMode(this); + else + _supportActionMode = appCompatActivity.StartSupportActionMode(this); + } + + ContextView = view; + + return true; + } + + void OnActionItemClickedImpl(IMenuItem item) + { + int index = item.ItemId; + MenuItem action = ActionModeContext.ContextActions[index]; + + action.Activate(); + } + + void OnContextActionCommandCanExecuteChanged(object sender, EventArgs eventArgs) + { + _actionModeNeedsUpdates = true; + _actionMode.Invalidate(); + } + + void OnContextActionPropertyChanged(object sender, PropertyChangedEventArgs e) + { + var action = (MenuItem)sender; + + if (e.PropertyName == MenuItem.CommandProperty.PropertyName) + { + if (action.Command != null) + action.Command.CanExecuteChanged += OnContextActionCommandCanExecuteChanged; + } + else + _actionModeNeedsUpdates = true; + } + + void OnContextActionPropertyChanging(object sender, PropertyChangingEventArgs e) + { + var action = (MenuItem)sender; + + if (e.PropertyName == MenuItem.CommandProperty.PropertyName) + { + if (action.Command != null) + action.Command.CanExecuteChanged -= OnContextActionCommandCanExecuteChanged; + } + } + + void OnContextItemsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + _actionModeNeedsUpdates = true; + _actionMode.Invalidate(); + } + + void OnDestroyActionModeImpl() + { + var changed = new PropertyChangedEventHandler(OnContextActionPropertyChanged); + var changing = new PropertyChangingEventHandler(OnContextActionPropertyChanging); + var commandChanged = new EventHandler(OnContextActionCommandCanExecuteChanged); + + ((INotifyCollectionChanged)ActionModeContext.ContextActions).CollectionChanged -= OnContextItemsChanged; + + for (var i = 0; i < ActionModeContext.ContextActions.Count; i++) + { + MenuItem action = ActionModeContext.ContextActions[i]; + action.PropertyChanged -= changed; + action.PropertyChanging -= changing; + + if (action.Command != null) + action.Command.CanExecuteChanged -= commandChanged; + } + ContextView = null; + + ActionModeContext = null; + _actionModeNeedsUpdates = false; + } + + bool OnPrepareActionModeImpl(IMenu menu) + { + if (_actionModeNeedsUpdates) + { + _actionModeNeedsUpdates = false; + + menu.Clear(); + CreateContextMenu(menu); + } + + return false; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Cells/BaseCellView.cs b/Xamarin.Forms.Platform.Android/Cells/BaseCellView.cs new file mode 100644 index 00000000..7855b01e --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Cells/BaseCellView.cs @@ -0,0 +1,208 @@ +using System.IO; +using System.Threading.Tasks; +using Android.Content; +using Android.Graphics; +using Android.Text; +using Android.Views; +using Android.Widget; +using AView = Android.Views.View; +using AColor = Android.Graphics.Color; +using AColorDraw = Android.Graphics.Drawables.ColorDrawable; + +namespace Xamarin.Forms.Platform.Android +{ + public class BaseCellView : LinearLayout, INativeElementView + { + public const double DefaultMinHeight = 44; + + readonly Color _androidDefaultTextColor; + readonly Cell _cell; + readonly TextView _detailText; + readonly ImageView _imageView; + readonly TextView _mainText; + Color _defaultDetailColor; + Color _defaultMainTextColor; + Color _detailTextColor; + string _detailTextText; + ImageSource _imageSource; + Color _mainTextColor; + string _mainTextText; + + public BaseCellView(Context context, Cell cell) : base(context) + { + _cell = cell; + SetMinimumWidth((int)context.ToPixels(25)); + SetMinimumHeight((int)context.ToPixels(25)); + Orientation = Orientation.Horizontal; + + var padding = (int)context.FromPixels(8); + SetPadding(padding, padding, padding, padding); + + _imageView = new ImageView(context); + var imageParams = new LayoutParams(ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.FillParent) + { + Width = (int)context.ToPixels(60), + Height = (int)context.ToPixels(60), + RightMargin = 0, + Gravity = GravityFlags.Center + }; + using(imageParams) + AddView(_imageView, imageParams); + + var textLayout = new LinearLayout(context) { Orientation = Orientation.Vertical }; + + _mainText = new TextView(context); + _mainText.SetSingleLine(true); + _mainText.Ellipsize = TextUtils.TruncateAt.End; + _mainText.SetPadding((int)context.ToPixels(15), padding, padding, padding); + _mainText.SetTextAppearance(context, global::Android.Resource.Attribute.TextAppearanceListItem); + using(var lp = new LayoutParams(ViewGroup.LayoutParams.FillParent, ViewGroup.LayoutParams.WrapContent)) + textLayout.AddView(_mainText, lp); + + _detailText = new TextView(context); + _detailText.SetSingleLine(true); + _detailText.Ellipsize = TextUtils.TruncateAt.End; + _detailText.SetPadding((int)context.ToPixels(15), padding, padding, padding); + _detailText.Visibility = ViewStates.Gone; + _detailText.SetTextAppearance(context, global::Android.Resource.Attribute.TextAppearanceListItemSmall); + using(var lp = new LayoutParams(ViewGroup.LayoutParams.FillParent, ViewGroup.LayoutParams.WrapContent)) + textLayout.AddView(_detailText, lp); + + var layoutParams = new LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.WrapContent) { Width = 0, Weight = 1, Gravity = GravityFlags.Center }; + + using(layoutParams) + AddView(textLayout, layoutParams); + + SetMinimumHeight((int)context.ToPixels(DefaultMinHeight)); + _androidDefaultTextColor = Color.FromUint((uint)_mainText.CurrentTextColor); + } + + public AView AccessoryView { get; private set; } + + public string DetailText + { + get { return _detailTextText; } + set + { + if (_detailTextText == value) + return; + + _detailTextText = value; + _detailText.Text = value; + _detailText.Visibility = string.IsNullOrEmpty(value) ? ViewStates.Gone : ViewStates.Visible; + } + } + + public string MainText + { + get { return _mainTextText; } + set + { + if (_mainTextText == value) + return; + + _mainTextText = value; + _mainText.Text = value; + } + } + + Element INativeElementView.Element + { + get { return _cell; } + } + + public void SetAccessoryView(AView view) + { + if (AccessoryView != null) + RemoveView(AccessoryView); + + if (view != null) + { + using(var layout = new LayoutParams(ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.FillParent)) + AddView(view, layout); + + AccessoryView = view; + } + } + + public void SetDefaultMainTextColor(Color defaultColor) + { + _defaultMainTextColor = defaultColor; + if (_mainTextColor == Color.Default) + _mainText.SetTextColor(defaultColor.ToAndroid()); + } + + public void SetDetailTextColor(Color color) + { + if (_detailTextColor == color) + return; + + if (_defaultDetailColor == Color.Default) + _defaultDetailColor = Color.FromUint((uint)_detailText.CurrentTextColor); + + _detailTextColor = color; + _detailText.SetTextColor(color.ToAndroid(_defaultDetailColor)); + } + + public void SetImageSource(ImageSource source) + { + UpdateBitmap(source, _imageSource); + _imageSource = source; + } + + public void SetImageVisible(bool visible) + { + _imageView.Visibility = visible ? ViewStates.Visible : ViewStates.Gone; + } + + public void SetIsEnabled(bool isEnable) + { + _mainText.Enabled = isEnable; + _detailText.Enabled = isEnable; + } + + public void SetMainTextColor(Color color) + { + Color defaultColorToSet = _defaultMainTextColor == Color.Default ? _androidDefaultTextColor : _defaultMainTextColor; + + _mainTextColor = color; + _mainText.SetTextColor(color.ToAndroid(defaultColorToSet)); + } + + public void SetRenderHeight(double height) + { + height = Context.ToPixels(height); + LayoutParameters = new LayoutParams(ViewGroup.LayoutParams.MatchParent, (int)(height == -1 ? ViewGroup.LayoutParams.WrapContent : height)); + } + + async void UpdateBitmap(ImageSource source, ImageSource previousSource = null) + { + if (Equals(source, previousSource)) + return; + + _imageView.SetImageResource(global::Android.Resource.Color.Transparent); + + Bitmap bitmap = null; + + IImageSourceHandler handler; + + if (source != null && (handler = Registrar.Registered.GetHandler<IImageSourceHandler>(source.GetType())) != null) + { + try + { + bitmap = await handler.LoadImageAsync(source, Context); + } + catch (TaskCanceledException) + { + } + catch (IOException e) + { + } + } + + _imageView.SetImageBitmap(bitmap); + if (bitmap != null) + bitmap.Dispose(); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Cells/CellFactory.cs b/Xamarin.Forms.Platform.Android/Cells/CellFactory.cs new file mode 100644 index 00000000..589236c3 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Cells/CellFactory.cs @@ -0,0 +1,41 @@ +using Android.Content; +using Android.Views; +using AView = Android.Views.View; +using AListView = Android.Widget.ListView; + +namespace Xamarin.Forms.Platform.Android +{ + public static class CellFactory + { + public static AView GetCell(Cell item, AView convertView, ViewGroup parent, Context context, View view) + { + CellRenderer renderer = CellRenderer.GetRenderer(item); + if (renderer == null) + { + renderer = Registrar.Registered.GetHandler<CellRenderer>(item.GetType()); + renderer.ParentView = view; + } + + AView result = renderer.GetCell(item, convertView, parent, context); + + if (view is TableView) + UpdateMinimumHeightFromParent(context, result, (TableView)view); + else if (view is ListView) + UpdateMinimumHeightFromParent(context, result, (ListView)view); + + return result; + } + + static void UpdateMinimumHeightFromParent(Context context, AView view, TableView table) + { + if (!table.HasUnevenRows && table.RowHeight > 0) + view.SetMinimumHeight((int)context.ToPixels(table.RowHeight)); + } + + static void UpdateMinimumHeightFromParent(Context context, AView view, ListView listView) + { + if (!listView.HasUnevenRows && listView.RowHeight > 0) + view.SetMinimumHeight((int)context.ToPixels(listView.RowHeight)); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Cells/CellRenderer.cs b/Xamarin.Forms.Platform.Android/Cells/CellRenderer.cs new file mode 100644 index 00000000..d9d4eee5 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Cells/CellRenderer.cs @@ -0,0 +1,131 @@ +using System; +using System.ComponentModel; +using Android.Content; +using Android.Views; +using Android.Widget; +using AView = Android.Views.View; +using Object = Java.Lang.Object; + +namespace Xamarin.Forms.Platform.Android +{ + public class CellRenderer : IRegisterable + { + static readonly PropertyChangedEventHandler PropertyChangedHandler = OnGlobalCellPropertyChanged; + + static readonly BindableProperty RendererProperty = BindableProperty.CreateAttached("Renderer", typeof(CellRenderer), typeof(Cell), null); + + EventHandler _onForceUpdateSizeRequested; + + public View ParentView { get; set; } + + protected Cell Cell { get; set; } + + public AView GetCell(Cell item, AView convertView, ViewGroup parent, Context context) + { + Performance.Start(); + + Cell = item; + Cell.PropertyChanged -= PropertyChangedHandler; + + SetRenderer(Cell, this); + + if (convertView != null) + { + Object tag = convertView.Tag; + var renderHolder = tag as RendererHolder; + if (renderHolder != null) + { + Cell oldCell = renderHolder.Renderer.Cell; + oldCell.SendDisappearing(); + + if (Cell != oldCell) + SetRenderer(oldCell, null); + } + } + + AView view = GetCellCore(item, convertView, parent, context); + + WireUpForceUpdateSizeRequested(item, view); + + var holder = view.Tag as RendererHolder; + if (holder == null) + view.Tag = new RendererHolder { Renderer = this }; + else + holder.Renderer = this; + + Cell.PropertyChanged += PropertyChangedHandler; + Cell.SendAppearing(); + + Performance.Stop(); + + return view; + } + + protected virtual AView GetCellCore(Cell item, AView convertView, ViewGroup parent, Context context) + { + Performance.Start(); + + LayoutInflater inflater = LayoutInflater.FromContext(context); + const int type = global::Android.Resource.Layout.SimpleListItem1; + AView view = inflater.Inflate(type, null); + + var textView = view.FindViewById<TextView>(global::Android.Resource.Id.Text1); + textView.Text = item.ToString(); + textView.SetBackgroundColor(global::Android.Graphics.Color.Transparent); + view.SetBackgroundColor(global::Android.Graphics.Color.Black); + + Performance.Stop(); + + return view; + } + + protected virtual void OnCellPropertyChanged(object sender, PropertyChangedEventArgs e) + { + } + + protected void WireUpForceUpdateSizeRequested(Cell cell, AView nativeCell) + { + cell.ForceUpdateSizeRequested -= _onForceUpdateSizeRequested; + + _onForceUpdateSizeRequested = delegate + { + // RenderHeight may not be changed, but that's okay, since we + // don't actually use the height argument in the OnMeasure override. + nativeCell.Measure(nativeCell.Width, (int)cell.RenderHeight); + nativeCell.SetMinimumHeight(nativeCell.MeasuredHeight); + nativeCell.SetMinimumWidth(nativeCell.MeasuredWidth); + }; + + cell.ForceUpdateSizeRequested += _onForceUpdateSizeRequested; + } + + internal static CellRenderer GetRenderer(BindableObject cell) + { + return (CellRenderer)cell.GetValue(RendererProperty); + } + + internal static void SetRenderer(BindableObject cell, CellRenderer renderer) + { + cell.SetValue(RendererProperty, renderer); + } + + static void OnGlobalCellPropertyChanged(object sender, PropertyChangedEventArgs e) + { + var cell = (Cell)sender; + CellRenderer renderer = GetRenderer(cell); + if (renderer == null) + { + cell.PropertyChanged -= PropertyChangedHandler; + return; + } + + renderer.OnCellPropertyChanged(sender, e); + ; + } + + class RendererHolder : Object + { + public CellRenderer Renderer; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Cells/EntryCellEditText.cs b/Xamarin.Forms.Platform.Android/Cells/EntryCellEditText.cs new file mode 100644 index 00000000..287804ba --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Cells/EntryCellEditText.cs @@ -0,0 +1,45 @@ +using System; +using Android.App; +using Android.Content; +using Android.Graphics; +using Android.Views; +using Android.Widget; + +namespace Xamarin.Forms.Platform.Android +{ + public sealed class EntryCellEditText : EditText + { + SoftInput _startingMode; + + public EntryCellEditText(Context context) : base(context) + { + } + + public override bool OnKeyPreIme(Keycode keyCode, KeyEvent e) + { + if (keyCode == Keycode.Back && e.Action == KeyEventActions.Down) + { + EventHandler handler = BackButtonPressed; + if (handler != null) + handler(this, EventArgs.Empty); + } + return base.OnKeyPreIme(keyCode, e); + } + + protected override void OnFocusChanged(bool gainFocus, FocusSearchDirection direction, Rect previouslyFocusedRect) + { + Window window = ((Activity)Context).Window; + if (gainFocus) + { + _startingMode = window.Attributes.SoftInputMode; + window.SetSoftInputMode(SoftInput.AdjustPan); + } + else + window.SetSoftInputMode(_startingMode); + + base.OnFocusChanged(gainFocus, direction, previouslyFocusedRect); + } + + internal event EventHandler BackButtonPressed; + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Cells/EntryCellRenderer.cs b/Xamarin.Forms.Platform.Android/Cells/EntryCellRenderer.cs new file mode 100644 index 00000000..00330928 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Cells/EntryCellRenderer.cs @@ -0,0 +1,119 @@ +using System.ComponentModel; +using Android.Content; +using Android.Views; + +namespace Xamarin.Forms.Platform.Android +{ + public class EntryCellRenderer : CellRenderer + { + EntryCellView _view; + + protected override global::Android.Views.View GetCellCore(Cell item, global::Android.Views.View convertView, ViewGroup parent, Context context) + { + if ((_view = convertView as EntryCellView) == null) + _view = new EntryCellView(context, item); + else + { + _view.TextChanged = null; + _view.FocusChanged = null; + _view.EditingCompleted = null; + } + + UpdateLabel(); + UpdateLabelColor(); + UpdatePlaceholder(); + UpdateKeyboard(); + UpdateHorizontalTextAlignment(); + UpdateText(); + UpdateIsEnabled(); + UpdateHeight(); + + _view.TextChanged = OnTextChanged; + _view.EditingCompleted = OnEditingCompleted; + + return _view; + } + + protected override void OnCellPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnCellPropertyChanged(sender, e); + + if (e.PropertyName == EntryCell.LabelProperty.PropertyName) + UpdateLabel(); + else if (e.PropertyName == EntryCell.TextProperty.PropertyName) + UpdateText(); + else if (e.PropertyName == EntryCell.PlaceholderProperty.PropertyName) + UpdatePlaceholder(); + else if (e.PropertyName == EntryCell.KeyboardProperty.PropertyName) + UpdateKeyboard(); + else if (e.PropertyName == EntryCell.LabelColorProperty.PropertyName) + UpdateLabelColor(); + else if (e.PropertyName == EntryCell.HorizontalTextAlignmentProperty.PropertyName) + UpdateHorizontalTextAlignment(); + else if (e.PropertyName == Cell.IsEnabledProperty.PropertyName) + UpdateIsEnabled(); + else if (e.PropertyName == "RenderHeight") + UpdateHeight(); + } + + void OnEditingCompleted() + { + var entryCell = (EntryCell)Cell; + entryCell.SendCompleted(); + } + + void OnTextChanged(string text) + { + var entryCell = (EntryCell)Cell; + entryCell.Text = text; + } + + void UpdateHeight() + { + _view.SetRenderHeight(Cell.RenderHeight); + } + + void UpdateHorizontalTextAlignment() + { + var entryCell = (EntryCell)Cell; + _view.EditText.Gravity = entryCell.HorizontalTextAlignment.ToHorizontalGravityFlags(); + } + + void UpdateIsEnabled() + { + var entryCell = (EntryCell)Cell; + _view.EditText.Enabled = entryCell.IsEnabled; + } + + void UpdateKeyboard() + { + var entryCell = (EntryCell)Cell; + _view.EditText.InputType = entryCell.Keyboard.ToInputType(); + } + + void UpdateLabel() + { + _view.LabelText = ((EntryCell)Cell).Label; + } + + void UpdateLabelColor() + { + _view.SetLabelTextColor(((EntryCell)Cell).LabelColor, global::Android.Resource.Color.PrimaryTextDark); + } + + void UpdatePlaceholder() + { + var entryCell = (EntryCell)Cell; + _view.EditText.Hint = entryCell.Placeholder; + } + + void UpdateText() + { + var entryCell = (EntryCell)Cell; + if (_view.EditText.Text == entryCell.Text) + return; + + _view.EditText.Text = entryCell.Text; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Cells/EntryCellView.cs b/Xamarin.Forms.Platform.Android/Cells/EntryCellView.cs new file mode 100644 index 00000000..c0206f1e --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Cells/EntryCellView.cs @@ -0,0 +1,145 @@ +using System; +using Android.Content; +using Android.Text; +using Android.Views; +using Android.Views.InputMethods; +using Android.Widget; +using Java.Lang; + +namespace Xamarin.Forms.Platform.Android +{ + public sealed class EntryCellView : LinearLayout, ITextWatcher, global::Android.Views.View.IOnFocusChangeListener, TextView.IOnEditorActionListener, INativeElementView + { + public const double DefaultMinHeight = 55; + + readonly Cell _cell; + readonly TextView _label; + + Color _labelTextColor; + string _labelTextText; + + public EntryCellView(Context context, Cell cell) : base(context) + { + _cell = cell; + SetMinimumWidth((int)context.ToPixels(50)); + SetMinimumHeight((int)context.ToPixels(85)); + Orientation = Orientation.Horizontal; + + var padding = (int)context.ToPixels(8); + SetPadding((int)context.ToPixels(15), padding, padding, padding); + + _label = new TextView(context); + _label.SetTextAppearance(context, global::Android.Resource.Attribute.TextAppearanceListItem); + + var layoutParams = new LayoutParams(ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.WrapContent) { Gravity = GravityFlags.CenterVertical }; + using(layoutParams) + AddView(_label, layoutParams); + + EditText = new EntryCellEditText(context); + EditText.AddTextChangedListener(this); + EditText.OnFocusChangeListener = this; + EditText.SetOnEditorActionListener(this); + EditText.ImeOptions = ImeAction.Done; + EditText.BackButtonPressed += OnBackButtonPressed; + //editText.SetBackgroundDrawable (null); + layoutParams = new LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.WrapContent) { Width = 0, Weight = 1, Gravity = GravityFlags.FillHorizontal | GravityFlags.Center }; + using(layoutParams) + AddView(EditText, layoutParams); + } + + public Action EditingCompleted { get; set; } + + public EntryCellEditText EditText { get; } + + public Action<bool> FocusChanged { get; set; } + + public string LabelText + { + get { return _labelTextText; } + set + { + if (_labelTextText == value) + return; + + _labelTextText = value; + _label.Text = value; + } + } + + public Action<string> TextChanged { get; set; } + + public Element Element + { + get { return _cell; } + } + + bool TextView.IOnEditorActionListener.OnEditorAction(TextView v, ImeAction actionId, KeyEvent e) + { + if (actionId == ImeAction.Done) + { + OnKeyboardDoneButtonPressed(EditText, EventArgs.Empty); + EditText.ClearFocus(); + v.HideKeyboard(); + } + + // Fire Completed and dismiss keyboard for hardware / physical keyboards + if (actionId == ImeAction.ImeNull && e.KeyCode == Keycode.Enter) + { + OnKeyboardDoneButtonPressed(EditText, EventArgs.Empty); + EditText.ClearFocus(); + v.HideKeyboard(); + } + + return true; + } + + void IOnFocusChangeListener.OnFocusChange(global::Android.Views.View view, bool hasFocus) + { + Action<bool> focusChanged = FocusChanged; + if (focusChanged != null) + focusChanged(hasFocus); + } + + void ITextWatcher.AfterTextChanged(IEditable s) + { + } + + void ITextWatcher.BeforeTextChanged(ICharSequence s, int start, int count, int after) + { + } + + void ITextWatcher.OnTextChanged(ICharSequence s, int start, int before, int count) + { + Action<string> changed = TextChanged; + if (changed != null) + changed(s != null ? s.ToString() : null); + } + + public void SetLabelTextColor(Color color, int defaultColorResourceId) + { + if (_labelTextColor == color) + return; + + _labelTextColor = color; + _label.SetTextColor(color.ToAndroid(defaultColorResourceId)); + } + + public void SetRenderHeight(double height) + { + SetMinimumHeight((int)Context.ToPixels(height == -1 ? DefaultMinHeight : height)); + } + + void OnBackButtonPressed(object sender, EventArgs e) + { + // TODO Clear focus + } + + void OnKeyboardDoneButtonPressed(object sender, EventArgs e) + { + // TODO Clear focus + Action editingCompleted = EditingCompleted; + if (editingCompleted != null) + editingCompleted(); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Cells/ImageCellRenderer.cs b/Xamarin.Forms.Platform.Android/Cells/ImageCellRenderer.cs new file mode 100644 index 00000000..62815ed6 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Cells/ImageCellRenderer.cs @@ -0,0 +1,37 @@ +using System.ComponentModel; +using Android.Content; +using Android.Views; + +namespace Xamarin.Forms.Platform.Android +{ + public class ImageCellRenderer : TextCellRenderer + { + protected override global::Android.Views.View GetCellCore(Cell item, global::Android.Views.View convertView, ViewGroup parent, Context context) + { + var result = (BaseCellView)base.GetCellCore(item, convertView, parent, context); + + UpdateImage(); + + return result; + } + + protected override void OnCellPropertyChanged(object sender, PropertyChangedEventArgs args) + { + base.OnCellPropertyChanged(sender, args); + if (args.PropertyName == ImageCell.ImageSourceProperty.PropertyName) + UpdateImage(); + } + + void UpdateImage() + { + var cell = (ImageCell)Cell; + if (cell.ImageSource != null) + { + View.SetImageVisible(true); + View.SetImageSource(cell.ImageSource); + } + else + View.SetImageVisible(false); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Cells/SwitchCellRenderer.cs b/Xamarin.Forms.Platform.Android/Cells/SwitchCellRenderer.cs new file mode 100644 index 00000000..bba9d260 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Cells/SwitchCellRenderer.cs @@ -0,0 +1,55 @@ +using System.ComponentModel; +using Android.Content; +using Android.Views; +using AView = Android.Views.View; +using ASwitch = Android.Widget.Switch; + +namespace Xamarin.Forms.Platform.Android +{ + public class SwitchCellRenderer : CellRenderer + { + const double DefaultHeight = 30; + SwitchCellView _view; + + protected override AView GetCellCore(Cell item, AView convertView, ViewGroup parent, Context context) + { + var cell = (SwitchCell)Cell; + + if ((_view = convertView as SwitchCellView) == null) + _view = new SwitchCellView(context, item); + + _view.Cell = cell; + + UpdateText(); + UpdateChecked(); + UpdateHeight(); + + return _view; + } + + protected override void OnCellPropertyChanged(object sender, PropertyChangedEventArgs args) + { + if (args.PropertyName == SwitchCell.TextProperty.PropertyName) + UpdateText(); + else if (args.PropertyName == SwitchCell.OnProperty.PropertyName) + UpdateChecked(); + else if (args.PropertyName == "RenderHeight") + UpdateHeight(); + } + + void UpdateChecked() + { + ((ASwitch)_view.AccessoryView).Checked = ((SwitchCell)Cell).On; + } + + void UpdateHeight() + { + _view.SetRenderHeight(Cell.RenderHeight); + } + + void UpdateText() + { + _view.MainText = ((SwitchCell)Cell).Text; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Cells/SwitchCellView.cs b/Xamarin.Forms.Platform.Android/Cells/SwitchCellView.cs new file mode 100644 index 00000000..f15c41b0 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Cells/SwitchCellView.cs @@ -0,0 +1,25 @@ +using Android.Content; +using Android.Widget; + +namespace Xamarin.Forms.Platform.Android +{ + public class SwitchCellView : BaseCellView, CompoundButton.IOnCheckedChangeListener + { + public SwitchCellView(Context context, Cell cell) : base(context, cell) + { + var sw = new global::Android.Widget.Switch(context); + sw.SetOnCheckedChangeListener(this); + + SetAccessoryView(sw); + + SetImageVisible(false); + } + + public SwitchCell Cell { get; set; } + + public void OnCheckedChanged(CompoundButton buttonView, bool isChecked) + { + Cell.On = isChecked; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Cells/TextCellRenderer.cs b/Xamarin.Forms.Platform.Android/Cells/TextCellRenderer.cs new file mode 100644 index 00000000..cbaaa2b7 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Cells/TextCellRenderer.cs @@ -0,0 +1,78 @@ +using System.ComponentModel; +using Android.Content; +using Android.Views; +using AView = Android.Views.View; +using AColor = Android.Graphics.Color; + +namespace Xamarin.Forms.Platform.Android +{ + public class TextCellRenderer : CellRenderer + { + internal TextCellView View { get; private set; } + + protected override AView GetCellCore(Cell item, AView convertView, ViewGroup parent, Context context) + { + if ((View = convertView as TextCellView) == null) + View = new TextCellView(context, item); + + UpdateMainText(); + UpdateDetailText(); + UpdateHeight(); + UpdateIsEnabled(); + View.SetImageVisible(false); + + return View; + } + + protected override void OnCellPropertyChanged(object sender, PropertyChangedEventArgs args) + { + if (args.PropertyName == TextCell.TextProperty.PropertyName || args.PropertyName == TextCell.TextColorProperty.PropertyName) + UpdateMainText(); + else if (args.PropertyName == TextCell.DetailProperty.PropertyName || args.PropertyName == TextCell.DetailColorProperty.PropertyName) + UpdateDetailText(); + else if (args.PropertyName == Cell.IsEnabledProperty.PropertyName) + UpdateIsEnabled(); + else if (args.PropertyName == "RenderHeight") + UpdateHeight(); + } + + void UpdateDetailText() + { + var cell = (TextCell)Cell; + View.DetailText = cell.Detail; + View.SetDetailTextColor(cell.DetailColor); + } + + void UpdateHeight() + { + View.SetRenderHeight(Cell.RenderHeight); + } + + void UpdateIsEnabled() + { + var cell = (TextCell)Cell; + View.SetIsEnabled(cell.IsEnabled); + } + + void UpdateMainText() + { + var cell = (TextCell)Cell; + View.MainText = cell.Text; + + if (!TemplatedItemsList<ItemsView<Cell>, Cell>.GetIsGroupHeader(cell)) + View.SetDefaultMainTextColor(Color.Accent); + else + View.SetDefaultMainTextColor(Color.Default); + + View.SetMainTextColor(cell.TextColor); + } + + // ensure we don't get other people's BaseCellView's + internal class TextCellView : BaseCellView + { + public TextCellView(Context context, Cell cell) : base(context, cell) + { + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Cells/ViewCellRenderer.cs b/Xamarin.Forms.Platform.Android/Cells/ViewCellRenderer.cs new file mode 100644 index 00000000..4bf83395 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Cells/ViewCellRenderer.cs @@ -0,0 +1,178 @@ +using Android.Content; +using Android.Views; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android +{ + public class ViewCellRenderer : CellRenderer + { + protected override AView GetCellCore(Cell item, AView convertView, ViewGroup parent, Context context) + { + Performance.Start(); + var cell = (ViewCell)item; + + var container = convertView as ViewCellContainer; + if (container != null) + { + container.Update(cell); + Performance.Stop(); + return container; + } + + BindableProperty unevenRows = null, rowHeight = null; + if (ParentView is TableView) + { + unevenRows = TableView.HasUnevenRowsProperty; + rowHeight = TableView.RowHeightProperty; + } + else if (ParentView is ListView) + { + unevenRows = ListView.HasUnevenRowsProperty; + rowHeight = ListView.RowHeightProperty; + } + + IVisualElementRenderer view = Platform.CreateRenderer(cell.View); + Platform.SetRenderer(cell.View, view); + cell.View.IsPlatformEnabled = true; + var c = new ViewCellContainer(context, view, cell, ParentView, unevenRows, rowHeight); + + Performance.Stop(); + + return c; + } + + internal class ViewCellContainer : ViewGroup, INativeElementView + { + readonly View _parent; + readonly BindableProperty _rowHeight; + readonly BindableProperty _unevenRows; + IVisualElementRenderer _view; + ViewCell _viewCell; + + public ViewCellContainer(Context context, IVisualElementRenderer view, ViewCell viewCell, View parent, BindableProperty unevenRows, BindableProperty rowHeight) : base(context) + { + _view = view; + _parent = parent; + _unevenRows = unevenRows; + _rowHeight = rowHeight; + _viewCell = viewCell; + AddView(view.ViewGroup); + UpdateIsEnabled(); + } + + protected bool ParentHasUnevenRows + { + get { return (bool)_parent.GetValue(_unevenRows); } + } + + protected int ParentRowHeight + { + get { return (int)_parent.GetValue(_rowHeight); } + } + + public Element Element + { + get { return _viewCell; } + } + + public override bool OnInterceptTouchEvent(MotionEvent ev) + { + if (!Enabled) + return true; + return base.OnInterceptTouchEvent(ev); + } + + public void Update(ViewCell cell) + { + Performance.Start(); + + var renderer = GetChildAt(0) as IVisualElementRenderer; + var viewHandlerType = Registrar.Registered.GetHandlerType(cell.View.GetType()) ?? typeof(Platform.DefaultRenderer); + if (renderer != null && renderer.GetType() == viewHandlerType) + { + Performance.Start("Reuse"); + _viewCell = cell; + + cell.View.DisableLayout = true; + foreach (VisualElement c in cell.View.Descendants()) + c.DisableLayout = true; + + Performance.Start("Reuse.SetElement"); + renderer.SetElement(cell.View); + Performance.Stop("Reuse.SetElement"); + + Platform.SetRenderer(cell.View, _view); + + cell.View.DisableLayout = false; + foreach (VisualElement c in cell.View.Descendants()) + c.DisableLayout = false; + + var viewAsLayout = cell.View as Layout; + if (viewAsLayout != null) + viewAsLayout.ForceLayout(); + + Invalidate(); + + Performance.Stop("Reuse"); + Performance.Stop(); + return; + } + + RemoveView(_view.ViewGroup); + Platform.SetRenderer(_viewCell.View, null); + _viewCell.View.IsPlatformEnabled = false; + _view.ViewGroup.Dispose(); + + _viewCell = cell; + _view = Platform.CreateRenderer(_viewCell.View); + + Platform.SetRenderer(_viewCell.View, _view); + AddView(_view.ViewGroup); + + UpdateIsEnabled(); + + Performance.Stop(); + } + + public void UpdateIsEnabled() + { + Enabled = _viewCell.IsEnabled; + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + Performance.Start(); + + double width = Context.FromPixels(r - l); + double height = Context.FromPixels(b - t); + + Performance.Start("Element.Layout"); + Xamarin.Forms.Layout.LayoutChildIntoBoundingRegion(_view.Element, new Rectangle(0, 0, width, height)); + Performance.Stop("Element.Layout"); + + _view.UpdateLayout(); + Performance.Stop(); + } + + protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + Performance.Start(); + + int width = MeasureSpec.GetSize(widthMeasureSpec); + int height; + + if (ParentHasUnevenRows) + { + SizeRequest measure = _view.Element.Measure(Context.FromPixels(width), double.PositiveInfinity, MeasureFlags.IncludeMargins); + height = (int)Context.ToPixels(_viewCell.Height > 0 ? _viewCell.Height : measure.Request.Height); + } + else + height = (int)Context.ToPixels(ParentRowHeight == -1 ? BaseCellView.DefaultMinHeight : ParentRowHeight); + + SetMeasuredDimension(width, height); + + Performance.Stop(); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/ColorExtensions.cs b/Xamarin.Forms.Platform.Android/ColorExtensions.cs new file mode 100644 index 00000000..afe7286d --- /dev/null +++ b/Xamarin.Forms.Platform.Android/ColorExtensions.cs @@ -0,0 +1,40 @@ +using Android.Content.Res; +using AColor = Android.Graphics.Color; + +namespace Xamarin.Forms.Platform.Android +{ + public static class ColorExtensions + { + static readonly int[][] ColorStates = { new[] { global::Android.Resource.Attribute.StateEnabled }, new[] { -global::Android.Resource.Attribute.StateEnabled } }; + + public static AColor ToAndroid(this Color self) + { + return new AColor((byte)(byte.MaxValue * self.R), (byte)(byte.MaxValue * self.G), (byte)(byte.MaxValue * self.B), (byte)(byte.MaxValue * self.A)); + } + + public static AColor ToAndroid(this Color self, int defaultColorResourceId) + { + if (self == Color.Default) + { + using(Resources resources = Resources.System) + return resources.GetColor(defaultColorResourceId); + } + + return ToAndroid(self); + } + + public static AColor ToAndroid(this Color self, Color defaultColor) + { + if (self == Color.Default) + return defaultColor.ToAndroid(); + + return ToAndroid(self); + } + + public static ColorStateList ToAndroidPreserveDisabled(this Color color, ColorStateList defaults) + { + int disabled = defaults.GetColorForState(ColorStates[1], color.ToAndroid()); + return new ColorStateList(ColorStates, new[] { color.ToAndroid().ToArgb(), disabled }); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/ContextExtensions.cs b/Xamarin.Forms.Platform.Android/ContextExtensions.cs new file mode 100644 index 00000000..9a22c570 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/ContextExtensions.cs @@ -0,0 +1,58 @@ +using System; +using System.Runtime.CompilerServices; +using Android.Content; +using Android.Util; +using Android.Views.InputMethods; + +namespace Xamarin.Forms.Platform.Android +{ + public static class ContextExtensions + { + static float s_displayDensity = float.MinValue; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double FromPixels(this Context self, double pixels) + { + SetupMetrics(self); + + return pixels / s_displayDensity; + } + + public static void HideKeyboard(this Context self, global::Android.Views.View view) + { + var service = (InputMethodManager)self.GetSystemService(Context.InputMethodService); + service.HideSoftInputFromWindow(view.WindowToken, 0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float ToPixels(this Context self, double dp) + { + SetupMetrics(self); + + return (float)Math.Round(dp * s_displayDensity); + } + + internal static double GetThemeAttributeDp(this Context self, int resource) + { + using(var value = new TypedValue()) + { + if (!self.Theme.ResolveAttribute(resource, value, true)) + return -1; + + var pixels = (double)TypedValue.ComplexToDimension(value.Data, self.Resources.DisplayMetrics); + + return self.FromPixels(pixels); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void SetupMetrics(Context context) + { + if (s_displayDensity != float.MinValue) + return; + + using(DisplayMetrics metrics = context.Resources.DisplayMetrics) + s_displayDensity = metrics.Density; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Deserializer.cs b/Xamarin.Forms.Platform.Android/Deserializer.cs new file mode 100644 index 00000000..4c62ad1a --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Deserializer.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO.IsolatedStorage; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using System.Xml; + +namespace Xamarin.Forms.Platform.Android +{ + internal class Deserializer : IDeserializer + { + const string PropertyStoreFile = "PropertyStore.forms"; + + public Task<IDictionary<string, object>> DeserializePropertiesAsync() + { + // Deserialize property dictionary to local storage + // Make sure to use Internal + return Task.Run(() => + { + using(IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication()) + using(IsolatedStorageFileStream stream = store.OpenFile(PropertyStoreFile, System.IO.FileMode.OpenOrCreate)) + using(XmlDictionaryReader reader = XmlDictionaryReader.CreateBinaryReader(stream, XmlDictionaryReaderQuotas.Max)) + { + if (stream.Length == 0) + return null; + + try + { + var dcs = new DataContractSerializer(typeof(Dictionary<string, object>)); + return (IDictionary<string, object>)dcs.ReadObject(reader); + } + catch (Exception e) + { + Debug.WriteLine("Could not deserialize properties: " + e.Message); + Log.Warning("Xamarin.Forms PropertyStore", $"Exception while reading Application properties: {e}"); + } + } + + return null; + }); + } + + public Task SerializePropertiesAsync(IDictionary<string, object> properties) + { + properties = new Dictionary<string, object>(properties); + // Serialize property dictionary to local storage + // Make sure to use Internal + return Task.Run(() => + { + var success = false; + using(IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication()) + using(IsolatedStorageFileStream stream = store.OpenFile(PropertyStoreFile + ".tmp", System.IO.FileMode.OpenOrCreate)) + using(XmlDictionaryWriter writer = XmlDictionaryWriter.CreateBinaryWriter(stream)) + { + try + { + var dcs = new DataContractSerializer(typeof(Dictionary<string, object>)); + dcs.WriteObject(writer, properties); + writer.Flush(); + success = true; + } + catch (Exception e) + { + Debug.WriteLine("Could not serialize properties: " + e.Message); + Log.Warning("Xamarin.Forms PropertyStore", $"Exception while writing Application properties: {e}"); + } + } + + if (!success) + return; + using(IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication()) + { + try + { + if (store.FileExists(PropertyStoreFile)) + store.DeleteFile(PropertyStoreFile); + store.MoveFile(PropertyStoreFile + ".tmp", PropertyStoreFile); + } + catch (Exception e) + { + Debug.WriteLine("Could not move new serialized property file over old: " + e.Message); + Log.Warning("Xamarin.Forms PropertyStore", $"Exception while writing Application properties: {e}"); + } + } + }); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/ElementChangedEventArgs.cs b/Xamarin.Forms.Platform.Android/ElementChangedEventArgs.cs new file mode 100644 index 00000000..cb529bd9 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/ElementChangedEventArgs.cs @@ -0,0 +1,17 @@ +using System; + +namespace Xamarin.Forms.Platform.Android +{ + public class ElementChangedEventArgs<TElement> : EventArgs where TElement : Element + { + public ElementChangedEventArgs(TElement oldElement, TElement newElement) + { + OldElement = oldElement; + NewElement = newElement; + } + + public TElement NewElement { get; private set; } + + public TElement OldElement { get; private set; } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/ExportCellAttribute.cs b/Xamarin.Forms.Platform.Android/ExportCellAttribute.cs new file mode 100644 index 00000000..96aec6cb --- /dev/null +++ b/Xamarin.Forms.Platform.Android/ExportCellAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace Xamarin.Forms +{ + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class ExportCellAttribute : HandlerAttribute + { + public ExportCellAttribute(Type handler, Type target) : base(handler, target) + { + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/ExportImageSourceHandlerAttribute.cs b/Xamarin.Forms.Platform.Android/ExportImageSourceHandlerAttribute.cs new file mode 100644 index 00000000..4c5c01fb --- /dev/null +++ b/Xamarin.Forms.Platform.Android/ExportImageSourceHandlerAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace Xamarin.Forms +{ + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class ExportImageSourceHandlerAttribute : HandlerAttribute + { + public ExportImageSourceHandlerAttribute(Type handler, Type target) : base(handler, target) + { + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/ExportRendererAttribute.cs b/Xamarin.Forms.Platform.Android/ExportRendererAttribute.cs new file mode 100644 index 00000000..bf18653b --- /dev/null +++ b/Xamarin.Forms.Platform.Android/ExportRendererAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace Xamarin.Forms +{ + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class ExportRendererAttribute : HandlerAttribute + { + public ExportRendererAttribute(Type handler, Type target) : base(handler, target) + { + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Extensions.cs b/Xamarin.Forms.Platform.Android/Extensions.cs new file mode 100644 index 00000000..c326555a --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Extensions.cs @@ -0,0 +1,40 @@ +using Android.Content.Res; +using Android.Views; + +namespace Xamarin.Forms.Platform.Android +{ + public static class Extensions + { + internal static IMenuItem FindMenuItemByNameOrIcon(this IMenu menu, string menuName, string iconName) + { + if (menu.Size() == 1) + return menu.GetItem(0); + + for (var i = 0; i < menu.Size(); i++) + { + IMenuItem menuItem = menu.GetItem(i); + if (menuItem.TitleFormatted != null && menuName == menuItem.TitleFormatted.ToString()) + return menuItem; + + if (!string.IsNullOrEmpty(iconName)) + { + // TODO : search by iconName + } + } + return null; + } + + internal static DeviceOrientation ToDeviceOrientation(this Orientation orientation) + { + switch (orientation) + { + case Orientation.Landscape: + return DeviceOrientation.Landscape; + case Orientation.Portrait: + return DeviceOrientation.Portrait; + default: + return DeviceOrientation.Other; + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Forms.cs b/Xamarin.Forms.Platform.Android/Forms.cs new file mode 100644 index 00000000..8eb50380 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Forms.cs @@ -0,0 +1,514 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.IsolatedStorage; +using System.Linq; +using System.Linq.Expressions; +using System.Net.Http; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Android.App; +using Android.Content; +using Android.Content.Res; +using Android.OS; +using Android.Util; +using Xamarin.Forms.Platform.Android; +using Resource = Android.Resource; +using Trace = System.Diagnostics.Trace; + +namespace Xamarin.Forms +{ + public static class Forms + { + const int TabletCrossover = 600; + + static bool? s_supportsProgress; + + static bool? s_isLollipopOrNewer; + + public static Context Context { get; internal set; } + + public static bool IsInitialized { get; private set; } + + internal static bool IsLollipopOrNewer + { + get + { + if (!s_isLollipopOrNewer.HasValue) + s_isLollipopOrNewer = (int)Build.VERSION.SdkInt >= 21; + return s_isLollipopOrNewer.Value; + } + } + + internal static bool SupportsProgress + { + get + { + var activity = Context as Activity; + if (!s_supportsProgress.HasValue) + { + int progressCircularId = Context.Resources.GetIdentifier("progress_circular", "id", "android"); + if (progressCircularId > 0 && activity != null) + s_supportsProgress = activity.FindViewById(progressCircularId) != null; + else + s_supportsProgress = true; + } + return s_supportsProgress.Value; + } + } + + internal static AndroidTitleBarVisibility TitleBarVisibility { get; private set; } + + // Provide backwards compat for Forms.Init and AndroidActivity + // Why is bundle a param if never used? + public static void Init(Context activity, Bundle bundle) + { + Assembly resourceAssembly = Assembly.GetCallingAssembly(); + SetupInit(activity, resourceAssembly); + } + + public static void Init(Context activity, Bundle bundle, Assembly resourceAssembly) + { + SetupInit(activity, resourceAssembly); + } + + public static void SetTitleBarVisibility(AndroidTitleBarVisibility visibility) + { + TitleBarVisibility = visibility; + } + + public static event EventHandler<ViewInitializedEventArgs> ViewInitialized; + + internal static void SendViewInitialized(this VisualElement self, global::Android.Views.View nativeView) + { + EventHandler<ViewInitializedEventArgs> viewInitialized = ViewInitialized; + if (viewInitialized != null) + viewInitialized(self, new ViewInitializedEventArgs { View = self, NativeView = nativeView }); + } + + static void SetupInit(Context activity, Assembly resourceAssembly) + { + Context = activity; + + ResourceManager.Init(resourceAssembly); + + // Detect if legacy device and use appropriate accent color + // Hardcoded because could not get color from the theme drawable + var sdkVersion = (int)Build.VERSION.SdkInt; + if (sdkVersion <= 10) + { + // legacy theme button pressed color + Color.Accent = Color.FromHex("#fffeaa0c"); + } + else + { + // Holo dark light blue + Color.Accent = Color.FromHex("#ff33b5e5"); + } + + if (!IsInitialized) + Log.Listeners.Add(new DelegateLogListener((c, m) => Trace.WriteLine(m, c))); + + Device.OS = TargetPlatform.Android; + Device.PlatformServices = new AndroidPlatformServices(); + + // use field and not property to avoid exception in getter + if (Device.info != null) + { + ((AndroidDeviceInfo)Device.info).Dispose(); + Device.info = null; + } + + // probably could be done in a better way + var deviceInfoProvider = activity as IDeviceInfoProvider; + if (deviceInfoProvider != null) + Device.Info = new AndroidDeviceInfo(deviceInfoProvider); + + var ticker = Ticker.Default as AndroidTicker; + if (ticker != null) + ticker.Dispose(); + Ticker.Default = new AndroidTicker(); + + if (!IsInitialized) + { + Registrar.RegisterAll(new[] { typeof(ExportRendererAttribute), typeof(ExportCellAttribute), typeof(ExportImageSourceHandlerAttribute) }); + } + + int minWidthDp = Context.Resources.Configuration.SmallestScreenWidthDp; + + Device.Idiom = minWidthDp >= TabletCrossover ? TargetIdiom.Tablet : TargetIdiom.Phone; + + if (ExpressionSearch.Default == null) + ExpressionSearch.Default = new AndroidExpressionSearch(); + + IsInitialized = true; + } + + class AndroidDeviceInfo : DeviceInfo + { + readonly IDeviceInfoProvider _formsActivity; + readonly Size _pixelScreenSize; + readonly double _scalingFactor; + + Orientation _previousOrientation = Orientation.Undefined; + + public AndroidDeviceInfo(IDeviceInfoProvider formsActivity) + { + _formsActivity = formsActivity; + CheckOrientationChanged(_formsActivity.Resources.Configuration.Orientation); + formsActivity.ConfigurationChanged += ConfigurationChanged; + + using(DisplayMetrics display = formsActivity.Resources.DisplayMetrics) + { + _scalingFactor = display.Density; + _pixelScreenSize = new Size(display.WidthPixels, display.HeightPixels); + ScaledScreenSize = new Size(_pixelScreenSize.Width / _scalingFactor, _pixelScreenSize.Width / _scalingFactor); + } + } + + public override Size PixelScreenSize + { + get { return _pixelScreenSize; } + } + + public override Size ScaledScreenSize { get; } + + public override double ScalingFactor + { + get { return _scalingFactor; } + } + + protected override void Dispose(bool disposing) + { + _formsActivity.ConfigurationChanged -= ConfigurationChanged; + base.Dispose(disposing); + } + + void CheckOrientationChanged(Orientation orientation) + { + if (!_previousOrientation.Equals(orientation)) + CurrentOrientation = orientation.ToDeviceOrientation(); + + _previousOrientation = orientation; + } + + void ConfigurationChanged(object sender, EventArgs e) + { + CheckOrientationChanged(_formsActivity.Resources.Configuration.Orientation); + } + } + + class AndroidExpressionSearch : ExpressionVisitor, IExpressionSearch + { + List<object> _results; + Type _targetType; + + public List<T> FindObjects<T>(Expression expression) where T : class + { + _results = new List<object>(); + _targetType = typeof(T); + Visit(expression); + return _results.Select(o => o as T).ToList(); + } + + protected override Expression VisitMember(MemberExpression node) + { + if (node.Expression is ConstantExpression && node.Member is FieldInfo) + { + object container = ((ConstantExpression)node.Expression).Value; + object value = ((FieldInfo)node.Member).GetValue(container); + + if (_targetType.IsInstanceOfType(value)) + _results.Add(value); + } + return base.VisitMember(node); + } + } + + class AndroidPlatformServices : IPlatformServices + { + static readonly MD5CryptoServiceProvider Checksum = new MD5CryptoServiceProvider(); + double _buttonDefaultSize; + double _editTextDefaultSize; + double _labelDefaultSize; + double _largeSize; + double _mediumSize; + + double _microSize; + double _smallSize; + + public void BeginInvokeOnMainThread(Action action) + { + var activity = Context as Activity; + if (activity != null) + activity.RunOnUiThread(action); + } + + public ITimer CreateTimer(Action<object> callback) + { + return new _Timer(new Timer(o => callback(o))); + } + + public ITimer CreateTimer(Action<object> callback, object state, int dueTime, int period) + { + return new _Timer(new Timer(o => callback(o), state, dueTime, period)); + } + + public ITimer CreateTimer(Action<object> callback, object state, long dueTime, long period) + { + return new _Timer(new Timer(o => callback(o), state, dueTime, period)); + } + + public ITimer CreateTimer(Action<object> callback, object state, TimeSpan dueTime, TimeSpan period) + { + return new _Timer(new Timer(o => callback(o), state, dueTime, period)); + } + + public ITimer CreateTimer(Action<object> callback, object state, uint dueTime, uint period) + { + return new _Timer(new Timer(o => callback(o), state, dueTime, period)); + } + + public Assembly[] GetAssemblies() + { + return AppDomain.CurrentDomain.GetAssemblies(); + } + + public string GetMD5Hash(string input) + { + byte[] bytes = Checksum.ComputeHash(Encoding.UTF8.GetBytes(input)); + var ret = new char[32]; + for (var i = 0; i < 16; i++) + { + ret[i * 2] = (char)Hex(bytes[i] >> 4); + ret[i * 2 + 1] = (char)Hex(bytes[i] & 0xf); + } + return new string(ret); + } + + public double GetNamedSize(NamedSize size, Type targetElementType, bool useOldSizes) + { + if (_smallSize == 0) + { + _smallSize = ConvertTextAppearanceToSize(Resource.Attribute.TextAppearanceSmall, Resource.Style.TextAppearanceDeviceDefaultSmall, 12); + _mediumSize = ConvertTextAppearanceToSize(Resource.Attribute.TextAppearanceMedium, Resource.Style.TextAppearanceDeviceDefaultMedium, 14); + _largeSize = ConvertTextAppearanceToSize(Resource.Attribute.TextAppearanceLarge, Resource.Style.TextAppearanceDeviceDefaultLarge, 18); + _buttonDefaultSize = ConvertTextAppearanceToSize(Resource.Attribute.TextAppearanceButton, Resource.Style.TextAppearanceDeviceDefaultWidgetButton, 14); + _editTextDefaultSize = ConvertTextAppearanceToSize(Resource.Style.TextAppearanceWidgetEditText, Resource.Style.TextAppearanceDeviceDefaultWidgetEditText, 18); + _labelDefaultSize = _smallSize; + // as decreed by the android docs, ALL HAIL THE ANDROID DOCS, ALL GLORY TO THE DOCS, PRAISE HYPNOTOAD + _microSize = Math.Max(1, _smallSize - (_mediumSize - _smallSize)); + } + + if (useOldSizes) + { + switch (size) + { + case NamedSize.Default: + if (typeof(Button).IsAssignableFrom(targetElementType)) + return _buttonDefaultSize; + if (typeof(Label).IsAssignableFrom(targetElementType)) + return _labelDefaultSize; + if (typeof(Editor).IsAssignableFrom(targetElementType) || typeof(Entry).IsAssignableFrom(targetElementType) || typeof(SearchBar).IsAssignableFrom(targetElementType)) + return _editTextDefaultSize; + return 14; + case NamedSize.Micro: + return 10; + case NamedSize.Small: + return 12; + case NamedSize.Medium: + return 14; + case NamedSize.Large: + return 18; + default: + throw new ArgumentOutOfRangeException("size"); + } + } + switch (size) + { + case NamedSize.Default: + if (typeof(Button).IsAssignableFrom(targetElementType)) + return _buttonDefaultSize; + if (typeof(Label).IsAssignableFrom(targetElementType)) + return _labelDefaultSize; + if (typeof(Editor).IsAssignableFrom(targetElementType) || typeof(Entry).IsAssignableFrom(targetElementType)) + return _editTextDefaultSize; + return _mediumSize; + case NamedSize.Micro: + return _microSize; + case NamedSize.Small: + return _smallSize; + case NamedSize.Medium: + return _mediumSize; + case NamedSize.Large: + return _largeSize; + default: + throw new ArgumentOutOfRangeException("size"); + } + } + + public async Task<Stream> GetStreamAsync(Uri uri, CancellationToken cancellationToken) + { + using(var client = new HttpClient()) + using(HttpResponseMessage response = await client.GetAsync(uri, cancellationToken)) + return await response.Content.ReadAsStreamAsync(); + } + + public IIsolatedStorageFile GetUserStoreForApplication() + { + return new _IsolatedStorageFile(IsolatedStorageFile.GetUserStoreForApplication()); + } + + public bool IsInvokeRequired + { + get + { + using(Looper my = Looper.MyLooper()) + using(Looper main = Looper.MainLooper) + return my != main; + } + } + + public void OpenUriAction(Uri uri) + { + global::Android.Net.Uri aUri = global::Android.Net.Uri.Parse(uri.ToString()); + var intent = new Intent(Intent.ActionView, aUri); + Context.StartActivity(intent); + } + + public void StartTimer(TimeSpan interval, Func<bool> callback) + { + Timer timer = null; + TimerCallback onTimeout = o => BeginInvokeOnMainThread(() => + { + if (callback()) + return; + + timer.Dispose(); + }); + timer = new Timer(onTimeout, null, interval, interval); + } + + double ConvertTextAppearanceToSize(int themeDefault, int deviceDefault, double defaultValue) + { + double myValue; + + if (TryGetTextAppearance(themeDefault, out myValue)) + return myValue; + if (TryGetTextAppearance(deviceDefault, out myValue)) + return myValue; + return defaultValue; + } + + static int Hex(int v) + { + if (v < 10) + return '0' + v; + return 'a' + v - 10; + } + + static bool TryGetTextAppearance(int appearance, out double val) + { + val = 0; + try + { + using(var value = new TypedValue()) + { + if (Context.Theme.ResolveAttribute(appearance, value, true)) + { + var textSizeAttr = new[] { Resource.Attribute.TextSize }; + const int indexOfAttrTextSize = 0; + using(TypedArray array = Context.ObtainStyledAttributes(value.Data, textSizeAttr)) + { + val = Context.FromPixels(array.GetDimensionPixelSize(indexOfAttrTextSize, -1)); + return true; + } + } + } + } + catch (Exception ex) + { + // Before you ask, yes, Exception. I know. But thats what android throws, new Exception... YAY BINDINGS + // log exception using insights if possible + } + return false; + } + + public class _Timer : ITimer + { + readonly Timer _timer; + + public _Timer(Timer timer) + { + _timer = timer; + } + + public void Change(int dueTime, int period) + { + _timer.Change(dueTime, period); + } + + public void Change(long dueTime, long period) + { + _timer.Change(dueTime, period); + } + + public void Change(TimeSpan dueTime, TimeSpan period) + { + _timer.Change(dueTime, period); + } + + public void Change(uint dueTime, uint period) + { + _timer.Change(dueTime, period); + } + } + + public class _IsolatedStorageFile : IIsolatedStorageFile + { + readonly IsolatedStorageFile _isolatedStorageFile; + + public _IsolatedStorageFile(IsolatedStorageFile isolatedStorageFile) + { + _isolatedStorageFile = isolatedStorageFile; + } + + public Task CreateDirectoryAsync(string path) + { + _isolatedStorageFile.CreateDirectory(path); + return Task.FromResult(true); + } + + public Task<bool> GetDirectoryExistsAsync(string path) + { + return Task.FromResult(_isolatedStorageFile.DirectoryExists(path)); + } + + public Task<bool> GetFileExistsAsync(string path) + { + return Task.FromResult(_isolatedStorageFile.FileExists(path)); + } + + public Task<DateTimeOffset> GetLastWriteTimeAsync(string path) + { + return Task.FromResult(_isolatedStorageFile.GetLastWriteTime(path)); + } + + public Task<Stream> OpenFileAsync(string path, FileMode mode, FileAccess access) + { + Stream stream = _isolatedStorageFile.OpenFile(path, (System.IO.FileMode)mode, (System.IO.FileAccess)access); + return Task.FromResult(stream); + } + + public Task<Stream> OpenFileAsync(string path, FileMode mode, FileAccess access, FileShare share) + { + Stream stream = _isolatedStorageFile.OpenFile(path, (System.IO.FileMode)mode, (System.IO.FileAccess)access, (System.IO.FileShare)share); + return Task.FromResult(stream); + } + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/FormsApplicationActivity.cs b/Xamarin.Forms.Platform.Android/FormsApplicationActivity.cs new file mode 100644 index 00000000..c05c79f6 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/FormsApplicationActivity.cs @@ -0,0 +1,319 @@ +using System; +using System.ComponentModel; +using System.Linq; +using Android.App; +using Android.Content; +using Android.Content.Res; +using Android.OS; +using Android.Views; +using Android.Widget; + +namespace Xamarin.Forms.Platform.Android +{ + public class FormsApplicationActivity : Activity, IDeviceInfoProvider, IStartActivityForResult + { + public delegate bool BackButtonPressedEventHandler(object sender, EventArgs e); + + readonly ConcurrentDictionary<int, Action<Result, Intent>> _activityResultCallbacks = new ConcurrentDictionary<int, Action<Result, Intent>>(); + + Application _application; + Platform _canvas; + AndroidApplicationLifecycleState _currentState; + LinearLayout _layout; + + int _nextActivityResultCallbackKey; + + AndroidApplicationLifecycleState _previousState; + + protected FormsApplicationActivity() + { + _previousState = AndroidApplicationLifecycleState.Uninitialized; + _currentState = AndroidApplicationLifecycleState.Uninitialized; + } + + public event EventHandler ConfigurationChanged; + + int IStartActivityForResult.RegisterActivityResultCallback(Action<Result, Intent> callback) + { + int requestCode = _nextActivityResultCallbackKey; + + while (!_activityResultCallbacks.TryAdd(requestCode, callback)) + { + _nextActivityResultCallbackKey += 1; + requestCode = _nextActivityResultCallbackKey; + } + + _nextActivityResultCallbackKey += 1; + + return requestCode; + } + + void IStartActivityForResult.UnregisterActivityResultCallback(int requestCode) + { + Action<Result, Intent> callback; + _activityResultCallbacks.TryRemove(requestCode, out callback); + } + + public static event BackButtonPressedEventHandler BackPressed; + + public override void OnBackPressed() + { + if (BackPressed != null && BackPressed(this, EventArgs.Empty)) + return; + base.OnBackPressed(); + } + + public override void OnConfigurationChanged(Configuration newConfig) + { + base.OnConfigurationChanged(newConfig); + EventHandler handler = ConfigurationChanged; + if (handler != null) + handler(this, new EventArgs()); + } + + // FIXME: THIS SHOULD NOT BE MANDATORY + // or + // This should be specified in an interface and formalized, perhaps even provide a stock AndroidActivity users + // can derive from to avoid having to do any work. + public override bool OnOptionsItemSelected(IMenuItem item) + { + if (item.ItemId == global::Android.Resource.Id.Home) + _canvas.SendHomeClicked(); + return base.OnOptionsItemSelected(item); + } + + public override bool OnPrepareOptionsMenu(IMenu menu) + { + _canvas.PrepareMenu(menu); + return base.OnPrepareOptionsMenu(menu); + } + + [Obsolete("Please use protected LoadApplication (Application app) instead")] + public void SetPage(Page page) + { + var application = new DefaultApplication { MainPage = page }; + LoadApplication(application); + } + + protected void LoadApplication(Application application) + { + if (application == null) + throw new ArgumentNullException("application"); + + _application = application; + Xamarin.Forms.Application.Current = application; + + application.PropertyChanged += AppOnPropertyChanged; + + SetMainPage(); + } + + protected override void OnActivityResult(int requestCode, Result resultCode, Intent data) + { + base.OnActivityResult(requestCode, resultCode, data); + + Action<Result, Intent> callback; + + if (_activityResultCallbacks.TryGetValue(requestCode, out callback)) + callback(resultCode, data); + } + + protected override void OnCreate(Bundle savedInstanceState) + { + Window.RequestFeature(WindowFeatures.IndeterminateProgress); + + base.OnCreate(savedInstanceState); + + _layout = new LinearLayout(BaseContext); + SetContentView(_layout); + + Xamarin.Forms.Application.ClearCurrent(); + + _previousState = _currentState; + _currentState = AndroidApplicationLifecycleState.OnCreate; + + OnStateChanged(); + } + + protected override void OnDestroy() + { + // may never be called + base.OnDestroy(); + + MessagingCenter.Unsubscribe<Page, AlertArguments>(this, Page.AlertSignalName); + MessagingCenter.Unsubscribe<Page, bool>(this, Page.BusySetSignalName); + MessagingCenter.Unsubscribe<Page, ActionSheetArguments>(this, Page.ActionSheetSignalName); + + if (_canvas != null) + ((IDisposable)_canvas).Dispose(); + } + + protected override void OnPause() + { + _layout.HideKeyboard(true); + + // Stop animations or other ongoing actions that could consume CPU + // Commit unsaved changes, build only if users expect such changes to be permanently saved when thy leave such as a draft email + // Release system resources, such as broadcast receivers, handles to sensors (like GPS), or any resources that may affect battery life when your activity is paused. + // Avoid writing to permanent storage and CPU intensive tasks + base.OnPause(); + + _previousState = _currentState; + _currentState = AndroidApplicationLifecycleState.OnPause; + + OnStateChanged(); + } + + protected override void OnRestart() + { + base.OnRestart(); + + _previousState = _currentState; + _currentState = AndroidApplicationLifecycleState.OnRestart; + + OnStateChanged(); + } + + protected override void OnResume() + { + // counterpart to OnPause + base.OnResume(); + + _previousState = _currentState; + _currentState = AndroidApplicationLifecycleState.OnResume; + + OnStateChanged(); + } + + protected override void OnStart() + { + base.OnStart(); + + _previousState = _currentState; + _currentState = AndroidApplicationLifecycleState.OnStart; + + OnStateChanged(); + } + + // Scenarios that stop and restart you app + // -- Switches from your app to another app, activity restarts when clicking on the app again. + // -- Action in your app that starts a new Activity, the current activity is stopped and the second is created, pressing back restarts the activity + // -- The user recieves a phone call while using your app on his or her phone + protected override void OnStop() + { + // writing to storage happens here! + // full UI obstruction + // users focus in another activity + // perform heavy load shutdown operations + // clean up resources + // clean up everything that may leak memory + base.OnStop(); + + _previousState = _currentState; + _currentState = AndroidApplicationLifecycleState.OnStop; + + OnStateChanged(); + } + + void AppOnPropertyChanged(object sender, PropertyChangedEventArgs args) + { + if (args.PropertyName == "MainPage") + InternalSetPage(_application.MainPage); + } + + void InternalSetPage(Page page) + { + if (!Forms.IsInitialized) + throw new InvalidOperationException("Call Forms.Init (Activity, Bundle) before this"); + + if (_canvas != null) + { + _canvas.SetPage(page); + return; + } + + var busyCount = 0; + MessagingCenter.Subscribe(this, Page.BusySetSignalName, (Page sender, bool enabled) => + { + busyCount = Math.Max(0, enabled ? busyCount + 1 : busyCount - 1); + + if (!Forms.SupportsProgress) + return; + + SetProgressBarIndeterminate(true); + UpdateProgressBarVisibility(busyCount > 0); + }); + + UpdateProgressBarVisibility(busyCount > 0); + + MessagingCenter.Subscribe(this, Page.AlertSignalName, (Page sender, AlertArguments arguments) => + { + AlertDialog alert = new AlertDialog.Builder(this).Create(); + alert.SetTitle(arguments.Title); + alert.SetMessage(arguments.Message); + if (arguments.Accept != null) + alert.SetButton((int)DialogButtonType.Positive, arguments.Accept, (o, args) => arguments.SetResult(true)); + alert.SetButton((int)DialogButtonType.Negative, arguments.Cancel, (o, args) => arguments.SetResult(false)); + alert.CancelEvent += (o, args) => { arguments.SetResult(false); }; + alert.Show(); + }); + + MessagingCenter.Subscribe(this, Page.ActionSheetSignalName, (Page sender, ActionSheetArguments arguments) => + { + var builder = new AlertDialog.Builder(this); + builder.SetTitle(arguments.Title); + string[] items = arguments.Buttons.ToArray(); + builder.SetItems(items, (sender2, args) => { arguments.Result.TrySetResult(items[args.Which]); }); + + if (arguments.Cancel != null) + builder.SetPositiveButton(arguments.Cancel, delegate { arguments.Result.TrySetResult(arguments.Cancel); }); + + if (arguments.Destruction != null) + builder.SetNegativeButton(arguments.Destruction, delegate { arguments.Result.TrySetResult(arguments.Destruction); }); + + AlertDialog dialog = builder.Create(); + builder.Dispose(); + //to match current functionality of renderer we set cancelable on outside + //and return null + dialog.SetCanceledOnTouchOutside(true); + dialog.CancelEvent += (sender3, e) => { arguments.SetResult(null); }; + dialog.Show(); + }); + + _canvas = new Platform(this); + if (_application != null) + _application.Platform = _canvas; + _canvas.SetPage(page); + _layout.AddView(_canvas.GetViewGroup()); + } + + void OnStateChanged() + { + if (_application == null) + return; + + if (_previousState == AndroidApplicationLifecycleState.OnCreate && _currentState == AndroidApplicationLifecycleState.OnStart) + _application.SendStart(); + else if (_previousState == AndroidApplicationLifecycleState.OnStop && _currentState == AndroidApplicationLifecycleState.OnRestart) + _application.SendResume(); + else if (_previousState == AndroidApplicationLifecycleState.OnPause && _currentState == AndroidApplicationLifecycleState.OnStop) + _application.SendSleepAsync().Wait(); + } + + void SetMainPage() + { + InternalSetPage(_application.MainPage); + } + + void UpdateProgressBarVisibility(bool isBusy) + { + if (!Forms.SupportsProgress) + return; + SetProgressBarIndeterminateVisibility(isBusy); + } + + internal class DefaultApplication : Application + { + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/GenericMenuClickListener.cs b/Xamarin.Forms.Platform.Android/GenericMenuClickListener.cs new file mode 100644 index 00000000..8a6dd3b8 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/GenericMenuClickListener.cs @@ -0,0 +1,22 @@ +using System; +using Android.Views; +using Object = Java.Lang.Object; + +namespace Xamarin.Forms.Platform.Android +{ + internal class GenericMenuClickListener : Object, IMenuItemOnMenuItemClickListener + { + readonly Action _callback; + + public GenericMenuClickListener(Action callback) + { + _callback = callback; + } + + public bool OnMenuItemClick(IMenuItem item) + { + _callback(); + return true; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/GetDesiredSizeDelegate.cs b/Xamarin.Forms.Platform.Android/GetDesiredSizeDelegate.cs new file mode 100644 index 00000000..11f77ebf --- /dev/null +++ b/Xamarin.Forms.Platform.Android/GetDesiredSizeDelegate.cs @@ -0,0 +1,4 @@ +namespace Xamarin.Forms.Platform.Android +{ + public delegate SizeRequest? GetDesiredSizeDelegate(NativeViewWrapperRenderer renderer, int widthConstraint, int heightConstraint); +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/IDeviceInfoProvider.cs b/Xamarin.Forms.Platform.Android/IDeviceInfoProvider.cs new file mode 100644 index 00000000..c9810b16 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/IDeviceInfoProvider.cs @@ -0,0 +1,12 @@ +using System; +using Android.Content.Res; + +namespace Xamarin.Forms.Platform.Android +{ + public interface IDeviceInfoProvider + { + Resources Resources { get; } + + event EventHandler ConfigurationChanged; + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/IPlatformLayout.cs b/Xamarin.Forms.Platform.Android/IPlatformLayout.cs new file mode 100644 index 00000000..af30fc92 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/IPlatformLayout.cs @@ -0,0 +1,7 @@ +namespace Xamarin.Forms.Platform.Android +{ + internal interface IPlatformLayout + { + void OnLayout(bool changed, int l, int t, int r, int b); + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/IStartActivityForResult.cs b/Xamarin.Forms.Platform.Android/IStartActivityForResult.cs new file mode 100644 index 00000000..71de9793 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/IStartActivityForResult.cs @@ -0,0 +1,14 @@ +using System; +using Android.App; +using Android.Content; +using Android.OS; + +namespace Xamarin.Forms.Platform.Android +{ + internal interface IStartActivityForResult + { + int RegisterActivityResultCallback(Action<Result, Intent> callback); + void StartActivityForResult(Intent intent, int requestCode, Bundle options = null); + void UnregisterActivityResultCallback(int requestCode); + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/IVisualElementRenderer.cs b/Xamarin.Forms.Platform.Android/IVisualElementRenderer.cs new file mode 100644 index 00000000..99393516 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/IVisualElementRenderer.cs @@ -0,0 +1,22 @@ +using System; +using Android.Views; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android +{ + public interface IVisualElementRenderer : IRegisterable, IDisposable + { + VisualElement Element { get; } + + VisualElementTracker Tracker { get; } + + ViewGroup ViewGroup { get; } + + event EventHandler<VisualElementChangedEventArgs> ElementChanged; + + SizeRequest GetDesiredSize(int widthConstraint, int heightConstraint); + + void SetElement(VisualElement element); + void UpdateLayout(); + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/InnerGestureListener.cs b/Xamarin.Forms.Platform.Android/InnerGestureListener.cs new file mode 100644 index 00000000..f83a1ac7 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/InnerGestureListener.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Android.Runtime; +using Android.Views; +using Object = Java.Lang.Object; + +namespace Xamarin.Forms.Platform.Android +{ + internal class InnerGestureListener : Object, GestureDetector.IOnGestureListener, GestureDetector.IOnDoubleTapListener + { + bool _isScrolling; + Func<bool> _scrollCompleteDelegate; + Func<float, float, int, bool> _scrollDelegate; + Func<int, bool> _scrollStartedDelegate; + Func<int, bool> _tapDelegate; + Func<int, IEnumerable<TapGestureRecognizer>> _tapGestureRecognizers; + + public InnerGestureListener(Func<int, bool> tapDelegate, Func<int, IEnumerable<TapGestureRecognizer>> tapGestureRecognizers, Func<float, float, int, bool> scrollDelegate, + Func<int, bool> scrollStartedDelegate, Func<bool> scrollCompleteDelegate) + { + if (tapDelegate == null) + throw new ArgumentNullException("tapDelegate"); + if (tapGestureRecognizers == null) + throw new ArgumentNullException("tapGestureRecognizers"); + if (scrollDelegate == null) + throw new ArgumentNullException("scrollDelegate"); + if (scrollStartedDelegate == null) + throw new ArgumentNullException("scrollStartedDelegate"); + if (scrollCompleteDelegate == null) + throw new ArgumentNullException("scrollCompleteDelegate"); + + _tapDelegate = tapDelegate; + _tapGestureRecognizers = tapGestureRecognizers; + _scrollDelegate = scrollDelegate; + _scrollStartedDelegate = scrollStartedDelegate; + _scrollCompleteDelegate = scrollCompleteDelegate; + } + + // This is needed because GestureRecognizer callbacks can be delayed several hundred milliseconds + // which can result in the need to resurect this object if it has already been disposed. We dispose + // eagerly to allow easier garbage collection of the renderer + internal InnerGestureListener(IntPtr handle, JniHandleOwnership ownership) : base(handle, ownership) + { + } + + bool GestureDetector.IOnDoubleTapListener.OnDoubleTap(MotionEvent e) + { + if (_tapDelegate == null || _tapGestureRecognizers == null) + return false; + return _tapDelegate(2); + } + + bool GestureDetector.IOnDoubleTapListener.OnDoubleTapEvent(MotionEvent e) + { + return false; + } + + bool GestureDetector.IOnDoubleTapListener.OnSingleTapConfirmed(MotionEvent e) + { + if (_tapDelegate == null || _tapGestureRecognizers == null) + return false; + + // optimization: only wait for a second tap if there is a double tap handler + if (!HasDoubleTapHandler()) + return false; + + return _tapDelegate(1); + } + + bool GestureDetector.IOnGestureListener.OnDown(MotionEvent e) + { + return false; + } + + bool GestureDetector.IOnGestureListener.OnFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) + { + EndScrolling(); + + return false; + } + + void GestureDetector.IOnGestureListener.OnLongPress(MotionEvent e) + { + } + + bool GestureDetector.IOnGestureListener.OnScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) + { + if (_scrollDelegate == null || e1 == null || e2 == null) + return false; + + if (!_isScrolling && _scrollStartedDelegate != null) + _scrollStartedDelegate(e2.PointerCount); + + _isScrolling = true; + + float totalX = e2.GetX() - e1.GetX(); + float totalY = e2.GetY() - e1.GetY(); + + return _scrollDelegate(totalX, totalY, e2.PointerCount); + } + + void GestureDetector.IOnGestureListener.OnShowPress(MotionEvent e) + { + } + + bool GestureDetector.IOnGestureListener.OnSingleTapUp(MotionEvent e) + { + if (_tapDelegate == null || _tapGestureRecognizers == null) + return false; + + // optimization: do not wait for a second tap if there is no double tap handler + if (HasDoubleTapHandler()) + return false; + + return _tapDelegate(1); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _tapDelegate = null; + _tapGestureRecognizers = null; + _scrollDelegate = null; + _scrollStartedDelegate = null; + _scrollCompleteDelegate = null; + } + + base.Dispose(disposing); + } + + void EndScrolling() + { + if (_isScrolling && _scrollCompleteDelegate != null) + _scrollCompleteDelegate(); + + _isScrolling = false; + } + + bool HasDoubleTapHandler() + { + return _tapGestureRecognizers(2).Any(); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/InnerScaleListener.cs b/Xamarin.Forms.Platform.Android/InnerScaleListener.cs new file mode 100644 index 00000000..4a6c6581 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/InnerScaleListener.cs @@ -0,0 +1,68 @@ +using System; +using Android.Runtime; +using Android.Views; + +namespace Xamarin.Forms.Platform.Android +{ + internal class InnerScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener + { + Func<float, Point, bool> _pinchDelegate; + Action _pinchEndedDelegate; + Func<Point, bool> _pinchStartedDelegate; + + public InnerScaleListener(Func<float, Point, bool> pinchDelegate, Func<Point, bool> pinchStarted, Action pinchEnded) + { + if (pinchDelegate == null) + throw new ArgumentNullException("pinchDelegate"); + + if (pinchStarted == null) + throw new ArgumentNullException("pinchStarted"); + + if (pinchEnded == null) + throw new ArgumentNullException("pinchEnded"); + + _pinchDelegate = pinchDelegate; + _pinchStartedDelegate = pinchStarted; + _pinchEndedDelegate = pinchEnded; + } + + // This is needed because GestureRecognizer callbacks can be delayed several hundred milliseconds + // which can result in the need to resurect this object if it has already been disposed. We dispose + // eagerly to allow easier garbage collection of the renderer + internal InnerScaleListener(IntPtr handle, JniHandleOwnership ownership) : base(handle, ownership) + { + } + + public override bool OnScale(ScaleGestureDetector detector) + { + float cur = detector.CurrentSpan; + float last = detector.PreviousSpan; + + if (Math.Abs(cur - last) < 10) + return false; + + return _pinchDelegate(detector.ScaleFactor, new Point(Forms.Context.FromPixels(detector.FocusX), Forms.Context.FromPixels(detector.FocusY))); + } + + public override bool OnScaleBegin(ScaleGestureDetector detector) + { + return _pinchStartedDelegate(new Point(Forms.Context.FromPixels(detector.FocusX), Forms.Context.FromPixels(detector.FocusY))); + } + + public override void OnScaleEnd(ScaleGestureDetector detector) + { + _pinchEndedDelegate(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _pinchDelegate = null; + _pinchStartedDelegate = null; + _pinchEndedDelegate = null; + } + base.Dispose(disposing); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/KeyboardManager.cs b/Xamarin.Forms.Platform.Android/KeyboardManager.cs new file mode 100644 index 00000000..649c99bd --- /dev/null +++ b/Xamarin.Forms.Platform.Android/KeyboardManager.cs @@ -0,0 +1,41 @@ +using System; +using Android.Content; +using Android.OS; +using Android.Views.InputMethods; +using Android.Widget; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android +{ + internal static class KeyboardManager + { + internal static void HideKeyboard(this AView inputView, bool overrideValidation = false) + { + using(var inputMethodManager = (InputMethodManager)Forms.Context.GetSystemService(Context.InputMethodService)) + { + IBinder windowToken = null; + + if (!overrideValidation && !(inputView is EditText || inputView is TextView || inputView is SearchView)) + throw new ArgumentException("inputView should be of type EditText, SearchView, or TextView"); + + windowToken = inputView.WindowToken; + if (windowToken != null) + inputMethodManager.HideSoftInputFromWindow(windowToken, HideSoftInputFlags.None); + } + } + + internal static void ShowKeyboard(this AView inputView) + { + using(var inputMethodManager = (InputMethodManager)Forms.Context.GetSystemService(Context.InputMethodService)) + { + if (inputView is EditText || inputView is TextView || inputView is SearchView) + { + inputMethodManager.ShowSoftInput(inputView, ShowFlags.Forced); + inputMethodManager.ToggleSoftInput(ShowFlags.Forced, HideSoftInputFlags.ImplicitOnly); + } + else + throw new ArgumentException("inputView should be of type EditText, SearchView, or TextView"); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/LayoutExtensions.cs b/Xamarin.Forms.Platform.Android/LayoutExtensions.cs new file mode 100644 index 00000000..90427b61 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/LayoutExtensions.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android +{ + public static class LayoutExtensions + { + public static void Add(this IList<View> children, AView view, GetDesiredSizeDelegate getDesiredSizeDelegate = null, OnLayoutDelegate onLayoutDelegate = null, + OnMeasureDelegate onMeasureDelegate = null) + { + children.Add(view.ToView(getDesiredSizeDelegate, onLayoutDelegate, onMeasureDelegate)); + } + + public static View ToView(this AView view, GetDesiredSizeDelegate getDesiredSizeDelegate = null, OnLayoutDelegate onLayoutDelegate = null, OnMeasureDelegate onMeasureDelegate = null) + { + return new NativeViewWrapper(view, getDesiredSizeDelegate, onLayoutDelegate, onMeasureDelegate); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/MeasureSpecFactory.cs b/Xamarin.Forms.Platform.Android/MeasureSpecFactory.cs new file mode 100644 index 00000000..9ab854c5 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/MeasureSpecFactory.cs @@ -0,0 +1,20 @@ +using Android.Views; + +namespace Xamarin.Forms.Platform.Android +{ + internal static class MeasureSpecFactory + { + public static int GetSize(int measureSpec) + { + const int modeMask = 0x3 << 30; + return measureSpec & ~modeMask; + } + + // Literally does the same thing as the android code, 1000x faster because no bridge cross + // benchmarked by calling 1,000,000 times in a loop on actual device + public static int MakeMeasureSpec(int size, MeasureSpecMode mode) + { + return size + (int)mode; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/NativeViewWrapper.cs b/Xamarin.Forms.Platform.Android/NativeViewWrapper.cs new file mode 100644 index 00000000..eecef9bc --- /dev/null +++ b/Xamarin.Forms.Platform.Android/NativeViewWrapper.cs @@ -0,0 +1,22 @@ +namespace Xamarin.Forms.Platform.Android +{ + public class NativeViewWrapper : View + { + public NativeViewWrapper(global::Android.Views.View nativeView, GetDesiredSizeDelegate getDesiredSizeDelegate = null, OnLayoutDelegate onLayoutDelegate = null, + OnMeasureDelegate onMeasureDelegate = null) + { + GetDesiredSizeDelegate = getDesiredSizeDelegate; + NativeView = nativeView; + OnLayoutDelegate = onLayoutDelegate; + OnMeasureDelegate = onMeasureDelegate; + } + + public GetDesiredSizeDelegate GetDesiredSizeDelegate { get; } + + public global::Android.Views.View NativeView { get; } + + public OnLayoutDelegate OnLayoutDelegate { get; } + + public OnMeasureDelegate OnMeasureDelegate { get; } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/NativeViewWrapperRenderer.cs b/Xamarin.Forms.Platform.Android/NativeViewWrapperRenderer.cs new file mode 100644 index 00000000..933f436f --- /dev/null +++ b/Xamarin.Forms.Platform.Android/NativeViewWrapperRenderer.cs @@ -0,0 +1,61 @@ +namespace Xamarin.Forms.Platform.Android +{ + public class NativeViewWrapperRenderer : ViewRenderer<NativeViewWrapper, global::Android.Views.View> + { + public override SizeRequest GetDesiredSize(int widthConstraint, int heightConstraint) + { + if (Element?.GetDesiredSizeDelegate == null) + return base.GetDesiredSize(widthConstraint, heightConstraint); + + // The user has specified a different implementation of GetDesiredSizeDelegate + SizeRequest? result = Element.GetDesiredSizeDelegate(this, widthConstraint, heightConstraint); + + // If the delegate returns a SizeRequest, we use it; if it returns null, + // fall back to the default implementation + return result ?? base.GetDesiredSize(widthConstraint, heightConstraint); + } + + protected override void OnElementChanged(ElementChangedEventArgs<NativeViewWrapper> e) + { + base.OnElementChanged(e); + + if (e.OldElement == null) + { + SetNativeControl(Element.NativeView); + Control.LayoutChange += (sender, args) => Element?.InvalidateMeasure(InvalidationTrigger.MeasureChanged); + } + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + if (Element?.OnLayoutDelegate == null) + { + base.OnLayout(changed, l, t, r, b); + return; + } + + // The user has specified a different implementation of OnLayout + bool handled = Element.OnLayoutDelegate(this, changed, l, t, r, b); + + // If the delegate wasn't able to handle the request, fall back to the default implementation + if (!handled) + base.OnLayout(changed, l, t, r, b); + } + + protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + if (Element?.OnMeasureDelegate == null) + { + base.OnMeasure(widthMeasureSpec, heightMeasureSpec); + return; + } + + // The user has specified a different implementation of OnMeasure + bool handled = Element.OnMeasureDelegate(this, widthMeasureSpec, heightMeasureSpec); + + // If the delegate wasn't able to handle the request, fall back to the default implementation + if (!handled) + base.OnMeasure(widthMeasureSpec, heightMeasureSpec); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/OnLayoutDelegate.cs b/Xamarin.Forms.Platform.Android/OnLayoutDelegate.cs new file mode 100644 index 00000000..bf5170d4 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/OnLayoutDelegate.cs @@ -0,0 +1,4 @@ +namespace Xamarin.Forms.Platform.Android +{ + public delegate bool OnLayoutDelegate(NativeViewWrapperRenderer renderer, bool changed, int l, int t, int r, int b); +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/OnMeasureDelegate.cs b/Xamarin.Forms.Platform.Android/OnMeasureDelegate.cs new file mode 100644 index 00000000..48502d23 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/OnMeasureDelegate.cs @@ -0,0 +1,4 @@ +namespace Xamarin.Forms.Platform.Android +{ + public delegate bool OnMeasureDelegate(NativeViewWrapperRenderer renderer, int widthMeasureSpec, int heightMeasureSpec); +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/PanGestureHandler.cs b/Xamarin.Forms.Platform.Android/PanGestureHandler.cs new file mode 100644 index 00000000..a552964c --- /dev/null +++ b/Xamarin.Forms.Platform.Android/PanGestureHandler.cs @@ -0,0 +1,69 @@ +using System; + +namespace Xamarin.Forms.Platform.Android +{ + internal class PanGestureHandler + { + readonly Func<double, double> _pixelTranslation; + + public PanGestureHandler(Func<View> getView, Func<double, double> pixelTranslation) + { + _pixelTranslation = pixelTranslation; + GetView = getView; + } + + Func<View> GetView { get; } + + public bool OnPan(float x, float y, int pointerCount) + { + View view = GetView(); + + if (view == null) + return false; + + var result = false; + foreach (PanGestureRecognizer panGesture in + view.GestureRecognizers.GetGesturesFor<PanGestureRecognizer>(g => g.TouchPoints == pointerCount)) + { + ((IPanGestureController)panGesture).SendPan(view, _pixelTranslation(x), _pixelTranslation(y), Application.Current.PanGestureId); + result = true; + } + + return result; + } + + public bool OnPanComplete() + { + View view = GetView(); + + if (view == null) + return false; + + var result = false; + foreach (PanGestureRecognizer panGesture in view.GestureRecognizers.GetGesturesFor<PanGestureRecognizer>()) + { + ((IPanGestureController)panGesture).SendPanCompleted(view, Application.Current.PanGestureId); + result = true; + } + Application.Current.PanGestureId++; + return result; + } + + public bool OnPanStarted(int pointerCount) + { + View view = GetView(); + + if (view == null) + return false; + + var result = false; + foreach (PanGestureRecognizer panGesture in + view.GestureRecognizers.GetGesturesFor<PanGestureRecognizer>(g => g.TouchPoints == pointerCount)) + { + ((IPanGestureController)panGesture).SendPanStarted(view, Application.Current.PanGestureId); + result = true; + } + return result; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/PinchGestureHandler.cs b/Xamarin.Forms.Platform.Android/PinchGestureHandler.cs new file mode 100644 index 00000000..bc06531c --- /dev/null +++ b/Xamarin.Forms.Platform.Android/PinchGestureHandler.cs @@ -0,0 +1,76 @@ +using System; +using System.Linq; + +namespace Xamarin.Forms.Platform.Android +{ + internal class PinchGestureHandler + { + double _pinchStartingScale = 1; + + public PinchGestureHandler(Func<View> getView) + { + GetView = getView; + } + + public bool IsPinchSupported + { + get + { + View view = GetView(); + return view != null && view.GestureRecognizers.GetGesturesFor<PinchGestureRecognizer>().Any(); + } + } + + Func<View> GetView { get; } + + // A View can have at most one pinch gesture, so we just need to look for one (or none) + PinchGestureRecognizer PinchGesture => GetView()?.GestureRecognizers.GetGesturesFor<PinchGestureRecognizer>().FirstOrDefault(); + + public bool OnPinch(float scale, Point scalePoint) + { + View view = GetView(); + + if (view == null) + return false; + + PinchGestureRecognizer pinchGesture = PinchGesture; + if (pinchGesture == null) + return true; + + var scalePointTransformed = new Point(scalePoint.X / view.Width, scalePoint.Y / view.Height); + ((IPinchGestureController)pinchGesture).SendPinch(view, 1 + (scale - 1) * _pinchStartingScale, scalePointTransformed); + + return true; + } + + public void OnPinchEnded() + { + View view = GetView(); + + if (view == null) + return; + + PinchGestureRecognizer pinchGesture = PinchGesture; + ((IPinchGestureController)pinchGesture)?.SendPinchEnded(view); + } + + public bool OnPinchStarted(Point scalePoint) + { + View view = GetView(); + + if (view == null) + return false; + + PinchGestureRecognizer pinchGesture = PinchGesture; + if (pinchGesture == null) + return false; + + _pinchStartingScale = view.Scale; + + var scalePointTransformed = new Point(scalePoint.X / view.Width, scalePoint.Y / view.Height); + + ((IPinchGestureController)pinchGesture).SendPinchStarted(view, scalePointTransformed); + return true; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Platform.cs b/Xamarin.Forms.Platform.Android/Platform.cs new file mode 100644 index 00000000..1de13b41 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Platform.cs @@ -0,0 +1,1056 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using Android.App; +using Android.Content; +using Android.Content.Res; +using Android.Graphics; +using Android.Graphics.Drawables; +using Android.Support.V4.App; +using Android.Util; +using Android.Views; +using Android.Widget; +using Xamarin.Forms.Platform.Android.AppCompat; +using FragmentManager = Android.Support.V4.App.FragmentManager; + +namespace Xamarin.Forms.Platform.Android +{ + public class Platform : BindableObject, IPlatform, INavigation, IDisposable, IPlatformLayout + { + internal const string CloseContextActionsSignalName = "Xamarin.CloseContextActions"; + + internal static readonly BindableProperty RendererProperty = BindableProperty.CreateAttached("Renderer", typeof(IVisualElementRenderer), typeof(Platform), default(IVisualElementRenderer), + propertyChanged: (bindable, oldvalue, newvalue) => + { + var view = bindable as VisualElement; + if (view != null) + view.IsPlatformEnabled = newvalue != null; + }); + + internal static readonly BindableProperty PageContextProperty = BindableProperty.CreateAttached("PageContext", typeof(Context), typeof(Platform), null); + + readonly Context _context; + + readonly PlatformRenderer _renderer; + readonly ToolbarTracker _toolbarTracker = new ToolbarTracker(); + + NavigationPage _currentNavigationPage; + + TabbedPage _currentTabbedPage; + + Color _defaultActionBarTitleTextColor; + + bool _disposed; + + bool _ignoreAndroidSelection; + + Page _navigationPageCurrentPage; + NavigationModel _navModel = new NavigationModel(); + + internal Platform(Context context) + { + _context = context; + + _defaultActionBarTitleTextColor = SetDefaultActionBarTitleTextColor(); + + _renderer = new PlatformRenderer(context, this); + + FormsApplicationActivity.BackPressed += HandleBackPressed; + + _toolbarTracker.CollectionChanged += ToolbarTrackerOnCollectionChanged; + } + + #region IPlatform implementation + + internal Page Page { get; private set; } + + #endregion + + ActionBar ActionBar + { + get { return ((Activity)_context).ActionBar; } + } + + MasterDetailPage CurrentMasterDetailPage { get; set; } + + NavigationPage CurrentNavigationPage + { + get { return _currentNavigationPage; } + set + { + if (_currentNavigationPage == value) + return; + + if (_currentNavigationPage != null) + { + _currentNavigationPage.Pushed -= CurrentNavigationPageOnPushed; + _currentNavigationPage.Popped -= CurrentNavigationPageOnPopped; + _currentNavigationPage.PoppedToRoot -= CurrentNavigationPageOnPoppedToRoot; + _currentNavigationPage.PropertyChanged -= CurrentNavigationPageOnPropertyChanged; + } + + RegisterNavPageCurrent(null); + + _currentNavigationPage = value; + + if (_currentNavigationPage != null) + { + _currentNavigationPage.Pushed += CurrentNavigationPageOnPushed; + _currentNavigationPage.Popped += CurrentNavigationPageOnPopped; + _currentNavigationPage.PoppedToRoot += CurrentNavigationPageOnPoppedToRoot; + _currentNavigationPage.PropertyChanged += CurrentNavigationPageOnPropertyChanged; + RegisterNavPageCurrent(_currentNavigationPage.CurrentPage); + } + + UpdateActionBarBackgroundColor(); + UpdateActionBarTextColor(); + UpdateActionBarUpImageColor(); + UpdateActionBarTitle(); + } + } + + TabbedPage CurrentTabbedPage + { + get { return _currentTabbedPage; } + set + { + if (_currentTabbedPage == value) + return; + + if (_currentTabbedPage != null) + { + _currentTabbedPage.PagesChanged -= CurrentTabbedPageChildrenChanged; + _currentTabbedPage.PropertyChanged -= CurrentTabbedPageOnPropertyChanged; + + if (value == null) + ActionBar.RemoveAllTabs(); + } + + _currentTabbedPage = value; + + if (_currentTabbedPage != null) + { + _currentTabbedPage.PagesChanged += CurrentTabbedPageChildrenChanged; + _currentTabbedPage.PropertyChanged += CurrentTabbedPageOnPropertyChanged; + } + + UpdateActionBarTitle(); + + ActionBar.NavigationMode = value == null ? ActionBarNavigationMode.Standard : ActionBarNavigationMode.Tabs; + CurrentTabbedPageChildrenChanged(null, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + } + + ActionBarDrawerToggle MasterDetailPageToggle { get; set; } + + void IDisposable.Dispose() + { + if (_disposed) + return; + _disposed = true; + + SetPage(null); + + FormsApplicationActivity.BackPressed -= HandleBackPressed; + _toolbarTracker.CollectionChanged -= ToolbarTrackerOnCollectionChanged; + _toolbarTracker.Target = null; + + CurrentNavigationPage = null; + CurrentMasterDetailPage = null; + CurrentTabbedPage = null; + } + + void INavigation.InsertPageBefore(Page page, Page before) + { + throw new InvalidOperationException("InsertPageBefore is not supported globally on Android, please use a NavigationPage."); + } + + IReadOnlyList<Page> INavigation.ModalStack => _navModel.Modals.ToList(); + + IReadOnlyList<Page> INavigation.NavigationStack => new List<Page>(); + + Task<Page> INavigation.PopAsync() + { + return ((INavigation)this).PopAsync(true); + } + + Task<Page> INavigation.PopAsync(bool animated) + { + throw new InvalidOperationException("PopAsync is not supported globally on Android, please use a NavigationPage."); + } + + Task<Page> INavigation.PopModalAsync() + { + return ((INavigation)this).PopModalAsync(true); + } + + Task<Page> INavigation.PopModalAsync(bool animated) + { + Page modal = _navModel.PopModal(); + + modal.SendDisappearing(); + var source = new TaskCompletionSource<Page>(); + + IVisualElementRenderer modalRenderer = GetRenderer(modal); + if (modalRenderer != null) + { + if (animated) + { + modalRenderer.ViewGroup.Animate().Alpha(0).ScaleX(0.8f).ScaleY(0.8f).SetDuration(250).SetListener(new GenericAnimatorListener + { + OnEnd = a => + { + modalRenderer.ViewGroup.RemoveFromParent(); + modalRenderer.Dispose(); + source.TrySetResult(modal); + _navModel.CurrentPage?.SendAppearing(); + } + }); + } + else + { + modalRenderer.ViewGroup.RemoveFromParent(); + modalRenderer.Dispose(); + source.TrySetResult(modal); + _navModel.CurrentPage?.SendAppearing(); + } + } + + _toolbarTracker.Target = _navModel.Roots.Last(); + UpdateActionBar(); + + return source.Task; + } + + Task INavigation.PopToRootAsync() + { + return ((INavigation)this).PopToRootAsync(true); + } + + Task INavigation.PopToRootAsync(bool animated) + { + throw new InvalidOperationException("PopToRootAsync is not supported globally on Android, please use a NavigationPage."); + } + + Task INavigation.PushAsync(Page root) + { + return ((INavigation)this).PushAsync(root, true); + } + + Task INavigation.PushAsync(Page root, bool animated) + { + throw new InvalidOperationException("PushAsync is not supported globally on Android, please use a NavigationPage."); + } + + Task INavigation.PushModalAsync(Page modal) + { + return ((INavigation)this).PushModalAsync(modal, true); + } + + async Task INavigation.PushModalAsync(Page modal, bool animated) + { + _navModel.CurrentPage?.SendDisappearing(); + + _navModel.PushModal(modal); + + modal.Platform = this; + + await PresentModal(modal, animated); + + // Verify that the modal is still on the stack + if (_navModel.CurrentPage == modal) + modal.SendAppearing(); + + _toolbarTracker.Target = _navModel.Roots.Last(); + + UpdateActionBar(); + } + + void INavigation.RemovePage(Page page) + { + throw new InvalidOperationException("RemovePage is not supported globally on Android, please use a NavigationPage."); + } + + public static IVisualElementRenderer CreateRenderer(VisualElement element) + { + UpdateGlobalContext(element); + + IVisualElementRenderer renderer = Registrar.Registered.GetHandler<IVisualElementRenderer>(element.GetType()) ?? new DefaultRenderer(); + renderer.SetElement(element); + + return renderer; + } + + public static IVisualElementRenderer GetRenderer(VisualElement bindable) + { + return (IVisualElementRenderer)bindable.GetValue(RendererProperty); + } + + public static void SetRenderer(VisualElement bindable, IVisualElementRenderer value) + { + bindable.SetValue(RendererProperty, value); + } + + public void UpdateActionBarTextColor() + { + SetActionBarTextColor(); + } + + protected override void OnBindingContextChanged() + { + SetInheritedBindingContext(Page, BindingContext); + + base.OnBindingContextChanged(); + } + + internal static IVisualElementRenderer CreateRenderer(VisualElement element, FragmentManager fragmentManager) + { + UpdateGlobalContext(element); + + IVisualElementRenderer renderer = Registrar.Registered.GetHandler<IVisualElementRenderer>(element.GetType()) ?? new DefaultRenderer(); + + var managesFragments = renderer as IManageFragments; + managesFragments?.SetFragmentManager(fragmentManager); + + renderer.SetElement(element); + + return renderer; + } + + internal static Context GetPageContext(BindableObject bindable) + { + return (Context)bindable.GetValue(PageContextProperty); + } + + internal ViewGroup GetViewGroup() + { + return _renderer; + } + + internal void PrepareMenu(IMenu menu) + { + foreach (ToolbarItem item in _toolbarTracker.ToolbarItems) + item.PropertyChanged -= HandleToolbarItemPropertyChanged; + menu.Clear(); + + if (!ShouldShowActionBarTitleArea()) + return; + + foreach (ToolbarItem item in _toolbarTracker.ToolbarItems) + { + item.PropertyChanged += HandleToolbarItemPropertyChanged; + if (item.Order == ToolbarItemOrder.Secondary) + { + IMenuItem menuItem = menu.Add(item.Text); + menuItem.SetEnabled(item.IsEnabled); + menuItem.SetOnMenuItemClickListener(new GenericMenuClickListener(item.Activate)); + } + else + { + IMenuItem menuItem = menu.Add(item.Text); + if (!string.IsNullOrEmpty(item.Icon)) + { + Drawable iconBitmap = _context.Resources.GetDrawable(item.Icon); + if (iconBitmap != null) + menuItem.SetIcon(iconBitmap); + } + menuItem.SetEnabled(item.IsEnabled); + menuItem.SetShowAsAction(ShowAsAction.Always); + menuItem.SetOnMenuItemClickListener(new GenericMenuClickListener(item.Activate)); + } + } + } + + internal async void SendHomeClicked() + { + if (UpButtonShouldNavigate()) + { + if (NavAnimationInProgress) + return; + NavAnimationInProgress = true; + await CurrentNavigationPage.PopAsync(); + NavAnimationInProgress = false; + } + else if (CurrentMasterDetailPage != null) + { + if (CurrentMasterDetailPage.ShouldShowSplitMode && CurrentMasterDetailPage.IsPresented) + return; + CurrentMasterDetailPage.IsPresented = !CurrentMasterDetailPage.IsPresented; + } + } + + internal void SetPage(Page newRoot) + { + var layout = false; + if (Page != null) + { + _renderer.RemoveAllViews(); + + foreach (IVisualElementRenderer rootRenderer in _navModel.Roots.Select(GetRenderer)) + rootRenderer.Dispose(); + _navModel = new NavigationModel(); + + layout = true; + } + + if (newRoot == null) + return; + + _navModel.Push(newRoot, null); + + Page = newRoot; + Page.Platform = this; + AddChild(Page, layout); + + ((Application)Page.RealParent).NavigationProxy.Inner = this; + + _toolbarTracker.Target = newRoot; + + UpdateActionBar(); + } + + internal static void SetPageContext(BindableObject bindable, Context context) + { + bindable.SetValue(PageContextProperty, context); + } + + internal void UpdateActionBar() + { + List<Page> relevantAncestors = AncestorPagesOfPage(_navModel.CurrentPage); + + IEnumerable<NavigationPage> navPages = relevantAncestors.OfType<NavigationPage>(); + if (navPages.Count() > 1) + throw new Exception("Android only allows one navigation page on screen at a time"); + NavigationPage navPage = navPages.FirstOrDefault(); + + IEnumerable<TabbedPage> tabbedPages = relevantAncestors.OfType<TabbedPage>(); + if (tabbedPages.Count() > 1) + throw new Exception("Android only allows one tabbed page on screen at a time"); + TabbedPage tabbedPage = tabbedPages.FirstOrDefault(); + + CurrentMasterDetailPage = relevantAncestors.OfType<MasterDetailPage>().FirstOrDefault(); + CurrentNavigationPage = navPage; + CurrentTabbedPage = tabbedPage; + + if (navPage != null && navPage.CurrentPage == null) + { + throw new InvalidOperationException("NavigationPage must have a root Page before being used. Either call PushAsync with a valid Page, or pass a Page to the constructor before usage."); + } + + UpdateActionBarTitle(); + + if (ShouldShowActionBarTitleArea() || tabbedPage != null) + ShowActionBar(); + else + HideActionBar(); + UpdateMasterDetailToggle(); + } + + internal void UpdateActionBarBackgroundColor() + { + if (!((Activity)_context).ActionBar.IsShowing) + return; + Color colorToUse = Color.Default; + if (CurrentNavigationPage != null) + { +#pragma warning disable 618 + if (CurrentNavigationPage.Tint != Color.Default) + colorToUse = CurrentNavigationPage.Tint; +#pragma warning restore 618 + else if (CurrentNavigationPage.BarBackgroundColor != Color.Default) + colorToUse = CurrentNavigationPage.BarBackgroundColor; + } + using(Drawable drawable = colorToUse == Color.Default ? GetActionBarBackgroundDrawable() : new ColorDrawable(colorToUse.ToAndroid())) + ((Activity)_context).ActionBar.SetBackgroundDrawable(drawable); + } + + internal void UpdateMasterDetailToggle(bool update = false) + { + if (CurrentMasterDetailPage == null) + { + if (MasterDetailPageToggle == null) + return; + // clear out the icon + ClearMasterDetailToggle(); + return; + } + if (!CurrentMasterDetailPage.ShouldShowToolbarButton() || string.IsNullOrEmpty(CurrentMasterDetailPage.Master.Icon) || + (CurrentMasterDetailPage.ShouldShowSplitMode && CurrentMasterDetailPage.IsPresented)) + { + //clear out existing icon; + ClearMasterDetailToggle(); + return; + } + + if (MasterDetailPageToggle == null || update) + { + ClearMasterDetailToggle(); + GetNewMasterDetailToggle(); + } + + bool state; + if (CurrentNavigationPage == null) + state = true; + else + state = !UpButtonShouldNavigate(); + if (state == MasterDetailPageToggle.DrawerIndicatorEnabled) + return; + MasterDetailPageToggle.DrawerIndicatorEnabled = state; + MasterDetailPageToggle.SyncState(); + } + + internal void UpdateNavigationTitleBar() + { + UpdateActionBarTitle(); + UpdateActionBar(); + UpdateActionBarUpImageColor(); + } + + void AddChild(VisualElement view, bool layout = false) + { + if (GetRenderer(view) != null) + return; + + SetPageContext(view, _context); + IVisualElementRenderer renderView = CreateRenderer(view); + SetRenderer(view, renderView); + + if (layout) + view.Layout(new Rectangle(0, 0, _context.FromPixels(_renderer.Width), _context.FromPixels(_renderer.Height))); + + _renderer.AddView(renderView.ViewGroup); + } + + ActionBar.Tab AddTab(Page page, int index) + { + ActionBar actionBar = ((Activity)_context).ActionBar; + TabbedPage currentTabs = CurrentTabbedPage; + + ActionBar.Tab atab = actionBar.NewTab(); + atab.SetText(page.Title); + atab.TabSelected += (sender, e) => + { + if (!_ignoreAndroidSelection) + currentTabs.CurrentPage = page; + }; + actionBar.AddTab(atab, index); + + page.PropertyChanged += PagePropertyChanged; + return atab; + } + + List<Page> AncestorPagesOfPage(Page root) + { + var result = new List<Page>(); + if (root == null) + return result; + + if (root is IPageContainer<Page>) + { + var navPage = (IPageContainer<Page>)root; + result.AddRange(AncestorPagesOfPage(navPage.CurrentPage)); + } + else if (root is MasterDetailPage) + result.AddRange(AncestorPagesOfPage(((MasterDetailPage)root).Detail)); + else + { + foreach (Page page in root.InternalChildren.OfType<Page>()) + result.AddRange(AncestorPagesOfPage(page)); + } + + result.Add(root); + return result; + } + + void ClearMasterDetailToggle() + { + if (MasterDetailPageToggle == null) + return; + + MasterDetailPageToggle.DrawerIndicatorEnabled = false; + MasterDetailPageToggle.SyncState(); + MasterDetailPageToggle.Dispose(); + MasterDetailPageToggle = null; + } + + void CurrentNavigationPageOnPopped(object sender, NavigationEventArgs eventArg) + { + UpdateNavigationTitleBar(); + } + + void CurrentNavigationPageOnPoppedToRoot(object sender, EventArgs eventArgs) + { + UpdateNavigationTitleBar(); + } + + void CurrentNavigationPageOnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == NavigationPage.TintProperty.PropertyName) + UpdateActionBarBackgroundColor(); + else if (e.PropertyName == NavigationPage.BarBackgroundColorProperty.PropertyName) + UpdateActionBarBackgroundColor(); + else if (e.PropertyName == NavigationPage.BarTextColorProperty.PropertyName) + { + UpdateActionBarTextColor(); + UpdateActionBarUpImageColor(); + } + else if (e.PropertyName == NavigationPage.CurrentPageProperty.PropertyName) + RegisterNavPageCurrent(CurrentNavigationPage.CurrentPage); + } + + void CurrentNavigationPageOnPushed(object sender, NavigationEventArgs eventArg) + { + UpdateNavigationTitleBar(); + } + + void CurrentTabbedPageChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (CurrentTabbedPage == null) + return; + + _ignoreAndroidSelection = true; + + e.Apply((o, index, create) => AddTab((Page)o, index), (o, index) => RemoveTab((Page)o, index), Reset); + + if (CurrentTabbedPage.CurrentPage != null) + { + Page page = CurrentTabbedPage.CurrentPage; + int index = TabbedPage.GetIndex(page); + if (index >= 0 && index < CurrentTabbedPage.Children.Count) + ActionBar.GetTabAt(index).Select(); + } + + _ignoreAndroidSelection = false; + } + + void CurrentTabbedPageOnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName != "CurrentPage") + return; + + UpdateActionBar(); + + // If we switch tabs while pushing a new page, UpdateActionBar() can set currentTabbedPage to null + if (_currentTabbedPage == null) + return; + + NavAnimationInProgress = true; + + Page page = _currentTabbedPage.CurrentPage; + if (page == null) + { + ActionBar.SelectTab(null); + NavAnimationInProgress = false; + return; + } + + int index = TabbedPage.GetIndex(page); + if (ActionBar.SelectedNavigationIndex == index || index >= ActionBar.NavigationItemCount) + { + NavAnimationInProgress = false; + return; + } + + ActionBar.SelectTab(ActionBar.GetTabAt(index)); + + NavAnimationInProgress = false; + } + + Drawable GetActionBarBackgroundDrawable() + { + int[] backgroundDataArray = { global::Android.Resource.Attribute.Background }; + + using(var outVal = new TypedValue()) + { + _context.Theme.ResolveAttribute(global::Android.Resource.Attribute.ActionBarStyle, outVal, true); + TypedArray actionBarStyle = _context.Theme.ObtainStyledAttributes(outVal.ResourceId, backgroundDataArray); + + Drawable result = actionBarStyle.GetDrawable(0); + actionBarStyle.Recycle(); + return result; + } + } + + void GetNewMasterDetailToggle() + { + int icon = ResourceManager.GetDrawableByName(CurrentMasterDetailPage.Master.Icon); + var drawer = GetRenderer(CurrentMasterDetailPage) as MasterDetailRenderer; + if (drawer == null) + return; + MasterDetailPageToggle = new ActionBarDrawerToggle(_context as Activity, drawer, icon, 0, 0); + MasterDetailPageToggle.SyncState(); + } + + bool HandleBackPressed(object sender, EventArgs e) + { + if (NavAnimationInProgress) + return true; + + Page root = _navModel.Roots.Last(); + bool handled = root.SendBackButtonPressed(); + + return handled; + } + + void HandleToolbarItemPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == MenuItem.IsEnabledProperty.PropertyName) + (_context as Activity).InvalidateOptionsMenu(); + else if (e.PropertyName == MenuItem.TextProperty.PropertyName) + (_context as Activity).InvalidateOptionsMenu(); + else if (e.PropertyName == MenuItem.IconProperty.PropertyName) + (_context as Activity).InvalidateOptionsMenu(); + } + + void HideActionBar() + { + ReloadToolbarItems(); + UpdateActionBarHomeAsUp(ActionBar); + ActionBar.Hide(); + } + + void NavigationPageCurrentPageOnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == NavigationPage.HasNavigationBarProperty.PropertyName) + UpdateActionBar(); + else if (e.PropertyName == Page.TitleProperty.PropertyName) + UpdateActionBarTitle(); + } + + void PagePropertyChanged(object sender, PropertyChangedEventArgs args) + { + if (args.PropertyName == Page.TitleProperty.PropertyName) + { + ActionBar actionBar = ((Activity)_context).ActionBar; + TabbedPage currentTabs = CurrentTabbedPage; + + if (currentTabs == null || actionBar.TabCount == 0) + return; + + var page = sender as Page; + ActionBar.Tab atab = actionBar.GetTabAt(currentTabs.Children.IndexOf(page)); + atab.SetText(page.Title); + } + } + + Task PresentModal(Page modal, bool animated) + { + IVisualElementRenderer modalRenderer = GetRenderer(modal); + if (modalRenderer == null) + { + SetPageContext(modal, _context); + modalRenderer = CreateRenderer(modal); + SetRenderer(modal, modalRenderer); + + if (modal.BackgroundColor == Color.Default && modal.BackgroundImage == null) + modalRenderer.ViewGroup.SetWindowBackground(); + } + modalRenderer.Element.Layout(new Rectangle(0, 0, _context.FromPixels(_renderer.Width), _context.FromPixels(_renderer.Height))); + _renderer.AddView(modalRenderer.ViewGroup); + + var source = new TaskCompletionSource<bool>(); + NavAnimationInProgress = true; + if (animated) + { + modalRenderer.ViewGroup.Alpha = 0; + modalRenderer.ViewGroup.ScaleX = 0.8f; + modalRenderer.ViewGroup.ScaleY = 0.8f; + modalRenderer.ViewGroup.Animate().Alpha(1).ScaleX(1).ScaleY(1).SetDuration(250).SetListener(new GenericAnimatorListener + { + OnEnd = a => + { + source.TrySetResult(false); + NavAnimationInProgress = false; + }, + OnCancel = a => + { + source.TrySetResult(true); + NavAnimationInProgress = false; + } + }); + } + else + { + NavAnimationInProgress = false; + source.TrySetResult(true); + } + + return source.Task; + } + + void RegisterNavPageCurrent(Page page) + { + if (_navigationPageCurrentPage != null) + _navigationPageCurrentPage.PropertyChanged -= NavigationPageCurrentPageOnPropertyChanged; + + _navigationPageCurrentPage = page; + + if (_navigationPageCurrentPage != null) + _navigationPageCurrentPage.PropertyChanged += NavigationPageCurrentPageOnPropertyChanged; + } + + void ReloadToolbarItems() + { + var activity = (Activity)_context; + activity.InvalidateOptionsMenu(); + } + + void RemoveTab(Page page, int index) + { + ActionBar actionBar = ((Activity)_context).ActionBar; + page.PropertyChanged -= PagePropertyChanged; + actionBar.RemoveTabAt(index); + } + + void Reset() + { + ActionBar.RemoveAllTabs(); + + if (CurrentTabbedPage == null) + return; + + var i = 0; + foreach (Page tab in CurrentTabbedPage.Children.OfType<Page>()) + { + ActionBar.Tab realTab = AddTab(tab, i++); + if (tab == CurrentTabbedPage.CurrentPage) + realTab.Select(); + } + } + + void SetActionBarTextColor() + { + Color navigationBarTextColor = CurrentNavigationPage == null ? Color.Default : CurrentNavigationPage.BarTextColor; + TextView actionBarTitleTextView = null; + + int actionBarTitleId = _context.Resources.GetIdentifier("action_bar_title", "id", "android"); + if (actionBarTitleId > 0) + actionBarTitleTextView = ((Activity)_context).FindViewById<TextView>(actionBarTitleId); + + if (actionBarTitleTextView != null && navigationBarTextColor != Color.Default) + actionBarTitleTextView.SetTextColor(navigationBarTextColor.ToAndroid()); + else if (actionBarTitleTextView != null && navigationBarTextColor == Color.Default) + actionBarTitleTextView.SetTextColor(_defaultActionBarTitleTextColor.ToAndroid()); + } + + Color SetDefaultActionBarTitleTextColor() + { + var defaultTitleTextColor = new Color(); + + TextView actionBarTitleTextView = null; + + int actionBarTitleId = _context.Resources.GetIdentifier("action_bar_title", "id", "android"); + if (actionBarTitleId > 0) + actionBarTitleTextView = ((Activity)_context).FindViewById<TextView>(actionBarTitleId); + + if (actionBarTitleTextView != null) + { + ColorStateList defaultTitleColorList = actionBarTitleTextView.TextColors; + string defaultColorHex = defaultTitleColorList.DefaultColor.ToString("X"); + defaultTitleTextColor = Color.FromHex(defaultColorHex); + } + + return defaultTitleTextColor; + } + + bool ShouldShowActionBarTitleArea() + { + if (Forms.TitleBarVisibility == AndroidTitleBarVisibility.Never) + return false; + + bool hasMasterDetailPage = CurrentMasterDetailPage != null; + bool navigated = CurrentNavigationPage != null && CurrentNavigationPage.StackDepth > 1; + bool navigationPageHasNavigationBar = CurrentNavigationPage != null && NavigationPage.GetHasNavigationBar(CurrentNavigationPage.CurrentPage); + return navigationPageHasNavigationBar || (hasMasterDetailPage && !navigated); + } + + bool ShouldUpdateActionBarUpColor() + { + bool hasMasterDetailPage = CurrentMasterDetailPage != null; + bool navigated = CurrentNavigationPage != null && CurrentNavigationPage.StackDepth > 1; + return (hasMasterDetailPage && navigated) || !hasMasterDetailPage; + } + + void ShowActionBar() + { + ReloadToolbarItems(); + UpdateActionBarHomeAsUp(ActionBar); + ActionBar.Show(); + UpdateActionBarBackgroundColor(); + UpdateActionBarTextColor(); + } + + void ToolbarTrackerOnCollectionChanged(object sender, EventArgs eventArgs) + { + ReloadToolbarItems(); + } + + bool UpButtonShouldNavigate() + { + if (CurrentNavigationPage == null) + return false; + + bool pagePushed = CurrentNavigationPage.StackDepth > 1; + bool pushedPageHasBackButton = NavigationPage.GetHasBackButton(CurrentNavigationPage.CurrentPage); + + return pagePushed && pushedPageHasBackButton; + } + + void UpdateActionBarHomeAsUp(ActionBar actionBar) + { + bool showHomeAsUp = ShouldShowActionBarTitleArea() && (CurrentMasterDetailPage != null || UpButtonShouldNavigate()); + actionBar.SetDisplayHomeAsUpEnabled(showHomeAsUp); + } + + void UpdateActionBarTitle() + { + Page view = null; + if (CurrentNavigationPage != null) + view = CurrentNavigationPage.CurrentPage; + else if (CurrentTabbedPage != null) + view = CurrentTabbedPage.CurrentPage; + + if (view == null) + return; + + ActionBar actionBar = ((Activity)_context).ActionBar; + + var useLogo = false; + var showHome = false; + var showTitle = false; + + if (ShouldShowActionBarTitleArea()) + { + actionBar.Title = view.Title; + FileImageSource titleIcon = NavigationPage.GetTitleIcon(view); + if (!string.IsNullOrWhiteSpace(titleIcon)) + { + actionBar.SetLogo(_context.Resources.GetDrawable(titleIcon)); + useLogo = true; + showHome = true; + showTitle = true; + } + else + { + showHome = true; + showTitle = true; + } + } + + ActionBarDisplayOptions options = 0; + if (useLogo) + options = options | ActionBarDisplayOptions.UseLogo; + if (showHome) + options = options | ActionBarDisplayOptions.ShowHome; + if (showTitle) + options = options | ActionBarDisplayOptions.ShowTitle; + actionBar.SetDisplayOptions(options, ActionBarDisplayOptions.UseLogo | ActionBarDisplayOptions.ShowTitle | ActionBarDisplayOptions.ShowHome); + + UpdateActionBarHomeAsUp(actionBar); + } + + void UpdateActionBarUpImageColor() + { + Color navigationBarTextColor = CurrentNavigationPage == null ? Color.Default : CurrentNavigationPage.BarTextColor; + ImageView actionBarUpImageView = null; + + int actionBarUpId = _context.Resources.GetIdentifier("up", "id", "android"); + if (actionBarUpId > 0) + actionBarUpImageView = ((Activity)_context).FindViewById<ImageView>(actionBarUpId); + + if (actionBarUpImageView != null && navigationBarTextColor != Color.Default) + { + if (ShouldUpdateActionBarUpColor()) + actionBarUpImageView.SetColorFilter(navigationBarTextColor.ToAndroid(), PorterDuff.Mode.SrcIn); + else + actionBarUpImageView.SetColorFilter(null); + } + else if (actionBarUpImageView != null && navigationBarTextColor == Color.Default) + actionBarUpImageView.SetColorFilter(null); + } + + static void UpdateGlobalContext(VisualElement view) + { + Element parent = view; + while (!Application.IsApplicationOrNull(parent.RealParent)) + parent = parent.RealParent; + + var rootPage = parent as Page; + if (rootPage != null) + { + Context context = GetPageContext(rootPage); + if (context != null) + Forms.Context = context; + } + } + + internal class DefaultRenderer : VisualElementRenderer<View> + { + } + + #region IPlatformEngine implementation + + void IPlatformLayout.OnLayout(bool changed, int l, int t, int r, int b) + { + if (changed) + { + // ActionBar title text color resets on rotation, make sure to update + UpdateActionBarTextColor(); + foreach (Page modal in _navModel.Roots.ToList()) + modal.Layout(new Rectangle(0, 0, _context.FromPixels(r - l), _context.FromPixels(b - t))); + } + + foreach (IVisualElementRenderer view in _navModel.Roots.Select(GetRenderer)) + view.UpdateLayout(); + } + + SizeRequest IPlatform.GetNativeSize(VisualElement view, double widthConstraint, double heightConstraint) + { + Performance.Start(); + + // FIXME: potential crash + IVisualElementRenderer viewRenderer = GetRenderer(view); + + // negative numbers have special meanings to android they don't to us + widthConstraint = widthConstraint <= -1 ? double.PositiveInfinity : _context.ToPixels(widthConstraint); + heightConstraint = heightConstraint <= -1 ? double.PositiveInfinity : _context.ToPixels(heightConstraint); + + int width = !double.IsPositiveInfinity(widthConstraint) + ? MeasureSpecFactory.MakeMeasureSpec((int)widthConstraint, MeasureSpecMode.AtMost) + : MeasureSpecFactory.MakeMeasureSpec(0, MeasureSpecMode.Unspecified); + + int height = !double.IsPositiveInfinity(heightConstraint) + ? MeasureSpecFactory.MakeMeasureSpec((int)heightConstraint, MeasureSpecMode.AtMost) + : MeasureSpecFactory.MakeMeasureSpec(0, MeasureSpecMode.Unspecified); + + SizeRequest rawResult = viewRenderer.GetDesiredSize(width, height); + if (rawResult.Minimum == Size.Zero) + rawResult.Minimum = rawResult.Request; + var result = new SizeRequest(new Size(_context.FromPixels(rawResult.Request.Width), _context.FromPixels(rawResult.Request.Height)), + new Size(_context.FromPixels(rawResult.Minimum.Width), _context.FromPixels(rawResult.Minimum.Height))); + + Performance.Stop(); + return result; + } + + bool _navAnimationInProgress; + + internal bool NavAnimationInProgress + { + get { return _navAnimationInProgress; } + set + { + if (_navAnimationInProgress == value) + return; + _navAnimationInProgress = value; + if (value) + MessagingCenter.Send(this, CloseContextActionsSignalName); + } + } + + #endregion + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/PlatformEffect.cs b/Xamarin.Forms.Platform.Android/PlatformEffect.cs new file mode 100644 index 00000000..6830b722 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/PlatformEffect.cs @@ -0,0 +1,9 @@ +using Android.Views; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android +{ + public abstract class PlatformEffect : PlatformEffect<ViewGroup, AView> + { + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/PlatformRenderer.cs b/Xamarin.Forms.Platform.Android/PlatformRenderer.cs new file mode 100644 index 00000000..4fbc0293 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/PlatformRenderer.cs @@ -0,0 +1,81 @@ +using System; +using Android.App; +using Android.Content; +using Android.Views; +using Android.Widget; + +namespace Xamarin.Forms.Platform.Android +{ + internal class PlatformRenderer : ViewGroup + { + readonly IPlatformLayout _canvas; + Point _downPosition; + + DateTime _downTime; + + public PlatformRenderer(Context context, IPlatformLayout canvas) : base(context) + { + _canvas = canvas; + Focusable = true; + FocusableInTouchMode = true; + } + + public override bool DispatchTouchEvent(MotionEvent e) + { + if (e.Action == MotionEventActions.Down) + { + _downTime = DateTime.UtcNow; + _downPosition = new Point(e.RawX, e.RawY); + } + + if (e.Action != MotionEventActions.Up) + return base.DispatchTouchEvent(e); + + global::Android.Views.View currentView = ((Activity)Context).CurrentFocus; + bool result = base.DispatchTouchEvent(e); + + do + { + if (!(currentView is EditText)) + break; + + global::Android.Views.View newCurrentView = ((Activity)Context).CurrentFocus; + + if (currentView != newCurrentView) + break; + + double distance = _downPosition.Distance(new Point(e.RawX, e.RawY)); + + if (distance > Context.ToPixels(20) || DateTime.UtcNow - _downTime > TimeSpan.FromMilliseconds(200)) + break; + + var location = new int[2]; + currentView.GetLocationOnScreen(location); + + float x = e.RawX + currentView.Left - location[0]; + float y = e.RawY + currentView.Top - location[1]; + + var rect = new Rectangle(currentView.Left, currentView.Top, currentView.Width, currentView.Height); + + if (rect.Contains(x, y)) + break; + + Context.HideKeyboard(currentView); + RequestFocus(); + } while (false); + + return result; + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + SetMeasuredDimension(r - l, b - t); + _canvas?.OnLayout(changed, l, t, r, b); + } + + protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + SetMeasuredDimension(MeasureSpec.GetSize(widthMeasureSpec), MeasureSpec.GetSize(heightMeasureSpec)); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Properties/AssemblyInfo.cs b/Xamarin.Forms.Platform.Android/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..03f4fa6a --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Properties/AssemblyInfo.cs @@ -0,0 +1,65 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using Xamarin.Forms; +using Xamarin.Forms.Platform.Android; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("Xamarin.Forms.Platform.Android")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCulture("")] +[assembly: InternalsVisibleTo("Xamarin.Forms.Platform")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] + +#if ROOT_RENDERERS +[assembly: ExportRenderer (typeof (BoxView), typeof (BoxRenderer))] +[assembly: ExportRenderer (typeof (Entry), typeof (EntryRenderer))] +[assembly: ExportRenderer (typeof (Editor), typeof (EditorRenderer))] +[assembly: ExportRenderer (typeof (Label), typeof (LabelRenderer))] +[assembly: ExportRenderer (typeof (Image), typeof (ImageRenderer))] +[assembly: ExportRenderer (typeof (Button), typeof (ButtonRenderer))] +[assembly: ExportRenderer (typeof (TableView), typeof (TableViewRenderer))] +[assembly: ExportRenderer (typeof (ListView), typeof (ListViewRenderer))] +[assembly: ExportRenderer (typeof (Slider), typeof (SliderRenderer))] +[assembly: ExportRenderer (typeof (WebView), typeof (WebViewRenderer))] +[assembly: ExportRenderer (typeof (SearchBar), typeof (SearchBarRenderer))] +[assembly: ExportRenderer (typeof (Switch), typeof (SwitchRenderer))] +[assembly: ExportRenderer (typeof (DatePicker), typeof (DatePickerRenderer))] +[assembly: ExportRenderer (typeof (TimePicker), typeof (TimePickerRenderer))] +[assembly: ExportRenderer (typeof (Picker), typeof (PickerRenderer))] +[assembly: ExportRenderer (typeof (Stepper), typeof (StepperRenderer))] +[assembly: ExportRenderer (typeof (ProgressBar), typeof (ProgressBarRenderer))] +[assembly: ExportRenderer (typeof (ScrollView), typeof (ScrollViewRenderer))] +[assembly: ExportRenderer (typeof (Toolbar), typeof (ToolbarRenderer))] +[assembly: ExportRenderer (typeof (ActivityIndicator), typeof (ActivityIndicatorRenderer))] +[assembly: ExportRenderer (typeof (Frame), typeof (FrameRenderer))] +[assembly: ExportRenderer (typeof (NavigationMenu), typeof (NavigationMenuRenderer))] +[assembly: ExportRenderer (typeof (OpenGLView), typeof (OpenGLViewRenderer))] + +[assembly: ExportRenderer (typeof (TabbedPage), typeof (TabbedRenderer))] +[assembly: ExportRenderer (typeof (NavigationPage), typeof (NavigationRenderer))] +[assembly: ExportRenderer (typeof (CarouselPage), typeof (CarouselPageRenderer))] +[assembly: ExportRenderer (typeof (Page), typeof (PageRenderer))] +[assembly: ExportRenderer (typeof (MasterDetailPage), typeof (MasterDetailRenderer))] +#endif + +[assembly: ExportRenderer(typeof(NativeViewWrapper), typeof(NativeViewWrapperRenderer))] +[assembly: ExportCell(typeof(Cell), typeof(CellRenderer))] +[assembly: ExportCell(typeof(EntryCell), typeof(EntryCellRenderer))] +[assembly: ExportCell(typeof(SwitchCell), typeof(SwitchCellRenderer))] +[assembly: ExportCell(typeof(TextCell), typeof(TextCellRenderer))] +[assembly: ExportCell(typeof(ImageCell), typeof(ImageCellRenderer))] +[assembly: ExportCell(typeof(ViewCell), typeof(ViewCellRenderer))] +[assembly: ExportImageSourceHandler(typeof(FileImageSource), typeof(FileImageSourceHandler))] +[assembly: ExportImageSourceHandler(typeof(StreamImageSource), typeof(StreamImagesourceHandler))] +[assembly: ExportImageSourceHandler(typeof(UriImageSource), typeof(ImageLoaderSourceHandler))] +[assembly: Xamarin.Forms.Dependency(typeof(Deserializer))] +[assembly: Xamarin.Forms.Dependency(typeof(ResourcesProvider))] +[assembly: Preserve]
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/RendererFactory.cs b/Xamarin.Forms.Platform.Android/RendererFactory.cs new file mode 100644 index 00000000..7d0b03b3 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/RendererFactory.cs @@ -0,0 +1,13 @@ +using System; + +namespace Xamarin.Forms.Platform.Android +{ + public static class RendererFactory + { + [Obsolete("Use Platform.CreateRenderer")] + public static IVisualElementRenderer GetRenderer(VisualElement view) + { + return Platform.CreateRenderer(view); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/RendererPool.cs b/Xamarin.Forms.Platform.Android/RendererPool.cs new file mode 100644 index 00000000..95a28778 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/RendererPool.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; + +namespace Xamarin.Forms.Platform.Android +{ + public sealed class RendererPool + { + readonly Dictionary<Type, Stack<IVisualElementRenderer>> _freeRenderers = new Dictionary<Type, Stack<IVisualElementRenderer>>(); + + readonly VisualElement _oldElement; + + readonly IVisualElementRenderer _parent; + + public RendererPool(IVisualElementRenderer renderer, VisualElement oldElement) + { + if (renderer == null) + throw new ArgumentNullException("renderer"); + + if (oldElement == null) + throw new ArgumentNullException("oldElement"); + + _oldElement = oldElement; + _parent = renderer; + } + + public void ClearChildrenRenderers() + { + if (_parent.Element.LogicalChildren.Count == 0) + return; + ClearChildrenRenderers(_oldElement); + } + + public IVisualElementRenderer GetFreeRenderer(VisualElement view) + { + if (view == null) + throw new ArgumentNullException("view"); + + Type rendererType = Registrar.Registered.GetHandlerType(view.GetType()) ?? typeof(ViewRenderer); + + Stack<IVisualElementRenderer> renderers; + if (!_freeRenderers.TryGetValue(rendererType, out renderers) || renderers.Count == 0) + return null; + + IVisualElementRenderer renderer = renderers.Pop(); + renderer.SetElement(view); + return renderer; + } + + void ClearChildrenRenderers(VisualElement view) + { + if (view == null) + return; + + foreach (Element logicalChild in view.LogicalChildren) + { + var child = logicalChild as VisualElement; + if (child != null) + { + IVisualElementRenderer renderer = Platform.GetRenderer(child); + if (renderer == null) + continue; + + if (renderer.ViewGroup.Parent != _parent.ViewGroup) + continue; + + renderer.ViewGroup.RemoveFromParent(); + + Platform.SetRenderer(child, null); + PushRenderer(renderer); + } + } + + if (_parent.ViewGroup.ChildCount != 0) + _parent.ViewGroup.RemoveAllViews(); + } + + void PushRenderer(IVisualElementRenderer renderer) + { + Type rendererType = renderer.GetType(); + + Stack<IVisualElementRenderer> renderers; + if (!_freeRenderers.TryGetValue(rendererType, out renderers)) + _freeRenderers[rendererType] = renderers = new Stack<IVisualElementRenderer>(); + + renderers.Push(renderer); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/AHorizontalScrollView.cs b/Xamarin.Forms.Platform.Android/Renderers/AHorizontalScrollView.cs new file mode 100644 index 00000000..6a64a35b --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/AHorizontalScrollView.cs @@ -0,0 +1,62 @@ +using Android.Content; +using Android.Views; +using Android.Widget; + +namespace Xamarin.Forms.Platform.Android +{ + public class AHorizontalScrollView : HorizontalScrollView + { + readonly ScrollViewRenderer _renderer; + + public AHorizontalScrollView(Context context, ScrollViewRenderer renderer) : base(context) + { + _renderer = renderer; + } + + internal bool IsBidirectional { get; set; } + + public override bool OnInterceptTouchEvent(MotionEvent ev) + { + // set the start point for the bidirectional scroll; + // Down is swallowed by other controls, so we'll just sneak this in here without actually preventing + // other controls from getting the event. + if (IsBidirectional && ev.Action == MotionEventActions.Down) + { + _renderer.LastY = ev.RawY; + _renderer.LastX = ev.RawX; + } + + return base.OnInterceptTouchEvent(ev); + } + + public override bool OnTouchEvent(MotionEvent ev) + { + // The nested ScrollViews will allow us to scroll EITHER vertically OR horizontally in a single gesture. + // This will allow us to also scroll diagonally. + // We'll fall through to the base event so we still get the fling from the ScrollViews. + // We have to do this in both ScrollViews, since a single gesture will be owned by one or the other, depending + // on the initial direction of movement (i.e., horizontal/vertical). + if (IsBidirectional) + { + float dX = _renderer.LastX - ev.RawX; + float dY = _renderer.LastY - ev.RawY; + _renderer.LastY = ev.RawY; + _renderer.LastX = ev.RawX; + if (ev.Action == MotionEventActions.Move) + { + var parent = (global::Android.Widget.ScrollView)Parent; + parent.ScrollBy(0, (int)dY); + ScrollBy((int)dX, 0); + } + } + return base.OnTouchEvent(ev); + } + + protected override void OnScrollChanged(int l, int t, int oldl, int oldt) + { + base.OnScrollChanged(l, t, oldl, oldt); + + _renderer.UpdateScrollPosition(Forms.Context.FromPixels(l), Forms.Context.FromPixels(t)); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ActionSheetRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/ActionSheetRenderer.cs new file mode 100644 index 00000000..ca51a709 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/ActionSheetRenderer.cs @@ -0,0 +1,83 @@ +using System; +using Android.App; +using Android.Graphics; +using Android.OS; +using Android.Views; +using Android.Widget; +using AButton = Android.Widget.Button; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android +{ + [Obsolete("ActionSheet now uses default implementation.")] + public class ActionSheetRenderer : Dialog, AView.IOnClickListener + { + readonly ActionSheetArguments _arguments; + readonly LinearLayout _layout; + + internal ActionSheetRenderer(ActionSheetArguments actionSheetArguments) : base(Forms.Context) + { + _arguments = actionSheetArguments; + _layout = new LinearLayout(Context); + } + + void AView.IOnClickListener.OnClick(AView v) + { + var button = (AButton)v; + _arguments.SetResult(button.Text); + Hide(); + } + + public override void Cancel() + { + base.Cancel(); + _arguments.SetResult(null); + } + + public override void OnAttachedToWindow() + { + base.OnAttachedToWindow(); + Window.SetGravity(GravityFlags.CenterVertical); + Window.SetLayout(-1, -2); + } + + protected override void OnCreate(Bundle savedInstanceState) + { + base.OnCreate(savedInstanceState); + + SetCanceledOnTouchOutside(true); + + _layout.Orientation = Orientation.Vertical; + + using(var layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FillParent, ViewGroup.LayoutParams.FillParent)) + SetContentView(_layout, layoutParams); + + if (_arguments.Destruction != null) + { + AButton destruct = AddButton(_arguments.Destruction); + destruct.Background.SetColorFilter(new Color(1, 0, 0, 1).ToAndroid(), PorterDuff.Mode.Multiply); + } + + foreach (string button in _arguments.Buttons) + AddButton(button); + + if (_arguments.Cancel != null) + { + AButton cancel = AddButton(_arguments.Cancel); + cancel.Background.SetColorFilter(new Color(0.5, 0.5, 0.5, 1).ToAndroid(), PorterDuff.Mode.Multiply); + } + + SetTitle(_arguments.Title); + } + + AButton AddButton(string name) + { + var button = new AButton(Context) { Text = name }; + button.SetOnClickListener(this); + + _layout.AddView(button); + + return button; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ActivityIndicatorRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/ActivityIndicatorRenderer.cs new file mode 100644 index 00000000..e2314c61 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/ActivityIndicatorRenderer.cs @@ -0,0 +1,59 @@ +using System.ComponentModel; +using Android.Graphics; +using Android.OS; +using Android.Views; +using AProgressBar = Android.Widget.ProgressBar; + +namespace Xamarin.Forms.Platform.Android +{ + public class ActivityIndicatorRenderer : ViewRenderer<ActivityIndicator, AProgressBar> + { + public ActivityIndicatorRenderer() + { + AutoPackage = false; + } + + protected override void OnElementChanged(ElementChangedEventArgs<ActivityIndicator> e) + { + base.OnElementChanged(e); + + AProgressBar progressBar = Control; + if (progressBar == null) + { + progressBar = new AProgressBar(Context) { Indeterminate = true }; + SetNativeControl(progressBar); + } + + UpdateColor(); + UpdateVisibility(); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == ActivityIndicator.IsRunningProperty.PropertyName) + UpdateVisibility(); + else if (e.PropertyName == ActivityIndicator.ColorProperty.PropertyName) + UpdateColor(); + } + + void UpdateColor() + { + if (Build.VERSION.SdkInt < BuildVersionCodes.Lollipop) + return; + + Color color = Element.Color; + + if (!color.IsDefault) + Control.IndeterminateDrawable.SetColorFilter(color.ToAndroid(), PorterDuff.Mode.SrcIn); + else + Control.IndeterminateDrawable.ClearColorFilter(); + } + + void UpdateVisibility() + { + Control.Visibility = Element.IsRunning ? ViewStates.Visible : ViewStates.Invisible; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/AlignmentExtensions.cs b/Xamarin.Forms.Platform.Android/Renderers/AlignmentExtensions.cs new file mode 100644 index 00000000..dd05f166 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/AlignmentExtensions.cs @@ -0,0 +1,33 @@ +using Android.Views; + +namespace Xamarin.Forms.Platform.Android +{ + internal static class AlignmentExtensions + { + internal static GravityFlags ToHorizontalGravityFlags(this TextAlignment alignment) + { + switch (alignment) + { + case TextAlignment.Center: + return GravityFlags.CenterHorizontal; + case TextAlignment.End: + return GravityFlags.Right; + default: + return GravityFlags.Left; + } + } + + internal static GravityFlags ToVerticalGravityFlags(this TextAlignment alignment) + { + switch (alignment) + { + case TextAlignment.Start: + return GravityFlags.Top; + case TextAlignment.End: + return GravityFlags.Bottom; + default: + return GravityFlags.CenterVertical; + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/BoxRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/BoxRenderer.cs new file mode 100644 index 00000000..725829f4 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/BoxRenderer.cs @@ -0,0 +1,43 @@ +using System.ComponentModel; +using Android.Views; + +namespace Xamarin.Forms.Platform.Android +{ + public class BoxRenderer : VisualElementRenderer<BoxView> + { + public BoxRenderer() + { + AutoPackage = false; + } + + public override bool OnTouchEvent(MotionEvent e) + { + base.OnTouchEvent(e); + return !Element.InputTransparent; + } + + protected override void OnElementChanged(ElementChangedEventArgs<BoxView> e) + { + base.OnElementChanged(e); + UpdateBackgroundColor(); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == BoxView.ColorProperty.PropertyName || e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName) + UpdateBackgroundColor(); + } + + void UpdateBackgroundColor() + { + Color colorToSet = Element.Color; + + if (colorToSet == Color.Default) + colorToSet = Element.BackgroundColor; + + SetBackgroundColor(colorToSet.ToAndroid(Color.Transparent)); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ButtonDrawable.cs b/Xamarin.Forms.Platform.Android/Renderers/ButtonDrawable.cs new file mode 100644 index 00000000..f25f9301 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/ButtonDrawable.cs @@ -0,0 +1,150 @@ +using System.Linq; +using Android.Graphics; +using Android.Graphics.Drawables; + +namespace Xamarin.Forms.Platform.Android +{ + internal class ButtonDrawable : Drawable + { + bool _isDisposed; + Bitmap _normalBitmap; + bool _pressed; + Bitmap _pressedBitmap; + + public ButtonDrawable() + { + _pressed = false; + } + + public Button Button { get; set; } + + public override bool IsStateful + { + get { return true; } + } + + public override int Opacity + { + get { return 0; } + } + + public override void Draw(Canvas canvas) + { + int width = Bounds.Width(); + int height = Bounds.Height(); + + if (width <= 0 || height <= 0) + return; + + if (_normalBitmap == null || _normalBitmap.Height != height || _normalBitmap.Width != width) + { + Reset(); + + _normalBitmap = CreateBitmap(false, width, height); + _pressedBitmap = CreateBitmap(true, width, height); + } + + Bitmap bitmap = GetState().Contains(global::Android.Resource.Attribute.StatePressed) ? _pressedBitmap : _normalBitmap; + canvas.DrawBitmap(bitmap, 0, 0, new Paint()); + } + + public void Reset() + { + if (_normalBitmap != null) + { + _normalBitmap.Recycle(); + _normalBitmap.Dispose(); + _normalBitmap = null; + } + + if (_pressedBitmap != null) + { + _pressedBitmap.Recycle(); + _pressedBitmap.Dispose(); + _pressedBitmap = null; + } + } + + public override void SetAlpha(int alpha) + { + } + + public override void SetColorFilter(ColorFilter cf) + { + } + + protected override void Dispose(bool disposing) + { + if (_isDisposed) + return; + + _isDisposed = true; + + if (disposing) + Reset(); + + base.Dispose(disposing); + } + + protected override bool OnStateChange(int[] state) + { + bool old = _pressed; + _pressed = state.Contains(global::Android.Resource.Attribute.StatePressed); + if (_pressed != old) + { + InvalidateSelf(); + return true; + } + return false; + } + + Bitmap CreateBitmap(bool pressed, int width, int height) + { + Bitmap bitmap = Bitmap.CreateBitmap(width, height, Bitmap.Config.Argb8888); + using(var canvas = new Canvas(bitmap)) + { + DrawBackground(canvas, width, height, pressed); + DrawOutline(canvas, width, height); + } + + return bitmap; + } + + void DrawBackground(Canvas canvas, int width, int height, bool pressed) + { + var paint = new Paint { AntiAlias = true }; + var path = new Path(); + + float borderRadius = Forms.Context.ToPixels(Button.BorderRadius); + + path.AddRoundRect(new RectF(0, 0, width, height), borderRadius, borderRadius, Path.Direction.Cw); + + paint.Color = pressed ? Button.BackgroundColor.AddLuminosity(-0.1).ToAndroid() : Button.BackgroundColor.ToAndroid(); + paint.SetStyle(Paint.Style.Fill); + canvas.DrawPath(path, paint); + } + + void DrawOutline(Canvas canvas, int width, int height) + { + if (Button.BorderWidth <= 0) + return; + + using(var paint = new Paint { AntiAlias = true }) + using(var path = new Path()) + { + float borderWidth = Forms.Context.ToPixels(Button.BorderWidth); + float inset = borderWidth / 2; + + // adjust border radius so outer edge of stroke is same radius as border radius of background + float borderRadius = Forms.Context.ToPixels(Button.BorderRadius) - inset; + + path.AddRoundRect(new RectF(inset, inset, width - inset, height - inset), borderRadius, borderRadius, Path.Direction.Cw); + paint.StrokeWidth = borderWidth; + paint.SetStyle(Paint.Style.Stroke); + paint.Color = Button.BorderColor.ToAndroid(); + + canvas.DrawPath(path, paint); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ButtonRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/ButtonRenderer.cs new file mode 100644 index 00000000..eb9b884f --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/ButtonRenderer.cs @@ -0,0 +1,252 @@ +using System; +using System.ComponentModel; +using Android.Content.Res; +using Android.Graphics; +using Android.Graphics.Drawables; +using Android.Util; +using AButton = Android.Widget.Button; +using AView = Android.Views.View; +using Object = Java.Lang.Object; + +namespace Xamarin.Forms.Platform.Android +{ + public class ButtonRenderer : ViewRenderer<Button, AButton>, AView.IOnAttachStateChangeListener + { + ButtonDrawable _backgroundDrawable; + ColorStateList _buttonDefaulTextColors; + Drawable _defaultDrawable; + float _defaultFontSize; + Typeface _defaultTypeface; + bool _drawableEnabled; + + bool _isDisposed; + + public ButtonRenderer() + { + AutoPackage = false; + } + + AButton NativeButton + { + get { return Control; } + } + + public void OnViewAttachedToWindow(AView attachedView) + { + UpdateText(); + } + + public void OnViewDetachedFromWindow(AView detachedView) + { + } + + public override SizeRequest GetDesiredSize(int widthConstraint, int heightConstraint) + { + UpdateText(); + return base.GetDesiredSize(widthConstraint, heightConstraint); + } + + protected override void Dispose(bool disposing) + { + if (_isDisposed) + return; + + _isDisposed = true; + + if (disposing) + { + if (_backgroundDrawable != null) + { + _backgroundDrawable.Dispose(); + _backgroundDrawable = null; + } + } + + base.Dispose(disposing); + } + + protected override void OnElementChanged(ElementChangedEventArgs<Button> e) + { + base.OnElementChanged(e); + + if (e.OldElement == null) + { + AButton button = Control; + if (button == null) + { + button = new AButton(Context); + button.SetOnClickListener(ButtonClickListener.Instance.Value); + button.Tag = this; + SetNativeControl(button); + + button.AddOnAttachStateChangeListener(this); + } + } + else + { + if (_drawableEnabled) + { + _drawableEnabled = false; + _backgroundDrawable.Reset(); + _backgroundDrawable = null; + } + } + + UpdateAll(); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == Button.TextProperty.PropertyName) + UpdateText(); + else if (e.PropertyName == Button.TextColorProperty.PropertyName) + UpdateTextColor(); + else if (e.PropertyName == VisualElement.IsEnabledProperty.PropertyName) + UpdateEnabled(); + else if (e.PropertyName == Button.FontProperty.PropertyName) + UpdateFont(); + else if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName) + UpdateDrawable(); + else if (e.PropertyName == Button.ImageProperty.PropertyName) + UpdateBitmap(); + else if (e.PropertyName == VisualElement.IsVisibleProperty.PropertyName) + UpdateText(); + + if (_drawableEnabled && + (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName || e.PropertyName == Button.BorderColorProperty.PropertyName || e.PropertyName == Button.BorderRadiusProperty.PropertyName || + e.PropertyName == Button.BorderWidthProperty.PropertyName)) + { + _backgroundDrawable.Reset(); + Control.Invalidate(); + } + + base.OnElementPropertyChanged(sender, e); + } + + protected override void UpdateBackgroundColor() + { + // Do nothing, the drawable handles this now + } + + void UpdateAll() + { + UpdateFont(); + UpdateText(); + UpdateBitmap(); + UpdateTextColor(); + UpdateEnabled(); + UpdateDrawable(); + } + + async void UpdateBitmap() + { + if (Element.Image != null && !string.IsNullOrEmpty(Element.Image.File)) + { + Drawable image = Context.Resources.GetDrawable(Element.Image.File); + Control.SetCompoundDrawablesWithIntrinsicBounds(image, null, null, null); + if (image != null) + image.Dispose(); + } + else + Control.SetCompoundDrawablesWithIntrinsicBounds(null, null, null, null); + } + + void UpdateDrawable() + { + if (Element.BackgroundColor == Color.Default) + { + if (!_drawableEnabled) + return; + + if (_defaultDrawable != null) + Control.SetBackgroundDrawable(_defaultDrawable); + + _drawableEnabled = false; + } + else + { + if (_backgroundDrawable == null) + _backgroundDrawable = new ButtonDrawable(); + + _backgroundDrawable.Button = Element; + + if (_drawableEnabled) + return; + + if (_defaultDrawable == null) + _defaultDrawable = Control.Background; + + Control.SetBackgroundDrawable(_backgroundDrawable); + _drawableEnabled = true; + } + + Control.Invalidate(); + } + + void UpdateEnabled() + { + Control.Enabled = Element.IsEnabled; + } + + void UpdateFont() + { + Button button = Element; + if (button.Font == Font.Default && _defaultFontSize == 0f) + return; + + if (_defaultFontSize == 0f) + { + _defaultTypeface = NativeButton.Typeface; + _defaultFontSize = NativeButton.TextSize; + } + + if (button.Font == Font.Default) + { + NativeButton.Typeface = _defaultTypeface; + NativeButton.SetTextSize(ComplexUnitType.Px, _defaultFontSize); + } + else + { + NativeButton.Typeface = button.Font.ToTypeface(); + NativeButton.SetTextSize(ComplexUnitType.Sp, button.Font.ToScaledPixel()); + } + } + + void UpdateText() + { + NativeButton.Text = Element.Text; + } + + void UpdateTextColor() + { + Color color = Element.TextColor; + + if (color.IsDefault) + { + if (_buttonDefaulTextColors == null) + return; + + NativeButton.SetTextColor(_buttonDefaulTextColors); + } + else + { + _buttonDefaulTextColors = _buttonDefaulTextColors ?? Control.TextColors; + + // Set the new enabled state color, preserving the default disabled state color + NativeButton.SetTextColor(color.ToAndroidPreserveDisabled(_buttonDefaulTextColors)); + } + } + + class ButtonClickListener : Object, IOnClickListener + { + public static readonly Lazy<ButtonClickListener> Instance = new Lazy<ButtonClickListener>(() => new ButtonClickListener()); + + public void OnClick(AView v) + { + var renderer = v.Tag as ButtonRenderer; + if (renderer != null) + ((IButtonController)renderer.Element).SendClicked(); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/CarouselPageAdapter.cs b/Xamarin.Forms.Platform.Android/Renderers/CarouselPageAdapter.cs new file mode 100644 index 00000000..345501d5 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/CarouselPageAdapter.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Specialized; +using System.Linq; +using Android.Content; +using Android.Support.V4.View; +using Android.Views; +using Object = Java.Lang.Object; + +namespace Xamarin.Forms.Platform.Android +{ + internal class CarouselPageAdapter : PagerAdapter, ViewPager.IOnPageChangeListener + { + readonly Context _context; + readonly ViewPager _pager; + bool _ignoreAndroidSelection; + CarouselPage _page; + + public CarouselPageAdapter(ViewPager pager, CarouselPage page, Context context) + { + _pager = pager; + _page = page; + _context = context; + + page.PagesChanged += OnPagesChanged; + } + + public override int Count + { + get { return _page.Children.Count(); } + } + + public void OnPageScrolled(int position, float positionOffset, int positionOffsetPixels) + { + } + + public void OnPageScrollStateChanged(int state) + { + } + + public void OnPageSelected(int position) + { + if (_ignoreAndroidSelection) + return; + + int currentItem = _pager.CurrentItem; + _page.CurrentPage = currentItem >= 0 && currentItem < _page.LogicalChildren.Count ? _page.LogicalChildren[currentItem] as ContentPage : null; + } + + public override void DestroyItem(ViewGroup p0, int p1, Object p2) + { + var holder = (ObjectJavaBox<Tuple<ViewGroup, Page, int>>)p2; + Page destroyedPage = holder.Instance.Item2; + + IVisualElementRenderer renderer = Platform.GetRenderer(destroyedPage); + renderer.ViewGroup.RemoveFromParent(); + holder.Instance.Item1.RemoveFromParent(); + } + + public override int GetItemPosition(Object item) + { + // The int is the current index. + var holder = (ObjectJavaBox<Tuple<ViewGroup, Page, int>>)item; + Element parent = holder.Instance.Item2.RealParent; + if (parent == null) + return PositionNone; + + // Unfortunately we can't just call CarouselPage.GetIndex, because we need to know + // if the item has been removed. We could update MultiPage<T> to set removed items' index + // to -1 to support this if it ever becomes an issue. + int index = ((CarouselPage)parent).Children.IndexOf(holder.Instance.Item2); + if (index == -1) + return PositionNone; + + if (index != holder.Instance.Item3) + { + holder.Instance = new Tuple<ViewGroup, Page, int>(holder.Instance.Item1, holder.Instance.Item2, index); + return index; + } + + return PositionUnchanged; + } + + public override Object InstantiateItem(ViewGroup container, int position) + { + ContentPage child = _page.Children.ElementAt(position); + if (Platform.GetRenderer(child) == null) + Platform.SetRenderer(child, Platform.CreateRenderer(child)); + + IVisualElementRenderer renderer = Platform.GetRenderer(child); + renderer.ViewGroup.RemoveFromParent(); + + ViewGroup frame = new PageContainer(_context, renderer); + + container.AddView(frame); + + return new ObjectJavaBox<Tuple<ViewGroup, Page, int>>(new Tuple<ViewGroup, Page, int>(frame, child, position)); + } + + public override bool IsViewFromObject(global::Android.Views.View p0, Object p1) + { + var holder = (ObjectJavaBox<Tuple<ViewGroup, Page, int>>)p1; + ViewGroup frame = holder.Instance.Item1; + return p0 == frame; + } + + public void UpdateCurrentItem() + { + if (_page.CurrentPage == null) + throw new InvalidOperationException("CarouselPage has no children."); + + int index = CarouselPage.GetIndex(_page.CurrentPage); + if (index >= 0 && index < _page.Children.Count) + _pager.CurrentItem = index; + } + + protected override void Dispose(bool disposing) + { + if (disposing && _page != null) + { + foreach (Element element in _page.LogicalChildren) + { + var childPage = element as VisualElement; + + if (childPage == null) + continue; + + IVisualElementRenderer childPageRenderer = Platform.GetRenderer(childPage); + if (childPageRenderer != null) + { + childPageRenderer.ViewGroup.RemoveFromParent(); + childPageRenderer.Dispose(); + Platform.SetRenderer(childPage, null); + } + } + _page.PagesChanged -= OnPagesChanged; + _page = null; + } + base.Dispose(disposing); + } + + void OnPagesChanged(object sender, NotifyCollectionChangedEventArgs e) + { + _ignoreAndroidSelection = true; + + NotifyDataSetChanged(); + + _ignoreAndroidSelection = false; + + if (_page.CurrentPage == null) + return; + + UpdateCurrentItem(); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/CarouselPageRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/CarouselPageRenderer.cs new file mode 100644 index 00000000..1533005a --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/CarouselPageRenderer.cs @@ -0,0 +1,100 @@ +using System.ComponentModel; +using Android.Support.V4.View; +using Android.Views; + +namespace Xamarin.Forms.Platform.Android +{ + public class CarouselPageRenderer : VisualElementRenderer<CarouselPage> + { + ViewPager _viewPager; + + public CarouselPageRenderer() + { + AutoPackage = false; + } + + protected override void Dispose(bool disposing) + { + if (disposing && _viewPager != null) + { + if (_viewPager.Adapter != null) + _viewPager.Adapter.Dispose(); + _viewPager.Dispose(); + _viewPager = null; + } + base.Dispose(disposing); + } + + protected override void OnAttachedToWindow() + { + base.OnAttachedToWindow(); + var adapter = new CarouselPageAdapter(_viewPager, Element, Context); + _viewPager.Adapter = adapter; + _viewPager.SetOnPageChangeListener(adapter); + + adapter.UpdateCurrentItem(); + + Element.SendAppearing(); + } + + protected override void OnDetachedFromWindow() + { + base.OnDetachedFromWindow(); + Element.SendDisappearing(); + } + + protected override void OnElementChanged(ElementChangedEventArgs<CarouselPage> e) + { + base.OnElementChanged(e); + + if (_viewPager != null) + { + RemoveView(_viewPager); + _viewPager.SetOnPageChangeListener(null); + _viewPager.Dispose(); + } + + _viewPager = new ViewPager(Context); + + AddView(_viewPager); + + _viewPager.OffscreenPageLimit = int.MaxValue; + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == "CurrentPage" && Element.CurrentPage != null) + { + if (!Element.Batched) + UpdateCurrentItem(); + } + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + base.OnLayout(changed, l, t, r, b); + if (_viewPager != null) + { + _viewPager.Measure(MeasureSpecFactory.MakeMeasureSpec(r - l, MeasureSpecMode.Exactly), MeasureSpecFactory.MakeMeasureSpec(b - t, MeasureSpecMode.Exactly)); + _viewPager.Layout(0, 0, r - l, b - t); + } + } + + protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + _viewPager.Measure(widthMeasureSpec, heightMeasureSpec); + SetMeasuredDimension(_viewPager.MeasuredWidth, _viewPager.MeasuredHeight); + } + + void UpdateCurrentItem() + { + int index = CarouselPage.GetIndex(Element.CurrentPage); + if (index < 0 || index >= Element.LogicalChildren.Count) + return; + + _viewPager.CurrentItem = index; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/CarouselViewExtensions.cs b/Xamarin.Forms.Platform.Android/Renderers/CarouselViewExtensions.cs new file mode 100644 index 00000000..6349b2b7 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/CarouselViewExtensions.cs @@ -0,0 +1,69 @@ +using System; +using System.Diagnostics; +using System.Linq; +using Android.Content; +using Android.Graphics; + +namespace Xamarin.Forms.Platform.Android +{ + internal static class CarouselViewExtensions + { + internal static int Area(this System.Drawing.Rectangle rectangle) + { + return rectangle.Width * rectangle.Height; + } + + internal static IntVector BoundTranslation(this System.Drawing.Rectangle viewport, IntVector delta, System.Drawing.Rectangle bound) + { + // TODO: generalize the math + Debug.Assert(delta.X == 0 || delta.Y == 0); + + IntVector start = viewport.LeadingCorner(delta); + IntVector end = start + delta; + IntVector clampedEnd = end.Clamp(bound); + IntVector clampedDelta = clampedEnd - start; + return clampedDelta; + } + + internal static IntVector Center(this System.Drawing.Rectangle rectangle) + { + return (IntVector)rectangle.Location + (IntVector)rectangle.Size / 2; + } + + internal static IntVector Clamp(this IntVector position, System.Drawing.Rectangle bound) + { + return new IntVector(position.X.Clamp(bound.Left, bound.Right), position.Y.Clamp(bound.Top, bound.Bottom)); + } + + internal static IntVector LeadingCorner(this System.Drawing.Rectangle rectangle, IntVector delta) + { + return new IntVector(delta.X < 0 ? rectangle.Left : rectangle.Right, delta.Y < 0 ? rectangle.Top : rectangle.Bottom); + } + + internal static bool LexicographicallyLess(this System.Drawing.Point source, System.Drawing.Point target) + { + if (source.X < target.X) + return true; + + if (source.X > target.X) + return false; + + return source.Y < target.Y; + } + + internal static Rect ToAndroidRectangle(this System.Drawing.Rectangle rectangle) + { + return new Rect(rectangle.Left, right: rectangle.Right, top: rectangle.Top, bottom: rectangle.Bottom); + } + + internal static Rectangle ToFormsRectangle(this System.Drawing.Rectangle rectangle, Context context) + { + return new Rectangle(context.FromPixels(rectangle.Left), context.FromPixels(rectangle.Top), context.FromPixels(rectangle.Width), context.FromPixels(rectangle.Height)); + } + + internal static int[] ToRange(this Tuple<int, int> startAndCount) + { + return Enumerable.Range(startAndCount.Item1, startAndCount.Item2).ToArray(); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/CarouselViewRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/CarouselViewRenderer.cs new file mode 100644 index 00000000..f63b6fd9 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/CarouselViewRenderer.cs @@ -0,0 +1,353 @@ +using System; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using Android.Support.V7.Widget; +using AndroidListView = Android.Widget.ListView; +using static System.Diagnostics.Debug; +using Observer = Android.Support.V7.Widget.RecyclerView.AdapterDataObserver; +using BclDebug = System.Diagnostics.Debug; +using IntRectangle = System.Drawing.Rectangle; +using IntSize = System.Drawing.Size; + +namespace Xamarin.Forms.Platform.Android +{ + public class CarouselViewRenderer : ViewRenderer<CarouselView, RecyclerView> + { + PhysicalLayoutManager _physicalLayout; + int _position; + + public CarouselViewRenderer() + { + AutoPackage = false; + } + + ItemViewAdapter Adapter + { + get { return (ItemViewAdapter)Control.GetAdapter(); } + } + + new RecyclerView Control + { + get + { + Initialize(); + return base.Control; + } + } + + ICarouselViewController Controller => Element; + + PhysicalLayoutManager LayoutManager + { + get { return (PhysicalLayoutManager)Control.GetLayoutManager(); } + } + + protected override Size MinimumSize() + { + return new Size(40, 40); + } + + protected override void OnElementChanged(ElementChangedEventArgs<CarouselView> e) + { + CarouselView oldElement = e.OldElement; + if (oldElement != null) + e.OldElement.CollectionChanged -= OnCollectionChanged; + + base.OnElementChanged(e); + Initialize(); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == "Position" && _position != Element.Position) + _physicalLayout.ScrollToPosition(Element.Position); + + base.OnElementPropertyChanged(sender, e); + } + + protected override void OnLayout(bool changed, int left, int top, int right, int bottom) + { + int width = right - left; + int height = bottom - top; + + LayoutManager.Layout(width, height); + + base.OnLayout(changed, left, top, right, bottom); + + Control.Measure(new MeasureSpecification(width, MeasureSpecificationType.Exactly), new MeasureSpecification(height, MeasureSpecificationType.Exactly)); + + Control.Layout(0, 0, width, height); + } + + void Initialize() + { + // cache hit? Check if the view page is already created + RecyclerView recyclerView = base.Control; + if (recyclerView != null) + return; + + // cache miss + recyclerView = new RecyclerView(Context); + SetNativeControl(recyclerView); + + // layoutManager + recyclerView.SetLayoutManager(_physicalLayout = new PhysicalLayoutManager(Context, new VirtualLayoutManager(), Element.Position)); + + // swiping + var dragging = false; + recyclerView.AddOnScrollListener(new OnScrollListener(onDragStart: () => dragging = true, onDragEnd: () => + { + dragging = false; + IntVector velocity = _physicalLayout.Velocity; + + int target = velocity.X > 0 ? _physicalLayout.VisiblePositions().Max() : _physicalLayout.VisiblePositions().Min(); + _physicalLayout.ScrollToPosition(target); + })); + + // scrolling + var scrolling = false; + _physicalLayout.OnBeginScroll += position => scrolling = true; + _physicalLayout.OnEndScroll += position => scrolling = false; + + // appearing + _physicalLayout.OnAppearing += appearingPosition => { Controller.SendPositionAppearing(appearingPosition); }; + + // disappearing + _physicalLayout.OnDisappearing += disappearingPosition => + { + Controller.SendPositionDisappearing(disappearingPosition); + + // animation completed + if (!scrolling && !dragging) + { + _position = _physicalLayout.VisiblePositions().Single(); + + OnPositionChanged(); + OnItemChanged(); + } + }; + + // adapter + var adapter = new ItemViewAdapter(this); + adapter.RegisterAdapterDataObserver(new PositionUpdater(this)); + recyclerView.SetAdapter(adapter); + + // initialize properties + Element.Position = 0; + + // initialize events + Element.CollectionChanged += OnCollectionChanged; + } + + void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + Adapter.NotifyItemRangeInserted(e.NewStartingIndex, e.NewItems.Count); + break; + + case NotifyCollectionChangedAction.Move: + for (var i = 0; i < e.NewItems.Count; i++) + Adapter.NotifyItemMoved(e.OldStartingIndex + i, e.NewStartingIndex + i); + break; + + case NotifyCollectionChangedAction.Remove: + if (Element.Count == 0) + throw new InvalidOperationException("CarouselView must retain a least one item."); + + Adapter.NotifyItemRangeRemoved(e.OldStartingIndex, e.OldItems.Count); + break; + + case NotifyCollectionChangedAction.Replace: + Adapter.NotifyItemRangeChanged(e.OldStartingIndex, e.OldItems.Count); + break; + + case NotifyCollectionChangedAction.Reset: + Adapter.NotifyDataSetChanged(); + break; + + default: + throw new Exception($"Enum value '{(int)e.Action}' is not a member of NotifyCollectionChangedAction enumeration."); + } + } + + void OnItemChanged() + { + object item = ((IItemViewController)Element).GetItem(_position); + Controller.SendSelectedItemChanged(item); + } + + void OnPositionChanged() + { + Element.Position = _position; + Controller.SendSelectedPositionChanged(_position); + } + + // http://developer.android.com/reference/android/support/v7/widget/RecyclerView.html + // http://developer.android.com/training/material/lists-cards.html + // http://wiresareobsolete.com/2014/09/building-a-recyclerview-layoutmanager-part-1/ + + class OnScrollListener : RecyclerView.OnScrollListener + { + readonly Action _onDragEnd; + readonly Action _onDragStart; + ScrollState _lastScrollState; + + internal OnScrollListener(Action onDragEnd, Action onDragStart) + { + _onDragEnd = onDragEnd; + _onDragStart = onDragStart; + } + + public override void OnScrollStateChanged(RecyclerView recyclerView, int newState) + { + var state = (ScrollState)newState; + if (_lastScrollState != ScrollState.Dragging && state == ScrollState.Dragging) + _onDragStart(); + + if (_lastScrollState == ScrollState.Dragging && state != ScrollState.Dragging) + _onDragEnd(); + + _lastScrollState = state; + base.OnScrollStateChanged(recyclerView, newState); + } + + enum ScrollState + { + Idle, + Dragging, + Settling + } + } + + class PositionUpdater : Observer + { + readonly CarouselViewRenderer _carouselView; + + internal PositionUpdater(CarouselViewRenderer carouselView) + { + _carouselView = carouselView; + } + + public override void OnItemRangeInserted(int positionStart, int itemCount) + { + // removal after the current position won't change current position + if (positionStart > _carouselView._position) + ; + + // raise position changed + else + { + _carouselView._position += itemCount; + _carouselView.OnPositionChanged(); + } + + base.OnItemRangeInserted(positionStart, itemCount); + } + + public override void OnItemRangeMoved(int fromPosition, int toPosition, int itemCount) + { + base.OnItemRangeMoved(fromPosition, toPosition, itemCount); + } + + public override void OnItemRangeRemoved(int positionStart, int itemCount) + { + Assert(itemCount == 1); + + // removal after the current position won't change current position + if (positionStart > _carouselView._position) + ; + + // raise item changed + else if (positionStart == _carouselView._position && positionStart != _carouselView.Adapter.ItemCount) + { + _carouselView.OnItemChanged(); + return; + } + + // raise position changed + else + { + _carouselView._position -= itemCount; + _carouselView.OnPositionChanged(); + } + + base.OnItemRangeRemoved(positionStart, itemCount); + } + } + + internal class VirtualLayoutManager : PhysicalLayoutManager.VirtualLayoutManager + { + const int Columns = 1; + + IntSize _itemSize; + + internal override bool CanScrollHorizontally => true; + + internal override bool CanScrollVertically => false; + + public override string ToString() + { + return $"itemSize={_itemSize}"; + } + + internal override IntRectangle GetBounds(int originPosition, RecyclerView.State state) + => new IntRectangle(LayoutItem(originPosition, 0).Location, new IntSize(_itemSize.Width * state.ItemCount, _itemSize.Height)); + + internal override Tuple<int, int> GetPositions(int positionOrigin, int itemCount, IntRectangle viewport, bool includeBuffer) + { + // returns one item off-screen in either direction. + int buffer = includeBuffer ? 1 : 0; + int left = GetPosition(itemCount, positionOrigin - buffer, viewport.Left); + int right = GetPosition(itemCount, positionOrigin + buffer, viewport.Right, true); + + int start = left; + int count = right - left + 1; + return new Tuple<int, int>(start, count); + } + + internal override void Layout(int positionOffset, IntSize viewportSize, ref IntVector offset) + { + int width = viewportSize.Width / Columns; + int height = viewportSize.Height; + + if (_itemSize.Width != 0) + offset *= (double)width / _itemSize.Width; + + _itemSize = new IntSize(width, height); + } + + internal override IntRectangle LayoutItem(int positionOffset, int position) + { + // measure + IntSize size = _itemSize; + + // layout + var location = new IntVector((position - positionOffset) * size.Width, 0); + + // allocate + return new IntRectangle(location, size); + } + + int GetPosition(int itemCount, int positionOrigin, int x, bool exclusive = false) + { + int position = x / _itemSize.Width + positionOrigin; + bool hasRemainder = x % _itemSize.Width != 0; + + if (hasRemainder && x < 0) + position--; + + if (!hasRemainder && exclusive) + position--; + + position = position.Clamp(0, itemCount - 1); + return position; + } + } + } + + // RecyclerView virtualizes indexes (adapter position <-> viewGroup child index) + // PhysicalLayoutManager virtualizes location (regular layout <-> screen) +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ConditionalFocusLayout.cs b/Xamarin.Forms.Platform.Android/Renderers/ConditionalFocusLayout.cs new file mode 100644 index 00000000..6368d023 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/ConditionalFocusLayout.cs @@ -0,0 +1,48 @@ +using Android.Content; +using Android.Views; +using Android.Widget; + +namespace Xamarin.Forms.Platform.Android +{ + internal class ConditionalFocusLayout : LinearLayout, global::Android.Views.View.IOnTouchListener + { + public ConditionalFocusLayout(Context context) : base(context) + { + SetOnTouchListener(this); + } + + public bool OnTouch(global::Android.Views.View v, MotionEvent e) + { + bool allowFocus = v is EditText; + DescendantFocusability = allowFocus ? DescendantFocusability.AfterDescendants : DescendantFocusability.BlockDescendants; + return false; + } + + internal void ApplyTouchListenersToSpecialCells(Cell item) + { + DescendantFocusability = DescendantFocusability.BlockDescendants; + + global::Android.Views.View aView = GetChildAt(0); + (aView as EntryCellView)?.EditText.SetOnTouchListener(this); + + var viewCell = item as ViewCell; + if (viewCell == null || viewCell?.View == null) + return; + + IVisualElementRenderer renderer = Platform.GetRenderer(viewCell.View); + if (renderer?.ViewGroup?.ChildCount != 0) + (renderer.ViewGroup.GetChildAt(0) as EditText)?.SetOnTouchListener(this); + + foreach (Element descendant in viewCell.View.Descendants()) + { + var element = descendant as VisualElement; + if (element == null) + continue; + renderer = Platform.GetRenderer(element); + if (renderer?.ViewGroup?.ChildCount == 0) + continue; + (renderer.ViewGroup.GetChildAt(0) as EditText)?.SetOnTouchListener(this); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/DatePickerRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/DatePickerRenderer.cs new file mode 100644 index 00000000..11d9a0d0 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/DatePickerRenderer.cs @@ -0,0 +1,154 @@ +using System; +using System.ComponentModel; +using Android.App; +using Android.Widget; +using AView = Android.Views.View; +using Object = Java.Lang.Object; + +namespace Xamarin.Forms.Platform.Android +{ + public class DatePickerRenderer : ViewRenderer<DatePicker, EditText> + { + DatePickerDialog _dialog; + bool _disposed; + + public DatePickerRenderer() + { + AutoPackage = false; + if (Forms.IsLollipopOrNewer) + Device.Info.PropertyChanged += DeviceInfoPropertyChanged; + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + if (Forms.IsLollipopOrNewer) + Device.Info.PropertyChanged -= DeviceInfoPropertyChanged; + + _disposed = true; + if (_dialog != null) + { + _dialog.Hide(); + _dialog.Dispose(); + _dialog = null; + } + } + base.Dispose(disposing); + } + + protected override void OnElementChanged(ElementChangedEventArgs<DatePicker> e) + { + base.OnElementChanged(e); + + if (e.OldElement == null) + { + var textField = new EditText(Context) { Focusable = false, Clickable = true, Tag = this }; + + textField.SetOnClickListener(TextFieldClickHandler.Instance); + SetNativeControl(textField); + } + + SetDate(Element.Date); + + UpdateMinimumDate(); + UpdateMaximumDate(); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == "Date" || e.PropertyName == DatePicker.FormatProperty.PropertyName) + SetDate(Element.Date); + else if (e.PropertyName == "MinimumDate") + UpdateMinimumDate(); + else if (e.PropertyName == "MaximumDate") + UpdateMaximumDate(); + } + + internal override void OnFocusChangeRequested(object sender, VisualElement.FocusRequestArgs e) + { + base.OnFocusChangeRequested(sender, e); + + if (e.Focus) + OnTextFieldClicked(); + else if (_dialog != null) + { + _dialog.Hide(); + ((IElementController)Element).SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, false); + Control.ClearFocus(); + _dialog = null; + } + } + + void CreateDatePickerDialog(int year, int month, int day) + { + DatePicker view = Element; + _dialog = new DatePickerDialog(Context, (o, e) => + { + view.Date = e.Date; + ((IElementController)view).SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, false); + Control.ClearFocus(); + _dialog = null; + }, year, month, day); + } + + void DeviceInfoPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == "CurrentOrientation") + { + DatePickerDialog currentDialog = _dialog; + if (currentDialog != null && currentDialog.IsShowing) + { + currentDialog.Dismiss(); + CreateDatePickerDialog(currentDialog.DatePicker.Year, currentDialog.DatePicker.Month, currentDialog.DatePicker.DayOfMonth); + _dialog.Show(); + } + } + } + + void OnTextFieldClicked() + { + DatePicker view = Element; + ((IElementController)view).SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, true); + + CreateDatePickerDialog(view.Date.Year, view.Date.Month - 1, view.Date.Day); + + UpdateMinimumDate(); + UpdateMaximumDate(); + _dialog.Show(); + } + + void SetDate(DateTime date) + { + Control.Text = date.ToString(Element.Format); + } + + void UpdateMaximumDate() + { + if (_dialog != null) + { + _dialog.DatePicker.MaxDate = (long)Element.MaximumDate.ToUniversalTime().Subtract(DateTime.MinValue.AddYears(1969)).TotalMilliseconds; + } + } + + void UpdateMinimumDate() + { + if (_dialog != null) + { + _dialog.DatePicker.MinDate = (long)Element.MinimumDate.ToUniversalTime().Subtract(DateTime.MinValue.AddYears(1969)).TotalMilliseconds; + } + } + + class TextFieldClickHandler : Object, IOnClickListener + { + public static readonly TextFieldClickHandler Instance = new TextFieldClickHandler(); + + public void OnClick(AView v) + { + ((DatePickerRenderer)v.Tag).OnTextFieldClicked(); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/DescendantFocusToggler.cs b/Xamarin.Forms.Platform.Android/Renderers/DescendantFocusToggler.cs new file mode 100644 index 00000000..1f54b7c5 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/DescendantFocusToggler.cs @@ -0,0 +1,42 @@ +using System; +using Android.Views; + +namespace Xamarin.Forms.Platform.Android +{ + internal class DescendantFocusToggler : IDescendantFocusToggler + { + public bool RequestFocus(global::Android.Views.View control, Func<bool> baseRequestFocus) + { + IViewParent ancestor = control.Parent; + var previousFocusability = DescendantFocusability.BlockDescendants; + ConditionalFocusLayout cfl = null; + + // Work our way up through the tree until we find a ConditionalFocusLayout + while (ancestor is ViewGroup) + { + cfl = ancestor as ConditionalFocusLayout; + + if (cfl != null) + { + previousFocusability = cfl.DescendantFocusability; + // Toggle DescendantFocusability to allow this control to get focus + cfl.DescendantFocusability = DescendantFocusability.AfterDescendants; + break; + } + + ancestor = ancestor.Parent; + } + + // Call the original RequestFocus implementation for the View + bool result = baseRequestFocus(); + + if (cfl != null) + { + // Toggle descendantfocusability back to whatever it was + cfl.DescendantFocusability = previousFocusability; + } + + return result; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/EditorEditText.cs b/Xamarin.Forms.Platform.Android/Renderers/EditorEditText.cs new file mode 100644 index 00000000..24273121 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/EditorEditText.cs @@ -0,0 +1,42 @@ +using System; +using Android.Content; +using Android.Graphics; +using Android.Views; +using Android.Widget; + +namespace Xamarin.Forms.Platform.Android +{ + public class EditorEditText : EditText, IDescendantFocusToggler + { + DescendantFocusToggler _descendantFocusToggler; + + internal EditorEditText(Context context) : base(context) + { + } + + bool IDescendantFocusToggler.RequestFocus(global::Android.Views.View control, Func<bool> baseRequestFocus) + { + _descendantFocusToggler = _descendantFocusToggler ?? new DescendantFocusToggler(); + + return _descendantFocusToggler.RequestFocus(control, baseRequestFocus); + } + + public override bool OnKeyPreIme(Keycode keyCode, KeyEvent e) + { + if (keyCode == Keycode.Back && e.Action == KeyEventActions.Down) + { + EventHandler handler = OnBackKeyboardPressed; + if (handler != null) + handler(this, EventArgs.Empty); + } + return base.OnKeyPreIme(keyCode, e); + } + + public override bool RequestFocus(FocusSearchDirection direction, Rect previouslyFocusedRect) + { + return (this as IDescendantFocusToggler).RequestFocus(this, () => base.RequestFocus(direction, previouslyFocusedRect)); + } + + internal event EventHandler OnBackKeyboardPressed; + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/EditorRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/EditorRenderer.cs new file mode 100644 index 00000000..14078abc --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/EditorRenderer.cs @@ -0,0 +1,140 @@ +using System.ComponentModel; +using Android.Content.Res; +using Android.Text; +using Android.Util; +using Android.Views; +using Java.Lang; + +namespace Xamarin.Forms.Platform.Android +{ + public class EditorRenderer : ViewRenderer<Editor, EditorEditText>, ITextWatcher + { + ColorStateList _defaultColors; + + public EditorRenderer() + { + AutoPackage = false; + } + + void ITextWatcher.AfterTextChanged(IEditable s) + { + } + + void ITextWatcher.BeforeTextChanged(ICharSequence s, int start, int count, int after) + { + } + + void ITextWatcher.OnTextChanged(ICharSequence s, int start, int before, int count) + { + if (string.IsNullOrEmpty(Element.Text) && s.Length() == 0) + return; + + if (Element.Text != s.ToString()) + ((IElementController)Element).SetValueFromRenderer(Editor.TextProperty, s.ToString()); + } + + protected override void OnElementChanged(ElementChangedEventArgs<Editor> e) + { + base.OnElementChanged(e); + + HandleKeyboardOnFocus = true; + + EditorEditText edit = Control; + if (edit == null) + { + edit = new EditorEditText(Context); + + SetNativeControl(edit); + edit.AddTextChangedListener(this); + edit.OnBackKeyboardPressed += (sender, args) => + { + Element.SendCompleted(); + edit.ClearFocus(); + }; + } + + edit.SetSingleLine(false); + edit.Gravity = GravityFlags.Top; + edit.SetHorizontallyScrolling(false); + + UpdateText(); + UpdateInputType(); + UpdateTextColor(); + UpdateFont(); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == Editor.TextProperty.PropertyName) + UpdateText(); + else if (e.PropertyName == InputView.KeyboardProperty.PropertyName) + UpdateInputType(); + else if (e.PropertyName == Editor.TextColorProperty.PropertyName) + UpdateTextColor(); + else if (e.PropertyName == Editor.FontAttributesProperty.PropertyName) + UpdateFont(); + else if (e.PropertyName == Editor.FontFamilyProperty.PropertyName) + UpdateFont(); + else if (e.PropertyName == Editor.FontSizeProperty.PropertyName) + UpdateFont(); + + base.OnElementPropertyChanged(sender, e); + } + + internal override void OnNativeFocusChanged(bool hasFocus) + { + if (Element.IsFocused && !hasFocus) // Editor has requested an unfocus, fire completed event + Element.SendCompleted(); + } + + void UpdateFont() + { + Control.Typeface = Element.ToTypeface(); + Control.SetTextSize(ComplexUnitType.Sp, (float)Element.FontSize); + } + + void UpdateInputType() + { + Editor model = Element; + EditorEditText edit = Control; + edit.InputType = model.Keyboard.ToInputType() | InputTypes.TextFlagMultiLine; + } + + void UpdateText() + { + string newText = Element.Text ?? ""; + + if (Control.Text == newText) + return; + + Control.Text = newText; + Control.SetSelection(newText.Length); + } + + void UpdateTextColor() + { + if (Element.TextColor.IsDefault) + { + if (_defaultColors == null) + { + // This control has always had the default colors; nothing to update + return; + } + + // This control is being set back to the default colors + Control.SetTextColor(_defaultColors); + } + else + { + if (_defaultColors == null) + { + // Keep track of the default colors so we can return to them later + // and so we can preserve the default disabled color + _defaultColors = Control.TextColors; + } + + Control.SetTextColor(Element.TextColor.ToAndroidPreserveDisabled(_defaultColors)); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/EntryEditText.cs b/Xamarin.Forms.Platform.Android/Renderers/EntryEditText.cs new file mode 100644 index 00000000..de03a414 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/EntryEditText.cs @@ -0,0 +1,42 @@ +using System; +using Android.Content; +using Android.Graphics; +using Android.Views; +using Android.Widget; + +namespace Xamarin.Forms.Platform.Android +{ + public class EntryEditText : EditText, IDescendantFocusToggler + { + DescendantFocusToggler _descendantFocusToggler; + + internal EntryEditText(Context context) : base(context) + { + } + + bool IDescendantFocusToggler.RequestFocus(global::Android.Views.View control, Func<bool> baseRequestFocus) + { + _descendantFocusToggler = _descendantFocusToggler ?? new DescendantFocusToggler(); + + return _descendantFocusToggler.RequestFocus(control, baseRequestFocus); + } + + public override bool OnKeyPreIme(Keycode keyCode, KeyEvent e) + { + if (keyCode == Keycode.Back && e.Action == KeyEventActions.Down) + { + EventHandler handler = OnKeyboardBackPressed; + if (handler != null) + handler(this, EventArgs.Empty); + } + return base.OnKeyPreIme(keyCode, e); + } + + public override bool RequestFocus(FocusSearchDirection direction, Rect previouslyFocusedRect) + { + return (this as IDescendantFocusToggler).RequestFocus(this, () => base.RequestFocus(direction, previouslyFocusedRect)); + } + + internal event EventHandler OnKeyboardBackPressed; + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/EntryRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/EntryRenderer.cs new file mode 100644 index 00000000..24824536 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/EntryRenderer.cs @@ -0,0 +1,189 @@ +using System.ComponentModel; +using Android.Content.Res; +using Android.Text; +using Android.Util; +using Android.Views; +using Android.Views.InputMethods; +using Android.Widget; +using Java.Lang; + +namespace Xamarin.Forms.Platform.Android +{ + public class EntryRenderer : ViewRenderer<Entry, EntryEditText>, ITextWatcher, TextView.IOnEditorActionListener + { + ColorStateList _hintTextColorDefault; + ColorStateList _textColorDefault; + EntryEditText _textView; + + public EntryRenderer() + { + AutoPackage = false; + } + + bool TextView.IOnEditorActionListener.OnEditorAction(TextView v, ImeAction actionId, KeyEvent e) + { + // Fire Completed and dismiss keyboard for hardware / physical keyboards + if (actionId == ImeAction.Done || (actionId == ImeAction.ImeNull && e.KeyCode == Keycode.Enter)) + { + Control.ClearFocus(); + v.HideKeyboard(); + Element.SendCompleted(); + } + + return true; + } + + void ITextWatcher.AfterTextChanged(IEditable s) + { + } + + void ITextWatcher.BeforeTextChanged(ICharSequence s, int start, int count, int after) + { + } + + void ITextWatcher.OnTextChanged(ICharSequence s, int start, int before, int count) + { + if (string.IsNullOrEmpty(Element.Text) && s.Length() == 0) + return; + + ((IElementController)Element).SetValueFromRenderer(Entry.TextProperty, s.ToString()); + } + + protected override void OnElementChanged(ElementChangedEventArgs<Entry> e) + { + base.OnElementChanged(e); + + HandleKeyboardOnFocus = true; + + if (e.OldElement == null) + { + _textView = new EntryEditText(Context); + _textView.ImeOptions = ImeAction.Done; + _textView.AddTextChangedListener(this); + _textView.SetOnEditorActionListener(this); + _textView.OnKeyboardBackPressed += (sender, args) => _textView.ClearFocus(); + SetNativeControl(_textView); + } + + _textView.Hint = Element.Placeholder; + _textView.Text = Element.Text; + UpdateInputType(); + + UpdateColor(); + UpdateAlignment(); + UpdateFont(); + UpdatePlaceholderColor(); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == Entry.PlaceholderProperty.PropertyName) + Control.Hint = Element.Placeholder; + else if (e.PropertyName == Entry.IsPasswordProperty.PropertyName) + UpdateInputType(); + else if (e.PropertyName == Entry.TextProperty.PropertyName) + { + if (Control.Text != Element.Text) + { + Control.Text = Element.Text; + if (Control.IsFocused) + { + Control.SetSelection(Control.Text.Length); + Control.ShowKeyboard(); + } + } + } + else if (e.PropertyName == Entry.TextColorProperty.PropertyName) + UpdateColor(); + else if (e.PropertyName == InputView.KeyboardProperty.PropertyName) + UpdateInputType(); + else if (e.PropertyName == Entry.HorizontalTextAlignmentProperty.PropertyName) + UpdateAlignment(); + else if (e.PropertyName == Entry.FontAttributesProperty.PropertyName) + UpdateFont(); + else if (e.PropertyName == Entry.FontFamilyProperty.PropertyName) + UpdateFont(); + else if (e.PropertyName == Entry.FontSizeProperty.PropertyName) + UpdateFont(); + else if (e.PropertyName == Entry.PlaceholderColorProperty.PropertyName) + UpdatePlaceholderColor(); + + base.OnElementPropertyChanged(sender, e); + } + + void UpdateAlignment() + { + Control.Gravity = Element.HorizontalTextAlignment.ToHorizontalGravityFlags(); + } + + void UpdateColor() + { + if (Element.TextColor.IsDefault) + { + if (_textColorDefault == null) + { + // This control has always had the default colors; nothing to update + return; + } + + // This control is being set back to the default colors + Control.SetTextColor(_textColorDefault); + } + else + { + if (_textColorDefault == null) + { + // Keep track of the default colors so we can return to them later + // and so we can preserve the default disabled color + _textColorDefault = Control.TextColors; + } + + Control.SetTextColor(Element.TextColor.ToAndroidPreserveDisabled(_textColorDefault)); + } + } + + void UpdateFont() + { + Control.Typeface = Element.ToTypeface(); + Control.SetTextSize(ComplexUnitType.Sp, (float)Element.FontSize); + } + + void UpdateInputType() + { + Entry model = Element; + _textView.InputType = model.Keyboard.ToInputType(); + if (model.IsPassword && ((_textView.InputType & InputTypes.ClassText) == InputTypes.ClassText)) + _textView.InputType = _textView.InputType | InputTypes.TextVariationPassword; + if (model.IsPassword && ((_textView.InputType & InputTypes.ClassNumber) == InputTypes.ClassNumber)) + _textView.InputType = _textView.InputType | InputTypes.NumberVariationPassword; + } + + void UpdatePlaceholderColor() + { + Color placeholderColor = Element.PlaceholderColor; + + if (placeholderColor.IsDefault) + { + if (_hintTextColorDefault == null) + { + // This control has always had the default colors; nothing to update + return; + } + + // This control is being set back to the default colors + Control.SetHintTextColor(_hintTextColorDefault); + } + else + { + if (_hintTextColorDefault == null) + { + // Keep track of the default colors so we can return to them later + // and so we can preserve the default disabled color + _hintTextColorDefault = Control.HintTextColors; + } + + Control.SetHintTextColor(placeholderColor.ToAndroidPreserveDisabled(_hintTextColorDefault)); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/FileImageSourceHandler.cs b/Xamarin.Forms.Platform.Android/Renderers/FileImageSourceHandler.cs new file mode 100644 index 00000000..edf7dc37 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/FileImageSourceHandler.cs @@ -0,0 +1,17 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Android.Content; +using Android.Graphics; + +namespace Xamarin.Forms.Platform.Android +{ + public sealed class FileImageSourceHandler : IImageSourceHandler + { + public async Task<Bitmap> LoadImageAsync(ImageSource imagesource, Context context, CancellationToken cancelationToken = default(CancellationToken)) + { + string file = ((FileImageSource)imagesource).File; + return await (File.Exists(file) ? BitmapFactory.DecodeFileAsync(file) : context.Resources.GetBitmapAsync(file)).ConfigureAwait(false); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/FontExtensions.cs b/Xamarin.Forms.Platform.Android/Renderers/FontExtensions.cs new file mode 100644 index 00000000..da458642 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/FontExtensions.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using Android.Graphics; + +namespace Xamarin.Forms.Platform.Android +{ + public static class FontExtensions + { + static readonly Dictionary<Tuple<string, FontAttributes>, Typeface> Typefaces = new Dictionary<Tuple<string, FontAttributes>, Typeface>(); + + static Typeface s_defaultTypeface; + + public static float ToScaledPixel(this Font self) + { + if (self.IsDefault) + return 14; + + if (self.UseNamedSize) + { + switch (self.NamedSize) + { + case NamedSize.Micro: + return 10; + case NamedSize.Small: + return 12; + case NamedSize.Default: + case NamedSize.Medium: + return 14; + case NamedSize.Large: + return 18; + } + } + + return (float)self.FontSize; + } + + public static Typeface ToTypeface(this Font self) + { + if (self.IsDefault) + return s_defaultTypeface ?? (s_defaultTypeface = Typeface.Default); + + var key = new Tuple<string, FontAttributes>(self.FontFamily, self.FontAttributes); + Typeface result; + if (Typefaces.TryGetValue(key, out result)) + return result; + + var style = TypefaceStyle.Normal; + if ((self.FontAttributes & (FontAttributes.Bold | FontAttributes.Italic)) == (FontAttributes.Bold | FontAttributes.Italic)) + style = TypefaceStyle.BoldItalic; + else if ((self.FontAttributes & FontAttributes.Bold) != 0) + style = TypefaceStyle.Bold; + else if ((self.FontAttributes & FontAttributes.Italic) != 0) + style = TypefaceStyle.Italic; + + if (self.FontFamily != null) + result = Typeface.Create(self.FontFamily, style); + else + result = Typeface.Create(Typeface.Default, style); + + Typefaces[key] = result; + return result; + } + + internal static bool IsDefault(this IFontElement self) + { + return self.FontFamily == null && self.FontSize == Device.GetNamedSize(NamedSize.Default, typeof(Label), true) && self.FontAttributes == FontAttributes.None; + } + + internal static Typeface ToTypeface(this IFontElement self) + { + var key = new Tuple<string, FontAttributes>(self.FontFamily, self.FontAttributes); + Typeface result; + if (Typefaces.TryGetValue(key, out result)) + return result; + + var style = TypefaceStyle.Normal; + if ((self.FontAttributes & (FontAttributes.Bold | FontAttributes.Italic)) == (FontAttributes.Bold | FontAttributes.Italic)) + style = TypefaceStyle.BoldItalic; + else if ((self.FontAttributes & FontAttributes.Bold) != 0) + style = TypefaceStyle.Bold; + else if ((self.FontAttributes & FontAttributes.Italic) != 0) + style = TypefaceStyle.Italic; + + if (self.FontFamily != null) + result = Typeface.Create(self.FontFamily, style); + else + result = Typeface.Create(Typeface.Default, style); + + Typefaces[key] = result; + return result; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/FormattedStringExtensions.cs b/Xamarin.Forms.Platform.Android/Renderers/FormattedStringExtensions.cs new file mode 100644 index 00000000..2a65c98f --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/FormattedStringExtensions.cs @@ -0,0 +1,90 @@ +using System.Text; +using Android.Graphics; +using Android.Text; +using Android.Text.Style; +using Android.Util; +using Android.Widget; + +namespace Xamarin.Forms.Platform.Android +{ + public static class FormattedStringExtensions + { + public static SpannableString ToAttributed(this FormattedString formattedString, Font defaultFont, Color defaultForegroundColor, TextView view) + { + if (formattedString == null) + return null; + + var builder = new StringBuilder(); + foreach (Span span in formattedString.Spans) + { + if (span.Text == null) + continue; + + builder.Append(span.Text); + } + + var spannable = new SpannableString(builder.ToString()); + + var c = 0; + foreach (Span span in formattedString.Spans) + { + if (span.Text == null) + continue; + + int start = c; + int end = start + span.Text.Length; + c = end; + + if (span.ForegroundColor != Color.Default) + { + spannable.SetSpan(new ForegroundColorSpan(span.ForegroundColor.ToAndroid()), start, end, SpanTypes.InclusiveExclusive); + } + else if (defaultForegroundColor != Color.Default) + { + spannable.SetSpan(new ForegroundColorSpan(defaultForegroundColor.ToAndroid()), start, end, SpanTypes.InclusiveExclusive); + } + + if (span.BackgroundColor != Color.Default) + { + spannable.SetSpan(new BackgroundColorSpan(span.BackgroundColor.ToAndroid()), start, end, SpanTypes.InclusiveExclusive); + } + + if (!span.IsDefault()) + spannable.SetSpan(new FontSpan(span.Font, view), start, end, SpanTypes.InclusiveInclusive); + else if (defaultFont != Font.Default) + spannable.SetSpan(new FontSpan(defaultFont, view), start, end, SpanTypes.InclusiveInclusive); + } + return spannable; + } + + class FontSpan : MetricAffectingSpan + { + public FontSpan(Font font, TextView view) + { + Font = font; + TextView = view; + } + + public Font Font { get; } + + public TextView TextView { get; } + + public override void UpdateDrawState(TextPaint tp) + { + Apply(tp); + } + + public override void UpdateMeasureState(TextPaint p) + { + Apply(p); + } + + void Apply(Paint paint) + { + paint.SetTypeface(Font.ToTypeface()); + float value = Font.ToScaledPixel(); + paint.TextSize = TypedValue.ApplyDimension(ComplexUnitType.Sp, value, TextView.Resources.DisplayMetrics); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/FormsImageView.cs b/Xamarin.Forms.Platform.Android/Renderers/FormsImageView.cs new file mode 100644 index 00000000..ae32e16c --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/FormsImageView.cs @@ -0,0 +1,36 @@ +using System; +using Android.Content; +using Android.Runtime; +using Android.Widget; + +namespace Xamarin.Forms.Platform.Android +{ + internal class FormsImageView : ImageView + { + bool _skipInvalidate; + + public FormsImageView(Context context) : base(context) + { + } + + protected FormsImageView(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + + public override void Invalidate() + { + if (_skipInvalidate) + { + _skipInvalidate = false; + return; + } + + base.Invalidate(); + } + + public void SkipInvalidate() + { + _skipInvalidate = true; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/FormsTextView.cs b/Xamarin.Forms.Platform.Android/Renderers/FormsTextView.cs new file mode 100644 index 00000000..1dc25ea9 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/FormsTextView.cs @@ -0,0 +1,41 @@ +using System; +using Android.Content; +using Android.Runtime; +using Android.Util; +using Android.Widget; + +namespace Xamarin.Forms.Platform.Android +{ + internal class FormsTextView : TextView + { + bool _skip; + + public FormsTextView(Context context) : base(context) + { + } + + public FormsTextView(Context context, IAttributeSet attrs) : base(context, attrs) + { + } + + public FormsTextView(Context context, IAttributeSet attrs, int defStyle) : base(context, attrs, defStyle) + { + } + + protected FormsTextView(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + + public override void Invalidate() + { + if (!_skip) + base.Invalidate(); + _skip = false; + } + + public void SkipNextInvalidate() + { + _skip = true; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/FormsWebChromeClient.cs b/Xamarin.Forms.Platform.Android/Renderers/FormsWebChromeClient.cs new file mode 100644 index 00000000..5d885774 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/FormsWebChromeClient.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using Android.App; +using Android.Content; +using Android.Webkit; +using Object = Java.Lang.Object; + +namespace Xamarin.Forms.Platform.Android +{ + public class FormsWebChromeClient : WebChromeClient + { + IStartActivityForResult _context; + List<int> _requestCodes; + + public override bool OnShowFileChooser(global::Android.Webkit.WebView webView, IValueCallback filePathCallback, FileChooserParams fileChooserParams) + { + base.OnShowFileChooser(webView, filePathCallback, fileChooserParams); + return ChooseFile(filePathCallback, fileChooserParams.CreateIntent(), fileChooserParams.Title); + } + + public void UnregisterCallbacks() + { + if (_requestCodes == null || _requestCodes.Count == 0 || _context == null) + return; + + foreach (int requestCode in _requestCodes) + _context.UnregisterActivityResultCallback(requestCode); + + _requestCodes = null; + } + + protected bool ChooseFile(IValueCallback filePathCallback, Intent intent, string title) + { + Action<Result, Intent> callback = (resultCode, intentData) => + { + if (filePathCallback == null) + return; + + Object result = ParseResult(resultCode, intentData); + filePathCallback.OnReceiveValue(result); + }; + + _requestCodes = _requestCodes ?? new List<int>(); + + int newRequestCode = _context.RegisterActivityResultCallback(callback); + + _requestCodes.Add(newRequestCode); + + _context.StartActivityForResult(Intent.CreateChooser(intent, title), newRequestCode); + + return true; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + UnregisterCallbacks(); + base.Dispose(disposing); + } + + protected virtual Object ParseResult(Result resultCode, Intent data) + { + return FileChooserParams.ParseResult((int)resultCode, data); + } + + internal void SetContext(IStartActivityForResult startActivityForResult) + { + if (startActivityForResult == null) + throw new ArgumentNullException(nameof(startActivityForResult)); + + _context = startActivityForResult; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/FrameRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/FrameRenderer.cs new file mode 100644 index 00000000..63fb5da4 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/FrameRenderer.cs @@ -0,0 +1,195 @@ +using System.ComponentModel; +using Android.Graphics; +using Android.Graphics.Drawables; +using AButton = Android.Widget.Button; +using ACanvas = Android.Graphics.Canvas; +using GlobalResource = Android.Resource; + +namespace Xamarin.Forms.Platform.Android +{ + public class FrameRenderer : VisualElementRenderer<Frame> + { + bool _disposed; + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing && !_disposed) + { + Background.Dispose(); + _disposed = true; + } + } + + protected override void OnElementChanged(ElementChangedEventArgs<Frame> e) + { + base.OnElementChanged(e); + + if (e.NewElement != null && e.OldElement == null) + UpdateBackground(); + } + + void UpdateBackground() + { + SetBackgroundDrawable(new FrameDrawable(Element)); + } + + class FrameDrawable : Drawable + { + readonly Frame _frame; + + bool _isDisposed; + Bitmap _normalBitmap; + + public FrameDrawable(Frame frame) + { + _frame = frame; + frame.PropertyChanged += FrameOnPropertyChanged; + } + + public override bool IsStateful + { + get { return false; } + } + + public override int Opacity + { + get { return 0; } + } + + public override void Draw(ACanvas canvas) + { + int width = Bounds.Width(); + int height = Bounds.Height(); + + if (width <= 0 || height <= 0) + { + if (_normalBitmap != null) + { + _normalBitmap.Dispose(); + _normalBitmap = null; + } + return; + } + + if (_normalBitmap == null || _normalBitmap.Height != height || _normalBitmap.Width != width) + { + // If the user changes the orientation of the screen, make sure to detroy reference before + // reassigning a new bitmap reference. + if (_normalBitmap != null) + { + _normalBitmap.Dispose(); + _normalBitmap = null; + } + + _normalBitmap = CreateBitmap(false, width, height); + } + Bitmap bitmap = _normalBitmap; + using(var paint = new Paint()) + canvas.DrawBitmap(bitmap, 0, 0, paint); + } + + public override void SetAlpha(int alpha) + { + } + + public override void SetColorFilter(ColorFilter cf) + { + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_isDisposed) + { + if (_normalBitmap != null) + { + _normalBitmap.Dispose(); + _normalBitmap = null; + } + + _isDisposed = true; + } + + base.Dispose(disposing); + } + + protected override bool OnStateChange(int[] state) + { + return false; + } + + Bitmap CreateBitmap(bool pressed, int width, int height) + { + Bitmap bitmap; + using(Bitmap.Config config = Bitmap.Config.Argb8888) + bitmap = Bitmap.CreateBitmap(width, height, config); + + using(var canvas = new ACanvas(bitmap)) + { + DrawBackground(canvas, width, height, pressed); + DrawOutline(canvas, width, height); + } + + return bitmap; + } + + void DrawBackground(ACanvas canvas, int width, int height, bool pressed) + { + using(var paint = new Paint { AntiAlias = true }) + using(var path = new Path()) + using(Path.Direction direction = Path.Direction.Cw) + using(Paint.Style style = Paint.Style.Fill) + using(var rect = new RectF(0, 0, width, height)) + { + float rx = Forms.Context.ToPixels(5); + float ry = Forms.Context.ToPixels(5); + path.AddRoundRect(rect, rx, ry, direction); + + global::Android.Graphics.Color color = _frame.BackgroundColor.ToAndroid(); + + paint.SetStyle(style); + paint.Color = color; + + canvas.DrawPath(path, paint); + } + } + + void DrawOutline(ACanvas canvas, int width, int height) + { + using(var paint = new Paint { AntiAlias = true }) + using(var path = new Path()) + using(Path.Direction direction = Path.Direction.Cw) + using(Paint.Style style = Paint.Style.Stroke) + using(var rect = new RectF(0, 0, width, height)) + { + float rx = Forms.Context.ToPixels(5); + float ry = Forms.Context.ToPixels(5); + path.AddRoundRect(rect, rx, ry, direction); + + paint.StrokeWidth = 1; + paint.SetStyle(style); + paint.Color = _frame.OutlineColor.ToAndroid(); + + canvas.DrawPath(path, paint); + } + } + + void FrameOnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName || e.PropertyName == Frame.OutlineColorProperty.PropertyName) + { + using(var canvas = new ACanvas(_normalBitmap)) + { + int width = Bounds.Width(); + int height = Bounds.Height(); + canvas.DrawColor(global::Android.Graphics.Color.Black, PorterDuff.Mode.Clear); + DrawBackground(canvas, width, height, false); + DrawOutline(canvas, width, height); + } + InvalidateSelf(); + } + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/GenericAnimatorListener.cs b/Xamarin.Forms.Platform.Android/Renderers/GenericAnimatorListener.cs new file mode 100644 index 00000000..c89f007c --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/GenericAnimatorListener.cs @@ -0,0 +1,41 @@ +using System; +using Android.Animation; + +namespace Xamarin.Forms.Platform.Android +{ + public class GenericAnimatorListener : AnimatorListenerAdapter + { + public Action<Animator> OnCancel { get; set; } + + public Action<Animator> OnEnd { get; set; } + + public Action<Animator> OnRepeat { get; set; } + + public override void OnAnimationCancel(Animator animation) + { + if (OnCancel != null) + OnCancel(animation); + base.OnAnimationCancel(animation); + } + + public override void OnAnimationEnd(Animator animation) + { + if (OnEnd != null) + OnEnd(animation); + base.OnAnimationEnd(animation); + } + + public override void OnAnimationRepeat(Animator animation) + { + if (OnRepeat != null) + OnRepeat(animation); + base.OnAnimationRepeat(animation); + } + + protected override void JavaFinalize() + { + OnCancel = OnRepeat = OnEnd = null; + base.JavaFinalize(); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/IDescendantFocusToggler.cs b/Xamarin.Forms.Platform.Android/Renderers/IDescendantFocusToggler.cs new file mode 100644 index 00000000..b908e9ad --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/IDescendantFocusToggler.cs @@ -0,0 +1,9 @@ +using System; + +namespace Xamarin.Forms.Platform.Android +{ + internal interface IDescendantFocusToggler + { + bool RequestFocus(global::Android.Views.View control, Func<bool> baseRequestFocus); + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/IImageSourceHandler.cs b/Xamarin.Forms.Platform.Android/Renderers/IImageSourceHandler.cs new file mode 100644 index 00000000..a072e4dc --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/IImageSourceHandler.cs @@ -0,0 +1,12 @@ +using System.Threading; +using System.Threading.Tasks; +using Android.Content; +using Android.Graphics; + +namespace Xamarin.Forms.Platform.Android +{ + public interface IImageSourceHandler : IRegisterable + { + Task<Bitmap> LoadImageAsync(ImageSource imagesource, Context context, CancellationToken cancelationToken = default(CancellationToken)); + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/IToolbarButton.cs b/Xamarin.Forms.Platform.Android/Renderers/IToolbarButton.cs new file mode 100644 index 00000000..a409c41c --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/IToolbarButton.cs @@ -0,0 +1,7 @@ +namespace Xamarin.Forms.Platform.Android +{ + internal interface IToolbarButton + { + ToolbarItem Item { get; set; } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ImageExtensions.cs b/Xamarin.Forms.Platform.Android/Renderers/ImageExtensions.cs new file mode 100644 index 00000000..e48682c3 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/ImageExtensions.cs @@ -0,0 +1,26 @@ +using Android.Widget; + +namespace Xamarin.Forms.Platform.Android +{ + internal static class ImageExtensions + { + static ImageView.ScaleType s_fill; + static ImageView.ScaleType s_aspectFill; + static ImageView.ScaleType s_aspectFit; + + public static ImageView.ScaleType ToScaleType(this Aspect aspect) + { + switch (aspect) + { + case Aspect.Fill: + return s_fill ?? (s_fill = ImageView.ScaleType.FitXy); + case Aspect.AspectFill: + return s_aspectFill ?? (s_aspectFill = ImageView.ScaleType.CenterCrop); + default: + case Aspect.AspectFit: + return s_aspectFit ?? (s_aspectFit = ImageView.ScaleType.FitCenter); + ; + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ImageLoaderSourceHandler.cs b/Xamarin.Forms.Platform.Android/Renderers/ImageLoaderSourceHandler.cs new file mode 100644 index 00000000..541dbeaf --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/ImageLoaderSourceHandler.cs @@ -0,0 +1,22 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Android.Content; +using Android.Graphics; + +namespace Xamarin.Forms.Platform.Android +{ + public sealed class ImageLoaderSourceHandler : IImageSourceHandler + { + public async Task<Bitmap> LoadImageAsync(ImageSource imagesource, Context context, CancellationToken cancelationToken = default(CancellationToken)) + { + var imageLoader = imagesource as UriImageSource; + if (imageLoader != null && imageLoader.Uri != null) + { + using(Stream imageStream = await imageLoader.GetStreamAsync(cancelationToken).ConfigureAwait(false)) + return await BitmapFactory.DecodeStreamAsync(imageStream).ConfigureAwait(false); + } + return null; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ImageRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/ImageRenderer.cs new file mode 100644 index 00000000..a080e58b --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/ImageRenderer.cs @@ -0,0 +1,108 @@ +using System; +using System.ComponentModel; +using System.IO; +using System.Threading.Tasks; +using Android.Graphics; +using AImageView = Android.Widget.ImageView; + +namespace Xamarin.Forms.Platform.Android +{ + public class ImageRenderer : ViewRenderer<Image, AImageView> + { + bool _isDisposed; + + public ImageRenderer() + { + AutoPackage = false; + } + + protected override void Dispose(bool disposing) + { + if (_isDisposed) + return; + + _isDisposed = true; + + base.Dispose(disposing); + } + + protected override void OnElementChanged(ElementChangedEventArgs<Image> e) + { + base.OnElementChanged(e); + + if (e.OldElement == null) + { + var view = new FormsImageView(Context); + SetNativeControl(view); + } + + UpdateBitmap(e.OldElement); + UpdateAspect(); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == Image.SourceProperty.PropertyName) + UpdateBitmap(); + else if (e.PropertyName == Image.AspectProperty.PropertyName) + UpdateAspect(); + } + + void UpdateAspect() + { + AImageView.ScaleType type = Element.Aspect.ToScaleType(); + Control.SetScaleType(type); + } + + async void UpdateBitmap(Image previous = null) + { + if (Device.IsInvokeRequired) + throw new InvalidOperationException("Image Bitmap must not be updated from background thread"); + + Bitmap bitmap = null; + + ImageSource source = Element.Source; + IImageSourceHandler handler; + + if (previous != null && Equals(previous.Source, Element.Source)) + return; + + ((IElementController)Element).SetValueFromRenderer(Image.IsLoadingPropertyKey, true); + + var formsImageView = Control as FormsImageView; + if (formsImageView != null) + formsImageView.SkipInvalidate(); + + Control.SetImageResource(global::Android.Resource.Color.Transparent); + + if (source != null && (handler = Registrar.Registered.GetHandler<IImageSourceHandler>(source.GetType())) != null) + { + try + { + bitmap = await handler.LoadImageAsync(source, Context); + } + catch (TaskCanceledException) + { + } + catch (IOException e) + { + } + } + + if (Element == null || !Equals(Element.Source, source)) + return; + + if (!_isDisposed) + { + Control.SetImageBitmap(bitmap); + if (bitmap != null) + bitmap.Dispose(); + + ((IElementController)Element).SetValueFromRenderer(Image.IsLoadingPropertyKey, false); + ((IVisualElementController)Element).NativeSizeChanged(); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/IntVector.cs b/Xamarin.Forms.Platform.Android/Renderers/IntVector.cs new file mode 100644 index 00000000..e5e0986c --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/IntVector.cs @@ -0,0 +1,86 @@ +namespace Xamarin.Forms.Platform.Android +{ + internal struct IntVector + { + public static explicit operator IntVector(System.Drawing.Size size) + { + return new IntVector(size.Width, size.Height); + } + + public static explicit operator IntVector(System.Drawing.Point point) + { + return new IntVector(point.X, point.Y); + } + + public static implicit operator System.Drawing.Point(IntVector vector) + { + return new System.Drawing.Point(vector.X, vector.Y); + } + + public static implicit operator System.Drawing.Size(IntVector vector) + { + return new System.Drawing.Size(vector.X, vector.Y); + } + + public static bool operator ==(IntVector lhs, IntVector rhs) + { + return lhs.X == rhs.X && lhs.Y == rhs.Y; + } + + public static bool operator !=(IntVector lhs, IntVector rhs) + { + return !(lhs == rhs); + } + + public static System.Drawing.Rectangle operator -(System.Drawing.Rectangle source, IntVector vector) => source + -vector; + + public static System.Drawing.Rectangle operator +(System.Drawing.Rectangle source, IntVector vector) => new System.Drawing.Rectangle(source.Location + vector, source.Size); + + public static System.Drawing.Point operator -(System.Drawing.Point point, IntVector delta) => point + -delta; + + public static System.Drawing.Point operator +(System.Drawing.Point point, IntVector delta) => new System.Drawing.Point(point.X + delta.X, point.Y + delta.Y); + + public static IntVector operator -(IntVector vector, IntVector other) => vector + -other; + + public static IntVector operator +(IntVector vector, IntVector other) => new IntVector(vector.X + other.X, vector.Y + other.Y); + + public static IntVector operator -(IntVector vector) => vector * -1; + + public static IntVector operator *(IntVector vector, int scaler) => new IntVector(vector.X * scaler, vector.Y * scaler); + + public static IntVector operator /(IntVector vector, int scaler) => new IntVector(vector.X / scaler, vector.Y / scaler); + + public static IntVector operator *(IntVector vector, double scaler) => new IntVector((int)(vector.X * scaler), (int)(vector.Y * scaler)); + + public static IntVector operator /(IntVector vector, double scaler) => vector * (1 / scaler); + + internal static IntVector Origin = new IntVector(0, 0); + internal static IntVector XUnit = new IntVector(1, 0); + internal static IntVector YUnit = new IntVector(0, 1); + + internal IntVector(int x, int y) + { + X = x; + Y = y; + } + + internal int X { get; } + + internal int Y { get; } + + public override bool Equals(object obj) + { + return base.Equals(obj); + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + public override string ToString() + { + return $"{X},{Y}"; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ItemViewAdapter.cs b/Xamarin.Forms.Platform.Android/Renderers/ItemViewAdapter.cs new file mode 100644 index 00000000..84dfa171 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/ItemViewAdapter.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using Android.Support.V7.Widget; +using Android.Views; + +namespace Xamarin.Forms.Platform.Android +{ + internal class ItemViewAdapter : RecyclerView.Adapter + { + readonly IVisualElementRenderer _renderer; + readonly Dictionary<int, object> _typeByTypeId; + readonly Dictionary<object, int> _typeIdByType; + int _nextItemTypeId; + + public ItemViewAdapter(IVisualElementRenderer carouselRenderer) + { + _renderer = carouselRenderer; + _typeByTypeId = new Dictionary<int, object>(); + _typeIdByType = new Dictionary<object, int>(); + _nextItemTypeId = 0; + } + + public override int ItemCount + { + get { return Element.Count; } + } + + IItemViewController Controller + { + get { return Element; } + } + + ItemsView Element + { + get { return (ItemsView)_renderer.Element; } + } + + public override int GetItemViewType(int position) + { + // get item and type from ItemSource and ItemTemplate + object item = Controller.GetItem(position); + object type = Controller.GetItemType(item); + + // map type as DataTemplate to type as Id + int id = default(int); + if (!_typeIdByType.TryGetValue(type, out id)) + { + id = _nextItemTypeId++; + _typeByTypeId[id] = type; + _typeIdByType[type] = id; + } + return id; + } + + public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position) + { + var carouselHolder = (CarouselViewHolder)holder; + + object item = Controller.GetItem(position); + Controller.BindView(carouselHolder.View, item); + } + + public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType) + { + // create view from type + object type = _typeByTypeId[viewType]; + View view = Controller.CreateView(type); + + // create renderer for view + IVisualElementRenderer renderer = Platform.CreateRenderer(view); + Platform.SetRenderer(view, renderer); + + // package renderer + view + return new CarouselViewHolder(view, renderer); + } + + class CarouselViewHolder : RecyclerView.ViewHolder + { + public CarouselViewHolder(View view, IVisualElementRenderer renderer) : base(renderer.ViewGroup) + { + VisualElementRenderer = renderer; + View = view; + } + + public View View { get; } + + public IVisualElementRenderer VisualElementRenderer { get; } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/KeyboardExtensions.cs b/Xamarin.Forms.Platform.Android/Renderers/KeyboardExtensions.cs new file mode 100644 index 00000000..ab168b6d --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/KeyboardExtensions.cs @@ -0,0 +1,77 @@ +using Android.Text; + +namespace Xamarin.Forms.Platform.Android +{ + internal static class KeyboardExtensions + { + public static InputTypes ToInputType(this Keyboard self) + { + var result = new InputTypes(); + + // ClassText: !autocaps, spellcheck, suggestions + // TextFlagNoSuggestions: !autocaps, !spellcheck, !suggestions + // InputTypes.ClassText | InputTypes.TextFlagCapSentences autocaps, spellcheck, suggestions + // InputTypes.ClassText | InputTypes.TextFlagCapSentences | InputTypes.TextFlagNoSuggestions; autocaps, !spellcheck, !suggestions + + if (self == Keyboard.Default) + result = InputTypes.ClassText | InputTypes.TextVariationNormal; + else if (self == Keyboard.Chat) + result = InputTypes.ClassText | InputTypes.TextFlagCapSentences | InputTypes.TextFlagNoSuggestions; + else if (self == Keyboard.Email) + result = InputTypes.ClassText | InputTypes.TextVariationEmailAddress; + else if (self == Keyboard.Numeric) + result = InputTypes.ClassNumber | InputTypes.NumberFlagDecimal; + else if (self == Keyboard.Telephone) + result = InputTypes.ClassPhone; + else if (self == Keyboard.Text) + result = InputTypes.ClassText | InputTypes.TextFlagCapSentences; + else if (self == Keyboard.Url) + result = InputTypes.ClassText | InputTypes.TextVariationUri; + else if (self is CustomKeyboard) + { + var custom = (CustomKeyboard)self; + bool capitalizedSentenceEnabled = (custom.Flags & KeyboardFlags.CapitalizeSentence) == KeyboardFlags.CapitalizeSentence; + bool spellcheckEnabled = (custom.Flags & KeyboardFlags.Spellcheck) == KeyboardFlags.Spellcheck; + bool suggestionsEnabled = (custom.Flags & KeyboardFlags.Suggestions) == KeyboardFlags.Suggestions; + + if (!capitalizedSentenceEnabled && !spellcheckEnabled && !suggestionsEnabled) + result = InputTypes.ClassText | InputTypes.TextFlagNoSuggestions; + + if (!capitalizedSentenceEnabled && !spellcheckEnabled && suggestionsEnabled) + { + // Due to the nature of android, TextFlagAutoCorrect includes Spellcheck + Log.Warning(null, "On Android, KeyboardFlags.Suggestions enables KeyboardFlags.Spellcheck as well due to a platform limitation."); + result = InputTypes.ClassText | InputTypes.TextFlagAutoCorrect; + } + + if (!capitalizedSentenceEnabled && spellcheckEnabled && !suggestionsEnabled) + result = InputTypes.ClassText | InputTypes.TextFlagAutoComplete; + + if (!capitalizedSentenceEnabled && spellcheckEnabled && suggestionsEnabled) + result = InputTypes.ClassText | InputTypes.TextFlagAutoCorrect; + + if (capitalizedSentenceEnabled && !spellcheckEnabled && !suggestionsEnabled) + result = InputTypes.ClassText | InputTypes.TextFlagCapSentences | InputTypes.TextFlagNoSuggestions; + + if (capitalizedSentenceEnabled && !spellcheckEnabled && suggestionsEnabled) + { + // Due to the nature of android, TextFlagAutoCorrect includes Spellcheck + Log.Warning(null, "On Android, KeyboardFlags.Suggestions enables KeyboardFlags.Spellcheck as well due to a platform limitation."); + result = InputTypes.ClassText | InputTypes.TextFlagCapSentences | InputTypes.TextFlagAutoCorrect; + } + + if (capitalizedSentenceEnabled && spellcheckEnabled && !suggestionsEnabled) + result = InputTypes.ClassText | InputTypes.TextFlagCapSentences | InputTypes.TextFlagAutoComplete; + + if (capitalizedSentenceEnabled && spellcheckEnabled && suggestionsEnabled) + result = InputTypes.ClassText | InputTypes.TextFlagCapSentences | InputTypes.TextFlagAutoCorrect; + } + else + { + // Should never happens + result = InputTypes.TextVariationNormal; + } + return result; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/LabelRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/LabelRenderer.cs new file mode 100644 index 00000000..45068838 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/LabelRenderer.cs @@ -0,0 +1,213 @@ +using System; +using System.ComponentModel; +using Android.Content.Res; +using Android.Graphics; +using Android.Text; +using Android.Util; +using Android.Widget; +using AColor = Android.Graphics.Color; + +namespace Xamarin.Forms.Platform.Android +{ + public class LabelRenderer : ViewRenderer<Label, TextView> + { + ColorStateList _labelTextColorDefault; + int _lastConstraintHeight; + int _lastConstraintWidth; + + SizeRequest? _lastSizeRequest; + float _lastTextSize = -1f; + Typeface _lastTypeface; + + Color _lastUpdateColor = Color.Default; + FormsTextView _view; + bool _wasFormatted; + + public LabelRenderer() + { + AutoPackage = false; + } + + public override SizeRequest GetDesiredSize(int widthConstraint, int heightConstraint) + { + if (_lastSizeRequest.HasValue) + { + // if we are measuring the same thing, no need to waste the time + bool canRecycleLast = widthConstraint == _lastConstraintWidth && heightConstraint == _lastConstraintHeight; + + if (!canRecycleLast) + { + // if the last time we measured the returned size was all around smaller than the passed constraint + // and the constraint is bigger than the last size request, we can assume the newly measured size request + // will not change either. + int lastConstraintWidthSize = MeasureSpecFactory.GetSize(_lastConstraintWidth); + int lastConstraintHeightSize = MeasureSpecFactory.GetSize(_lastConstraintHeight); + + int currentConstraintWidthSize = MeasureSpecFactory.GetSize(widthConstraint); + int currentConstraintHeightSize = MeasureSpecFactory.GetSize(heightConstraint); + + bool lastWasSmallerThanConstraints = _lastSizeRequest.Value.Request.Width < lastConstraintWidthSize && _lastSizeRequest.Value.Request.Height < lastConstraintHeightSize; + + bool currentConstraintsBiggerThanLastRequest = currentConstraintWidthSize >= _lastSizeRequest.Value.Request.Width && currentConstraintHeightSize >= _lastSizeRequest.Value.Request.Height; + + canRecycleLast = lastWasSmallerThanConstraints && currentConstraintsBiggerThanLastRequest; + } + + if (canRecycleLast) + return _lastSizeRequest.Value; + } + + SizeRequest result = base.GetDesiredSize(widthConstraint, heightConstraint); + result.Minimum = new Size(Math.Min(Context.ToPixels(10), result.Request.Width), result.Request.Height); + + _lastConstraintWidth = widthConstraint; + _lastConstraintHeight = heightConstraint; + _lastSizeRequest = result; + + return result; + } + + protected override void OnElementChanged(ElementChangedEventArgs<Label> e) + { + base.OnElementChanged(e); + if (_view == null) + { + _view = new FormsTextView(Context); + _labelTextColorDefault = _view.TextColors; + SetNativeControl(_view); + } + + if (e.OldElement == null) + { + UpdateText(); + UpdateLineBreakMode(); + UpdateGravity(); + } + else + { + _view.SkipNextInvalidate(); + UpdateText(); + if (e.OldElement.LineBreakMode != e.NewElement.LineBreakMode) + UpdateLineBreakMode(); + if (e.OldElement.HorizontalTextAlignment != e.NewElement.HorizontalTextAlignment || e.OldElement.VerticalTextAlignment != e.NewElement.VerticalTextAlignment) + UpdateGravity(); + } + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == Label.HorizontalTextAlignmentProperty.PropertyName || e.PropertyName == Label.VerticalTextAlignmentProperty.PropertyName) + UpdateGravity(); + else if (e.PropertyName == Label.TextColorProperty.PropertyName) + UpdateText(); + else if (e.PropertyName == Label.FontProperty.PropertyName) + UpdateText(); + else if (e.PropertyName == Label.LineBreakModeProperty.PropertyName) + UpdateLineBreakMode(); + else if (e.PropertyName == Label.TextProperty.PropertyName || e.PropertyName == Label.FormattedTextProperty.PropertyName) + UpdateText(); + } + + void UpdateColor() + { + Color c = Element.TextColor; + if (c == _lastUpdateColor) + return; + _lastUpdateColor = c; + + if (c.IsDefault) + _view.SetTextColor(_labelTextColorDefault); + else + _view.SetTextColor(c.ToAndroid()); + } + + void UpdateFont() + { + Font f = Element.Font; + + Typeface newTypeface = f.ToTypeface(); + if (newTypeface != _lastTypeface) + { + _view.Typeface = newTypeface; + _lastTypeface = newTypeface; + } + + float newTextSize = f.ToScaledPixel(); + if (newTextSize != _lastTextSize) + { + _view.SetTextSize(ComplexUnitType.Sp, newTextSize); + _lastTextSize = newTextSize; + } + } + + void UpdateGravity() + { + Label label = Element; + + _view.Gravity = label.HorizontalTextAlignment.ToHorizontalGravityFlags() | label.VerticalTextAlignment.ToVerticalGravityFlags(); + + _lastSizeRequest = null; + } + + void UpdateLineBreakMode() + { + switch (Element.LineBreakMode) + { + case LineBreakMode.NoWrap: + _view.SetSingleLine(true); + _view.Ellipsize = null; + break; + case LineBreakMode.WordWrap: + _view.SetSingleLine(false); + _view.Ellipsize = null; + _view.SetMaxLines(100); + break; + case LineBreakMode.CharacterWrap: + _view.SetSingleLine(false); + _view.Ellipsize = null; + _view.SetMaxLines(100); + break; + case LineBreakMode.HeadTruncation: + _view.SetSingleLine(true); + _view.Ellipsize = TextUtils.TruncateAt.Start; + break; + case LineBreakMode.TailTruncation: + _view.SetSingleLine(true); + _view.Ellipsize = TextUtils.TruncateAt.End; + break; + case LineBreakMode.MiddleTruncation: + _view.SetSingleLine(true); + _view.Ellipsize = TextUtils.TruncateAt.Middle; + break; + } + _lastSizeRequest = null; + } + + void UpdateText() + { + if (Element.FormattedText != null) + { + FormattedString formattedText = Element.FormattedText ?? Element.Text; + _view.TextFormatted = formattedText.ToAttributed(Element.Font, Element.TextColor, _view); + _wasFormatted = true; + } + else + { + if (_wasFormatted) + { + _view.SetTextColor(_labelTextColorDefault); + _lastUpdateColor = Color.Default; + } + _view.Text = Element.Text; + UpdateColor(); + UpdateFont(); + + _wasFormatted = false; + } + + _lastSizeRequest = null; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ListViewAdapter.cs b/Xamarin.Forms.Platform.Android/Renderers/ListViewAdapter.cs new file mode 100644 index 00000000..28f1696e --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/ListViewAdapter.cs @@ -0,0 +1,531 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using Android.Content; +using Android.Util; +using Android.Views; +using Android.Widget; +using AView = Android.Views.View; +using AListView = Android.Widget.ListView; + +namespace Xamarin.Forms.Platform.Android +{ + internal sealed class ListViewAdapter : CellAdapter + { + const int DefaultGroupHeaderTemplateId = 0; + const int DefaultItemTemplateId = 1; + + static int s_dividerHorizontalDarkId = int.MinValue; + + internal static readonly BindableProperty IsSelectedProperty = BindableProperty.CreateAttached("IsSelected", typeof(bool), typeof(Cell), false); + + readonly Context _context; + readonly ListView _listView; + readonly AListView _realListView; + readonly Dictionary<DataTemplate, int> _templateToId = new Dictionary<DataTemplate, int>(); + int _dataTemplateIncrementer = 2; // lets start at not 0 because + Cell _enabledCheckCell; + + bool _fromNative; + AView _lastSelected; + WeakReference<Cell> _selectedCell; + + public ListViewAdapter(Context context, AListView realListView, ListView listView) : base(context) + { + _context = context; + _realListView = realListView; + _listView = listView; + + if (listView.SelectedItem != null) + SelectItem(listView.SelectedItem); + + listView.TemplatedItems.CollectionChanged += OnCollectionChanged; + listView.TemplatedItems.GroupedCollectionChanged += OnGroupedCollectionChanged; + listView.ItemSelected += OnItemSelected; + + realListView.OnItemClickListener = this; + realListView.OnItemLongClickListener = this; + + MessagingCenter.Subscribe<Platform>(this, Platform.CloseContextActionsSignalName, p => CloseContextAction()); + } + + public override int Count + { + get + { + int count = _listView.TemplatedItems.Count; + + if (_listView.IsGroupingEnabled) + { + for (var i = 0; i < _listView.TemplatedItems.Count; i++) + count += _listView.TemplatedItems.GetGroup(i).Count; + } + + return count; + } + } + + public AView FooterView { get; set; } + + public override bool HasStableIds + { + get { return false; } + } + + public AView HeaderView { get; set; } + + public bool IsAttachedToWindow { get; set; } + + public override object this[int index] + { + get + { + if (_listView.IsGroupingEnabled) + { + Cell cell = GetCellForPosition(index); + return cell.BindingContext; + } + + return _listView.ListProxy[index]; + } + } + + public override int ViewTypeCount + { + get { return 20; } + } + + public override bool AreAllItemsEnabled() + { + return false; + } + + public override long GetItemId(int position) + { + return position; + } + + public override int GetItemViewType(int position) + { + var group = 0; + var row = 0; + DataTemplate itemTemplate; + if (!_listView.IsGroupingEnabled) + itemTemplate = _listView.ItemTemplate; + else + { + group = _listView.TemplatedItems.GetGroupIndexFromGlobal(position, out row); + + if (row == 0) + { + itemTemplate = _listView.GroupHeaderTemplate; + if (itemTemplate == null) + return DefaultGroupHeaderTemplateId; + } + else + { + itemTemplate = _listView.ItemTemplate; + row--; + } + } + + if (itemTemplate == null) + return DefaultItemTemplateId; + + var selector = itemTemplate as DataTemplateSelector; + if (selector != null) + { + object item = null; + if (_listView.IsGroupingEnabled) + item = _listView.TemplatedItems.GetGroup(group).ListProxy[row]; + else + item = _listView.TemplatedItems.ListProxy[position]; + itemTemplate = selector.SelectTemplate(item, _listView); + } + int key; + if (!_templateToId.TryGetValue(itemTemplate, out key)) + { + _dataTemplateIncrementer++; + key = _dataTemplateIncrementer; + _templateToId[itemTemplate] = key; + } + return key; + } + + public override AView GetView(int position, AView convertView, ViewGroup parent) + { + Cell cell = null; + + Performance.Start(); + + ListViewCachingStrategy cachingStrategy = _listView.CachingStrategy; + var nextCellIsHeader = false; + if (cachingStrategy == ListViewCachingStrategy.RetainElement || convertView == null) + { + if (_listView.IsGroupingEnabled) + { + List<Cell> cells = GetCellsFromPosition(position, 2); + if (cells.Count > 0) + cell = cells[0]; + + if (cells.Count == 2) + nextCellIsHeader = TemplatedItemsList<ItemsView<Cell>, Cell>.GetIsGroupHeader(cells[1]); + } + + if (cell == null) + { + cell = GetCellForPosition(position); + if (cell == null) + return new AView(_context); + } + } + + var makeBline = true; + var layout = convertView as ConditionalFocusLayout; + if (layout != null) + { + makeBline = false; + convertView = layout.GetChildAt(0); + } + else + layout = new ConditionalFocusLayout(_context) { Orientation = Orientation.Vertical }; + + if (cachingStrategy == ListViewCachingStrategy.RecycleElement && convertView != null) + { + var boxedCell = (INativeElementView)convertView; + if (boxedCell == null) + { + throw new InvalidOperationException($"View for cell must implement {nameof(INativeElementView)} to enable recycling."); + } + cell = (Cell)boxedCell.Element; + + if (ActionModeContext == cell) + { + // This appears to never happen, the theory is android keeps all views alive that are currently selected for long-press (preventing them from being recycled). + // This is convenient since we wont have to worry about the user scrolling the cell offscreen and us losing our context actions. + ActionModeContext = null; + ContextView = null; + } + // We are going to re-set the Platform here because in some cases (headers mostly) its possible this is unset and + // when the binding context gets updated the measure passes will all fail. By applying this hear the Update call + // further down will result in correct layouts. + cell.Platform = _listView.Platform; + + cell.SendDisappearing(); + + int row = position; + var group = 0; + if (_listView.IsGroupingEnabled) + group = _listView.TemplatedItems.GetGroupIndexFromGlobal(position, out row); + + TemplatedItemsList<ItemsView<Cell>, Cell> templatedList = _listView.TemplatedItems.GetGroup(group); + + if (_listView.IsGroupingEnabled) + { + if (row == 0) + templatedList.UpdateHeader(cell, group); + else + templatedList.UpdateContent(cell, row - 1); + } + else + templatedList.UpdateContent(cell, row); + + cell.SendAppearing(); + + if (cell.BindingContext == ActionModeObject) + { + ActionModeContext = cell; + ContextView = layout; + } + + if (ReferenceEquals(_listView.SelectedItem, cell.BindingContext)) + Select(_listView.IsGroupingEnabled ? row - 1 : row, layout); + else if (cell.BindingContext == ActionModeObject) + SetSelectedBackground(layout, true); + else + UnsetSelectedBackground(layout); + + Performance.Stop(); + return layout; + } + + AView view = CellFactory.GetCell(cell, convertView, parent, _context, _listView); + + Performance.Start("AddView"); + + if (!makeBline) + { + if (convertView != view) + { + layout.RemoveViewAt(0); + layout.AddView(view, 0); + } + } + else + layout.AddView(view, 0); + + Performance.Stop("AddView"); + + AView bline; + if (makeBline) + { + bline = new AView(_context) { LayoutParameters = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FillParent, 1) }; + + layout.AddView(bline); + } + else + bline = layout.GetChildAt(1); + + bool isHeader = TemplatedItemsList<ItemsView<Cell>, Cell>.GetIsGroupHeader(cell); + + Color separatorColor = _listView.SeparatorColor; + + if (nextCellIsHeader || _listView.SeparatorVisibility == SeparatorVisibility.None) + bline.SetBackgroundColor(global::Android.Graphics.Color.Transparent); + else if (isHeader || !separatorColor.IsDefault) + bline.SetBackgroundColor(separatorColor.ToAndroid(Color.Accent)); + else + { + if (s_dividerHorizontalDarkId == int.MinValue) + { + using(var value = new TypedValue()) + { + int id = global::Android.Resource.Drawable.DividerHorizontalDark; + if (_context.Theme.ResolveAttribute(global::Android.Resource.Attribute.ListDivider, value, true)) + id = value.ResourceId; + else if (_context.Theme.ResolveAttribute(global::Android.Resource.Attribute.Divider, value, true)) + id = value.ResourceId; + + s_dividerHorizontalDarkId = id; + } + } + + bline.SetBackgroundResource(s_dividerHorizontalDarkId); + } + + if ((bool)cell.GetValue(IsSelectedProperty)) + Select(position, layout); + else + UnsetSelectedBackground(layout); + + layout.ApplyTouchListenersToSpecialCells(cell); + + Performance.Stop(); + + return layout; + } + + public override bool IsEnabled(int position) + { + ListView list = _listView; + + if (list.IsGroupingEnabled) + { + int leftOver; + list.TemplatedItems.GetGroupIndexFromGlobal(position, out leftOver); + return leftOver > 0; + } + + if (list.CachingStrategy == ListViewCachingStrategy.RecycleElement) + { + if (_enabledCheckCell == null) + _enabledCheckCell = GetCellForPosition(position); + else + list.TemplatedItems.UpdateContent(_enabledCheckCell, position); + return _enabledCheckCell.IsEnabled; + } + + Cell item = GetCellForPosition(position); + return item.IsEnabled; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + CloseContextAction(); + MessagingCenter.Unsubscribe<Platform>(this, Platform.CloseContextActionsSignalName); + _realListView.OnItemClickListener = null; + _realListView.OnItemLongClickListener = null; + + _listView.TemplatedItems.CollectionChanged -= OnCollectionChanged; + _listView.TemplatedItems.GroupedCollectionChanged -= OnGroupedCollectionChanged; + _listView.ItemSelected -= OnItemSelected; + + if (_lastSelected != null) + { + _lastSelected.Dispose(); + _lastSelected = null; + } + } + + base.Dispose(disposing); + } + + protected override Cell GetCellForPosition(int position) + { + return GetCellsFromPosition(position, 1).FirstOrDefault(); + } + + protected override void HandleItemClick(AdapterView parent, AView view, int position, long id) + { + Cell cell = null; + + if (_listView.CachingStrategy == ListViewCachingStrategy.RecycleElement) + { + AView cellOwner = view; + var layout = cellOwner as ConditionalFocusLayout; + if (layout != null) + cellOwner = layout.GetChildAt(0); + cell = (Cell)((INativeElementView)cellOwner).Element; + } + + // All our ListView's have called AddHeaderView. This effectively becomes index 0, so our index 0 is index 1 to the listView. + position--; + + if (position < 0 || position >= Count) + return; + + Select(position, view); + _fromNative = true; + _listView.NotifyRowTapped(position, cell); + } + + // TODO: We can optimize this by storing the last position, group index and global index + // and increment/decrement from that starting place. + List<Cell> GetCellsFromPosition(int position, int take) + { + var cells = new List<Cell>(take); + if (position < 0) + return cells; + + if (!_listView.IsGroupingEnabled) + { + for (var x = 0; x < take; x++) + { + if (position + x >= _listView.TemplatedItems.Count) + return cells; + + cells.Add(_listView.TemplatedItems[x + position]); + } + + return cells; + } + + var i = 0; + var global = 0; + for (; i < _listView.TemplatedItems.Count; i++) + { + TemplatedItemsList<ItemsView<Cell>, Cell> group = _listView.TemplatedItems.GetGroup(i); + + if (global == position || cells.Count > 0) + { + cells.Add(group.HeaderContent); + if (cells.Count == take) + return cells; + } + + global++; + + if (global + group.Count < position) + { + global += group.Count; + continue; + } + + for (var g = 0; g < group.Count; g++) + { + if (global == position || cells.Count > 0) + { + cells.Add(group[g]); + if (cells.Count == take) + return cells; + } + + global++; + } + } + + return cells; + } + + void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + OnDataChanged(); + } + + void OnDataChanged() + { + if (IsAttachedToWindow) + NotifyDataSetChanged(); + else + { + // In a TabbedPage page with two pages, Page A and Page B with ListView, if A changes B's ListView, + // we need to reset the ListView's adapter to reflect the changes on page B + // If there header and footer are present at the reset time of the adapter + // they will be DOUBLE added to the ViewGround (the ListView) causing indexes to be off by one. + _realListView.RemoveHeaderView(HeaderView); + _realListView.RemoveFooterView(FooterView); + _realListView.Adapter = _realListView.Adapter; + _realListView.AddHeaderView(HeaderView); + _realListView.AddFooterView(FooterView); + } + } + + void OnGroupedCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + OnDataChanged(); + } + + void OnItemSelected(object sender, SelectedItemChangedEventArgs eventArg) + { + if (_fromNative) + { + _fromNative = false; + return; + } + + SelectItem(eventArg.SelectedItem); + } + + void Select(int index, AView view) + { + if (_lastSelected != null) + { + UnsetSelectedBackground(_lastSelected); + Cell previousCell; + if (_selectedCell.TryGetTarget(out previousCell)) + previousCell.SetValue(IsSelectedProperty, false); + } + + _lastSelected = view; + + if (index == -1) + return; + + Cell cell = GetCellForPosition(index); + cell.SetValue(IsSelectedProperty, true); + _selectedCell = new WeakReference<Cell>(cell); + + if (view != null) + SetSelectedBackground(view); + } + + void SelectItem(object item) + { + int position = _listView.TemplatedItems.GetGlobalIndexOfItem(item); + AView view = null; + if (position != -1) + view = _realListView.GetChildAt(position + 1 - _realListView.FirstVisiblePosition); + + Select(position, view); + } + + enum CellType + { + Row, + Header + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ListViewRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/ListViewRenderer.cs new file mode 100644 index 00000000..06a9554c --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/ListViewRenderer.cs @@ -0,0 +1,355 @@ +using System.ComponentModel; +using Android.App; +using Android.Content; +using Android.Support.V4.Widget; +using Android.Views; +using AListView = Android.Widget.ListView; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android +{ + public class ListViewRenderer : ViewRenderer<ListView, AListView>, SwipeRefreshLayout.IOnRefreshListener + { + ListViewAdapter _adapter; + IVisualElementRenderer _headerRenderer; + IVisualElementRenderer _footerRenderer; + Container _headerView; + Container _footerView; + bool _isAttached; + ScrollToRequestedEventArgs _pendingScrollTo; + + SwipeRefreshLayout _refresh; + + public ListViewRenderer() + { + AutoPackage = false; + } + + void SwipeRefreshLayout.IOnRefreshListener.OnRefresh() + { + IListViewController controller = Element; + controller.SendRefreshing(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_headerView == null) + return; + + if (_headerRenderer != null) + { + _headerRenderer.ViewGroup.RemoveAllViews(); + _headerRenderer.Dispose(); + _headerRenderer = null; + } + + if (_footerRenderer != null) + { + _footerRenderer.ViewGroup.RemoveAllViews(); + _footerRenderer.Dispose(); + _footerRenderer = null; + } + + _headerView.Dispose(); + _headerView = null; + + _footerView.Dispose(); + _footerView = null; + + if (_adapter != null) + { + _adapter.Dispose(); + _adapter = null; + } + + Element.ScrollToRequested -= OnScrollToRequested; + } + + base.Dispose(disposing); + } + + protected override Size MinimumSize() + { + return new Size(40, 40); + } + + protected override void OnAttachedToWindow() + { + base.OnAttachedToWindow(); + + _isAttached = true; + _adapter.IsAttachedToWindow = _isAttached; + } + + protected override void OnDetachedFromWindow() + { + base.OnDetachedFromWindow(); + + _isAttached = false; + _adapter.IsAttachedToWindow = _isAttached; + } + + protected override void OnElementChanged(ElementChangedEventArgs<ListView> e) + { + base.OnElementChanged(e); + + if (e.OldElement != null) + { + e.OldElement.ScrollToRequested -= OnScrollToRequested; + + if (_adapter != null) + { + _adapter.Dispose(); + _adapter = null; + } + } + + if (e.NewElement != null) + { + AListView nativeListView = Control; + if (nativeListView == null) + { + var ctx = (Activity)Context; + nativeListView = new AListView(ctx); + _refresh = new SwipeRefreshLayout(ctx); + _refresh.SetOnRefreshListener(this); + _refresh.AddView(nativeListView, LayoutParams.MatchParent); + SetNativeControl(nativeListView, _refresh); + + _headerView = new Container(ctx); + nativeListView.AddHeaderView(_headerView, null, false); + _footerView = new Container(ctx); + nativeListView.AddFooterView(_footerView, null, false); + } + + e.NewElement.ScrollToRequested += OnScrollToRequested; + + nativeListView.DividerHeight = 0; + nativeListView.Focusable = false; + nativeListView.DescendantFocusability = DescendantFocusability.AfterDescendants; + nativeListView.OnFocusChangeListener = this; + nativeListView.Adapter = _adapter = new ListViewAdapter(Context, nativeListView, e.NewElement); + _adapter.HeaderView = _headerView; + _adapter.FooterView = _footerView; + _adapter.IsAttachedToWindow = _isAttached; + + UpdateHeader(); + UpdateFooter(); + UpdateIsSwipeToRefreshEnabled(); + UpdateIsRefreshing(); + } + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == "HeaderElement") + UpdateHeader(); + else if (e.PropertyName == "FooterElement") + UpdateFooter(); + else if (e.PropertyName == "RefreshAllowed") + UpdateIsSwipeToRefreshEnabled(); + else if (e.PropertyName == ListView.IsPullToRefreshEnabledProperty.PropertyName) + UpdateIsSwipeToRefreshEnabled(); + else if (e.PropertyName == ListView.IsRefreshingProperty.PropertyName) + UpdateIsRefreshing(); + else if (e.PropertyName == ListView.SeparatorColorProperty.PropertyName || e.PropertyName == ListView.SeparatorVisibilityProperty.PropertyName) + _adapter.NotifyDataSetChanged(); + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + base.OnLayout(changed, l, t, r, b); + + if (_pendingScrollTo != null) + { + OnScrollToRequested(this, _pendingScrollTo); + _pendingScrollTo = null; + } + } + + void OnScrollToRequested(object sender, ScrollToRequestedEventArgs e) + { + if (!_isAttached) + { + _pendingScrollTo = e; + return; + } + + Cell cell; + int position; + + if (Element.IsGroupingEnabled) + { + var results = Element.TemplatedItems.GetGroupAndIndexOfItem(e.Group, e.Item); + if (results.Item1 == -1 || results.Item2 == -1) + return; + + TemplatedItemsList<ItemsView<Cell>, Cell> group = Element.TemplatedItems.GetGroup(results.Item1); + cell = group[results.Item2]; + + position = Element.TemplatedItems.GetGlobalIndexForGroup(group) + results.Item2 + 1; + } + else + { + position = Element.TemplatedItems.GetGlobalIndexOfItem(e.Item); + cell = Element.TemplatedItems[position]; + } + + //Android offsets position of cells when using header + int realPositionWithHeader = position + 1; + + if (e.Position == ScrollToPosition.MakeVisible) + { + if (e.ShouldAnimate) + Control.SmoothScrollToPosition(realPositionWithHeader); + else + Control.SetSelection(realPositionWithHeader); + return; + } + + int height = Control.Height; + var cellHeight = (int)cell.RenderHeight; + if (cellHeight == -1) + { + int first = Control.FirstVisiblePosition; + if (first <= position && position <= Control.LastVisiblePosition) + cellHeight = Control.GetChildAt(position - first).Height; + else + { + AView view = _adapter.GetView(position, null, null); + view.Measure(MeasureSpecFactory.MakeMeasureSpec(Control.Width, MeasureSpecMode.AtMost), MeasureSpecFactory.MakeMeasureSpec(0, MeasureSpecMode.Unspecified)); + cellHeight = view.MeasuredHeight; + } + } + + var y = 0; + + if (e.Position == ScrollToPosition.Center) + y = height / 2 - cellHeight / 2; + else if (e.Position == ScrollToPosition.End) + y = height - cellHeight; + + if (e.ShouldAnimate) + Control.SmoothScrollToPositionFromTop(realPositionWithHeader, y); + else + Control.SetSelectionFromTop(realPositionWithHeader, y); + } + + void UpdateFooter() + { + var footer = (VisualElement)((IListViewController)Element).FooterElement; + if (_footerRenderer != null && (footer == null || Registrar.Registered.GetHandlerType(footer.GetType()) != _footerRenderer.GetType())) + { + _footerView.Child = null; + _footerRenderer.Dispose(); + _footerRenderer = null; + } + + if (footer == null) + return; + + if (_footerRenderer != null) + _footerRenderer.SetElement(footer); + else + { + _footerRenderer = Platform.CreateRenderer(footer); + _footerView.Child = _footerRenderer; + } + + Platform.SetRenderer(footer, _footerRenderer); + } + + void UpdateHeader() + { + var header = (VisualElement)((IListViewController)Element).HeaderElement; + if (_headerRenderer != null && (header == null || Registrar.Registered.GetHandlerType(header.GetType()) != _headerRenderer.GetType())) + { + _headerView.Child = null; + _headerRenderer.Dispose(); + _headerRenderer = null; + } + + if (header == null) + return; + + if (_headerRenderer != null) + _headerRenderer.SetElement(header); + else + { + _headerRenderer = Platform.CreateRenderer(header); + _headerView.Child = _headerRenderer; + } + + Platform.SetRenderer(header, _headerRenderer); + } + + void UpdateIsRefreshing() + { + _refresh.Refreshing = Element.IsRefreshing; + } + + void UpdateIsSwipeToRefreshEnabled() + { + _refresh.Enabled = Element.IsPullToRefreshEnabled && (Element as IListViewController).RefreshAllowed; + } + + internal class Container : ViewGroup + { + IVisualElementRenderer _child; + + public Container(Context context) : base(context) + { + } + + public IVisualElementRenderer Child + { + set + { + if (_child != null) + RemoveView(_child.ViewGroup); + + _child = value; + + if (value != null) + AddView(value.ViewGroup); + } + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + if (_child == null) + return; + + _child.UpdateLayout(); + } + + protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + if (_child == null) + { + SetMeasuredDimension(0, 0); + return; + } + + VisualElement element = _child.Element; + + Context ctx = Context; + + var width = (int)ctx.FromPixels(MeasureSpecFactory.GetSize(widthMeasureSpec)); + + SizeRequest request = _child.Element.Measure(width, double.PositiveInfinity, MeasureFlags.IncludeMargins); + Xamarin.Forms.Layout.LayoutChildIntoBoundingRegion(_child.Element, new Rectangle(0, 0, width, request.Request.Height)); + + int widthSpec = MeasureSpecFactory.MakeMeasureSpec((int)ctx.ToPixels(width), MeasureSpecMode.Exactly); + int heightSpec = MeasureSpecFactory.MakeMeasureSpec((int)ctx.ToPixels(request.Request.Height), MeasureSpecMode.Exactly); + + _child.ViewGroup.Measure(widthMeasureSpec, heightMeasureSpec); + SetMeasuredDimension(widthSpec, heightSpec); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/MasterDetailContainer.cs b/Xamarin.Forms.Platform.Android/Renderers/MasterDetailContainer.cs new file mode 100644 index 00000000..6f92d90f --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/MasterDetailContainer.cs @@ -0,0 +1,138 @@ +using Android.Content; +using Android.Content.Res; +using Android.Views; + +namespace Xamarin.Forms.Platform.Android +{ + internal class MasterDetailContainer : ViewGroup + { + const int DefaultMasterSize = 320; + const int DefaultSmallMasterSize = 240; + readonly bool _isMaster; + readonly MasterDetailPage _parent; + VisualElement _childView; + + public MasterDetailContainer(MasterDetailPage parent, bool isMaster, Context context) : base(context) + { + _parent = parent; + _isMaster = isMaster; + } + + public VisualElement ChildView + { + get { return _childView; } + set + { + if (_childView == value) + return; + + RemoveAllViews(); + if (_childView != null) + DisposeChildRenderers(); + + _childView = value; + + if (_childView == null) + return; + + IVisualElementRenderer renderer = Platform.GetRenderer(_childView); + if (renderer == null) + Platform.SetRenderer(_childView, renderer = Platform.CreateRenderer(_childView)); + + if (renderer.ViewGroup.Parent != this) + { + if (renderer.ViewGroup.Parent != null) + renderer.ViewGroup.RemoveFromParent(); + SetDefaultBackgroundColor(renderer); + AddView(renderer.ViewGroup); + renderer.UpdateLayout(); + } + } + } + + public int TopPadding { get; set; } + + double DefaultWidthMaster + { + get + { + double w = Context.FromPixels(Resources.DisplayMetrics.WidthPixels); + return w < DefaultSmallMasterSize ? w : (w < DefaultMasterSize ? DefaultSmallMasterSize : DefaultMasterSize); + } + } + + public override bool OnInterceptTouchEvent(MotionEvent ev) + { + bool isShowingPopover = _parent.IsPresented && !_parent.ShouldShowSplitMode; + if (!_isMaster && isShowingPopover) + return true; + return base.OnInterceptTouchEvent(ev); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + RemoveAllViews(); + DisposeChildRenderers(); + } + base.Dispose(disposing); + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + if (_childView == null) + return; + + Rectangle bounds = GetBounds(_isMaster, l, t, r, b); + if (_isMaster) + _parent.MasterBounds = bounds; + else + _parent.DetailBounds = bounds; + + IVisualElementRenderer renderer = Platform.GetRenderer(_childView); + renderer.UpdateLayout(); + } + + void DisposeChildRenderers() + { + IVisualElementRenderer childRenderer = Platform.GetRenderer(_childView); + if (childRenderer != null) + childRenderer.Dispose(); + _childView.ClearValue(Platform.RendererProperty); + } + + Rectangle GetBounds(bool isMasterPage, int left, int top, int right, int bottom) + { + double width = Context.FromPixels(right - left); + double height = Context.FromPixels(bottom - top); + double xPos = 0; + + //splitview + if (_parent.ShouldShowSplitMode) + { + //to keep some behavior we have on iPad where you can toggle and it won't do anything + bool isDefaultNoToggle = _parent.MasterBehavior == MasterBehavior.Default; + xPos = isMasterPage ? 0 : (_parent.IsPresented || isDefaultNoToggle ? DefaultWidthMaster : 0); + width = isMasterPage ? DefaultWidthMaster : _parent.IsPresented || isDefaultNoToggle ? width - DefaultWidthMaster : width; + } + else + { + //popover make the master smaller + width = isMasterPage && (Device.Info.CurrentOrientation.IsLandscape() || Device.Idiom == TargetIdiom.Tablet) ? DefaultWidthMaster : width; + } + + double padding = Context.FromPixels(TopPadding); + return new Rectangle(xPos, padding, width, height - padding); + } + + void SetDefaultBackgroundColor(IVisualElementRenderer renderer) + { + if (ChildView.BackgroundColor == Color.Default) + { + TypedArray colors = Context.Theme.ObtainStyledAttributes(new[] { global::Android.Resource.Attribute.ColorBackground }); + renderer.ViewGroup.SetBackgroundColor(new global::Android.Graphics.Color(colors.GetColor(0, 0))); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/MasterDetailRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/MasterDetailRenderer.cs new file mode 100644 index 00000000..ce3fb267 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/MasterDetailRenderer.cs @@ -0,0 +1,348 @@ +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using Android.App; +using Android.Support.V4.Widget; +using Android.Views; +using AView = Android.Views.View; +using AColor = Android.Graphics.Drawables.ColorDrawable; + +namespace Xamarin.Forms.Platform.Android +{ + public class MasterDetailRenderer : DrawerLayout, IVisualElementRenderer, DrawerLayout.IDrawerListener + { + //from Android source code + const uint DefaultScrimColor = 0x99000000; + int _currentLockMode = -1; + MasterDetailContainer _detailLayout; + bool _isPresentingFromCore; + MasterDetailContainer _masterLayout; + MasterDetailPage _page; + bool _presented; + + public MasterDetailRenderer() : base(Forms.Context) + { + } + + public bool Presented + { + get { return _presented; } + set + { + if (value == _presented) + return; + UpdateSplitViewLayout(); + _presented = value; + if (_page.MasterBehavior == MasterBehavior.Default && _page.ShouldShowSplitMode) + return; + if (_presented) + OpenDrawer(_masterLayout); + else + CloseDrawer(_masterLayout); + } + } + + public void OnDrawerClosed(AView drawerView) + { + } + + public void OnDrawerOpened(AView drawerView) + { + } + + public void OnDrawerSlide(AView drawerView, float slideOffset) + { + } + + public void OnDrawerStateChanged(int newState) + { + _presented = IsDrawerVisible(_masterLayout); + UpdateIsPresented(); + } + + public VisualElement Element + { + get { return _page; } + } + + public event EventHandler<VisualElementChangedEventArgs> ElementChanged; + + public SizeRequest GetDesiredSize(int widthConstraint, int heightConstraint) + { + Measure(widthConstraint, heightConstraint); + return new SizeRequest(new Size(MeasuredWidth, MeasuredHeight)); + } + + public void SetElement(VisualElement element) + { + MasterDetailPage oldElement = _page; + _page = element as MasterDetailPage; + + _detailLayout = new MasterDetailContainer(_page, false, Context) { LayoutParameters = new LayoutParams(ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.WrapContent) }; + + _masterLayout = new MasterDetailContainer(_page, true, Context) + { + LayoutParameters = new LayoutParams(ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.WrapContent) { Gravity = (int)GravityFlags.Start } + }; + + AddView(_detailLayout); + + AddView(_masterLayout); + + var activity = Context as Activity; + activity.ActionBar.SetDisplayShowHomeEnabled(true); + activity.ActionBar.SetHomeButtonEnabled(true); + + UpdateBackgroundColor(_page); + UpdateBackgroundImage(_page); + + OnElementChanged(oldElement, element); + + if (oldElement != null) + oldElement.BackButtonPressed -= OnBackButtonPressed; + + if (_page != null) + _page.BackButtonPressed += OnBackButtonPressed; + + if (Tracker == null) + Tracker = new VisualElementTracker(this); + + _page.PropertyChanged += HandlePropertyChanged; + _page.Appearing += MasterDetailPageAppearing; + _page.Disappearing += MasterDetailPageDisappearing; + + UpdateMaster(); + UpdateDetail(); + + Device.Info.PropertyChanged += DeviceInfoPropertyChanged; + SetGestureState(); + + Presented = _page.IsPresented; + + SetDrawerListener(this); + + if (element != null) + element.SendViewInitialized(this); + + if (element != null && !string.IsNullOrEmpty(element.AutomationId)) + ContentDescription = element.AutomationId; + } + + public VisualElementTracker Tracker { get; private set; } + + public void UpdateLayout() + { + if (Tracker != null) + Tracker.UpdateLayout(); + } + + public ViewGroup ViewGroup + { + get { return this; } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (Tracker != null) + { + Tracker.Dispose(); + Tracker = null; + } + + if (_detailLayout != null) + { + _detailLayout.Dispose(); + _detailLayout = null; + } + + if (_masterLayout != null) + { + _masterLayout.Dispose(); + _masterLayout = null; + } + + Device.Info.PropertyChanged -= DeviceInfoPropertyChanged; + + if (_page != null) + { + _page.BackButtonPressed -= OnBackButtonPressed; + _page.PropertyChanged -= HandlePropertyChanged; + _page.Appearing -= MasterDetailPageAppearing; + _page.Disappearing -= MasterDetailPageDisappearing; + _page.ClearValue(Platform.RendererProperty); + _page = null; + } + } + + base.Dispose(disposing); + } + + protected override void OnAttachedToWindow() + { + base.OnAttachedToWindow(); + ((Page)Element).SendAppearing(); + } + + protected override void OnDetachedFromWindow() + { + base.OnDetachedFromWindow(); + ((Page)Element).SendDisappearing(); + } + + protected virtual void OnElementChanged(VisualElement oldElement, VisualElement newElement) + { + EventHandler<VisualElementChangedEventArgs> changed = ElementChanged; + if (changed != null) + changed(this, new VisualElementChangedEventArgs(oldElement, newElement)); + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + base.OnLayout(changed, l, t, r, b); + //hack to make the split layout handle touches the full width + if (_page.ShouldShowSplitMode && _masterLayout != null) + _masterLayout.Right = r; + } + + async void DeviceInfoPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == "CurrentOrientation") + { + if (!_page.ShouldShowSplitMode && Presented) + { + _page.CanChangeIsPresented = true; + //hack : when the orientation changes and we try to close the Master on Android + //sometimes Android picks the width of the screen previous to the rotation + //this leaves a little of the master visible, the hack is to delay for 50ms closing the drawer + await Task.Delay(50); + CloseDrawer(_masterLayout); + } + UpdateSplitViewLayout(); + } + } + + void HandleMasterPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == Page.TitleProperty.PropertyName || e.PropertyName == Page.IconProperty.PropertyName) + ((Platform)_page.Platform).UpdateMasterDetailToggle(true); + } + + void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == "Master") + UpdateMaster(); + else if (e.PropertyName == "Detail") + { + UpdateDetail(); + ((Platform)_page.Platform).UpdateActionBar(); + } + else if (e.PropertyName == MasterDetailPage.IsPresentedProperty.PropertyName) + { + _isPresentingFromCore = true; + Presented = _page.IsPresented; + _isPresentingFromCore = false; + } + else if (e.PropertyName == "IsGestureEnabled") + SetGestureState(); + else if (e.PropertyName == Page.BackgroundImageProperty.PropertyName) + UpdateBackgroundImage(_page); + if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName) + UpdateBackgroundColor(_page); + } + + void MasterDetailPageAppearing(object sender, EventArgs e) + { + if (_page.Master != null) + _page.Master.SendAppearing(); + + if (_page.Detail != null) + _page.Detail.SendAppearing(); + } + + void MasterDetailPageDisappearing(object sender, EventArgs e) + { + if (_page.Master != null) + _page.Master.SendDisappearing(); + + if (_page.Detail != null) + _page.Detail.SendDisappearing(); + } + + void OnBackButtonPressed(object sender, BackButtonPressedEventArgs backButtonPressedEventArgs) + { + if (IsDrawerOpen((int)GravityFlags.Start)) + { + if (_currentLockMode != LockModeLockedOpen) + { + CloseDrawer((int)GravityFlags.Start); + backButtonPressedEventArgs.Handled = true; + } + } + } + + void SetGestureState() + { + SetDrawerLockMode(_page.IsGestureEnabled ? LockModeUnlocked : LockModeLockedClosed); + } + + void SetLockMode(int lockMode) + { + if (_currentLockMode != lockMode) + { + SetDrawerLockMode(lockMode); + _currentLockMode = lockMode; + } + } + + void UpdateBackgroundColor(Page view) + { + if (view.BackgroundColor != Color.Default) + SetBackgroundColor(view.BackgroundColor.ToAndroid()); + } + + void UpdateBackgroundImage(Page view) + { + if (!string.IsNullOrEmpty(view.BackgroundImage)) + SetBackgroundDrawable(Context.Resources.GetDrawable(view.BackgroundImage)); + } + + void UpdateDetail() + { + Context.HideKeyboard(this); + _detailLayout.ChildView = _page.Detail; + } + + void UpdateIsPresented() + { + if (_isPresentingFromCore) + return; + if (Presented != _page.IsPresented) + ((IElementController)_page).SetValueFromRenderer(MasterDetailPage.IsPresentedProperty, Presented); + } + + void UpdateMaster() + { + if (_masterLayout != null && _masterLayout.ChildView != null) + _masterLayout.ChildView.PropertyChanged -= HandleMasterPropertyChanged; + _masterLayout.ChildView = _page.Master; + if (_page.Master != null) + _page.Master.PropertyChanged += HandleMasterPropertyChanged; + } + + void UpdateSplitViewLayout() + { + if (Device.Idiom == TargetIdiom.Tablet) + { + bool isShowingSplit = _page.ShouldShowSplitMode || (_page.ShouldShowSplitMode && _page.MasterBehavior != MasterBehavior.Default && _page.IsPresented); + SetLockMode(isShowingSplit ? LockModeLockedOpen : LockModeUnlocked); + unchecked + { + SetScrimColor(isShowingSplit ? Color.Transparent.ToAndroid() : (int)DefaultScrimColor); + } + ((Platform)_page.Platform).UpdateMasterDetailToggle(); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/MeasureSpecification.cs b/Xamarin.Forms.Platform.Android/Renderers/MeasureSpecification.cs new file mode 100644 index 00000000..fe38a560 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/MeasureSpecification.cs @@ -0,0 +1,41 @@ +namespace Xamarin.Forms.Platform.Android +{ + internal struct MeasureSpecification + { + public static explicit operator MeasureSpecification(int measureSpecification) + { + return new MeasureSpecification(measureSpecification); + } + + public static implicit operator int(MeasureSpecification measureSpecification) + { + return measureSpecification.Encode(); + } + + internal MeasureSpecification(int measureSpecification) + { + Value = measureSpecification & (int)~MeasureSpecificationType.Mask; + Type = (MeasureSpecificationType)(measureSpecification & (int)MeasureSpecificationType.Mask); + } + + internal MeasureSpecification(int value, MeasureSpecificationType measureSpecification) + { + Value = value; + Type = measureSpecification; + } + + internal int Value { get; } + + internal MeasureSpecificationType Type { get; } + + internal int Encode() + { + return Value | (int)Type; + } + + public override string ToString() + { + return string.Format("{0} {1}", Value, Type); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/MeasureSpecificationType.cs b/Xamarin.Forms.Platform.Android/Renderers/MeasureSpecificationType.cs new file mode 100644 index 00000000..bb61d841 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/MeasureSpecificationType.cs @@ -0,0 +1,13 @@ +using System; + +namespace Xamarin.Forms.Platform.Android +{ + [Flags] + internal enum MeasureSpecificationType + { + Unspecified = 0, + Exactly = 0x1 << 31, + AtMost = 0x1 << 32, + Mask = Exactly | AtMost + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/NavigationMenuRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/NavigationMenuRenderer.cs new file mode 100644 index 00000000..9678b9ca --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/NavigationMenuRenderer.cs @@ -0,0 +1,152 @@ +using System; +using System.ComponentModel; +using System.Linq; +using Android.Content; +using Android.Graphics; +using Android.Views; +using Android.Widget; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android +{ + public sealed class NavigationMenuRenderer : ViewRenderer + { + public NavigationMenuRenderer() + { + AutoPackage = false; + } + + GridView GridView + { + get { return Control as GridView; } + } + + NavigationMenu NavigationMenu + { + get { return Element as NavigationMenu; } + } + + protected override void OnElementChanged(ElementChangedEventArgs<View> e) + { + base.OnElementChanged(e); + + if (e.OldElement == null) + { + var grid = new GridView(Context); + grid.SetVerticalSpacing(20); + + SetNativeControl(grid); + } + + UpdateTargets(); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + switch (e.PropertyName) + { + case "Targets": + UpdateTargets(); + break; + } + } + + protected override void OnSizeChanged(int w, int h, int oldw, int oldh) + { + GridView.NumColumns = w > h ? 3 : 2; + base.OnSizeChanged(w, h, oldw, oldh); + } + + void UpdateTargets() + { + GridView.Adapter = new MenuAdapter(NavigationMenu); + } + + class MenuElementView : LinearLayout + { + readonly ImageButton _image; + readonly TextView _label; + string _icon; + + public MenuElementView(Context context) : base(context) + { + Orientation = Orientation.Vertical; + _image = new ImageButton(context); + _image.SetScaleType(ImageView.ScaleType.FitCenter); + _image.Click += (object sender, EventArgs e) => + { + if (Selected != null) + Selected(); + }; + AddView(_image, new LayoutParams(ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.WrapContent) { Gravity = GravityFlags.Center }); + + _label = new TextView(context) { TextAlignment = global::Android.Views.TextAlignment.Center, Gravity = GravityFlags.Center }; + AddView(_label, new LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.WrapContent)); + } + + public string Icon + { + get { return _icon; } + set + { + _icon = value; + Bitmap bitmap = Context.Resources.GetBitmap(_icon); + _image.SetImageBitmap(bitmap); + } + } + + public string Name + { + get { return _label.Text; } + set { _label.Text = value; } + } + + public Action Selected { get; set; } + } + + class MenuAdapter : BaseAdapter<Page> + { + readonly NavigationMenu _menu; + + public MenuAdapter(NavigationMenu menu) + { + _menu = menu; + } + + #region implemented abstract members of BaseAdapter + + public override Page this[int index] + { + get { return _menu.Targets.ElementAtOrDefault(index); } + } + + #endregion + + public override AView GetView(int position, AView convertView, ViewGroup parent) + { + MenuElementView menuItem = convertView as MenuElementView ?? new MenuElementView(parent.Context); + Page item = this[position]; + menuItem.Icon = item.Icon; + menuItem.Name = item.Title; + menuItem.Selected = () => _menu.SendTargetSelected(item); + return menuItem; + } + + #region implemented abstract members of BaseAdapter + + public override long GetItemId(int position) + { + return 0; + } + + public override int Count + { + get { return _menu.Targets.Count(); } + } + + #endregion + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/NavigationRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/NavigationRenderer.cs new file mode 100644 index 00000000..f0e6dc73 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/NavigationRenderer.cs @@ -0,0 +1,302 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Android.Views; +using AButton = Android.Widget.Button; +using AView = Android.Views.View; +using AndroidAnimation = Android.Animation; + +namespace Xamarin.Forms.Platform.Android +{ + public class NavigationRenderer : VisualElementRenderer<NavigationPage> + { + static ViewPropertyAnimator s_currentAnimation; + + Page _current; + Page _exitingPage; + + public NavigationRenderer() + { + AutoPackage = false; + } + + public Task<bool> PopToRootAsync(Page page, bool animated = true) + { + return OnPopToRootAsync(page, animated); + } + + public Task<bool> PopViewAsync(Page page, bool animated = true) + { + return OnPopViewAsync(page, animated); + } + + public Task<bool> PushViewAsync(Page page, bool animated = true) + { + return OnPushAsync(page, animated); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + foreach (VisualElement child in Element.InternalChildren) + { + IVisualElementRenderer renderer = Platform.GetRenderer(child); + if (renderer != null) + renderer.Dispose(); + } + + if (Element != null) + { + Element.PushRequested -= OnPushed; + Element.PopRequested -= OnPopped; + Element.PopToRootRequested -= OnPoppedToRoot; + Element.InsertPageBeforeRequested -= OnInsertPageBeforeRequested; + Element.RemovePageRequested -= OnRemovePageRequested; + } + } + + base.Dispose(disposing); + } + + protected override void OnAttachedToWindow() + { + base.OnAttachedToWindow(); + Element.SendAppearing(); + } + + protected override void OnDetachedFromWindow() + { + base.OnDetachedFromWindow(); + Element.SendDisappearing(); + } + + protected override void OnElementChanged(ElementChangedEventArgs<NavigationPage> e) + { + base.OnElementChanged(e); + + if (e.OldElement != null) + { + NavigationPage oldNav = e.OldElement; + oldNav.PushRequested -= OnPushed; + oldNav.PopRequested -= OnPopped; + oldNav.PopToRootRequested -= OnPoppedToRoot; + oldNav.InsertPageBeforeRequested -= OnInsertPageBeforeRequested; + oldNav.RemovePageRequested -= OnRemovePageRequested; + + RemoveAllViews(); + } + + NavigationPage nav = e.NewElement; + nav.PushRequested += OnPushed; + nav.PopRequested += OnPopped; + nav.PopToRootRequested += OnPoppedToRoot; + nav.InsertPageBeforeRequested += OnInsertPageBeforeRequested; + nav.RemovePageRequested += OnRemovePageRequested; + + // If there is already stuff on the stack we need to push it + nav.StackCopy.Reverse().ForEach(p => PushViewAsync(p, false)); + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + base.OnLayout(changed, l, t, r, b); + + for (var i = 0; i < ChildCount; i++) + GetChildAt(i).Layout(0, 0, r - l, b - t); + } + + protected virtual Task<bool> OnPopToRootAsync(Page page, bool animated) + { + return SwitchContentAsync(page, animated, true); + } + + protected virtual Task<bool> OnPopViewAsync(Page page, bool animated) + { + Page pageToShow = Element.StackCopy.Skip(1).FirstOrDefault(); + if (pageToShow == null) + return Task.FromResult(false); + + return SwitchContentAsync(pageToShow, animated, true); + } + + protected virtual Task<bool> OnPushAsync(Page view, bool animated) + { + return SwitchContentAsync(view, animated); + } + + void InsertPageBefore(Page page, Page before) + { + } + + void OnInsertPageBeforeRequested(object sender, NavigationRequestedEventArgs e) + { + InsertPageBefore(e.Page, e.BeforePage); + } + + void OnPopped(object sender, NavigationRequestedEventArgs e) + { + e.Task = PopViewAsync(e.Page, e.Animated); + } + + void OnPoppedToRoot(object sender, NavigationRequestedEventArgs e) + { + e.Task = PopToRootAsync(e.Page, e.Animated); + } + + void OnPushed(object sender, NavigationRequestedEventArgs e) + { + e.Task = PushViewAsync(e.Page, e.Animated); + } + + void OnRemovePageRequested(object sender, NavigationRequestedEventArgs e) + { + RemovePage(e.Page); + } + + void RemovePage(Page page) + { + IVisualElementRenderer rendererToRemove = Platform.GetRenderer(page); + PageContainer containerToRemove = rendererToRemove == null ? null : (PageContainer)rendererToRemove.ViewGroup.Parent; + + containerToRemove.RemoveFromParent(); + + if (rendererToRemove != null) + { + rendererToRemove.ViewGroup.RemoveFromParent(); + rendererToRemove.Dispose(); + } + + containerToRemove?.Dispose(); + + Device.StartTimer(TimeSpan.FromMilliseconds(0), () => + { + ((Platform)Element.Platform).UpdateNavigationTitleBar(); + return false; + }); + } + + Task<bool> SwitchContentAsync(Page view, bool animated, bool removed = false) + { + Context.HideKeyboard(this); + + IVisualElementRenderer rendererToAdd = Platform.GetRenderer(view); + bool existing = rendererToAdd != null; + if (!existing) + Platform.SetRenderer(view, rendererToAdd = Platform.CreateRenderer(view)); + + Page pageToRemove = _current; + IVisualElementRenderer rendererToRemove = pageToRemove == null ? null : Platform.GetRenderer(pageToRemove); + PageContainer containerToRemove = rendererToRemove == null ? null : (PageContainer)rendererToRemove.ViewGroup.Parent; + PageContainer containerToAdd = (PageContainer)rendererToAdd.ViewGroup.Parent ?? new PageContainer(Context, rendererToAdd); + + containerToAdd.SetWindowBackground(); + + _current = view; + + ((Platform)Element.Platform).NavAnimationInProgress = true; + + var tcs = new TaskCompletionSource<bool>(); + + if (animated) + { + if (s_currentAnimation != null) + s_currentAnimation.Cancel(); + + if (removed) + { + // animate out + if (containerToAdd.Parent != this) + AddView(containerToAdd, Element.LogicalChildren.IndexOf(rendererToAdd.Element)); + else + ((Page)rendererToAdd.Element).SendAppearing(); + containerToAdd.Visibility = ViewStates.Visible; + + if (containerToRemove != null) + { + Action<AndroidAnimation.Animator> done = a => + { + containerToRemove.Visibility = ViewStates.Gone; + containerToRemove.Alpha = 1; + containerToRemove.ScaleX = 1; + containerToRemove.ScaleY = 1; + RemoveView(containerToRemove); + + tcs.TrySetResult(true); + ((Platform)Element.Platform).NavAnimationInProgress = false; + + VisualElement removedElement = rendererToRemove.Element; + rendererToRemove.Dispose(); + if (removedElement != null) + Platform.SetRenderer(removedElement, null); + }; + + // should always happen + s_currentAnimation = containerToRemove.Animate().Alpha(0).ScaleX(0.8f).ScaleY(0.8f).SetDuration(250).SetListener(new GenericAnimatorListener { OnEnd = a => + { + s_currentAnimation = null; + done(a); + }, + OnCancel = done }); + } + } + else + { + bool containerAlreadyAdded = containerToAdd.Parent == this; + // animate in + if (!containerAlreadyAdded) + AddView(containerToAdd); + else + ((Page)rendererToAdd.Element).SendAppearing(); + + if (existing) + Element.ForceLayout(); + + containerToAdd.Alpha = 0; + containerToAdd.ScaleX = containerToAdd.ScaleY = 0.8f; + containerToAdd.Visibility = ViewStates.Visible; + s_currentAnimation = containerToAdd.Animate().Alpha(1).ScaleX(1).ScaleY(1).SetDuration(250).SetListener(new GenericAnimatorListener { OnEnd = a => + { + if (containerToRemove != null && containerToRemove.Handle != IntPtr.Zero) + { + containerToRemove.Visibility = ViewStates.Gone; + if (pageToRemove != null) + pageToRemove.SendDisappearing(); + } + s_currentAnimation = null; + tcs.TrySetResult(true); + ((Platform)Element.Platform).NavAnimationInProgress = false; + } }); + } + } + else + { + // just do it fast + if (containerToRemove != null) + { + if (removed) + RemoveView(containerToRemove); + else + containerToRemove.Visibility = ViewStates.Gone; + } + + if (containerToAdd.Parent != this) + AddView(containerToAdd); + else + ((Page)rendererToAdd.Element).SendAppearing(); + + if (containerToRemove != null && !removed) + pageToRemove.SendDisappearing(); + + if (existing) + Element.ForceLayout(); + + containerToAdd.Visibility = ViewStates.Visible; + tcs.SetResult(true); + ((Platform)Element.Platform).NavAnimationInProgress = false; + } + + return tcs.Task; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ObjectJavaBox.cs b/Xamarin.Forms.Platform.Android/Renderers/ObjectJavaBox.cs new file mode 100644 index 00000000..6addf745 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/ObjectJavaBox.cs @@ -0,0 +1,14 @@ +using Java.Lang; + +namespace Xamarin.Forms.Platform.Android +{ + internal sealed class ObjectJavaBox<T> : Object + { + public ObjectJavaBox(T instance) + { + Instance = instance; + } + + public T Instance { get; set; } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/OpenGLViewRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/OpenGLViewRenderer.cs new file mode 100644 index 00000000..5c787b76 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/OpenGLViewRenderer.cs @@ -0,0 +1,102 @@ +using System; +using System.ComponentModel; +using Android.Opengl; +using Javax.Microedition.Khronos.Opengles; +using EGLConfig = Javax.Microedition.Khronos.Egl.EGLConfig; +using Object = Java.Lang.Object; + +namespace Xamarin.Forms.Platform.Android +{ + internal class OpenGLViewRenderer : ViewRenderer<OpenGLView, GLSurfaceView> + { + bool _disposed; + + public OpenGLViewRenderer() + { + AutoPackage = false; + } + + protected override void Dispose(bool disposing) + { + if (!_disposed && disposing) + { + _disposed = true; + + if (Element != null) + ((IOpenGlViewController)Element).DisplayRequested -= Display; + } + base.Dispose(disposing); + } + + protected override void OnElementChanged(ElementChangedEventArgs<OpenGLView> e) + { + base.OnElementChanged(e); + + if (e.OldElement != null) + ((IOpenGlViewController)Element).DisplayRequested -= Display; + + if (e.NewElement != null) + { + GLSurfaceView surfaceView = Control; + if (surfaceView == null) + { + surfaceView = new GLSurfaceView(Context); + surfaceView.SetEGLContextClientVersion(2); + SetNativeControl(surfaceView); + } + + ((IOpenGlViewController)Element).DisplayRequested += Display; + surfaceView.SetRenderer(new Renderer(Element)); + SetRenderMode(); + } + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == OpenGLView.HasRenderLoopProperty.PropertyName) + SetRenderMode(); + } + + void Display(object sender, EventArgs eventArgs) + { + if (Element.HasRenderLoop) + return; + Control.RequestRender(); + } + + void SetRenderMode() + { + Control.RenderMode = Element.HasRenderLoop ? Rendermode.Continuously : Rendermode.WhenDirty; + } + + class Renderer : Object, GLSurfaceView.IRenderer + { + readonly OpenGLView _model; + Rectangle _rect; + + public Renderer(OpenGLView model) + { + _model = model; + } + + public void OnDrawFrame(IGL10 gl) + { + Action<Rectangle> onDisplay = _model.OnDisplay; + if (onDisplay == null) + return; + onDisplay(_rect); + } + + public void OnSurfaceChanged(IGL10 gl, int width, int height) + { + _rect = new Rectangle(0.0, 0.0, width, height); + } + + public void OnSurfaceCreated(IGL10 gl, EGLConfig config) + { + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/PageContainer.cs b/Xamarin.Forms.Platform.Android/Renderers/PageContainer.cs new file mode 100644 index 00000000..06e33e18 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/PageContainer.cs @@ -0,0 +1,30 @@ +using Android.Content; +using Android.Views; + +namespace Xamarin.Forms.Platform.Android +{ + internal class PageContainer : ViewGroup + { + public PageContainer(Context context, IVisualElementRenderer child, bool inFragment = false) : base(context) + { + AddView(child.ViewGroup); + Child = child; + IsInFragment = inFragment; + } + + public IVisualElementRenderer Child { get; set; } + + public bool IsInFragment { get; set; } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + Child.UpdateLayout(); + } + + protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + Child.ViewGroup.Measure(widthMeasureSpec, heightMeasureSpec); + SetMeasuredDimension(Child.ViewGroup.MeasuredWidth, Child.ViewGroup.MeasuredHeight); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/PageRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/PageRenderer.cs new file mode 100644 index 00000000..24d85aa8 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/PageRenderer.cs @@ -0,0 +1,71 @@ +using System.ComponentModel; +using Android.Views; + +namespace Xamarin.Forms.Platform.Android +{ + public class PageRenderer : VisualElementRenderer<Page> + { + public override bool OnTouchEvent(MotionEvent e) + { + base.OnTouchEvent(e); + + return true; + } + + protected override void Dispose(bool disposing) + { + Element?.SendDisappearing(); + base.Dispose(disposing); + } + + protected override void OnAttachedToWindow() + { + base.OnAttachedToWindow(); + var pageContainer = Parent as PageContainer; + if (pageContainer != null && pageContainer.IsInFragment) + return; + Element.SendAppearing(); + } + + protected override void OnDetachedFromWindow() + { + base.OnDetachedFromWindow(); + var pageContainer = Parent as PageContainer; + if (pageContainer != null && pageContainer.IsInFragment) + return; + Element.SendDisappearing(); + } + + protected override void OnElementChanged(ElementChangedEventArgs<Page> e) + { + Page view = e.NewElement; + base.OnElementChanged(e); + + UpdateBackgroundColor(view); + UpdateBackgroundImage(view); + + Clickable = true; + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + if (e.PropertyName == Page.BackgroundImageProperty.PropertyName) + UpdateBackgroundImage(Element); + if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName) + UpdateBackgroundColor(Element); + } + + void UpdateBackgroundColor(Page view) + { + if (view.BackgroundColor != Color.Default) + SetBackgroundColor(view.BackgroundColor.ToAndroid()); + } + + void UpdateBackgroundImage(Page view) + { + if (!string.IsNullOrEmpty(view.BackgroundImage)) + this.SetBackground(Context.Resources.GetDrawable(view.BackgroundImage)); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/PhysicalLayoutManager.cs b/Xamarin.Forms.Platform.Android/Renderers/PhysicalLayoutManager.cs new file mode 100644 index 00000000..64e859d7 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/PhysicalLayoutManager.cs @@ -0,0 +1,531 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Android.Content; +using Android.Graphics; +using Android.Support.V7.Widget; +using Android.Views; + +namespace Xamarin.Forms.Platform.Android +{ + internal class PhysicalLayoutManager : RecyclerView.LayoutManager + { + // ObservableCollection is our public entryway to this method and it only supports single item removal + internal const int MaxItemsRemoved = 1; + + static readonly int s_samplesCount = 5; + static Func<int, int> s_fixPosition = o => o; + + readonly Context _context; + readonly Queue<Action<RecyclerView.Recycler, RecyclerView.State>> _deferredLayout; + readonly List<IntVector> _samples; + readonly SeekAndSnapScroller _scroller; + readonly Dictionary<int, global::Android.Views.View> _viewByAdaptorPosition; + readonly VirtualLayoutManager _virtualLayout; + readonly HashSet<int> _visibleAdapterPosition; + AdapterChangeType _adapterChangeType; + IntVector _locationOffset; // upper left corner of screen is positionOrigin + locationOffset + int _positionOrigin; // coordinates are relative to the upper left corner of this element + + public PhysicalLayoutManager(Context context, VirtualLayoutManager virtualLayout, int positionOrigin) + { + _positionOrigin = positionOrigin; + _context = context; + _virtualLayout = virtualLayout; + _viewByAdaptorPosition = new Dictionary<int, global::Android.Views.View>(); + _visibleAdapterPosition = new HashSet<int>(); + _samples = Enumerable.Repeat(IntVector.Origin, s_samplesCount).ToList(); + _deferredLayout = new Queue<Action<RecyclerView.Recycler, RecyclerView.State>>(); + _scroller = new SeekAndSnapScroller(context, adapterPosition => + { + IntVector end = virtualLayout.LayoutItem(positionOrigin, adapterPosition).Center(); + IntVector begin = Viewport.Center(); + return end - begin; + }); + + _scroller.OnBeginScroll += adapterPosition => OnBeginScroll?.Invoke(adapterPosition); + _scroller.OnEndScroll += adapterPosition => OnEndScroll?.Invoke(adapterPosition); + } + + public IntVector Velocity => _samples.Aggregate((o, a) => o + a) / _samples.Count; + + public System.Drawing.Rectangle Viewport => Rectangle + _locationOffset; + + // helpers to deal with locations as IntRectangles and IntVectors + System.Drawing.Rectangle Rectangle => new System.Drawing.Rectangle(0, 0, Width, Height); + + public override bool CanScrollHorizontally() => _virtualLayout.CanScrollHorizontally; + + public override bool CanScrollVertically() => _virtualLayout.CanScrollVertically; + + public override global::Android.Views.View FindViewByPosition(int adapterPosition) + { + // Used by SmoothScrollToPosition to know when the view + // for the targeted adapterPosition has been attached. + + global::Android.Views.View view; + if (!_viewByAdaptorPosition.TryGetValue(adapterPosition, out view)) + return null; + return view; + } + + public override RecyclerView.LayoutParams GenerateDefaultLayoutParams() + { + return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.WrapContent); + } + + public void Layout(int width, int height) + { + // e.g. when rotated the width and height are updated the virtual layout will + // need to resize and provide a new viewport offset given the current one. + _virtualLayout.Layout(_positionOrigin, new System.Drawing.Size(width, height), ref _locationOffset); + } + + public override void OnAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) + { + RemoveAllViews(); + } + + public event Action<int> OnAppearing; + + public event Action<int> OnBeginScroll; + + public event Action<int> OnDisappearing; + + public event Action<int> OnEndScroll; + + public override void OnItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) + { + _adapterChangeType = AdapterChangeType.Added; + + _deferredLayout.Enqueue((recycler, state) => + { + KeyValuePair<int, global::Android.Views.View>[] viewByAdaptorPositionCopy = _viewByAdaptorPosition.ToArray(); + _viewByAdaptorPosition.Clear(); + foreach (KeyValuePair<int, global::Android.Views.View> pair in viewByAdaptorPositionCopy) + { + global::Android.Views.View view = pair.Value; + int position = pair.Key; + + // position unchanged + if (position < positionStart) + _viewByAdaptorPosition[position] = view; + + // position changed + else + _viewByAdaptorPosition[position + itemCount] = view; + } + + if (_positionOrigin >= positionStart) + _positionOrigin += itemCount; + }); + base.OnItemsAdded(recyclerView, positionStart, itemCount); + } + + public override void OnItemsChanged(RecyclerView recyclerView) + { + _adapterChangeType = AdapterChangeType.Changed; + + // low-fidelity change event; assume everything has changed. If adapter reports it has "stable IDs" then + // RecyclerView will attempt to synthesize high-fidelity change events: added, removed, moved, updated. + base.OnItemsChanged(recyclerView); + } + + public override void OnItemsMoved(RecyclerView recyclerView, int from, int toValue, int itemCount) + { + _adapterChangeType = AdapterChangeType.Moved; + base.OnItemsMoved(recyclerView, from, toValue, itemCount); + } + + public override void OnItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) + { + Debug.Assert(itemCount == MaxItemsRemoved); + _adapterChangeType = AdapterChangeType.Removed; + + int positionEnd = positionStart + itemCount; + + _deferredLayout.Enqueue((recycler, state) => + { + if (state.ItemCount == 0) + throw new InvalidOperationException("Cannot delete all items."); + + // re-map views to their new positions + KeyValuePair<int, global::Android.Views.View>[] viewByAdaptorPositionCopy = _viewByAdaptorPosition.ToArray(); + _viewByAdaptorPosition.Clear(); + foreach (KeyValuePair<int, global::Android.Views.View> pair in viewByAdaptorPositionCopy) + { + global::Android.Views.View view = pair.Value; + int position = pair.Key; + + // position unchanged + if (position < positionStart) + _viewByAdaptorPosition[position] = view; + + // position changed + else if (position >= positionEnd) + _viewByAdaptorPosition[position - itemCount] = view; + + // removed + else + { + _viewByAdaptorPosition[-1] = view; + if (_visibleAdapterPosition.Contains(position)) + _visibleAdapterPosition.Remove(position); + } + } + + // if removed origin then shift origin to first removed position + if (_positionOrigin >= positionStart && _positionOrigin < positionEnd) + { + _positionOrigin = positionStart; + + // if no items to right of removed origin then set origin to item prior to removed set + if (_positionOrigin >= state.ItemCount) + { + _positionOrigin = state.ItemCount - 1; + + if (!_viewByAdaptorPosition.ContainsKey(_positionOrigin)) + throw new InvalidOperationException("VirtualLayoutManager must add items to the left and right of the origin"); + } + } + + // if removed before origin then shift origin left + else if (_positionOrigin >= positionEnd) + _positionOrigin -= itemCount; + }); + + base.OnItemsRemoved(recyclerView, positionStart, itemCount); + } + + public override void OnItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) + { + _adapterChangeType = AdapterChangeType.Updated; + + // rebind rendered updated elements + _deferredLayout.Enqueue((recycler, state) => + { + for (var i = 0; i < itemCount; i++) + { + int position = positionStart + i; + + global::Android.Views.View view; + if (!_viewByAdaptorPosition.TryGetValue(position, out view)) + continue; + + recycler.BindViewToPosition(view, position); + } + }); + + base.OnItemsUpdated(recyclerView, positionStart, itemCount); + } + + public override void OnLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) + { + AdapterChangeType adapterChangeType = _adapterChangeType; + if (state.IsPreLayout) + adapterChangeType = default(AdapterChangeType); + + // adapter updates + if (!state.IsPreLayout) + { + while (_deferredLayout.Count > 0) + _deferredLayout.Dequeue()(recycler, state); + } + + // get visible items + int[] positions = _virtualLayout.GetPositions(_positionOrigin, state.ItemCount, Viewport, + // IsPreLayout => some type of data update of yet unknown type. Must assume update + // could be remove so virtualLayout must +1 off-screen left in case origin is + // removed and +n off-screen right to slide onscreen if a big item is removed + state.IsPreLayout || adapterChangeType == AdapterChangeType.Removed).ToRange(); + + // disappearing + List<int> disappearing = _viewByAdaptorPosition.Keys.Except(positions).ToList(); + + // defer cleanup of displaced items and lay them out off-screen so they animate off-screen + if (adapterChangeType == AdapterChangeType.Added) + { + positions = positions.Concat(disappearing).OrderBy(o => o).ToArray(); + disappearing.Clear(); + } + + // recycle + foreach (int position in disappearing) + { + global::Android.Views.View view = _viewByAdaptorPosition[position]; + + // remove + _viewByAdaptorPosition.Remove(position); + OnAppearingOrDisappearing(position, false); + + // scrap + new DecoratedView(this, view).DetachAndScrap(recycler); + } + + // TODO: Generalize + if (adapterChangeType == AdapterChangeType.Removed && _positionOrigin == state.ItemCount - 1) + { + System.Drawing.Rectangle vlayout = _virtualLayout.LayoutItem(_positionOrigin, _positionOrigin); + _locationOffset = new IntVector(vlayout.Width - Width, _locationOffset.Y); + } + + var nextLocationOffset = new System.Drawing.Point(int.MaxValue, int.MaxValue); + int nextPositionOrigin = int.MaxValue; + foreach (int position in positions) + { + // attach + global::Android.Views.View view; + if (!_viewByAdaptorPosition.TryGetValue(position, out view)) + AddView(_viewByAdaptorPosition[position] = view = recycler.GetViewForPosition(position)); + + // layout + var decoratedView = new DecoratedView(this, view); + System.Drawing.Rectangle layout = _virtualLayout.LayoutItem(_positionOrigin, position); + System.Drawing.Rectangle physicalLayout = layout - _locationOffset; + decoratedView.Layout(physicalLayout); + + bool isVisible = Viewport.IntersectsWith(layout); + if (isVisible) + OnAppearingOrDisappearing(position, true); + + // update offsets + if (isVisible && position < nextPositionOrigin) + { + nextLocationOffset = layout.Location; + nextPositionOrigin = position; + } + } + + // update origin + if (nextPositionOrigin != int.MaxValue) + { + _positionOrigin = nextPositionOrigin; + _locationOffset -= (IntVector)nextLocationOffset; + } + + // scrapped views not re-attached must be recycled (why isn't this done by Android, I dunno) + foreach (RecyclerView.ViewHolder viewHolder in recycler.ScrapList.ToArray()) + recycler.RecycleView(viewHolder.ItemView); + } + + public override int ScrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) + { + var delta = new IntVector(dx, 0); + ScrollBy(ref delta, recycler, state); + return delta.X; + } + + public override void ScrollToPosition(int adapterPosition) + { + if (adapterPosition < 0 || adapterPosition >= ItemCount) + throw new ArgumentException(nameof(adapterPosition)); + + _scroller.TargetPosition = adapterPosition; + StartSmoothScroll(_scroller); + } + + public override int ScrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) + { + var delta = new IntVector(0, dy); + ScrollBy(ref delta, recycler, state); + return delta.Y; + } + + public override void SmoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int adapterPosition) + { + ScrollToPosition(adapterPosition); + } + + // entry points + public override bool SupportsPredictiveItemAnimations() => true; + + public override string ToString() + { + return $"offset={_locationOffset}"; + } + + public IEnumerable<global::Android.Views.View> Views() + { + return _viewByAdaptorPosition.Values; + } + + public IEnumerable<int> VisiblePositions() + { + return _visibleAdapterPosition; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + } + + void OffsetChildren(IntVector delta) + { + OffsetChildrenHorizontal(-delta.X); + OffsetChildrenVertical(-delta.Y); + } + + void OnAppearingOrDisappearing(int position, bool isAppearing) + { + if (isAppearing) + { + if (!_visibleAdapterPosition.Contains(position)) + { + _visibleAdapterPosition.Add(position); + OnAppearing?.Invoke(position); + } + } + else + { + if (_visibleAdapterPosition.Contains(position)) + { + _visibleAdapterPosition.Remove(position); + OnDisappearing?.Invoke(position); + } + } + } + + void ScrollBy(ref IntVector delta, RecyclerView.Recycler recycler, RecyclerView.State state) + { + _adapterChangeType = default(AdapterChangeType); + + delta = Viewport.BoundTranslation(delta, _virtualLayout.GetBounds(_positionOrigin, state)); + + _locationOffset += delta; + _samples.Insert(0, delta); + _samples.RemoveAt(_samples.Count - 1); + + OffsetChildren(delta); + OnLayoutChildren(recycler, state); + } + + enum AdapterChangeType + { + Removed = 1, + Added, + Moved, + Updated, + Changed + } + + internal struct DecoratedView + { + public static implicit operator global::Android.Views.View(DecoratedView view) + { + return view._view; + } + + readonly PhysicalLayoutManager _layout; + readonly global::Android.Views.View _view; + + internal DecoratedView(PhysicalLayoutManager layout, global::Android.Views.View view) + { + _layout = layout; + _view = view; + } + + internal int Left => _layout.GetDecoratedLeft(_view); + + internal int Top => _layout.GetDecoratedTop(_view); + + internal int Bottom => _layout.GetDecoratedBottom(_view); + + internal int Right => _layout.GetDecoratedRight(_view); + + internal int Width => Right - Left; + + internal int Height => Bottom - Top; + + internal System.Drawing.Rectangle Rectangle => new System.Drawing.Rectangle(Left, Top, Width, Height); + + internal void Measure(int widthUsed, int heightUsed) + { + _layout.MeasureChild(_view, widthUsed, heightUsed); + } + + internal void MeasureWithMargins(int widthUsed, int heightUsed) + { + _layout.MeasureChildWithMargins(_view, widthUsed, heightUsed); + } + + internal void Layout(System.Drawing.Rectangle position) + { + var renderer = _view as IVisualElementRenderer; + renderer.Element.Layout(position.ToFormsRectangle(_layout._context)); + + _layout.LayoutDecorated(_view, position.Left, position.Top, position.Right, position.Bottom); + } + + internal void Add() + { + _layout.AddView(_view); + } + + internal void DetachAndScrap(RecyclerView.Recycler recycler) + { + _layout.DetachAndScrapView(_view, recycler); + } + } + + internal abstract class VirtualLayoutManager + { + internal abstract bool CanScrollHorizontally { get; } + + internal abstract bool CanScrollVertically { get; } + + internal abstract System.Drawing.Rectangle GetBounds(int positionOrigin, RecyclerView.State state); + + internal abstract Tuple<int, int> GetPositions(int positionOrigin, int itemCount, System.Drawing.Rectangle viewport, bool isPreLayout); + + internal abstract void Layout(int positionOrigin, System.Drawing.Size viewportSize, ref IntVector offset); + + internal abstract System.Drawing.Rectangle LayoutItem(int positionOrigin, int position); + } + + enum SnapPreference + { + None = 0, + Begin = 1, + End = -1 + } + + sealed class SeekAndSnapScroller : LinearSmoothScroller + { + readonly SnapPreference _snapPreference; + readonly Func<int, IntVector> _vectorToPosition; + + internal SeekAndSnapScroller(Context context, Func<int, IntVector> vectorToPosition, SnapPreference snapPreference = SnapPreference.None) : base(context) + { + _vectorToPosition = vectorToPosition; + _snapPreference = snapPreference; + } + + protected override int HorizontalSnapPreference => (int)_snapPreference; + + public override PointF ComputeScrollVectorForPosition(int targetPosition) + { + IntVector vector = _vectorToPosition(targetPosition); + return new PointF(vector.X, vector.Y); + } + + public event Action<int> OnBeginScroll; + + public event Action<int> OnEndScroll; + + protected override void OnStart() + { + OnBeginScroll?.Invoke(TargetPosition); + base.OnStart(); + } + + protected override void OnStop() + { + // expected this to be triggered with the animation stops but it + // actually seems to be triggered when the target is found + OnEndScroll?.Invoke(TargetPosition); + base.OnStop(); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/PickerRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/PickerRenderer.cs new file mode 100644 index 00000000..b9114d22 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/PickerRenderer.cs @@ -0,0 +1,165 @@ +using System; +using System.ComponentModel; +using System.Linq; +using Android.App; +using Android.Views; +using Android.Widget; +using ADatePicker = Android.Widget.DatePicker; +using ATimePicker = Android.Widget.TimePicker; +using Object = Java.Lang.Object; + +namespace Xamarin.Forms.Platform.Android +{ + public class PickerRenderer : ViewRenderer<Picker, EditText> + { + AlertDialog _dialog; + + bool _isDisposed; + + public PickerRenderer() + { + AutoPackage = false; + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_isDisposed) + { + _isDisposed = true; + ((ObservableList<string>)Element.Items).CollectionChanged -= RowsCollectionChanged; + } + + base.Dispose(disposing); + } + + protected override void OnElementChanged(ElementChangedEventArgs<Picker> e) + { + if (e.OldElement != null) + ((ObservableList<string>)e.OldElement.Items).CollectionChanged -= RowsCollectionChanged; + + if (e.NewElement != null) + { + ((ObservableList<string>)e.NewElement.Items).CollectionChanged += RowsCollectionChanged; + if (Control == null) + { + var textField = new EditText(Context) { Focusable = false, Clickable = true, Tag = this }; + textField.SetOnClickListener(PickerListener.Instance); + SetNativeControl(textField); + } + UpdatePicker(); + } + + base.OnElementChanged(e); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == Picker.TitleProperty.PropertyName) + UpdatePicker(); + if (e.PropertyName == Picker.SelectedIndexProperty.PropertyName) + UpdatePicker(); + } + + internal override void OnFocusChangeRequested(object sender, VisualElement.FocusRequestArgs e) + { + base.OnFocusChangeRequested(sender, e); + + if (e.Focus) + OnClick(); + else if (_dialog != null) + { + _dialog.Hide(); + ((IElementController)Element).SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, false); + Control.ClearFocus(); + _dialog = null; + } + } + + void OnClick() + { + Picker model = Element; + + var picker = new NumberPicker(Context); + if (model.Items != null && model.Items.Any()) + { + picker.MaxValue = model.Items.Count - 1; + picker.MinValue = 0; + picker.SetDisplayedValues(model.Items.ToArray()); + picker.WrapSelectorWheel = false; + picker.DescendantFocusability = DescendantFocusability.BlockDescendants; + picker.Value = model.SelectedIndex; + } + + var layout = new LinearLayout(Context) { Orientation = Orientation.Vertical }; + layout.AddView(picker); + + ((IElementController)Element).SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, true); + + var builder = new AlertDialog.Builder(Context); + builder.SetView(layout); + builder.SetTitle(model.Title ?? ""); + builder.SetNegativeButton(global::Android.Resource.String.Cancel, (s, a) => + { + ((IElementController)Element).SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, false); + // It is possible for the Content of the Page to be changed when Focus is changed. + // In this case, we'll lose our Control. + Control?.ClearFocus(); + _dialog = null; + }); + builder.SetPositiveButton(global::Android.Resource.String.Ok, (s, a) => + { + ((IElementController)Element).SetValueFromRenderer(Picker.SelectedIndexProperty, picker.Value); + // It is possible for the Content of the Page to be changed on SelectedIndexChanged. + // In this case, the Element & Control will no longer exist. + if (Element != null) + { + if (model.Items.Count > 0 && Element.SelectedIndex >= 0) + Control.Text = model.Items[Element.SelectedIndex]; + ((IElementController)Element).SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, false); + // It is also possible for the Content of the Page to be changed when Focus is changed. + // In this case, we'll lose our Control. + Control?.ClearFocus(); + } + _dialog = null; + }); + + (_dialog = builder.Create()).Show(); + } + + void RowsCollectionChanged(object sender, EventArgs e) + { + UpdatePicker(); + } + + void UpdatePicker() + { + Control.Hint = Element.Title; + + string oldText = Control.Text; + + if (Element.SelectedIndex == -1 || Element.Items == null) + Control.Text = null; + else + Control.Text = Element.Items[Element.SelectedIndex]; + + if (oldText != Control.Text) + ((IVisualElementController)Element).NativeSizeChanged(); + } + + class PickerListener : Object, IOnClickListener + { + public static readonly PickerListener Instance = new PickerListener(); + + public void OnClick(global::Android.Views.View v) + { + var renderer = v.Tag as PickerRenderer; + if (renderer == null) + return; + + renderer.OnClick(); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ProgressBarRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/ProgressBarRenderer.cs new file mode 100644 index 00000000..865732f7 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/ProgressBarRenderer.cs @@ -0,0 +1,40 @@ +using System.ComponentModel; +using AProgressBar = Android.Widget.ProgressBar; + +namespace Xamarin.Forms.Platform.Android +{ + public class ProgressBarRenderer : ViewRenderer<ProgressBar, AProgressBar> + { + public ProgressBarRenderer() + { + AutoPackage = false; + } + + protected override void OnElementChanged(ElementChangedEventArgs<ProgressBar> e) + { + base.OnElementChanged(e); + + if (e.OldElement == null) + { + var progressBar = new AProgressBar(Context, null, global::Android.Resource.Attribute.ProgressBarStyleHorizontal) { Indeterminate = false, Max = 10000 }; + + SetNativeControl(progressBar); + } + + UpdateProgress(); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == ProgressBar.ProgressProperty.PropertyName) + UpdateProgress(); + } + + void UpdateProgress() + { + Control.Progress = (int)(Element.Progress * 10000); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ScrollViewContainer.cs b/Xamarin.Forms.Platform.Android/Renderers/ScrollViewContainer.cs new file mode 100644 index 00000000..c79e6137 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/ScrollViewContainer.cs @@ -0,0 +1,75 @@ +using Android.Content; +using Android.Views; + +namespace Xamarin.Forms.Platform.Android +{ + internal class ScrollViewContainer : ViewGroup + { + readonly ScrollView _parent; + View _childView; + + public ScrollViewContainer(ScrollView parent, Context context) : base(context) + { + _parent = parent; + } + + public View ChildView + { + get { return _childView; } + set + { + if (_childView == value) + return; + + RemoveAllViews(); + + _childView = value; + + if (_childView == null) + return; + + IVisualElementRenderer renderer; + if ((renderer = Platform.GetRenderer(_childView)) == null) + Platform.SetRenderer(_childView, renderer = Platform.CreateRenderer(_childView)); + + if (renderer.ViewGroup.Parent != null) + renderer.ViewGroup.RemoveFromParent(); + + AddView(renderer.ViewGroup); + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + if (ChildCount > 0) + GetChildAt(0).Dispose(); + RemoveAllViews(); + _childView = null; + } + } + + protected override void OnLayout(bool changed, int left, int top, int right, int bottom) + { + if (_childView == null) + return; + + IVisualElementRenderer renderer = Platform.GetRenderer(_childView); + renderer.UpdateLayout(); + } + + protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + // we need to make sure we are big enough to be laid out at 0,0 + if (_childView != null) + { + SetMeasuredDimension((int)Context.ToPixels(_childView.Bounds.Right + _parent.Padding.Right), (int)Context.ToPixels(_childView.Bounds.Bottom + _parent.Padding.Bottom)); + } + else + SetMeasuredDimension((int)Context.ToPixels(_parent.Padding.Right), (int)Context.ToPixels(_parent.Padding.Bottom)); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ScrollViewRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/ScrollViewRenderer.cs new file mode 100644 index 00000000..15f04278 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/ScrollViewRenderer.cs @@ -0,0 +1,331 @@ +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using Android.Animation; +using Android.Graphics; +using Android.Views; +using Android.Widget; +using AScrollView = Android.Widget.ScrollView; + +namespace Xamarin.Forms.Platform.Android +{ + public class ScrollViewRenderer : AScrollView, IVisualElementRenderer + { + ScrollViewContainer _container; + HorizontalScrollView _hScrollView; + bool _isAttached; + + bool _isBidirectional; + ScrollToRequestedEventArgs _pendingScrollTo; + ScrollView _view; + + public ScrollViewRenderer() : base(Forms.Context) + { + } + + protected IScrollViewController Controller + { + get { return (IScrollViewController)Element; } + } + + internal float LastX { get; set; } + + internal float LastY { get; set; } + + public VisualElement Element + { + get { return _view; } + } + + public event EventHandler<VisualElementChangedEventArgs> ElementChanged; + + public SizeRequest GetDesiredSize(int widthConstraint, int heightConstraint) + { + Measure(widthConstraint, heightConstraint); + return new SizeRequest(new Size(MeasuredWidth, MeasuredHeight), new Size(40, 40)); + } + + public void SetElement(VisualElement element) + { + ScrollView oldElement = _view; + _view = (ScrollView)element; + + if (oldElement != null) + { + oldElement.PropertyChanged -= HandlePropertyChanged; + ((IScrollViewController)oldElement).ScrollToRequested -= OnScrollToRequested; + } + if (element != null) + { + OnElementChanged(new VisualElementChangedEventArgs(oldElement, element)); + + if (_container == null) + { + Tracker = new VisualElementTracker(this); + _container = new ScrollViewContainer(_view, Forms.Context); + } + + _view.PropertyChanged += HandlePropertyChanged; + Controller.ScrollToRequested += OnScrollToRequested; + + LoadContent(); + UpdateBackgroundColor(); + + UpdateOrientation(); + + element.SendViewInitialized(this); + + if (!string.IsNullOrEmpty(element.AutomationId)) + ContentDescription = element.AutomationId; + } + } + + public VisualElementTracker Tracker { get; private set; } + + public void UpdateLayout() + { + if (Tracker != null) + Tracker.UpdateLayout(); + } + + public ViewGroup ViewGroup + { + get { return this; } + } + + public override void Draw(Canvas canvas) + { + canvas.ClipRect(canvas.ClipBounds); + + base.Draw(canvas); + } + + public override bool OnInterceptTouchEvent(MotionEvent ev) + { + if (Element.InputTransparent) + return false; + + // set the start point for the bidirectional scroll; + // Down is swallowed by other controls, so we'll just sneak this in here without actually preventing + // other controls from getting the event. + if (_isBidirectional && ev.Action == MotionEventActions.Down) + { + LastY = ev.RawY; + LastX = ev.RawX; + } + + return base.OnInterceptTouchEvent(ev); + } + + public override bool OnTouchEvent(MotionEvent ev) + { + // The nested ScrollViews will allow us to scroll EITHER vertically OR horizontally in a single gesture. + // This will allow us to also scroll diagonally. + // We'll fall through to the base event so we still get the fling from the ScrollViews. + // We have to do this in both ScrollViews, since a single gesture will be owned by one or the other, depending + // on the initial direction of movement (i.e., horizontal/vertical). + if (_isBidirectional && !Element.InputTransparent) + { + float dX = LastX - ev.RawX; + float dY = LastY - ev.RawY; + LastY = ev.RawY; + LastX = ev.RawX; + if (ev.Action == MotionEventActions.Move) + { + ScrollBy(0, (int)dY); + foreach (AHorizontalScrollView child in this.GetChildrenOfType<AHorizontalScrollView>()) + { + child.ScrollBy((int)dX, 0); + break; + } + } + } + return base.OnTouchEvent(ev); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + SetElement(null); + + if (disposing) + { + Tracker.Dispose(); + Tracker = null; + RemoveAllViews(); + _container.Dispose(); + _container = null; + } + } + + protected override void OnAttachedToWindow() + { + base.OnAttachedToWindow(); + + _isAttached = true; + } + + protected override void OnDetachedFromWindow() + { + base.OnDetachedFromWindow(); + + _isAttached = false; + } + + protected virtual void OnElementChanged(VisualElementChangedEventArgs e) + { + EventHandler<VisualElementChangedEventArgs> changed = ElementChanged; + if (changed != null) + changed(this, e); + } + + protected override void OnLayout(bool changed, int left, int top, int right, int bottom) + { + base.OnLayout(changed, left, top, right, bottom); + if (_view.Content != null && _hScrollView != null) + _hScrollView.Layout(0, 0, right - left, Math.Max(bottom - top, (int)Context.ToPixels(_view.Content.Height))); + } + + protected override void OnScrollChanged(int l, int t, int oldl, int oldt) + { + base.OnScrollChanged(l, t, oldl, oldt); + + UpdateScrollPosition(Forms.Context.FromPixels(l), Forms.Context.FromPixels(t)); + } + + internal void UpdateScrollPosition(double x, double y) + { + if (_view != null) + Controller.SetScrolledPosition(x, y); + } + + static int GetDistance(double start, double position, double v) + { + return (int)(start + (position - start) * v); + } + + void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == "Content") + LoadContent(); + else if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName) + UpdateBackgroundColor(); + else if (e.PropertyName == ScrollView.OrientationProperty.PropertyName) + UpdateOrientation(); + } + + void LoadContent() + { + _container.ChildView = _view.Content; + } + + async void OnScrollToRequested(object sender, ScrollToRequestedEventArgs e) + { + if (!_isAttached) + { + _pendingScrollTo = e; + return; + } + + // 99.99% of the time simply queuing to the end of the execution queue should handle this case. + // However it is possible to end a layout cycle and STILL be layout requested. We want to + // back off until all are done, even if they trigger layout storms over and over. So we back off + // for 10ms tops then move on. + var cycle = 0; + while (IsLayoutRequested) + { + await Task.Delay(TimeSpan.FromMilliseconds(1)); + cycle++; + + if (cycle >= 10) + break; + } + + var x = (int)Forms.Context.ToPixels(e.ScrollX); + var y = (int)Forms.Context.ToPixels(e.ScrollY); + int currentX = _view.Orientation == ScrollOrientation.Horizontal ? _hScrollView.ScrollX : ScrollX; + int currentY = _view.Orientation == ScrollOrientation.Horizontal ? _hScrollView.ScrollY : ScrollY; + if (e.Mode == ScrollToMode.Element) + { + Point itemPosition = Controller.GetScrollPositionForElement(e.Element as VisualElement, e.Position); + + x = (int)Forms.Context.ToPixels(itemPosition.X); + y = (int)Forms.Context.ToPixels(itemPosition.Y); + } + if (e.ShouldAnimate) + { + ValueAnimator animator = ValueAnimator.OfFloat(0f, 1f); + animator.SetDuration(1000); + animator.Update += (o, animatorUpdateEventArgs) => + { + var v = (double)animatorUpdateEventArgs.Animation.AnimatedValue; + int distX = GetDistance(currentX, x, v); + int distY = GetDistance(currentY, y, v); + + if (_view == null) + { + // This is probably happening because the page with this Scroll View + // was popped off the stack during animation + animator.Cancel(); + return; + } + + if (_view.Orientation == ScrollOrientation.Horizontal) + _hScrollView.ScrollTo(distX, distY); + else + ScrollTo(distX, distY); + }; + animator.AnimationEnd += delegate + { + if (Controller == null) + return; + Controller.SendScrollFinished(); + }; + + animator.Start(); + } + else + { + if (_view.Orientation == ScrollOrientation.Horizontal) + _hScrollView.ScrollTo(x, y); + else + ScrollTo(x, y); + Controller.SendScrollFinished(); + } + } + + void UpdateBackgroundColor() + { + SetBackgroundColor(Element.BackgroundColor.ToAndroid(Color.Transparent)); + } + + void UpdateOrientation() + { + if (_view.Orientation == ScrollOrientation.Horizontal || _view.Orientation == ScrollOrientation.Both) + { + if (_hScrollView == null) + _hScrollView = new AHorizontalScrollView(Context, this); + + ((AHorizontalScrollView)_hScrollView).IsBidirectional = _isBidirectional = _view.Orientation == ScrollOrientation.Both; + + if (_hScrollView.Parent != this) + { + _container.RemoveFromParent(); + _hScrollView.AddView(_container); + AddView(_hScrollView); + } + } + else + { + if (_container.Parent != this) + { + _container.RemoveFromParent(); + if (_hScrollView != null) + _hScrollView.RemoveFromParent(); + AddView(_container); + } + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/SearchBarRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/SearchBarRenderer.cs new file mode 100644 index 00000000..e31b95aa --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/SearchBarRenderer.cs @@ -0,0 +1,236 @@ +using System.ComponentModel; +using System.Linq; +using Android.Content.Res; +using Android.Graphics; +using Android.OS; +using Android.Text; +using Android.Util; +using Android.Widget; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android +{ + public class SearchBarRenderer : ViewRenderer<SearchBar, SearchView>, SearchView.IOnQueryTextListener + { + EditText _editText; + ColorStateList _hintTextColorDefault; + InputTypes _inputType; + ColorStateList _textColorDefault; + + public SearchBarRenderer() + { + AutoPackage = false; + } + + bool SearchView.IOnQueryTextListener.OnQueryTextChange(string newText) + { + ((IElementController)Element).SetValueFromRenderer(SearchBar.TextProperty, newText); + + return true; + } + + bool SearchView.IOnQueryTextListener.OnQueryTextSubmit(string query) + { + Element.OnSearchButtonPressed(); + Control.ClearFocus(); + return true; + } + + protected override void OnElementChanged(ElementChangedEventArgs<SearchBar> e) + { + base.OnElementChanged(e); + + HandleKeyboardOnFocus = true; + + SearchView searchView = Control; + + if (searchView == null) + { + searchView = new SearchView(Context); + searchView.SetIconifiedByDefault(false); + searchView.Iconified = false; + SetNativeControl(searchView); + } + + BuildVersionCodes androidVersion = Build.VERSION.SdkInt; + if (androidVersion >= BuildVersionCodes.JellyBean) + _inputType = searchView.InputType; + else + { + // < API 16, Cannot get the default InputType for a SearchView + _inputType = InputTypes.ClassText | InputTypes.TextFlagAutoComplete | InputTypes.TextFlagNoSuggestions; + } + + searchView.ClearFocus(); + UpdatePlaceholder(); + UpdateText(); + UpdateEnabled(); + UpdateCancelButtonColor(); + UpdateFont(); + UpdateAlignment(); + UpdateTextColor(); + UpdatePlaceholderColor(); + + if (e.OldElement == null) + { + searchView.SetOnQueryTextListener(this); + searchView.SetOnQueryTextFocusChangeListener(this); + } + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == SearchBar.PlaceholderProperty.PropertyName) + UpdatePlaceholder(); + else if (e.PropertyName == SearchBar.TextProperty.PropertyName) + UpdateText(); + else if (e.PropertyName == SearchBar.CancelButtonColorProperty.PropertyName) + UpdateCancelButtonColor(); + else if (e.PropertyName == VisualElement.IsEnabledProperty.PropertyName) + UpdateEnabled(); + else if (e.PropertyName == SearchBar.FontAttributesProperty.PropertyName) + UpdateFont(); + else if (e.PropertyName == SearchBar.FontFamilyProperty.PropertyName) + UpdateFont(); + else if (e.PropertyName == SearchBar.FontSizeProperty.PropertyName) + UpdateFont(); + else if (e.PropertyName == SearchBar.HorizontalTextAlignmentProperty.PropertyName) + UpdateAlignment(); + else if (e.PropertyName == SearchBar.TextColorProperty.PropertyName) + UpdateTextColor(); + else if (e.PropertyName == SearchBar.PlaceholderColorProperty.PropertyName) + UpdatePlaceholderColor(); + } + + internal override void OnNativeFocusChanged(bool hasFocus) + { + if (hasFocus && !Element.IsEnabled) + Control.ClearFocus(); + } + + void UpdateAlignment() + { + _editText = _editText ?? Control.GetChildrenOfType<EditText>().FirstOrDefault(); + + if (_editText == null) + return; + + _editText.Gravity = Element.HorizontalTextAlignment.ToHorizontalGravityFlags() | Xamarin.Forms.TextAlignment.Center.ToVerticalGravityFlags(); + } + + void UpdateCancelButtonColor() + { + int searchViewCloseButtonId = Control.Resources.GetIdentifier("android:id/search_close_btn", null, null); + if (searchViewCloseButtonId != 0) + { + var image = FindViewById<ImageView>(searchViewCloseButtonId); + if (image != null && image.Drawable != null) + { + if (Element.CancelButtonColor != Color.Default) + image.Drawable.SetColorFilter(Element.CancelButtonColor.ToAndroid(), PorterDuff.Mode.SrcIn); + else + image.Drawable.ClearColorFilter(); + } + } + } + + void UpdateEnabled() + { + SearchBar model = Element; + SearchView control = Control; + if (!model.IsEnabled) + { + control.ClearFocus(); + // removes cursor in SearchView + control.SetInputType(InputTypes.Null); + } + else + control.SetInputType(_inputType); + } + + void UpdateFont() + { + _editText = _editText ?? Control.GetChildrenOfType<EditText>().FirstOrDefault(); + + if (_editText == null) + return; + + _editText.Typeface = Element.ToTypeface(); + _editText.SetTextSize(ComplexUnitType.Sp, (float)Element.FontSize); + } + + void UpdatePlaceholder() + { + Control.SetQueryHint(Element.Placeholder); + } + + void UpdatePlaceholderColor() + { + _editText = _editText ?? Control.GetChildrenOfType<EditText>().FirstOrDefault(); + + if (_editText == null) + return; + + Color placeholderColor = Element.PlaceholderColor; + + if (placeholderColor.IsDefault) + { + if (_hintTextColorDefault == null) + { + // This control has always had the default colors; nothing to update + return; + } + + // This control is being set back to the default colors + _editText.SetHintTextColor(_hintTextColorDefault); + } + else + { + // Keep track of the default colors so we can return to them later + // and so we can preserve the default disabled color + _hintTextColorDefault = _hintTextColorDefault ?? _editText.HintTextColors; + + _editText.SetHintTextColor(placeholderColor.ToAndroidPreserveDisabled(_hintTextColorDefault)); + } + } + + void UpdateText() + { + string query = Control.Query; + if (query != Element.Text) + Control.SetQuery(Element.Text, false); + } + + void UpdateTextColor() + { + _editText = _editText ?? Control.GetChildrenOfType<EditText>().FirstOrDefault(); + + if (_editText == null) + return; + + Color textColor = Element.TextColor; + + if (textColor.IsDefault) + { + if (_textColorDefault == null) + { + // This control has always had the default colors; nothing to update + return; + } + + // This control is being set back to the default colors + _editText.SetTextColor(_textColorDefault); + } + else + { + // Keep track of the default colors so we can return to them later + // and so we can preserve the default disabled color + _textColorDefault = _textColorDefault ?? _editText.TextColors; + + _editText.SetTextColor(textColor.ToAndroidPreserveDisabled(_textColorDefault)); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/SliderRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/SliderRenderer.cs new file mode 100644 index 00000000..694052ef --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/SliderRenderer.cs @@ -0,0 +1,98 @@ +using System.ComponentModel; +using Android.Graphics.Drawables; +using Android.OS; +using Android.Widget; + +namespace Xamarin.Forms.Platform.Android +{ + public class SliderRenderer : ViewRenderer<Slider, SeekBar>, SeekBar.IOnSeekBarChangeListener + { + double _max; + double _min; + + public SliderRenderer() + { + AutoPackage = false; + } + + double Value + { + get { return _min + (_max - _min) * (Control.Progress / 1000.0); } + set { Control.Progress = (int)((value - _min) / (_max - _min) * 1000.0); } + } + + void SeekBar.IOnSeekBarChangeListener.OnProgressChanged(SeekBar seekBar, int progress, bool fromUser) + { + ((IElementController)Element).SetValueFromRenderer(Slider.ValueProperty, Value); + } + + void SeekBar.IOnSeekBarChangeListener.OnStartTrackingTouch(SeekBar seekBar) + { + } + + void SeekBar.IOnSeekBarChangeListener.OnStopTrackingTouch(SeekBar seekBar) + { + } + + protected override void OnElementChanged(ElementChangedEventArgs<Slider> e) + { + base.OnElementChanged(e); + + if (e.OldElement == null) + { + var seekBar = new SeekBar(Context); + SetNativeControl(seekBar); + + seekBar.Max = 1000; + + seekBar.SetOnSeekBarChangeListener(this); + } + + Slider slider = e.NewElement; + _min = slider.Minimum; + _max = slider.Maximum; + Value = slider.Value; + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + Slider view = Element; + switch (e.PropertyName) + { + case "Maximum": + _max = view.Maximum; + break; + case "Minimum": + _min = view.Minimum; + break; + case "Value": + if (Value != view.Value) + Value = view.Value; + break; + } + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + base.OnLayout(changed, l, t, r, b); + + BuildVersionCodes androidVersion = Build.VERSION.SdkInt; + if (androidVersion >= BuildVersionCodes.JellyBean) + { + // Thumb only supported JellyBean and higher + + if (Control == null) + return; + + SeekBar seekbar = Control; + + Drawable thumb = seekbar.Thumb; + int thumbTop = seekbar.Height / 2 - thumb.IntrinsicHeight / 2; + + thumb.SetBounds(thumb.Bounds.Left, thumbTop, thumb.Bounds.Left + thumb.IntrinsicWidth, thumbTop + thumb.IntrinsicHeight); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/StepperRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/StepperRenderer.cs new file mode 100644 index 00000000..70eed60b --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/StepperRenderer.cs @@ -0,0 +1,94 @@ +using System.ComponentModel; +using Android.Views; +using Android.Widget; +using Java.Lang; +using AButton = Android.Widget.Button; + +namespace Xamarin.Forms.Platform.Android +{ + public class StepperRenderer : ViewRenderer<Stepper, LinearLayout> + { + AButton _downButton; + AButton _upButton; + + public StepperRenderer() + { + AutoPackage = false; + } + + protected override void OnElementChanged(ElementChangedEventArgs<Stepper> e) + { + base.OnElementChanged(e); + + if (e.OldElement == null) + { + _downButton = new AButton(Context) { Text = "-", Gravity = GravityFlags.Center, Tag = this }; + + _downButton.SetOnClickListener(StepperListener.Instance); + + _upButton = new AButton(Context) { Text = "+", Tag = this }; + + _upButton.SetOnClickListener(StepperListener.Instance); + _upButton.SetHeight((int)Context.ToPixels(10.0)); + + var layout = new LinearLayout(Context) { Orientation = Orientation.Horizontal }; + + layout.AddView(_downButton); + layout.AddView(_upButton); + + SetNativeControl(layout); + } + + UpdateButtonEnabled(); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + switch (e.PropertyName) + { + case "Minimum": + UpdateButtonEnabled(); + break; + case "Maximum": + UpdateButtonEnabled(); + break; + case "Value": + UpdateButtonEnabled(); + break; + case "IsEnabled": + UpdateButtonEnabled(); + break; + } + } + + void UpdateButtonEnabled() + { + Stepper view = Element; + _upButton.Enabled = view.IsEnabled ? view.Value < view.Maximum : view.IsEnabled; + _downButton.Enabled = view.IsEnabled ? view.Value > view.Minimum : view.IsEnabled; + } + + class StepperListener : Object, IOnClickListener + { + public static readonly StepperListener Instance = new StepperListener(); + + public void OnClick(global::Android.Views.View v) + { + var renderer = v.Tag as StepperRenderer; + if (renderer == null) + return; + + Stepper stepper = renderer.Element; + if (stepper == null) + return; + + if (v == renderer._upButton) + ((IElementController)stepper).SetValueFromRenderer(Stepper.ValueProperty, stepper.Value + stepper.Increment); + else if (v == renderer._downButton) + ((IElementController)stepper).SetValueFromRenderer(Stepper.ValueProperty, stepper.Value - stepper.Increment); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/StreamImagesourceHandler.cs b/Xamarin.Forms.Platform.Android/Renderers/StreamImagesourceHandler.cs new file mode 100644 index 00000000..708ed665 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/StreamImagesourceHandler.cs @@ -0,0 +1,22 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Android.Content; +using Android.Graphics; + +namespace Xamarin.Forms.Platform.Android +{ + public sealed class StreamImagesourceHandler : IImageSourceHandler + { + public async Task<Bitmap> LoadImageAsync(ImageSource imagesource, Context context, CancellationToken cancelationToken = default(CancellationToken)) + { + var streamsource = imagesource as StreamImageSource; + if (streamsource != null && streamsource.Stream != null) + { + using(Stream stream = await streamsource.GetStreamAsync(cancelationToken).ConfigureAwait(false)) + return await BitmapFactory.DecodeStreamAsync(stream).ConfigureAwait(false); + } + return null; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/SwitchRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/SwitchRenderer.cs new file mode 100644 index 00000000..39916b58 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/SwitchRenderer.cs @@ -0,0 +1,83 @@ +using System; +using Android.Widget; +using ASwitch = Android.Widget.Switch; + +namespace Xamarin.Forms.Platform.Android +{ + public class SwitchRenderer : ViewRenderer<Switch, ASwitch>, CompoundButton.IOnCheckedChangeListener + { + public SwitchRenderer() + { + AutoPackage = false; + } + + void CompoundButton.IOnCheckedChangeListener.OnCheckedChanged(CompoundButton buttonView, bool isChecked) + { + ((IViewController)Element).SetValueFromRenderer(Switch.IsToggledProperty, isChecked); + } + + public override SizeRequest GetDesiredSize(int widthConstraint, int heightConstraint) + { + SizeRequest sizeConstraint = base.GetDesiredSize(widthConstraint, heightConstraint); + + if (sizeConstraint.Request.Width == 0) + { + int width = widthConstraint; + if (widthConstraint <= 0) + width = (int)Context.GetThemeAttributeDp(global::Android.Resource.Attribute.SwitchMinWidth); + else if (widthConstraint <= 0) + width = 100; + + sizeConstraint = new SizeRequest(new Size(width, sizeConstraint.Request.Height), new Size(width, sizeConstraint.Minimum.Height)); + } + + return sizeConstraint; + } + + protected override void Dispose(bool disposing) + { + if (disposing && Control != null) + { + if (Element != null) + Element.Toggled -= HandleToggled; + + Control.SetOnCheckedChangeListener(null); + } + + base.Dispose(disposing); + } + + protected override void OnElementChanged(ElementChangedEventArgs<Switch> e) + { + base.OnElementChanged(e); + + if (e.OldElement != null) + e.OldElement.Toggled -= HandleToggled; + + if (e.NewElement != null) + { + if (Control == null) + { + var aswitch = new ASwitch(Context); + aswitch.SetOnCheckedChangeListener(this); + SetNativeControl(aswitch); + } + else + UpdateEnabled(); // Normally set by SetNativeControl, but not when the Control is reused. + + e.NewElement.Toggled += HandleToggled; + Control.Checked = e.NewElement.IsToggled; + } + } + + void HandleToggled(object sender, EventArgs e) + { + Control.Checked = Element.IsToggled; + } + + void UpdateEnabled() + { + Control.Enabled = Element.IsEnabled; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/TabbedRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/TabbedRenderer.cs new file mode 100644 index 00000000..35404a64 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/TabbedRenderer.cs @@ -0,0 +1,75 @@ +using System.ComponentModel; +using AButton = Android.Widget.Button; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android +{ + public class TabbedRenderer : VisualElementRenderer<TabbedPage> + { + public TabbedRenderer() + { + AutoPackage = false; + } + + protected override void Dispose(bool disposing) + { + if (disposing && Element != null && Element.Children.Count > 0) + { + RemoveAllViews(); + foreach (Page pageToRemove in Element.Children) + { + IVisualElementRenderer pageRenderer = Platform.GetRenderer(pageToRemove); + if (pageRenderer != null) + pageRenderer.Dispose(); + pageToRemove.ClearValue(Platform.RendererProperty); + } + } + + base.Dispose(disposing); + } + + protected override void OnAttachedToWindow() + { + base.OnAttachedToWindow(); + Element.SendAppearing(); + } + + protected override void OnDetachedFromWindow() + { + base.OnDetachedFromWindow(); + Element.SendDisappearing(); + } + + protected override void OnElementChanged(ElementChangedEventArgs<TabbedPage> e) + { + base.OnElementChanged(e); + + TabbedPage tabs = e.NewElement; + if (tabs.CurrentPage != null) + SwitchContent(tabs.CurrentPage); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == "CurrentPage") + SwitchContent(Element.CurrentPage); + } + + protected virtual void SwitchContent(Page view) + { + Context.HideKeyboard(this); + + RemoveAllViews(); + + if (view == null) + return; + + if (Platform.GetRenderer(view) == null) + Platform.SetRenderer(view, Platform.CreateRenderer(view)); + + AddView(Platform.GetRenderer(view).ViewGroup); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/TableViewModelRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/TableViewModelRenderer.cs new file mode 100644 index 00000000..a7659ed8 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/TableViewModelRenderer.cs @@ -0,0 +1,228 @@ +using Android.Content; +using Android.Util; +using Android.Views; +using Android.Widget; +using AView = Android.Views.View; +using AListView = Android.Widget.ListView; + +namespace Xamarin.Forms.Platform.Android +{ + public class TableViewModelRenderer : CellAdapter + { + readonly TableView _view; + protected readonly Context Context; + Cell _restoreFocus; + + public TableViewModelRenderer(Context context, AListView listView, TableView view) : base(context) + { + _view = view; + Context = context; + + view.ModelChanged += (sender, args) => NotifyDataSetChanged(); + + listView.OnItemClickListener = this; + listView.OnItemLongClickListener = this; + } + + public override int Count + { + get + { + var count = 0; + + //Get each adapter's count + 1 for the header + int section = _view.Model.GetSectionCount(); + for (var i = 0; i < section; i++) + count += _view.Model.GetRowCount(i) + 1; + + return count; + } + } + + public override object this[int position] + { + get + { + bool isHeader, nextIsHeader; + Cell cell = GetCellForPosition(position, out isHeader, out nextIsHeader); + return cell; + } + } + + public override int ViewTypeCount + { + get + { + //The headers count as a view type too + var viewTypeCount = 1; + + //Get each adapter's ViewTypeCount + int section = _view.Model.GetSectionCount(); + for (var i = 0; i < section; i++) + viewTypeCount += _view.Model.GetRowCount(i); + + return viewTypeCount; + } + } + + public override bool AreAllItemsEnabled() + { + return false; + } + + public override long GetItemId(int position) + { + return position; + } + + public override AView GetView(int position, AView convertView, ViewGroup parent) + { + object obj = this[position]; + if (obj == null) + return new AView(Context); + + bool isHeader, nextIsHeader; + Cell item = GetCellForPosition(position, out isHeader, out nextIsHeader); + + var makeBline = true; + var layout = convertView as ConditionalFocusLayout; + if (layout != null) + { + makeBline = false; + convertView = layout.GetChildAt(0); + } + else + layout = new ConditionalFocusLayout(Context) { Orientation = Orientation.Vertical }; + + AView aview = CellFactory.GetCell(item, convertView, parent, Context, _view); + + if (!makeBline) + { + if (convertView != aview) + { + layout.RemoveViewAt(0); + layout.AddView(aview, 0); + } + } + else + layout.AddView(aview, 0); + + AView bline; + if (makeBline) + { + bline = new AView(Context) { LayoutParameters = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FillParent, 1) }; + + layout.AddView(bline); + } + else + bline = layout.GetChildAt(1); + + if (isHeader) + bline.SetBackgroundColor(Color.Accent.ToAndroid()); + else if (nextIsHeader) + bline.SetBackgroundColor(global::Android.Graphics.Color.Transparent); + else + { + using(var value = new TypedValue()) + { + int id = global::Android.Resource.Drawable.DividerHorizontalDark; + if (Context.Theme.ResolveAttribute(global::Android.Resource.Attribute.ListDivider, value, true)) + id = value.ResourceId; + else if (Context.Theme.ResolveAttribute(global::Android.Resource.Attribute.Divider, value, true)) + id = value.ResourceId; + + bline.SetBackgroundResource(id); + } + } + + layout.ApplyTouchListenersToSpecialCells(item); + + if (_restoreFocus == item) + { + if (!aview.HasFocus) + aview.RequestFocus(); + + _restoreFocus = null; + } + else if (aview.HasFocus) + aview.ClearFocus(); + + return layout; + } + + public override bool IsEnabled(int position) + { + bool isHeader, nextIsHeader; + Cell item = GetCellForPosition(position, out isHeader, out nextIsHeader); + return !isHeader && item.IsEnabled; + } + + protected override Cell GetCellForPosition(int position) + { + bool isHeader, nextIsHeader; + return GetCellForPosition(position, out isHeader, out nextIsHeader); + } + + protected override void HandleItemClick(AdapterView parent, AView nview, int position, long id) + { + int sectionCount = _view.Model.GetSectionCount(); + for (var sectionIndex = 0; sectionIndex < sectionCount; sectionIndex++) + { + if (position == 0) + return; + + int size = _view.Model.GetRowCount(sectionIndex) + 1; + + if (position < size) + { + _view.Model.RowSelected(sectionIndex, position - 1); + return; + } + + position -= size; + } + } + + Cell GetCellForPosition(int position, out bool isHeader, out bool nextIsHeader) + { + isHeader = false; + nextIsHeader = false; + + int sectionCount = _view.Model.GetSectionCount(); + + for (var sectionIndex = 0; sectionIndex < sectionCount; sectionIndex ++) + { + int size = _view.Model.GetRowCount(sectionIndex) + 1; + + if (position == 0) + { + isHeader = true; + nextIsHeader = size == 0 && sectionIndex < sectionCount - 1; + + Cell header = _view.Model.GetHeaderCell(sectionIndex); + + Cell resultCell = null; + if (header != null) + resultCell = header; + + if (resultCell == null) + resultCell = new TextCell { Text = _view.Model.GetSectionTitle(sectionIndex) }; + + resultCell.Parent = _view; + + return resultCell; + } + + if (position < size) + { + nextIsHeader = position == size - 1; + return (Cell)_view.Model.GetItem(sectionIndex, position - 1); + } + + position -= size; + } + + return null; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/TableViewRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/TableViewRenderer.cs new file mode 100644 index 00000000..e4017a7e --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/TableViewRenderer.cs @@ -0,0 +1,44 @@ +using Android.Views; +using AView = Android.Views.View; +using AListView = Android.Widget.ListView; + +namespace Xamarin.Forms.Platform.Android +{ + public class TableViewRenderer : ViewRenderer<TableView, AListView> + { + public TableViewRenderer() + { + AutoPackage = false; + } + + protected virtual TableViewModelRenderer GetModelRenderer(AListView listView, TableView view) + { + return new TableViewModelRenderer(Context, listView, view); + } + + protected override Size MinimumSize() + { + return new Size(40, 40); + } + + protected override void OnElementChanged(ElementChangedEventArgs<TableView> e) + { + base.OnElementChanged(e); + + AListView listView = Control; + if (listView == null) + { + listView = new AListView(Context); + SetNativeControl(listView); + } + + listView.Focusable = false; + listView.DescendantFocusability = DescendantFocusability.AfterDescendants; + + TableView view = e.NewElement; + + TableViewModelRenderer source = GetModelRenderer(listView, view); + listView.Adapter = source; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/TimePickerRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/TimePickerRenderer.cs new file mode 100644 index 00000000..cfa676d0 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/TimePickerRenderer.cs @@ -0,0 +1,95 @@ +using System; +using System.ComponentModel; +using Android.App; +using Android.Widget; +using ADatePicker = Android.Widget.DatePicker; +using ATimePicker = Android.Widget.TimePicker; +using Object = Java.Lang.Object; + +namespace Xamarin.Forms.Platform.Android +{ + public class TimePickerRenderer : ViewRenderer<TimePicker, EditText>, TimePickerDialog.IOnTimeSetListener + { + AlertDialog _dialog; + + public TimePickerRenderer() + { + AutoPackage = false; + } + + void TimePickerDialog.IOnTimeSetListener.OnTimeSet(ATimePicker view, int hourOfDay, int minute) + { + ((IElementController)Element).SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, false); + + ((IElementController)Element).SetValueFromRenderer(TimePicker.TimeProperty, new TimeSpan(hourOfDay, minute, 0)); + Control.ClearFocus(); + _dialog = null; + } + + protected override void OnElementChanged(ElementChangedEventArgs<TimePicker> e) + { + base.OnElementChanged(e); + + if (e.OldElement == null) + { + var textField = new EditText(Context) { Focusable = false, Clickable = true, Tag = this }; + + textField.SetOnClickListener(TimePickerListener.Instance); + SetNativeControl(textField); + } + + SetTime(e.NewElement.Time); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == TimePicker.TimeProperty.PropertyName || e.PropertyName == TimePicker.FormatProperty.PropertyName) + SetTime(Element.Time); + } + + internal override void OnFocusChangeRequested(object sender, VisualElement.FocusRequestArgs e) + { + base.OnFocusChangeRequested(sender, e); + + if (e.Focus) + OnClick(); + else if (_dialog != null) + { + _dialog.Hide(); + ((IElementController)Element).SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, false); + Control.ClearFocus(); + _dialog = null; + } + } + + void OnClick() + { + TimePicker view = Element; + ((IElementController)Element).SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, true); + + _dialog = new TimePickerDialog(Context, this, view.Time.Hours, view.Time.Minutes, false); + _dialog.Show(); + } + + void SetTime(TimeSpan time) + { + Control.Text = DateTime.Today.Add(time).ToString(Element.Format); + } + + class TimePickerListener : Object, IOnClickListener + { + public static readonly TimePickerListener Instance = new TimePickerListener(); + + public void OnClick(global::Android.Views.View v) + { + var renderer = v.Tag as TimePickerRenderer; + if (renderer == null) + return; + + renderer.OnClick(); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ToolbarButton.cs b/Xamarin.Forms.Platform.Android/Renderers/ToolbarButton.cs new file mode 100644 index 00000000..74ff08b8 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/ToolbarButton.cs @@ -0,0 +1,36 @@ +using System; +using System.ComponentModel; +using Android.Content; + +namespace Xamarin.Forms.Platform.Android +{ + internal sealed class ToolbarButton : global::Android.Widget.Button, IToolbarButton + { + public ToolbarButton(Context context, ToolbarItem item) : base(context) + { + if (item == null) + throw new ArgumentNullException("item", "you should specify a ToolbarItem"); + Item = item; + Enabled = Item.IsEnabled; + Text = Item.Text; + SetBackgroundColor(new Color(0, 0, 0, 0).ToAndroid()); + Click += (sender, e) => Item.Activate(); + Item.PropertyChanged += HandlePropertyChanged; + } + + public ToolbarItem Item { get; set; } + + protected override void Dispose(bool disposing) + { + if (disposing) + Item.PropertyChanged -= HandlePropertyChanged; + base.Dispose(disposing); + } + + void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == MenuItem.IsEnabledProperty.PropertyName) + Enabled = Item.IsEnabled; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ToolbarImageButton.cs b/Xamarin.Forms.Platform.Android/Renderers/ToolbarImageButton.cs new file mode 100644 index 00000000..6d18db54 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/ToolbarImageButton.cs @@ -0,0 +1,41 @@ +using System; +using System.ComponentModel; +using Android.Content; +using Android.Graphics; +using Android.Widget; + +namespace Xamarin.Forms.Platform.Android +{ + internal sealed class ToolbarImageButton : ImageButton, IToolbarButton + { + public ToolbarImageButton(Context context, ToolbarItem item) : base(context) + { + if (item == null) + throw new ArgumentNullException("item", "you should specify a ToolbarItem"); + Item = item; + Enabled = item.IsEnabled; + Bitmap bitmap; + bitmap = Context.Resources.GetBitmap(Item.Icon); + SetImageBitmap(bitmap); + SetBackgroundColor(new Color(0, 0, 0, 0).ToAndroid()); + Click += (sender, e) => item.Activate(); + bitmap.Dispose(); + Item.PropertyChanged += HandlePropertyChanged; + } + + public ToolbarItem Item { get; set; } + + protected override void Dispose(bool disposing) + { + if (disposing) + Item.PropertyChanged -= HandlePropertyChanged; + base.Dispose(disposing); + } + + void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == MenuItem.IsEnabledProperty.PropertyName) + Enabled = Item.IsEnabled; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ToolbarRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/ToolbarRenderer.cs new file mode 100644 index 00000000..f40b75f3 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/ToolbarRenderer.cs @@ -0,0 +1,65 @@ +using Android.Widget; +using AScrollView = Android.Widget.ScrollView; +using AButton = Android.Widget.Button; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android +{ + public class ToolbarRenderer : ViewRenderer + { + public ToolbarRenderer() + { + AutoPackage = false; + } + + protected override void OnElementChanged(ElementChangedEventArgs<View> e) + { + base.OnElementChanged(e); + + if (e.OldElement == null) + { + var layout = new LinearLayout(Context); + layout.SetBackgroundColor(new Color(0.2, 0.2, 0.2, 0.5).ToAndroid()); + + layout.Orientation = Orientation.Horizontal; + + SetNativeControl(layout); + } + else + { + var oldToolbar = (Toolbar)e.OldElement; + oldToolbar.ItemAdded -= OnToolbarItemsChanged; + oldToolbar.ItemRemoved -= OnToolbarItemsChanged; + } + + UpdateChildren(); + + var toolbar = (Toolbar)e.NewElement; + toolbar.ItemAdded += OnToolbarItemsChanged; + toolbar.ItemRemoved += OnToolbarItemsChanged; + } + + void OnToolbarItemsChanged(object sender, ToolbarItemEventArgs e) + { + UpdateChildren(); + } + + void UpdateChildren() + { + RemoveAllViews(); + + foreach (ToolbarItem child in ((Toolbar)Element).Items) + { + AView view = null; + + if (!string.IsNullOrEmpty(child.Icon)) + view = new ToolbarImageButton(Context, child); + else + view = new AButton(Context); + + using(var param = new LinearLayout.LayoutParams(LayoutParams.WrapContent, (int)Context.ToPixels(48), 1)) + ((LinearLayout)Control).AddView(view, param); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ViewGroupExtensions.cs b/Xamarin.Forms.Platform.Android/Renderers/ViewGroupExtensions.cs new file mode 100644 index 00000000..dfcb9ca2 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/ViewGroupExtensions.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using AView = Android.Views.View; +using AViewGroup = Android.Views.ViewGroup; + +namespace Xamarin.Forms.Platform.Android +{ + internal static class ViewGroupExtensions + { + internal static IEnumerable<T> GetChildrenOfType<T>(this AViewGroup self) where T : AView + { + for (var i = 0; i < self.ChildCount; i++) + { + AView child = self.GetChildAt(i); + var typedChild = child as T; + if (typedChild != null) + yield return typedChild; + + if (child is AViewGroup) + { + IEnumerable<T> myChildren = (child as AViewGroup).GetChildrenOfType<T>(); + foreach (T nextChild in myChildren) + yield return nextChild; + } + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/WebViewRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/WebViewRenderer.cs new file mode 100644 index 00000000..d2c0f0d6 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/WebViewRenderer.cs @@ -0,0 +1,206 @@ +using System; +using System.ComponentModel; +using Android.Webkit; +using AWebView = Android.Webkit.WebView; + +namespace Xamarin.Forms.Platform.Android +{ + public class WebViewRenderer : ViewRenderer<WebView, AWebView>, IWebViewRenderer + { + bool _ignoreSourceChanges; + FormsWebChromeClient _webChromeClient; + + public WebViewRenderer() + { + AutoPackage = false; + } + + public void LoadHtml(string html, string baseUrl) + { + Control.LoadDataWithBaseURL(baseUrl == null ? "file:///android_asset/" : baseUrl, html, "text/html", "UTF-8", null); + } + + public void LoadUrl(string url) + { + Control.LoadUrl(url); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (Element != null) + { + if (Control != null) + Control.StopLoading(); + Element.EvalRequested -= OnEvalRequested; + Element.GoBackRequested -= OnGoBackRequested; + Element.GoForwardRequested -= OnGoForwardRequested; + + _webChromeClient?.Dispose(); + } + } + + base.Dispose(disposing); + } + + protected virtual FormsWebChromeClient GetFormsWebChromeClient() + { + return new FormsWebChromeClient(); + } + + protected override Size MinimumSize() + { + return new Size(Context.ToPixels(40), Context.ToPixels(40)); + } + + protected override void OnElementChanged(ElementChangedEventArgs<WebView> e) + { + base.OnElementChanged(e); + + if (Control == null) + { + var webView = new AWebView(Context); + webView.LayoutParameters = new global::Android.Widget.AbsoluteLayout.LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent, 0, 0); + webView.SetWebViewClient(new WebClient(this)); + + _webChromeClient = GetFormsWebChromeClient(); + _webChromeClient.SetContext(Context as IStartActivityForResult); + webView.SetWebChromeClient(_webChromeClient); + + webView.Settings.JavaScriptEnabled = true; + webView.Settings.DomStorageEnabled = true; + SetNativeControl(webView); + } + + if (e.OldElement != null) + { + e.OldElement.EvalRequested -= OnEvalRequested; + e.OldElement.GoBackRequested -= OnGoBackRequested; + e.OldElement.GoForwardRequested -= OnGoForwardRequested; + } + + if (e.NewElement != null) + { + e.NewElement.EvalRequested += OnEvalRequested; + e.NewElement.GoBackRequested += OnGoBackRequested; + e.NewElement.GoForwardRequested += OnGoForwardRequested; + } + + Load(); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + switch (e.PropertyName) + { + case "Source": + Load(); + break; + } + } + + void Load() + { + if (_ignoreSourceChanges) + return; + + if (Element.Source != null) + Element.Source.Load(this); + + UpdateCanGoBackForward(); + } + + void OnEvalRequested(object sender, EventArg<string> eventArg) + { + LoadUrl("javascript:" + eventArg.Data); + } + + void OnGoBackRequested(object sender, EventArgs eventArgs) + { + if (Control.CanGoBack()) + Control.GoBack(); + + UpdateCanGoBackForward(); + } + + void OnGoForwardRequested(object sender, EventArgs eventArgs) + { + if (Control.CanGoForward()) + Control.GoForward(); + + UpdateCanGoBackForward(); + } + + void UpdateCanGoBackForward() + { + if (Element == null || Control == null) + return; + Element.CanGoBack = Control.CanGoBack(); + Element.CanGoForward = Control.CanGoForward(); + } + + class WebClient : WebViewClient + { + WebNavigationResult _navigationResult = WebNavigationResult.Success; + WebViewRenderer _renderer; + + public WebClient(WebViewRenderer renderer) + { + if (renderer == null) + throw new ArgumentNullException("renderer"); + _renderer = renderer; + } + + public override void OnPageFinished(AWebView view, string url) + { + if (_renderer.Element == null || url == "file:///android_asset/") + return; + + var source = new UrlWebViewSource { Url = url }; + _renderer._ignoreSourceChanges = true; + ((IElementController)_renderer.Element).SetValueFromRenderer(WebView.SourceProperty, source); + _renderer._ignoreSourceChanges = false; + + var args = new WebNavigatedEventArgs(WebNavigationEvent.NewPage, source, url, _navigationResult); + + _renderer.Element.SendNavigated(args); + + _renderer.UpdateCanGoBackForward(); + + base.OnPageFinished(view, url); + } + + public override void OnReceivedError(AWebView view, ClientError errorCode, string description, string failingUrl) + { + _navigationResult = WebNavigationResult.Failure; + if (errorCode == ClientError.Timeout) + _navigationResult = WebNavigationResult.Timeout; + base.OnReceivedError(view, errorCode, description, failingUrl); + } + + public override bool ShouldOverrideUrlLoading(AWebView view, string url) + { + if (_renderer.Element == null) + return true; + + var args = new WebNavigatingEventArgs(WebNavigationEvent.NewPage, new UrlWebViewSource { Url = url }, url); + + _renderer.Element.SendNavigating(args); + _navigationResult = WebNavigationResult.Success; + + _renderer.UpdateCanGoBackForward(); + return args.Cancel; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (disposing) + _renderer = null; + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/ResourceManager.cs b/Xamarin.Forms.Platform.Android/ResourceManager.cs new file mode 100644 index 00000000..4c150a05 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/ResourceManager.cs @@ -0,0 +1,71 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Android.Content.Res; +using Android.Graphics; +using Android.Graphics.Drawables; +using Path = System.IO.Path; + +namespace Xamarin.Forms.Platform.Android +{ + public static class ResourceManager + { + public static Type DrawableClass { get; set; } + + public static Type ResourceClass { get; set; } + + public static Bitmap GetBitmap(this Resources resource, string name) + { + return BitmapFactory.DecodeResource(resource, IdFromTitle(name, DrawableClass)); + } + + public static Task<Bitmap> GetBitmapAsync(this Resources resource, string name) + { + return BitmapFactory.DecodeResourceAsync(resource, IdFromTitle(name, DrawableClass)); + } + + public static Drawable GetDrawable(this Resources resource, string name) + { + int id = IdFromTitle(name, DrawableClass); + if (id == 0) + { + Log.Warning("Could not load image named: {0}", name); + return null; + } + return resource.GetDrawable(id); + } + + public static int GetDrawableByName(string name) + { + return IdFromTitle(name, DrawableClass); + } + + public static int GetResourceByName(string name) + { + return IdFromTitle(name, ResourceClass); + } + + public static void Init(Assembly masterAssembly) + { + DrawableClass = masterAssembly.GetTypes().FirstOrDefault(x => x.Name == "Drawable"); + ResourceClass = masterAssembly.GetTypes().FirstOrDefault(x => x.Name == "Id"); + } + + internal static int IdFromTitle(string title, Type type) + { + string name = Path.GetFileNameWithoutExtension(title); + int id = GetId(type, name); + return id; // Resources.System.GetDrawable (Resource.Drawable.dashboard); + } + + static int GetId(Type type, string propertyName) + { + FieldInfo[] props = type.GetFields(); + FieldInfo prop = props.Select(p => p).FirstOrDefault(p => p.Name == propertyName); + if (prop != null) + return (int)prop.GetValue(type); + return 0; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/ResourcesProvider.cs b/Xamarin.Forms.Platform.Android/ResourcesProvider.cs new file mode 100644 index 00000000..334ef56a --- /dev/null +++ b/Xamarin.Forms.Platform.Android/ResourcesProvider.cs @@ -0,0 +1,68 @@ +using Android.Content; +using Android.Content.Res; +using Android.Util; + +namespace Xamarin.Forms.Platform.Android +{ + internal class ResourcesProvider : ISystemResourcesProvider + { + ResourceDictionary _dictionary; + + public IResourceDictionary GetSystemResources() + { + _dictionary = new ResourceDictionary(); + + UpdateStyles(); + + return _dictionary; + } + + public Style GetStyle(int style) + { + var result = new Style(typeof(Label)); + + double fontSize = 0; + string fontFamily = null; + global::Android.Graphics.Color defaultColor = global::Android.Graphics.Color.Argb(0, 0, 0, 0); + global::Android.Graphics.Color androidColor = defaultColor; + + Context context = Forms.Context; + using(var value = new TypedValue()) + { + if (context.Theme.ResolveAttribute(style, value, true)) + { + var styleattrs = new[] { global::Android.Resource.Attribute.TextSize, global::Android.Resource.Attribute.FontFamily, global::Android.Resource.Attribute.TextColor }; + using(TypedArray array = context.ObtainStyledAttributes(value.ResourceId, styleattrs)) + { + fontSize = context.FromPixels(array.GetDimensionPixelSize(0, -1)); + fontFamily = array.GetString(1); + androidColor = array.GetColor(2, defaultColor); + } + } + } + + if (fontSize > 0) + result.Setters.Add(new Setter { Property = Label.FontSizeProperty, Value = fontSize }); + + if (!string.IsNullOrEmpty(fontFamily)) + result.Setters.Add(new Setter { Property = Label.FontFamilyProperty, Value = fontFamily }); + + if (androidColor != defaultColor) + { + result.Setters.Add(new Setter { Property = Label.TextColorProperty, Value = Color.FromRgba(androidColor.R, androidColor.G, androidColor.B, androidColor.A) }); + } + + return result; + } + + void UpdateStyles() + { + _dictionary[Device.Styles.BodyStyleKey] = new Style(typeof(Label)); // do nothing, its fine + _dictionary[Device.Styles.TitleStyleKey] = GetStyle(global::Android.Resource.Attribute.TextAppearanceLarge); + _dictionary[Device.Styles.SubtitleStyleKey] = GetStyle(global::Android.Resource.Attribute.TextAppearanceMedium); + _dictionary[Device.Styles.CaptionStyleKey] = GetStyle(global::Android.Resource.Attribute.TextAppearanceSmall); + _dictionary[Device.Styles.ListItemTextStyleKey] = GetStyle(global::Android.Resource.Attribute.TextAppearanceListItem); + _dictionary[Device.Styles.ListItemDetailTextStyleKey] = GetStyle(global::Android.Resource.Attribute.TextAppearanceListItemSmall); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/TapGestureHandler.cs b/Xamarin.Forms.Platform.Android/TapGestureHandler.cs new file mode 100644 index 00000000..dcd8d6f7 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/TapGestureHandler.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Xamarin.Forms.Platform.Android +{ + internal class TapGestureHandler + { + public TapGestureHandler(Func<View> getView) + { + GetView = getView; + } + + Func<View> GetView { get; } + + public void OnSingleClick() + { + // only handle click if we don't have double tap registered + if (TapGestureRecognizers(2).Any()) + return; + + OnTap(1); + } + + public bool OnTap(int count) + { + View view = GetView(); + + if (view == null) + return false; + + IEnumerable<TapGestureRecognizer> gestureRecognizers = TapGestureRecognizers(count); + var result = false; + foreach (TapGestureRecognizer gestureRecognizer in gestureRecognizers) + { + gestureRecognizer.SendTapped(view); + result = true; + } + + return result; + } + + public IEnumerable<TapGestureRecognizer> TapGestureRecognizers(int count) + { + View view = GetView(); + if (view == null) + return Enumerable.Empty<TapGestureRecognizer>(); + + return view.GestureRecognizers.GetGesturesFor<TapGestureRecognizer>(recognizer => recognizer.NumberOfTapsRequired == count); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/ViewExtensions.cs b/Xamarin.Forms.Platform.Android/ViewExtensions.cs new file mode 100644 index 00000000..04a80ef2 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/ViewExtensions.cs @@ -0,0 +1,61 @@ +using Android.Content; +using Android.Graphics.Drawables; +using Android.OS; +using Android.Util; +using Android.Views; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android +{ + public static class ViewExtensions + { + static int s_apiLevel; + + public static void RemoveFromParent(this AView view) + { + if (view == null) + return; + if (view.Parent == null) + return; + ((ViewGroup)view.Parent).RemoveView(view); + } + + public static void SetBackground(this AView view, Drawable drawable) + { + if (s_apiLevel == 0) + s_apiLevel = (int)Build.VERSION.SdkInt; + + if (s_apiLevel < 16) + { +#pragma warning disable 618 + view.SetBackgroundDrawable(drawable); +#pragma warning restore 618 + } + else + view.Background = drawable; + } + + public static void SetWindowBackground(this AView view) + { + Context context = view.Context; + using(var background = new TypedValue()) + { + if (context.Theme.ResolveAttribute(global::Android.Resource.Attribute.WindowBackground, background, true)) + { + string type = context.Resources.GetResourceTypeName(background.ResourceId).ToLower(); + switch (type) + { + case "color": + global::Android.Graphics.Color color = context.Resources.GetColor(background.ResourceId); + view.SetBackgroundColor(color); + break; + case "drawable": + using(Drawable drawable = context.Resources.GetDrawable(background.ResourceId)) + view.SetBackgroundDrawable(drawable); + break; + } + } + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/ViewInitializedEventArgs.cs b/Xamarin.Forms.Platform.Android/ViewInitializedEventArgs.cs new file mode 100644 index 00000000..e6dc6a1d --- /dev/null +++ b/Xamarin.Forms.Platform.Android/ViewInitializedEventArgs.cs @@ -0,0 +1,11 @@ +using System; + +namespace Xamarin.Forms +{ + public class ViewInitializedEventArgs : EventArgs + { + public global::Android.Views.View NativeView { get; internal set; } + + public VisualElement View { get; internal set; } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/ViewPool.cs b/Xamarin.Forms.Platform.Android/ViewPool.cs new file mode 100644 index 00000000..b5c1ddc7 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/ViewPool.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using Android.Views; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android +{ + public class ViewPool : IDisposable + { + readonly Dictionary<Type, Stack<AView>> _freeViews = new Dictionary<Type, Stack<AView>>(); + readonly ViewGroup _viewGroup; + + bool _disposed; + + public ViewPool(ViewGroup viewGroup) + { + _viewGroup = viewGroup; + } + + public void Dispose() + { + if (_disposed) + return; + + foreach (Stack<AView> views in _freeViews.Values) + { + foreach (AView view in views) + view.Dispose(); + } + + _disposed = true; + } + + public void ClearChildren() + { + if (_disposed) + throw new ObjectDisposedException(null); + + ClearChildren(_viewGroup); + } + + public TView GetFreeView<TView>() where TView : AView + { + if (_disposed) + throw new ObjectDisposedException(null); + + Stack<AView> views; + if (_freeViews.TryGetValue(typeof(TView), out views) && views.Count > 0) + return (TView)views.Pop(); + + return null; + } + + void ClearChildren(ViewGroup group) + { + if (group == null) + return; + + int count = group.ChildCount; + for (var i = 0; i < count; i++) + { + AView child = group.GetChildAt(i); + + var g = child as ViewGroup; + if (g != null) + ClearChildren(g); + + Type childType = child.GetType(); + Stack<AView> stack; + if (!_freeViews.TryGetValue(childType, out stack)) + _freeViews[childType] = stack = new Stack<AView>(); + + stack.Push(child); + } + + group.RemoveAllViews(); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/ViewRenderer.cs b/Xamarin.Forms.Platform.Android/ViewRenderer.cs new file mode 100644 index 00000000..f5c094ba --- /dev/null +++ b/Xamarin.Forms.Platform.Android/ViewRenderer.cs @@ -0,0 +1,205 @@ +using System; +using System.ComponentModel; +using Android.App; +using Android.Views; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android +{ + public abstract class ViewRenderer : ViewRenderer<View, AView> + { + } + + public abstract class ViewRenderer<TView, TNativeView> : VisualElementRenderer<TView>, AView.IOnFocusChangeListener where TView : View where TNativeView : AView + { + ViewGroup _container; + + bool _disposed; + EventHandler<VisualElement.FocusRequestArgs> _focusChangeHandler; + + SoftInput _startingInputMode; + + internal bool HandleKeyboardOnFocus; + + public TNativeView Control { get; private set; } + + void IOnFocusChangeListener.OnFocusChange(AView v, bool hasFocus) + { + if (Element is Entry || Element is SearchBar || Element is Editor) + { + var isInViewCell = false; + Element parent = Element.RealParent; + while (!(parent is Page) && parent != null) + { + if (parent is Cell) + { + isInViewCell = true; + break; + } + parent = parent.RealParent; + } + + if (isInViewCell) + { + Window window = ((Activity)Context).Window; + if (hasFocus) + { + _startingInputMode = window.Attributes.SoftInputMode; + window.SetSoftInputMode(SoftInput.AdjustPan); + } + else + window.SetSoftInputMode(_startingInputMode); + } + } + OnNativeFocusChanged(hasFocus); + + ((IElementController)Element).SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, hasFocus); + } + + public override SizeRequest GetDesiredSize(int widthConstraint, int heightConstraint) + { + AView view = _container == this ? (AView)Control : _container; + view.Measure(widthConstraint, heightConstraint); + + return new SizeRequest(new Size(Control.MeasuredWidth, Control.MeasuredHeight), MinimumSize()); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + if (Control != null) + { + Control.RemoveFromParent(); + Control.Dispose(); + Control = null; + } + + if (_container != null && _container != this) + { + _container.RemoveFromParent(); + _container.Dispose(); + _container = null; + } + + _disposed = true; + } + + base.Dispose(disposing); + } + + protected override void OnElementChanged(ElementChangedEventArgs<TView> e) + { + base.OnElementChanged(e); + + if (_focusChangeHandler == null) + _focusChangeHandler = OnFocusChangeRequested; + + if (e.OldElement != null) + e.OldElement.FocusChangeRequested -= _focusChangeHandler; + + if (e.NewElement != null) + e.NewElement.FocusChangeRequested += _focusChangeHandler; + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == VisualElement.IsEnabledProperty.PropertyName) + UpdateIsEnabled(); + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + base.OnLayout(changed, l, t, r, b); + if (Control == null) + return; + + AView view = _container == this ? (AView)Control : _container; + + view.Measure(MeasureSpecFactory.MakeMeasureSpec(r - l, MeasureSpecMode.Exactly), MeasureSpecFactory.MakeMeasureSpec(b - t, MeasureSpecMode.Exactly)); + view.Layout(0, 0, r - l, b - t); + } + + protected override void OnRegisterEffect(PlatformEffect effect) + { + base.OnRegisterEffect(effect); + effect.Control = Control; + } + + protected override void SetAutomationId(string id) + { + if (Control == null) + base.SetAutomationId(id); + else + { + ContentDescription = id + "_Container"; + Control.ContentDescription = id; + } + } + + protected void SetNativeControl(TNativeView control) + { + SetNativeControl(control, this); + } + + internal virtual void OnFocusChangeRequested(object sender, VisualElement.FocusRequestArgs e) + { + if (Control == null) + return; + + if (e.Focus) + e.Result = Control.RequestFocus(); + else + { + e.Result = true; + Control.ClearFocus(); + } + + //handles keyboard on focus for Editor, Entry and SearchBar + if (HandleKeyboardOnFocus) + { + if (e.Focus) + Control.ShowKeyboard(); + else + Control.HideKeyboard(); + } + } + + internal virtual void OnNativeFocusChanged(bool hasFocus) + { + } + + internal override void SendVisualElementInitialized(VisualElement element, AView nativeView) + { + base.SendVisualElementInitialized(element, Control); + } + + internal void SetNativeControl(TNativeView control, ViewGroup container) + { + if (Control != null) + { + Control.OnFocusChangeListener = null; + RemoveView(Control); + } + + _container = container; + + Control = control; + + AView toAdd = container == this ? control : (AView)container; + AddView(toAdd, LayoutParams.MatchParent); + + Control.OnFocusChangeListener = this; + + UpdateIsEnabled(); + } + + void UpdateIsEnabled() + { + if (Control != null) + Control.Enabled = Element.IsEnabled; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/VisualElementChangedEventArgs.cs b/Xamarin.Forms.Platform.Android/VisualElementChangedEventArgs.cs new file mode 100644 index 00000000..de7c7e5d --- /dev/null +++ b/Xamarin.Forms.Platform.Android/VisualElementChangedEventArgs.cs @@ -0,0 +1,9 @@ +namespace Xamarin.Forms.Platform.Android +{ + public class VisualElementChangedEventArgs : ElementChangedEventArgs<VisualElement> + { + public VisualElementChangedEventArgs(VisualElement oldElement, VisualElement newElement) : base(oldElement, newElement) + { + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/VisualElementExtensions.cs b/Xamarin.Forms.Platform.Android/VisualElementExtensions.cs new file mode 100644 index 00000000..13f2fffa --- /dev/null +++ b/Xamarin.Forms.Platform.Android/VisualElementExtensions.cs @@ -0,0 +1,42 @@ +namespace Xamarin.Forms.Platform.Android +{ + public static class VisualElementExtensions + { + public static bool ShouldBeMadeClickable(this View view) + { + var shouldBeClickable = false; + for (var i = 0; i < view.GestureRecognizers.Count; i++) + { + IGestureRecognizer gesture = view.GestureRecognizers[i]; + if (gesture is TapGestureRecognizer || gesture is PinchGestureRecognizer || gesture is PanGestureRecognizer) + { + shouldBeClickable = true; + break; + } + } + + // do some evil + // This is required so that a layout only absorbs click events if it is not fully transparent + // However this is not desirable behavior in a ViewCell because it prevents the ViewCell from activating + if (view is Layout && view.BackgroundColor != Color.Transparent && view.BackgroundColor != Color.Default) + { + Element parent = view.RealParent; + var skip = false; + while (parent != null) + { + if (parent is ViewCell) + { + skip = true; + break; + } + parent = parent.RealParent; + } + + if (!skip) + shouldBeClickable = true; + } + + return shouldBeClickable; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/VisualElementPackager.cs b/Xamarin.Forms.Platform.Android/VisualElementPackager.cs new file mode 100644 index 00000000..e7e6ab6f --- /dev/null +++ b/Xamarin.Forms.Platform.Android/VisualElementPackager.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android +{ + public class VisualElementPackager : IDisposable + { + readonly EventHandler<ElementEventArgs> _childAddedHandler; + readonly EventHandler<ElementEventArgs> _childRemovedHandler; + readonly EventHandler _childReorderedHandler; + List<IVisualElementRenderer> _childViews; + + bool _disposed; + + IVisualElementRenderer _renderer; + + public VisualElementPackager(IVisualElementRenderer renderer) + { + if (renderer == null) + throw new ArgumentNullException("renderer"); + + _childAddedHandler = OnChildAdded; + _childRemovedHandler = OnChildRemoved; + _childReorderedHandler = OnChildrenReordered; + + _renderer = renderer; + _renderer.ElementChanged += (sender, args) => SetElement(args.OldElement, args.NewElement); + } + + public void Dispose() + { + if (_disposed) + return; + _disposed = true; + + if (_renderer != null) + { + if (_childViews != null) + { + _childViews.Clear(); + _childViews = null; + } + + _renderer.Element.ChildAdded -= _childAddedHandler; + _renderer.Element.ChildRemoved -= _childRemovedHandler; + + _renderer.Element.ChildrenReordered -= _childReorderedHandler; + _renderer = null; + } + } + + public void Load() + { + SetElement(null, _renderer.Element); + } + + void AddChild(VisualElement view, IVisualElementRenderer oldRenderer = null, RendererPool pool = null, bool sameChildren = false) + { + Performance.Start(); + + if (_childViews == null) + _childViews = new List<IVisualElementRenderer>(); + + IVisualElementRenderer renderer = oldRenderer; + if (pool != null) + renderer = pool.GetFreeRenderer(view); + if (renderer == null) + { + Performance.Start("New renderer"); + renderer = Platform.CreateRenderer(view); + Performance.Stop("New renderer"); + } + + if (renderer == oldRenderer) + { + Platform.SetRenderer(renderer.Element, null); + renderer.SetElement(view); + } + + Performance.Start("Set renderer"); + Platform.SetRenderer(view, renderer); + Performance.Stop("Set renderer"); + + Performance.Start("Add view"); + if (!sameChildren) + { + _renderer.ViewGroup.AddView(renderer.ViewGroup); + _childViews.Add(renderer); + } + Performance.Stop("Add view"); + + Performance.Stop(); + } + + void EnsureChildOrder() + { + for (var i = 0; i < _renderer.Element.LogicalChildren.Count; i++) + { + Element child = _renderer.Element.LogicalChildren[i]; + var element = (VisualElement)child; + if (element != null) + { + IVisualElementRenderer r = Platform.GetRenderer(element); + _renderer.ViewGroup.BringChildToFront(r.ViewGroup); + } + } + } + + void OnChildAdded(object sender, ElementEventArgs e) + { + var view = e.Element as VisualElement; + if (view != null) + AddChild(view); + if (_renderer.Element.LogicalChildren[_renderer.Element.LogicalChildren.Count - 1] != view) + EnsureChildOrder(); + } + + void OnChildRemoved(object sender, ElementEventArgs e) + { + Performance.Start(); + var view = e.Element as VisualElement; + if (view != null) + RemoveChild(view); + + Performance.Stop(); + } + + void OnChildrenReordered(object sender, EventArgs e) + { + EnsureChildOrder(); + } + + void RemoveChild(VisualElement view) + { + IVisualElementRenderer renderer = Platform.GetRenderer(view); + _childViews.Remove(renderer); + renderer.ViewGroup.RemoveFromParent(); + renderer.Dispose(); + } + + void SetElement(VisualElement oldElement, VisualElement newElement) + { + Performance.Start(); + + var sameChildrenTypes = false; + + ReadOnlyCollection<Element> newChildren = null, oldChildren = null; + + RendererPool pool = null; + if (oldElement != null) + { + if (newElement != null) + { + sameChildrenTypes = true; + + oldChildren = oldElement.LogicalChildren; + newChildren = newElement.LogicalChildren; + if (oldChildren.Count == newChildren.Count) + { + for (var i = 0; i < oldChildren.Count; i++) + { + if (oldChildren[i].GetType() != newChildren[i].GetType()) + { + sameChildrenTypes = false; + break; + } + } + } + else + sameChildrenTypes = false; + } + + oldElement.ChildAdded -= _childAddedHandler; + oldElement.ChildRemoved -= _childRemovedHandler; + + oldElement.ChildrenReordered -= _childReorderedHandler; + + if (!sameChildrenTypes) + { + _childViews = new List<IVisualElementRenderer>(); + pool = new RendererPool(_renderer, oldElement); + pool.ClearChildrenRenderers(); + } + } + + if (newElement != null) + { + Performance.Start("Setup"); + newElement.ChildAdded += _childAddedHandler; + newElement.ChildRemoved += _childRemovedHandler; + + newElement.ChildrenReordered += _childReorderedHandler; + + newChildren = newChildren ?? newElement.LogicalChildren; + + for (var i = 0; i < newChildren.Count; i++) + { + IVisualElementRenderer oldRenderer = null; + if (oldChildren != null && sameChildrenTypes) + oldRenderer = _childViews[i]; + + AddChild((VisualElement)newChildren[i], oldRenderer, pool, sameChildrenTypes); + } + +#if DEBUG + //if (renderer.Element.LogicalChildren.Any() && renderer.ViewGroup.ChildCount != renderer.Element.LogicalChildren.Count) + // throw new InvalidOperationException ("SetElement did not create the correct number of children"); +#endif + Performance.Stop("Setup"); + } + + Performance.Stop(); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/VisualElementRenderer.cs b/Xamarin.Forms.Platform.Android/VisualElementRenderer.cs new file mode 100644 index 00000000..41c97b18 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/VisualElementRenderer.cs @@ -0,0 +1,392 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using Android.Support.V4.View; +using Android.Views; +using AView = Android.Views.View; + +namespace Xamarin.Forms.Platform.Android +{ + public abstract class VisualElementRenderer<TElement> : FormsViewGroup, IVisualElementRenderer, AView.IOnTouchListener, AView.IOnClickListener, IEffectControlProvider where TElement : VisualElement + { + readonly List<EventHandler<VisualElementChangedEventArgs>> _elementChangedHandlers = new List<EventHandler<VisualElementChangedEventArgs>>(); + + readonly Lazy<GestureDetector> _gestureDetector; + readonly PanGestureHandler _panGestureHandler; + readonly PinchGestureHandler _pinchGestureHandler; + + readonly TapGestureHandler _tapGestureHandler; + + bool _clickable; + NotifyCollectionChangedEventHandler _collectionChangeHandler; + + VisualElementRendererFlags _flags = VisualElementRendererFlags.AutoPackage | VisualElementRendererFlags.AutoTrack; + + InnerGestureListener _gestureListener; + VisualElementPackager _packager; + PropertyChangedEventHandler _propertyChangeHandler; + Lazy<ScaleGestureDetector> _scaleDetector; + VelocityTracker _velocity; + + protected VisualElementRenderer() : base(Forms.Context) + { + _tapGestureHandler = new TapGestureHandler(() => View); + _panGestureHandler = new PanGestureHandler(() => View, Context.FromPixels); + _pinchGestureHandler = new PinchGestureHandler(() => View); + + _gestureDetector = + new Lazy<GestureDetector>( + () => + new GestureDetector( + _gestureListener = + new InnerGestureListener(_tapGestureHandler.OnTap, _tapGestureHandler.TapGestureRecognizers, _panGestureHandler.OnPan, _panGestureHandler.OnPanStarted, _panGestureHandler.OnPanComplete))); + + _scaleDetector = + new Lazy<ScaleGestureDetector>( + () => new ScaleGestureDetector(Context, new InnerScaleListener(_pinchGestureHandler.OnPinch, _pinchGestureHandler.OnPinchStarted, _pinchGestureHandler.OnPinchEnded), Handler)); + } + + public TElement Element { get; private set; } + + protected bool AutoPackage + { + get { return (_flags & VisualElementRendererFlags.AutoPackage) != 0; } + set + { + if (value) + _flags |= VisualElementRendererFlags.AutoPackage; + else + _flags &= ~VisualElementRendererFlags.AutoPackage; + } + } + + protected bool AutoTrack + { + get { return (_flags & VisualElementRendererFlags.AutoTrack) != 0; } + set + { + if (value) + _flags |= VisualElementRendererFlags.AutoTrack; + else + _flags &= ~VisualElementRendererFlags.AutoTrack; + } + } + + View View + { + get { return Element as View; } + } + + void IEffectControlProvider.RegisterEffect(Effect effect) + { + var platformEffect = effect as PlatformEffect; + if (platformEffect != null) + OnRegisterEffect(platformEffect); + } + + void IOnClickListener.OnClick(AView v) + { + _tapGestureHandler.OnSingleClick(); + } + + bool IOnTouchListener.OnTouch(AView v, MotionEvent e) + { + var handled = false; + if (_pinchGestureHandler.IsPinchSupported) + { + if (!_scaleDetector.IsValueCreated) + ScaleGestureDetectorCompat.SetQuickScaleEnabled(_scaleDetector.Value, true); + handled = _scaleDetector.Value.OnTouchEvent(e); + } + return _gestureDetector.Value.OnTouchEvent(e) || handled; + } + + VisualElement IVisualElementRenderer.Element + { + get { return Element; } + } + + event EventHandler<VisualElementChangedEventArgs> IVisualElementRenderer.ElementChanged + { + add { _elementChangedHandlers.Add(value); } + remove { _elementChangedHandlers.Remove(value); } + } + + public virtual SizeRequest GetDesiredSize(int widthConstraint, int heightConstraint) + { + Measure(widthConstraint, heightConstraint); + return new SizeRequest(new Size(MeasuredWidth, MeasuredHeight), MinimumSize()); + } + + void IVisualElementRenderer.SetElement(VisualElement element) + { + if (!(element is TElement)) + throw new ArgumentException("element is not of type " + typeof(TElement), "element"); + + SetElement((TElement)element); + } + + public VisualElementTracker Tracker { get; private set; } + + public void UpdateLayout() + { + Performance.Start(); + if (Tracker != null) + Tracker.UpdateLayout(); + + Performance.Stop(); + } + + public ViewGroup ViewGroup + { + get { return this; } + } + + public event EventHandler<ElementChangedEventArgs<TElement>> ElementChanged; + + public void SetElement(TElement element) + { + if (element == null) + throw new ArgumentNullException("element"); + + TElement oldElement = Element; + Element = element; + + Performance.Start(); + + if (oldElement != null) + { + oldElement.PropertyChanged -= _propertyChangeHandler; + UnsubscribeGestureRecognizers(oldElement); + } + + // element may be allowed to be passed as null in the future + if (element != null) + { + Color currentColor = oldElement != null ? oldElement.BackgroundColor : Color.Default; + if (element.BackgroundColor != currentColor) + UpdateBackgroundColor(); + } + + if (_propertyChangeHandler == null) + _propertyChangeHandler = OnElementPropertyChanged; + + element.PropertyChanged += _propertyChangeHandler; + SubscribeGestureRecognizers(element); + + if (oldElement == null) + { + SetOnClickListener(this); + SetOnTouchListener(this); + SoundEffectsEnabled = false; + } + + InputTransparent = Element.InputTransparent; + + // must be updated AFTER SetOnClickListener is called + // SetOnClickListener implicitly calls Clickable = true + UpdateGestureRecognizers(true); + + OnElementChanged(new ElementChangedEventArgs<TElement>(oldElement, element)); + + if (AutoPackage && _packager == null) + SetPackager(new VisualElementPackager(this)); + + if (AutoTrack && Tracker == null) + SetTracker(new VisualElementTracker(this)); + + if (element != null) + SendVisualElementInitialized(element, this); + + var controller = (IElementController)oldElement; + if (controller != null && controller.EffectControlProvider == this) + controller.EffectControlProvider = null; + + controller = element; + if (controller != null) + controller.EffectControlProvider = this; + + if (element != null && !string.IsNullOrEmpty(element.AutomationId)) + SetAutomationId(element.AutomationId); + + Performance.Stop(); + } + + protected override void Dispose(bool disposing) + { + if ((_flags & VisualElementRendererFlags.Disposed) != 0) + return; + _flags |= VisualElementRendererFlags.Disposed; + + if (disposing) + { + if (Tracker != null) + { + Tracker.Dispose(); + Tracker = null; + } + + if (_packager != null) + { + _packager.Dispose(); + _packager = null; + } + + if (_scaleDetector != null && _scaleDetector.IsValueCreated) + { + _scaleDetector.Value.Dispose(); + _scaleDetector = null; + } + + if (_gestureListener != null) + { + _gestureListener.Dispose(); + _gestureListener = null; + } + + int count = ChildCount; + for (var i = 0; i < count; i++) + { + AView child = GetChildAt(i); + child.Dispose(); + } + + RemoveAllViews(); + + if (Element != null) + { + Element.PropertyChanged -= _propertyChangeHandler; + UnsubscribeGestureRecognizers(Element); + + if (Platform.GetRenderer(Element) == this) + Platform.SetRenderer(Element, null); + + Element = null; + } + } + + base.Dispose(disposing); + } + + protected virtual Size MinimumSize() + { + return new Size(); + } + + protected virtual void OnElementChanged(ElementChangedEventArgs<TElement> e) + { + var args = new VisualElementChangedEventArgs(e.OldElement, e.NewElement); + for (var i = 0; i < _elementChangedHandlers.Count; i++) + _elementChangedHandlers[i](this, args); + + EventHandler<ElementChangedEventArgs<TElement>> changed = ElementChanged; + if (changed != null) + changed(this, e); + } + + protected virtual void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName) + UpdateBackgroundColor(); + else if (e.PropertyName == VisualElement.InputTransparentProperty.PropertyName) + InputTransparent = Element.InputTransparent; + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + if (Element == null) + return; + + ReadOnlyCollection<Element> children = Element.LogicalChildren; + for (var i = 0; i < children.Count; i++) + { + var visualElement = children[i] as VisualElement; + if (visualElement == null) + continue; + + IVisualElementRenderer renderer = Platform.GetRenderer(visualElement); + renderer?.UpdateLayout(); + } + } + + protected virtual void OnRegisterEffect(PlatformEffect effect) + { + effect.Container = this; + } + + protected virtual void SetAutomationId(string id) + { + ContentDescription = id; + } + + protected void SetPackager(VisualElementPackager packager) + { + _packager = packager; + packager.Load(); + } + + protected void SetTracker(VisualElementTracker tracker) + { + Tracker = tracker; + } + + protected virtual void UpdateBackgroundColor() + { + SetBackgroundColor(Element.BackgroundColor.ToAndroid()); + } + + internal virtual void SendVisualElementInitialized(VisualElement element, AView nativeView) + { + element.SendViewInitialized(nativeView); + } + + void HandleGestureRecognizerCollectionChanged(object sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs) + { + UpdateGestureRecognizers(); + } + + void SubscribeGestureRecognizers(VisualElement element) + { + var view = element as View; + if (view == null) + return; + + if (_collectionChangeHandler == null) + _collectionChangeHandler = HandleGestureRecognizerCollectionChanged; + + var observableCollection = (ObservableCollection<IGestureRecognizer>)view.GestureRecognizers; + observableCollection.CollectionChanged += _collectionChangeHandler; + } + + void UnsubscribeGestureRecognizers(VisualElement element) + { + var view = element as View; + if (view == null || _collectionChangeHandler == null) + return; + + var observableCollection = (ObservableCollection<IGestureRecognizer>)view.GestureRecognizers; + observableCollection.CollectionChanged -= _collectionChangeHandler; + } + + void UpdateClickable(bool force = false) + { + var view = Element as View; + if (view == null) + return; + + bool newValue = view.ShouldBeMadeClickable(); + if (force || _clickable != newValue) + Clickable = newValue; + } + + void UpdateGestureRecognizers(bool forceClick = false) + { + if (View == null) + return; + + UpdateClickable(forceClick); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/VisualElementRendererFlags.cs b/Xamarin.Forms.Platform.Android/VisualElementRendererFlags.cs new file mode 100644 index 00000000..cfabfe77 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/VisualElementRendererFlags.cs @@ -0,0 +1,12 @@ +using System; + +namespace Xamarin.Forms.Platform.Android +{ + [Flags] + public enum VisualElementRendererFlags + { + Disposed = 1 << 0, + AutoTrack = 1 << 1, + AutoPackage = 1 << 2 + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/VisualElementTracker.cs b/Xamarin.Forms.Platform.Android/VisualElementTracker.cs new file mode 100644 index 00000000..2b9815d3 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/VisualElementTracker.cs @@ -0,0 +1,396 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Android.Content; +using Android.OS; +using Android.Views; +using AView = Android.Views.View; +using Object = Java.Lang.Object; + +namespace Xamarin.Forms.Platform.Android +{ + public class VisualElementTracker : IDisposable + { + readonly EventHandler<EventArg<VisualElement>> _batchCommittedHandler; + readonly IList<string> _batchedProperties = new List<string>(); + readonly PropertyChangedEventHandler _propertyChangedHandler; + Context _context; + + bool _disposed; + + VisualElement _element; + bool _initialUpdateNeeded = true; + bool _layoutNeeded; + IVisualElementRenderer _renderer; + + public VisualElementTracker(IVisualElementRenderer renderer) + { + if (renderer == null) + throw new ArgumentNullException("renderer"); + + _batchCommittedHandler = HandleRedrawNeeded; + _propertyChangedHandler = HandlePropertyChanged; + + _renderer = renderer; + _context = renderer.ViewGroup.Context; + _renderer.ElementChanged += RendererOnElementChanged; + + VisualElement view = renderer.Element; + SetElement(null, view); + + renderer.ViewGroup.SetCameraDistance(3600); + + renderer.ViewGroup.AddOnAttachStateChangeListener(AttachTracker.Instance); + } + + public void Dispose() + { + if (_disposed) + return; + _disposed = true; + + SetElement(_element, null); + + if (_renderer != null) + { + _renderer.ElementChanged -= RendererOnElementChanged; + _renderer.ViewGroup.RemoveOnAttachStateChangeListener(AttachTracker.Instance); + _renderer = null; + _context = null; + } + } + + public void UpdateLayout() + { + Performance.Start(); + + VisualElement view = _renderer.Element; + AView aview = _renderer.ViewGroup; + + var x = (int)_context.ToPixels(view.X); + var y = (int)_context.ToPixels(view.Y); + var width = (int)_context.ToPixels(view.Width); + var height = (int)_context.ToPixels(view.Height); + + var formsViewGroup = aview as FormsViewGroup; + if (formsViewGroup == null) + { + Performance.Start("Measure"); + aview.Measure(MeasureSpecFactory.MakeMeasureSpec(width, MeasureSpecMode.Exactly), MeasureSpecFactory.MakeMeasureSpec(height, MeasureSpecMode.Exactly)); + Performance.Stop("Measure"); + + Performance.Start("Layout"); + aview.Layout(x, y, x + width, y + height); + Performance.Stop("Layout"); + } + else + { + Performance.Start("MeasureAndLayout"); + formsViewGroup.MeasureAndLayout(MeasureSpecFactory.MakeMeasureSpec(width, MeasureSpecMode.Exactly), MeasureSpecFactory.MakeMeasureSpec(height, MeasureSpecMode.Exactly), x, y, x + width, y + height); + Performance.Stop("MeasureAndLayout"); + } + + Performance.Stop(); + + //On Width or Height changes, the anchors needs to be updated + UpdateAnchorX(); + UpdateAnchorY(); + } + + void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == Layout.IsClippedToBoundsProperty.PropertyName) + { + UpdateClipToBounds(); + return; + } + + if (_renderer.Element.Batched) + { + if (e.PropertyName == VisualElement.XProperty.PropertyName || e.PropertyName == VisualElement.YProperty.PropertyName || e.PropertyName == VisualElement.WidthProperty.PropertyName || + e.PropertyName == VisualElement.HeightProperty.PropertyName) + _layoutNeeded = true; + else if (e.PropertyName == VisualElement.AnchorXProperty.PropertyName || e.PropertyName == VisualElement.AnchorYProperty.PropertyName || e.PropertyName == VisualElement.ScaleProperty.PropertyName || + e.PropertyName == VisualElement.RotationProperty.PropertyName || e.PropertyName == VisualElement.RotationXProperty.PropertyName || e.PropertyName == VisualElement.RotationYProperty.PropertyName || + e.PropertyName == VisualElement.IsVisibleProperty.PropertyName || e.PropertyName == VisualElement.OpacityProperty.PropertyName || + e.PropertyName == VisualElement.TranslationXProperty.PropertyName || e.PropertyName == VisualElement.TranslationYProperty.PropertyName) + { + if (!_batchedProperties.Contains(e.PropertyName)) + _batchedProperties.Add(e.PropertyName); + } + return; + } + + if (e.PropertyName == VisualElement.XProperty.PropertyName || e.PropertyName == VisualElement.YProperty.PropertyName || e.PropertyName == VisualElement.WidthProperty.PropertyName || + e.PropertyName == VisualElement.HeightProperty.PropertyName) + MaybeRequestLayout(); + else if (e.PropertyName == VisualElement.AnchorXProperty.PropertyName) + UpdateAnchorX(); + else if (e.PropertyName == VisualElement.AnchorYProperty.PropertyName) + UpdateAnchorY(); + else if (e.PropertyName == VisualElement.ScaleProperty.PropertyName) + UpdateScale(); + else if (e.PropertyName == VisualElement.RotationProperty.PropertyName) + UpdateRotation(); + else if (e.PropertyName == VisualElement.RotationXProperty.PropertyName) + UpdateRotationX(); + else if (e.PropertyName == VisualElement.RotationYProperty.PropertyName) + UpdateRotationY(); + else if (e.PropertyName == VisualElement.IsVisibleProperty.PropertyName) + UpdateIsVisible(); + else if (e.PropertyName == VisualElement.OpacityProperty.PropertyName) + UpdateOpacity(); + else if (e.PropertyName == VisualElement.TranslationXProperty.PropertyName) + UpdateTranslationX(); + else if (e.PropertyName == VisualElement.TranslationYProperty.PropertyName) + UpdateTranslationY(); + } + + void HandleRedrawNeeded(object sender, EventArg<VisualElement> e) + { + foreach (string propertyName in _batchedProperties) + HandlePropertyChanged(this, new PropertyChangedEventArgs(propertyName)); + _batchedProperties.Clear(); + + if (_layoutNeeded) + MaybeRequestLayout(); + _layoutNeeded = false; + } + + void HandleViewAttachedToWindow() + { + if (_initialUpdateNeeded) + { + UpdateNativeView(this, EventArgs.Empty); + _initialUpdateNeeded = false; + } + + UpdateClipToBounds(); + } + + void MaybeRequestLayout() + { + var isInLayout = false; + if ((int)Build.VERSION.SdkInt >= 18) + isInLayout = _renderer.ViewGroup.IsInLayout; + + if (!isInLayout && !_renderer.ViewGroup.IsLayoutRequested) + _renderer.ViewGroup.RequestLayout(); + } + + void RendererOnElementChanged(object sender, VisualElementChangedEventArgs args) + { + SetElement(args.OldElement, args.NewElement); + } + + void SetElement(VisualElement oldElement, VisualElement newElement) + { + if (oldElement != null) + { + oldElement.BatchCommitted -= _batchCommittedHandler; + oldElement.PropertyChanged -= _propertyChangedHandler; + _context = null; + } + + _element = newElement; + if (newElement != null) + { + newElement.BatchCommitted += _batchCommittedHandler; + newElement.PropertyChanged += _propertyChangedHandler; + _context = _renderer.ViewGroup.Context; + + if (oldElement != null) + { + AView view = _renderer.ViewGroup; + + // ReSharper disable CompareOfFloatsByEqualityOperator + if (oldElement.AnchorX != newElement.AnchorX) + UpdateAnchorX(); + if (oldElement.AnchorY != newElement.AnchorY) + UpdateAnchorY(); + if (oldElement.IsVisible != newElement.IsVisible) + UpdateIsVisible(); + if (oldElement.IsEnabled != newElement.IsEnabled) + view.Enabled = newElement.IsEnabled; + if (oldElement.Opacity != newElement.Opacity) + UpdateOpacity(); + if (oldElement.Rotation != newElement.Rotation) + UpdateRotation(); + if (oldElement.RotationX != newElement.RotationX) + UpdateRotationX(); + if (oldElement.RotationY != newElement.RotationY) + UpdateRotationY(); + if (oldElement.Scale != newElement.Scale) + UpdateScale(); + // ReSharper restore CompareOfFloatsByEqualityOperator + + _initialUpdateNeeded = false; + } + } + } + + void UpdateAnchorX() + { + VisualElement view = _renderer.Element; + AView aview = _renderer.ViewGroup; + + float currentPivot = aview.PivotX; + var target = (float)(view.AnchorX * _context.ToPixels(view.Width)); + if (currentPivot != target) + aview.PivotX = target; + } + + void UpdateAnchorY() + { + VisualElement view = _renderer.Element; + AView aview = _renderer.ViewGroup; + + float currentPivot = aview.PivotY; + var target = (float)(view.AnchorY * _context.ToPixels(view.Height)); + if (currentPivot != target) + aview.PivotY = target; + } + + void UpdateClipToBounds() + { + var layout = _renderer.Element as Layout; + var parent = _renderer.ViewGroup.Parent as ViewGroup; + + if (parent == null || layout == null) + return; + + bool shouldClip = layout.IsClippedToBounds; + + if ((int)Build.VERSION.SdkInt >= 18 && parent.ClipChildren == shouldClip) + return; + + parent.SetClipChildren(shouldClip); + parent.Invalidate(); + } + + void UpdateIsVisible() + { + VisualElement view = _renderer.Element; + AView aview = _renderer.ViewGroup; + + if (view.IsVisible && aview.Visibility != ViewStates.Visible) + aview.Visibility = ViewStates.Visible; + if (!view.IsVisible && aview.Visibility != ViewStates.Gone) + aview.Visibility = ViewStates.Gone; + } + + void UpdateNativeView(object sender, EventArgs e) + { + Performance.Start(); + + VisualElement view = _renderer.Element; + AView aview = _renderer.ViewGroup; + + if (aview is FormsViewGroup) + { + var formsViewGroup = (FormsViewGroup)aview; + formsViewGroup.SendBatchUpdate((float)(view.AnchorX * _context.ToPixels(view.Width)), (float)(view.AnchorY * _context.ToPixels(view.Height)), + (int)(view.IsVisible ? ViewStates.Visible : ViewStates.Invisible), view.IsEnabled, (float)view.Opacity, (float)view.Rotation, (float)view.RotationX, (float)view.RotationY, (float)view.Scale, + _context.ToPixels(view.TranslationX), _context.ToPixels(view.TranslationY)); + } + else + { + UpdateAnchorX(); + UpdateAnchorY(); + UpdateIsVisible(); + + if (view.IsEnabled != aview.Enabled) + aview.Enabled = view.IsEnabled; + + UpdateOpacity(); + UpdateRotation(); + UpdateRotationX(); + UpdateRotationY(); + UpdateScale(); + UpdateTranslationX(); + UpdateTranslationY(); + } + + Performance.Stop(); + } + + void UpdateOpacity() + { + Performance.Start(); + + VisualElement view = _renderer.Element; + AView aview = _renderer.ViewGroup; + + aview.Alpha = (float)view.Opacity; + + Performance.Stop(); + } + + void UpdateRotation() + { + VisualElement view = _renderer.Element; + AView aview = _renderer.ViewGroup; + + aview.Rotation = (float)view.Rotation; + } + + void UpdateRotationX() + { + VisualElement view = _renderer.Element; + AView aview = _renderer.ViewGroup; + + aview.RotationX = (float)view.RotationX; + } + + void UpdateRotationY() + { + VisualElement view = _renderer.Element; + AView aview = _renderer.ViewGroup; + + aview.RotationY = (float)view.RotationY; + } + + void UpdateScale() + { + VisualElement view = _renderer.Element; + AView aview = _renderer.ViewGroup; + + aview.ScaleX = (float)view.Scale; + aview.ScaleY = (float)view.Scale; + } + + void UpdateTranslationX() + { + VisualElement view = _renderer.Element; + AView aview = _renderer.ViewGroup; + + aview.TranslationX = _context.ToPixels(view.TranslationX); + } + + void UpdateTranslationY() + { + VisualElement view = _renderer.Element; + AView aview = _renderer.ViewGroup; + + aview.TranslationY = _context.ToPixels(view.TranslationY); + } + + class AttachTracker : Object, AView.IOnAttachStateChangeListener + { + public static readonly AttachTracker Instance = new AttachTracker(); + + public void OnViewAttachedToWindow(AView attachedView) + { + var renderer = attachedView as IVisualElementRenderer; + if (renderer == null || renderer.Tracker == null) + return; + + renderer.Tracker.HandleViewAttachedToWindow(); + } + + public void OnViewDetachedFromWindow(AView detachedView) + { + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj b/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj new file mode 100644 index 00000000..05fee22d --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj @@ -0,0 +1,259 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <ProductVersion>8.0.30703</ProductVersion> + <SchemaVersion>2.0</SchemaVersion> + <ProjectGuid>{0E16E70A-D6DD-4323-AD5D-363ABFF42D6A}</ProjectGuid> + <ProjectTypeGuids>{EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids> + <OutputType>Library</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>Xamarin.Forms.Platform.Android</RootNamespace> + <AssemblyName>Xamarin.Forms.Platform.Android</AssemblyName> + <FileAlignment>512</FileAlignment> + <AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile> + <GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies> + <TargetFrameworkVersion>v6.0</TargetFrameworkVersion> + <AndroidUseLatestPlatformSdk>true</AndroidUseLatestPlatformSdk> + <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> + <RestorePackages>true</RestorePackages> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + <DebugSymbols>true</DebugSymbols> + <DebugType>full</DebugType> + <Optimize>false</Optimize> + <OutputPath>bin\Debug\</OutputPath> + <DefineConstants>TRACE;DEBUG</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <OutputPath>bin\Release\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Turkey|AnyCPU'"> + <DebugSymbols>true</DebugSymbols> + <OutputPath>bin\Turkey\</OutputPath> + <DefineConstants>TRACE;DEBUG</DefineConstants> + <DebugType>full</DebugType> + <PlatformTarget>AnyCPU</PlatformTarget> + <GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies> + <ErrorReport>prompt</ErrorReport> + <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + <ItemGroup> + <Reference Include="Mono.Android" /> + <Reference Include="mscorlib" /> + <Reference Include="System" /> + <Reference Include="System.Core" /> + <Reference Include="System.Runtime.Serialization" /> + <Reference Include="System.Xml.Linq" /> + <Reference Include="System.Xml" /> + <Reference Include="System.Net.Http" /> + <Reference Include="Xamarin.Android.Support.v7.CardView"> + <HintPath>..\packages\Xamarin.Android.Support.v7.CardView.23.1.1.1\lib\MonoAndroid403\Xamarin.Android.Support.v7.CardView.dll</HintPath> + </Reference> + <Reference Include="Xamarin.Android.Support.v4"> + <HintPath>..\packages\Xamarin.Android.Support.v4.23.1.1.1\lib\MonoAndroid403\Xamarin.Android.Support.v4.dll</HintPath> + </Reference> + <Reference Include="Xamarin.Android.Support.v7.AppCompat"> + <HintPath>..\packages\Xamarin.Android.Support.v7.AppCompat.23.1.1.1\lib\MonoAndroid403\Xamarin.Android.Support.v7.AppCompat.dll</HintPath> + </Reference> + <Reference Include="Xamarin.Android.Support.Design"> + <HintPath>..\packages\Xamarin.Android.Support.Design.23.1.1.1\lib\MonoAndroid403\Xamarin.Android.Support.Design.dll</HintPath> + </Reference> + <Reference Include="Xamarin.GooglePlayServices.AppIndexing, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\Xamarin.GooglePlayServices.AppIndexing.29.0.0.1\lib\MonoAndroid41\Xamarin.GooglePlayServices.AppIndexing.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="Xamarin.GooglePlayServices.Base, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\Xamarin.GooglePlayServices.Base.29.0.0.1\lib\MonoAndroid41\Xamarin.GooglePlayServices.Base.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="Xamarin.GooglePlayServices.Basement, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\Xamarin.GooglePlayServices.Basement.29.0.0.1\lib\MonoAndroid41\Xamarin.GooglePlayServices.Basement.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="Xamarin.Android.Support.v7.RecyclerView, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\Xamarin.Android.Support.v7.RecyclerView.23.1.1.1\lib\MonoAndroid403\Xamarin.Android.Support.v7.RecyclerView.dll</HintPath> + <Private>True</Private> + </Reference> + </ItemGroup> + <ItemGroup> + <Compile Include="..\Xamarin.Forms.Core\Properties\GlobalAssemblyInfo.cs"> + <Link>Properties\GlobalAssemblyInfo.cs</Link> + </Compile> + <Compile Include="AndroidApplicationLifecycleState.cs" /> + <Compile Include="AndroidTitleBarVisibility.cs" /> + <Compile Include="AppCompat\FormsViewPager.cs" /> + <Compile Include="AppCompat\FragmentContainer.cs" /> + <Compile Include="AppCompat\FrameRenderer.cs" /> + <Compile Include="AppCompat\IManageFragments.cs" /> + <Compile Include="AppCompat\Platform.cs" /> + <Compile Include="AppCompat\Resource.cs" /> + <Compile Include="CellAdapter.cs" /> + <Compile Include="Cells\EntryCellEditText.cs" /> + <Compile Include="Cells\EntryCellView.cs" /> + <Compile Include="Cells\ImageCellRenderer.cs" /> + <Compile Include="Cells\SwitchCellView.cs" /> + <Compile Include="Deserializer.cs" /> + <Compile Include="AppCompat\FormsAppCompatActivity.cs" /> + <Compile Include="ElementChangedEventArgs.cs" /> + <Compile Include="ExportCellAttribute.cs" /> + <Compile Include="ExportImageSourceHandlerAttribute.cs" /> + <Compile Include="ExportRendererAttribute.cs" /> + <Compile Include="FormsApplicationActivity.cs" /> + <Compile Include="AndroidActivity.cs" /> + <Compile Include="AndroidTicker.cs" /> + <Compile Include="Cells\BaseCellView.cs" /> + <Compile Include="Cells\CellFactory.cs" /> + <Compile Include="Cells\CellRenderer.cs" /> + <Compile Include="Cells\EntryCellRenderer.cs" /> + <Compile Include="Cells\SwitchCellRenderer.cs" /> + <Compile Include="Cells\TextCellRenderer.cs" /> + <Compile Include="Cells\ViewCellRenderer.cs" /> + <Compile Include="ColorExtensions.cs" /> + <Compile Include="ContextExtensions.cs" /> + <Compile Include="GetDesiredSizeDelegate.cs" /> + <Compile Include="IDeviceInfoProvider.cs" /> + <Compile Include="InnerGestureListener.cs" /> + <Compile Include="InnerScaleListener.cs" /> + <Compile Include="IPlatformLayout.cs" /> + <Compile Include="AppCompat\ButtonRenderer.cs" /> + <Compile Include="AppCompat\MasterDetailPageRenderer.cs" /> + <Compile Include="AppCompat\NavigationPageRenderer.cs" /> + <Compile Include="AppCompat\SwitchRenderer.cs" /> + <Compile Include="AppCompat\TabbedPageRenderer.cs" /> + <Compile Include="MeasureSpecFactory.cs" /> + <Compile Include="NativeViewWrapper.cs" /> + <Compile Include="NativeViewWrapperRenderer.cs" /> + <Compile Include="OnLayoutDelegate.cs" /> + <Compile Include="OnMeasureDelegate.cs" /> + <Compile Include="PanGestureHandler.cs" /> + <Compile Include="PinchGestureHandler.cs" /> + <Compile Include="PlatformEffect.cs" /> + <Compile Include="LayoutExtensions.cs" /> + <Compile Include="Renderers\AHorizontalScrollView.cs" /> + <Compile Include="Renderers\ButtonDrawable.cs" /> + <Compile Include="Renderers\CarouselViewExtensions.cs" /> + <Compile Include="Renderers\CarouselViewRenderer.cs" /> + <Compile Include="Renderers\AlignmentExtensions.cs" /> + <Compile Include="IStartActivityForResult.cs" /> + <Compile Include="Renderers\ConditionalFocusLayout.cs" /> + <Compile Include="Renderers\DescendantFocusToggler.cs" /> + <Compile Include="Renderers\EditorEditText.cs" /> + <Compile Include="Renderers\EntryEditText.cs" /> + <Compile Include="Renderers\FileImageSourceHandler.cs" /> + <Compile Include="Renderers\FormattedStringExtensions.cs" /> + <Compile Include="Renderers\FormsImageView.cs" /> + <Compile Include="Renderers\FormsTextView.cs" /> + <Compile Include="Renderers\FormsWebChromeClient.cs" /> + <Compile Include="Renderers\GenericAnimatorListener.cs" /> + <Compile Include="Renderers\IDescendantFocusToggler.cs" /> + <Compile Include="Renderers\IImageSourceHandler.cs" /> + <Compile Include="Renderers\ImageExtensions.cs" /> + <Compile Include="Renderers\ImageLoaderSourceHandler.cs" /> + <Compile Include="Renderers\IntVector.cs" /> + <Compile Include="Renderers\ItemViewAdapter.cs" /> + <Compile Include="Renderers\IToolbarButton.cs" /> + <Compile Include="Renderers\KeyboardExtensions.cs" /> + <Compile Include="AppCompat\PickerRenderer.cs" /> + <Compile Include="AppCompat\ViewRenderer.cs" /> + <Compile Include="Renderers\MasterDetailContainer.cs" /> + <Compile Include="Renderers\MeasureSpecification.cs" /> + <Compile Include="Renderers\MeasureSpecificationType.cs" /> + <Compile Include="Renderers\PageContainer.cs" /> + <Compile Include="Renderers\PhysicalLayoutManager.cs" /> + <Compile Include="Renderers\ScrollViewContainer.cs" /> + <Compile Include="Renderers\StreamImagesourceHandler.cs" /> + <Compile Include="Renderers\ToolbarButton.cs" /> + <Compile Include="Renderers\ToolbarImageButton.cs" /> + <Compile Include="Renderers\ViewGroupExtensions.cs" /> + <Compile Include="TapGestureHandler.cs" /> + <Compile Include="ViewInitializedEventArgs.cs" /> + <Compile Include="VisualElementChangedEventArgs.cs" /> + <Compile Include="RendererPool.cs" /> + <Compile Include="ViewPool.cs" /> + <Compile Include="GenericMenuClickListener.cs" /> + <Compile Include="IVisualElementRenderer.cs" /> + <Compile Include="ViewRenderer.cs" /> + <Compile Include="Platform.cs" /> + <Compile Include="PlatformRenderer.cs" /> + <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="Forms.cs" /> + <Compile Include="RendererFactory.cs" /> + <Compile Include="Renderers\ActionSheetRenderer.cs" /> + <Compile Include="Renderers\ActivityIndicatorRenderer.cs" /> + <Compile Include="Renderers\BoxRenderer.cs" /> + <Compile Include="Renderers\ButtonRenderer.cs" /> + <Compile Include="Renderers\CarouselPageRenderer.cs" /> + <Compile Include="Renderers\DatePickerRenderer.cs" /> + <Compile Include="Renderers\EditorRenderer.cs" /> + <Compile Include="Renderers\EntryRenderer.cs" /> + <Compile Include="Renderers\FontExtensions.cs" /> + <Compile Include="Renderers\FrameRenderer.cs" /> + <Compile Include="Renderers\ImageRenderer.cs" /> + <Compile Include="Renderers\LabelRenderer.cs" /> + <Compile Include="Renderers\ListViewAdapter.cs" /> + <Compile Include="Renderers\ListViewRenderer.cs" /> + <Compile Include="Renderers\MasterDetailRenderer.cs" /> + <Compile Include="Renderers\NavigationMenuRenderer.cs" /> + <Compile Include="Renderers\NavigationRenderer.cs" /> + <Compile Include="Renderers\ObjectJavaBox.cs" /> + <Compile Include="Renderers\CarouselPageAdapter.cs" /> + <Compile Include="Renderers\PageRenderer.cs" /> + <Compile Include="Renderers\ProgressBarRenderer.cs" /> + <Compile Include="Renderers\ScrollViewRenderer.cs" /> + <Compile Include="Renderers\SearchBarRenderer.cs" /> + <Compile Include="Renderers\SliderRenderer.cs" /> + <Compile Include="Renderers\StepperRenderer.cs" /> + <Compile Include="Renderers\SwitchRenderer.cs" /> + <Compile Include="Renderers\TabbedRenderer.cs" /> + <Compile Include="Renderers\TableViewModelRenderer.cs" /> + <Compile Include="Renderers\TableViewRenderer.cs" /> + <Compile Include="Renderers\TimePickerRenderer.cs" /> + <Compile Include="Renderers\ToolbarRenderer.cs" /> + <Compile Include="Renderers\WebViewRenderer.cs" /> + <Compile Include="ResourceManager.cs" /> + <Compile Include="ViewExtensions.cs" /> + <Compile Include="VisualElementExtensions.cs" /> + <Compile Include="VisualElementPackager.cs" /> + <Compile Include="VisualElementRenderer.cs" /> + <Compile Include="VisualElementRendererFlags.cs" /> + <Compile Include="VisualElementTracker.cs" /> + <Compile Include="Renderers\PickerRenderer.cs" /> + <Compile Include="KeyboardManager.cs" /> + <Compile Include="ResourcesProvider.cs" /> + <Compile Include="Extensions.cs" /> + <Compile Include="Renderers\OpenGLViewRenderer.cs" /> + <Compile Include="AppCompat\CarouselPageRenderer.cs" /> + <Compile Include="AppCompat\FormsFragmentPagerAdapter.cs" /> + </ItemGroup> + <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" /> + <!-- To modify your build process, add your task inside one of the targets below and uncomment it. + Other similar extension points exist, see Microsoft.Common.targets. + <Target Name="BeforeBuild"> + </Target> + <Target Name="AfterBuild"> + </Target> + --> + <ItemGroup> + <ProjectReference Include="..\Xamarin.Forms.Core\Xamarin.Forms.Core.csproj"> + <Project>{57B8B73D-C3B5-4C42-869E-7B2F17D354AC}</Project> + <Name>Xamarin.Forms.Core</Name> + </ProjectReference> + <ProjectReference Include="..\Xamarin.Forms.Platform.Android.FormsViewGroup\Xamarin.Forms.Platform.Android.FormsViewGroup.csproj"> + <Project>{3b72465b-acae-43ae-9327-10f372fe5f80}</Project> + <Name>Xamarin.Forms.Platform.Android.FormsViewGroup</Name> + </ProjectReference> + </ItemGroup> + <ItemGroup> + <None Include="packages.config" /> + </ItemGroup> + <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" /> +</Project>
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.nuspec b/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.nuspec new file mode 100644 index 00000000..1e5676e3 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.nuspec @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<package > + <metadata> + <id>Xamarin.Forms.Platform.Android</id> + <version>$version$</version> + <title>Xamarin.Forms Android Platform Renderers</title> + <authors>Xamarin Inc.</authors> + <owners>Xamarin Inc.</owners> + <requireLicenseAcceptance>false</requireLicenseAcceptance> + <description>Xamarin.Forms platform abstruction library for mobile applications.</description> + <releaseNotes></releaseNotes> + <copyright>Copyright 2013</copyright> + </metadata> + <files> + <file src="..\GooglePlayServices\*.dll" target="lib\MonoAndroid40" /> + </files> +</package> diff --git a/Xamarin.Forms.Platform.Android/packages.config b/Xamarin.Forms.Platform.Android/packages.config new file mode 100644 index 00000000..d7a54544 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/packages.config @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<packages> + <package id="Xamarin.Android.Support.Design" version="23.1.1.1" targetFramework="monoandroid60" /> + <package id="Xamarin.Android.Support.v4" version="23.1.1.1" targetFramework="monoandroid60" /> + <package id="Xamarin.Android.Support.v7.AppCompat" version="23.1.1.1" targetFramework="monoandroid60" /> + <package id="Xamarin.Android.Support.v7.CardView" version="23.1.1.1" targetFramework="monoandroid60" /> + <package id="Xamarin.Android.Support.v7.RecyclerView" version="23.1.1.1" targetFramework="monoandroid60" /> + <package id="Xamarin.GooglePlayServices.AppIndexing" version="29.0.0.1" targetFramework="monoandroid60" /> + <package id="Xamarin.GooglePlayServices.Base" version="29.0.0.1" targetFramework="monoandroid60" /> + <package id="Xamarin.GooglePlayServices.Basement" version="29.0.0.1" targetFramework="monoandroid60" /> +</packages>
\ No newline at end of file |