diff options
Diffstat (limited to 'Xamarin.Forms.Platform.Android/Renderers')
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 |