summaryrefslogtreecommitdiff
path: root/Xamarin.Forms.Platform.Android
diff options
context:
space:
mode:
authorJason Smith <jason.smith@xamarin.com>2016-03-22 13:02:25 -0700
committerJason Smith <jason.smith@xamarin.com>2016-03-22 16:13:41 -0700
commit17fdde66d94155fc62a034fa6658995bef6fd6e5 (patch)
treeb5e5073a2a7b15cdbe826faa5c763e270a505729 /Xamarin.Forms.Platform.Android
downloadxamarin-forms-17fdde66d94155fc62a034fa6658995bef6fd6e5.tar.gz
xamarin-forms-17fdde66d94155fc62a034fa6658995bef6fd6e5.tar.bz2
xamarin-forms-17fdde66d94155fc62a034fa6658995bef6fd6e5.zip
Initial import
Diffstat (limited to 'Xamarin.Forms.Platform.Android')
-rw-r--r--Xamarin.Forms.Platform.Android/AndroidActivity.cs9
-rw-r--r--Xamarin.Forms.Platform.Android/AndroidApplicationLifecycleState.cs14
-rw-r--r--Xamarin.Forms.Platform.Android/AndroidTicker.cs43
-rw-r--r--Xamarin.Forms.Platform.Android/AndroidTitleBarVisibility.cs8
-rw-r--r--Xamarin.Forms.Platform.Android/AppCompat/ButtonRenderer.cs243
-rw-r--r--Xamarin.Forms.Platform.Android/AppCompat/CarouselPageRenderer.cs155
-rw-r--r--Xamarin.Forms.Platform.Android/AppCompat/FormsAppCompatActivity.cs452
-rw-r--r--Xamarin.Forms.Platform.Android/AppCompat/FormsFragmentPagerAdapter.cs59
-rw-r--r--Xamarin.Forms.Platform.Android/AppCompat/FormsViewPager.cs34
-rw-r--r--Xamarin.Forms.Platform.Android/AppCompat/FragmentContainer.cs111
-rw-r--r--Xamarin.Forms.Platform.Android/AppCompat/FrameRenderer.cs215
-rw-r--r--Xamarin.Forms.Platform.Android/AppCompat/IManageFragments.cs13
-rw-r--r--Xamarin.Forms.Platform.Android/AppCompat/MasterDetailPageRenderer.cs355
-rw-r--r--Xamarin.Forms.Platform.Android/AppCompat/NavigationPageRenderer.cs789
-rw-r--r--Xamarin.Forms.Platform.Android/AppCompat/PickerRenderer.cs141
-rw-r--r--Xamarin.Forms.Platform.Android/AppCompat/Platform.cs394
-rw-r--r--Xamarin.Forms.Platform.Android/AppCompat/Resource.cs31
-rw-r--r--Xamarin.Forms.Platform.Android/AppCompat/SwitchRenderer.cs92
-rw-r--r--Xamarin.Forms.Platform.Android/AppCompat/TabbedPageRenderer.cs322
-rw-r--r--Xamarin.Forms.Platform.Android/AppCompat/ViewRenderer.cs7
-rw-r--r--Xamarin.Forms.Platform.Android/CellAdapter.cs327
-rw-r--r--Xamarin.Forms.Platform.Android/Cells/BaseCellView.cs208
-rw-r--r--Xamarin.Forms.Platform.Android/Cells/CellFactory.cs41
-rw-r--r--Xamarin.Forms.Platform.Android/Cells/CellRenderer.cs131
-rw-r--r--Xamarin.Forms.Platform.Android/Cells/EntryCellEditText.cs45
-rw-r--r--Xamarin.Forms.Platform.Android/Cells/EntryCellRenderer.cs119
-rw-r--r--Xamarin.Forms.Platform.Android/Cells/EntryCellView.cs145
-rw-r--r--Xamarin.Forms.Platform.Android/Cells/ImageCellRenderer.cs37
-rw-r--r--Xamarin.Forms.Platform.Android/Cells/SwitchCellRenderer.cs55
-rw-r--r--Xamarin.Forms.Platform.Android/Cells/SwitchCellView.cs25
-rw-r--r--Xamarin.Forms.Platform.Android/Cells/TextCellRenderer.cs78
-rw-r--r--Xamarin.Forms.Platform.Android/Cells/ViewCellRenderer.cs178
-rw-r--r--Xamarin.Forms.Platform.Android/ColorExtensions.cs40
-rw-r--r--Xamarin.Forms.Platform.Android/ContextExtensions.cs58
-rw-r--r--Xamarin.Forms.Platform.Android/Deserializer.cs89
-rw-r--r--Xamarin.Forms.Platform.Android/ElementChangedEventArgs.cs17
-rw-r--r--Xamarin.Forms.Platform.Android/ExportCellAttribute.cs12
-rw-r--r--Xamarin.Forms.Platform.Android/ExportImageSourceHandlerAttribute.cs12
-rw-r--r--Xamarin.Forms.Platform.Android/ExportRendererAttribute.cs12
-rw-r--r--Xamarin.Forms.Platform.Android/Extensions.cs40
-rw-r--r--Xamarin.Forms.Platform.Android/Forms.cs514
-rw-r--r--Xamarin.Forms.Platform.Android/FormsApplicationActivity.cs319
-rw-r--r--Xamarin.Forms.Platform.Android/GenericMenuClickListener.cs22
-rw-r--r--Xamarin.Forms.Platform.Android/GetDesiredSizeDelegate.cs4
-rw-r--r--Xamarin.Forms.Platform.Android/IDeviceInfoProvider.cs12
-rw-r--r--Xamarin.Forms.Platform.Android/IPlatformLayout.cs7
-rw-r--r--Xamarin.Forms.Platform.Android/IStartActivityForResult.cs14
-rw-r--r--Xamarin.Forms.Platform.Android/IVisualElementRenderer.cs22
-rw-r--r--Xamarin.Forms.Platform.Android/InnerGestureListener.cs146
-rw-r--r--Xamarin.Forms.Platform.Android/InnerScaleListener.cs68
-rw-r--r--Xamarin.Forms.Platform.Android/KeyboardManager.cs41
-rw-r--r--Xamarin.Forms.Platform.Android/LayoutExtensions.cs19
-rw-r--r--Xamarin.Forms.Platform.Android/MeasureSpecFactory.cs20
-rw-r--r--Xamarin.Forms.Platform.Android/NativeViewWrapper.cs22
-rw-r--r--Xamarin.Forms.Platform.Android/NativeViewWrapperRenderer.cs61
-rw-r--r--Xamarin.Forms.Platform.Android/OnLayoutDelegate.cs4
-rw-r--r--Xamarin.Forms.Platform.Android/OnMeasureDelegate.cs4
-rw-r--r--Xamarin.Forms.Platform.Android/PanGestureHandler.cs69
-rw-r--r--Xamarin.Forms.Platform.Android/PinchGestureHandler.cs76
-rw-r--r--Xamarin.Forms.Platform.Android/Platform.cs1056
-rw-r--r--Xamarin.Forms.Platform.Android/PlatformEffect.cs9
-rw-r--r--Xamarin.Forms.Platform.Android/PlatformRenderer.cs81
-rw-r--r--Xamarin.Forms.Platform.Android/Properties/AssemblyInfo.cs65
-rw-r--r--Xamarin.Forms.Platform.Android/RendererFactory.cs13
-rw-r--r--Xamarin.Forms.Platform.Android/RendererPool.cs88
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/AHorizontalScrollView.cs62
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/ActionSheetRenderer.cs83
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/ActivityIndicatorRenderer.cs59
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/AlignmentExtensions.cs33
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/BoxRenderer.cs43
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/ButtonDrawable.cs150
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/ButtonRenderer.cs252
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/CarouselPageAdapter.cs155
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/CarouselPageRenderer.cs100
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/CarouselViewExtensions.cs69
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/CarouselViewRenderer.cs353
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/ConditionalFocusLayout.cs48
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/DatePickerRenderer.cs154
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/DescendantFocusToggler.cs42
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/EditorEditText.cs42
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/EditorRenderer.cs140
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/EntryEditText.cs42
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/EntryRenderer.cs189
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/FileImageSourceHandler.cs17
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/FontExtensions.cs93
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/FormattedStringExtensions.cs90
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/FormsImageView.cs36
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/FormsTextView.cs41
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/FormsWebChromeClient.cs74
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/FrameRenderer.cs195
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/GenericAnimatorListener.cs41
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/IDescendantFocusToggler.cs9
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/IImageSourceHandler.cs12
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/IToolbarButton.cs7
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/ImageExtensions.cs26
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/ImageLoaderSourceHandler.cs22
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/ImageRenderer.cs108
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/IntVector.cs86
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/ItemViewAdapter.cs89
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/KeyboardExtensions.cs77
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/LabelRenderer.cs213
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/ListViewAdapter.cs531
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/ListViewRenderer.cs355
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/MasterDetailContainer.cs138
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/MasterDetailRenderer.cs348
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/MeasureSpecification.cs41
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/MeasureSpecificationType.cs13
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/NavigationMenuRenderer.cs152
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/NavigationRenderer.cs302
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/ObjectJavaBox.cs14
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/OpenGLViewRenderer.cs102
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/PageContainer.cs30
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/PageRenderer.cs71
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/PhysicalLayoutManager.cs531
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/PickerRenderer.cs165
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/ProgressBarRenderer.cs40
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/ScrollViewContainer.cs75
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/ScrollViewRenderer.cs331
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/SearchBarRenderer.cs236
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/SliderRenderer.cs98
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/StepperRenderer.cs94
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/StreamImagesourceHandler.cs22
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/SwitchRenderer.cs83
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/TabbedRenderer.cs75
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/TableViewModelRenderer.cs228
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/TableViewRenderer.cs44
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/TimePickerRenderer.cs95
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/ToolbarButton.cs36
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/ToolbarImageButton.cs41
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/ToolbarRenderer.cs65
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/ViewGroupExtensions.cs27
-rw-r--r--Xamarin.Forms.Platform.Android/Renderers/WebViewRenderer.cs206
-rw-r--r--Xamarin.Forms.Platform.Android/ResourceManager.cs71
-rw-r--r--Xamarin.Forms.Platform.Android/ResourcesProvider.cs68
-rw-r--r--Xamarin.Forms.Platform.Android/TapGestureHandler.cs52
-rw-r--r--Xamarin.Forms.Platform.Android/ViewExtensions.cs61
-rw-r--r--Xamarin.Forms.Platform.Android/ViewInitializedEventArgs.cs11
-rw-r--r--Xamarin.Forms.Platform.Android/ViewPool.cs79
-rw-r--r--Xamarin.Forms.Platform.Android/ViewRenderer.cs205
-rw-r--r--Xamarin.Forms.Platform.Android/VisualElementChangedEventArgs.cs9
-rw-r--r--Xamarin.Forms.Platform.Android/VisualElementExtensions.cs42
-rw-r--r--Xamarin.Forms.Platform.Android/VisualElementPackager.cs217
-rw-r--r--Xamarin.Forms.Platform.Android/VisualElementRenderer.cs392
-rw-r--r--Xamarin.Forms.Platform.Android/VisualElementRendererFlags.cs12
-rw-r--r--Xamarin.Forms.Platform.Android/VisualElementTracker.cs396
-rw-r--r--Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj259
-rw-r--r--Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.nuspec17
-rw-r--r--Xamarin.Forms.Platform.Android/packages.config11
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)
+ {
+ }
+ }
+ }