summaryrefslogtreecommitdiff
path: root/Xamarin.Forms.Platform.Android/Renderers
diff options
context:
space:
mode:
Diffstat (limited to 'Xamarin.Forms.Platform.Android/Renderers')
-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
67 files changed, 7841 insertions, 0 deletions
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