Erhan Ballıeker

Asp.Net Core API Backend ve Xamarin.Forms İle Kelime Oyunu Bölüm 4 (Game Component – Custom Grid – Android Grid View – Circle Progress Bar)

Selamlar,

Son bir kaç yazıdır Xamarin.Forms ile geliştirdiğimiz kelime oyunun dan ve kullandığımız teknolojilerden bahsetmiştim.Şimdi de oyun ekranından bahsetmek üzere karşınızdayım.

Burada oyun componenti için custom renderer yazarken nelerle karşılaştık ne şekilde aştık bunlardan bahsetmek istiyorum.

Önce oyun ekranını bir hatırlayalım

WhatsApp Image 2019-05-31 at 22.10.46

Burada iki adet custom component mevcut.

  • Custom Circle Progress Bar (Zaman geri sayacı olarak kullandığımız)
  • Custom Grid

Önce Circle ProgressBar dan bahsedeyim.

public class CircleProgressBar : BoxView
    {
        public readonly BindableProperty BackColorProperty = BindableProperty.Create(nameof(BackColor), typeof(Color), typeof(CircleProgressBar), Color.Transparent);
        public readonly BindableProperty ForeColorProperty = BindableProperty.Create(nameof(ForeColor), typeof(Color), typeof(CircleProgressBar), Color.Transparent);
        public readonly BindableProperty BarHeightProperty = BindableProperty.Create(nameof(BarHeight), typeof(double), typeof(CircleProgressBar), default(double));
        public readonly BindableProperty MinimunProperty = BindableProperty.Create(nameof(Minimun), typeof(int), typeof(CircleProgressBar), default(int));
        public readonly BindableProperty MaximunProperty = BindableProperty.Create(nameof(Maximun), typeof(int), typeof(CircleProgressBar), default(int),propertyChanged: (b, o, n) =>
        {
            var bar = (CircleProgressBar)b;
            bar.Maximun = (int)n;
        });
        public readonly BindableProperty ValueProperty = BindableProperty.Create(nameof(Value), typeof(int), typeof(CircleProgressBar), default(int),
            BindingMode.TwoWay,
               (BindableProperty.ValidateValueDelegate)null,
               (obj, oldValue, newValue) => {
                   var bar = obj as CircleProgressBar;
                   if (bar.BindingContext is MainPageViewModel context)
                   {
                       bar.Value = context.RemainingTime;
                   }
               },
               (BindableProperty.BindingPropertyChangingDelegate)null,
               (BindableProperty.CoerceValueDelegate)null,
               (BindableProperty.CreateDefaultValueDelegate)null);

        public readonly BindableProperty AnimationDurationProperty = BindableProperty.Create(nameof(AnimationDuration), typeof(int), typeof(CircleProgressBar), default(int));
        public readonly BindableProperty TextSizeProperty = BindableProperty.Create(nameof(TextSize), typeof(int), typeof(CircleProgressBar), default(int));
        public readonly BindableProperty TextMarginProperty = BindableProperty.Create(nameof(TextMargin), typeof(int), typeof(CircleProgressBar), default(int));
        public readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(CircleProgressBar), string.Empty, propertyChanged: (b, o, n) =>
        {
            var bar = (CircleProgressBar)b;
            if (bar.BindingContext is MainPageViewModel context)
            {
                bar.Text = context.RemainingTime.ToString();
            }
        });
        public readonly BindableProperty TextColorProperty = BindableProperty.Create(nameof(TextColor), typeof(Color), typeof(CircleProgressBar), Color.Black);

        public CircleProgressBar()
        {
        }

        public Color BackColor
        {
            get { return (Color)GetValue(BackColorProperty); }
            set { SetValue(BackColorProperty, value); }
        }

        public Color ForeColor
        {
            get { return (Color)GetValue(ForeColorProperty); }
            set { SetValue(ForeColorProperty, value); }
        }

        public double BarHeight
        {
            get { return (double)GetValue(BarHeightProperty); }
            set { SetValue(BarHeightProperty, value); }
        }

        public int Minimun
        {
            get { return (int)GetValue(MinimunProperty); }
            set { SetValue(MinimunProperty, value); }
        }

        public int Maximun
        {
            get { return (int)GetValue(MaximunProperty); }
            set { SetValue(MaximunProperty, value); }
        }

        public int Value
        {
            get { return (int)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }

        public int AnimationDuration
        {
            get { return (int)GetValue(AnimationDurationProperty); }
            set { SetValue(AnimationDurationProperty, value); }
        }

        public int TextSize
        {
            get { return (int)GetValue(TextSizeProperty); }
            set { SetValue(TextSizeProperty, value); }
        }

        public int TextMargin
        {
            get { return (int)GetValue(TextMarginProperty); }
            set { SetValue(TextMarginProperty, value); }
        }

        public string Text
        {
            get { return GetValue(TextProperty).ToString(); }
            set { SetValue(TextProperty, value); }
        }

        public Color TextColor
        {
            get { return (Color)GetValue(TextColorProperty); }
            set { SetValue(TextColorProperty, value); }
        }
    }

Burada göreceğiniz gibi aslında circular progress bar oluşturmak için BoxView dan miras aldık. Gerekli bir kaç property i bindable olarak tanımladıktan sonra platforms spesifik taraflara geçip Custom Renderer larımızı yazmaya başladık

Aşağıda iOS Custom Renderer kodlarını görüyorsunuz.

BoxView ios tarafından temel de UIView kullandığı için ister BoxRenderer dan isterseniz VisualElementRenderer dan miras alarak custom renderer a başlayabilirsiniz.BoxRenderer da en nihayetinde VisualElementRenderer dan miras alıyor zaten.

[assembly: ExportRenderer(typeof(CircleProgressBar), typeof(CircleProgressBarRenderer))]    
namespace...iOS.CustomRenderers
{
    public class CircleProgressBarRenderer : VisualElementRenderer
    {
        CAShapeLayer backgroundCircle;
        CAShapeLayer indicatorCircle;
        UILabel indicatorLabel;
        CGSize indicatorLabelSize;
        int indicatorFontSize;

        double startAngle = 1.5 * Math.PI;

        public CircleProgressBarRenderer()  { }

        protected override void OnElementChanged(ElementChangedEventArgs e)
        {
            base.OnElementChanged(e);

            if (Element != null)
            {
                indicatorFontSize = Element.TextSize;

                backgroundCircle = new CAShapeLayer();

                CreateBackgroundCircle();

                CreateIndicatorCircle();

                CreateIndicatorLabel();
            }
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == "Text")
            {
                if (Element.BindingContext is MainPageViewModel model)
                {
                    indicatorLabel.Text = model.RemainingTime.ToString();
                    var val = CalculateValue();
                    animateBackwards(val);
                }
            }
        }

        public override void LayoutSubviews()
        {
            base.LayoutSubviews();

            double radius = CreatePathAndReturnRadius();

            double heightRatio = (radius - Element.TextMargin) / indicatorLabelSize.Height;
            double widthRatio = (radius - Element.TextMargin) / indicatorLabelSize.Width;
            double ratio = 1;
            if (heightRatio < widthRatio)                                   ratio = (radius - Element.TextMargin) / indicatorLabelSize.Height;             else                 ratio = (radius - Element.TextMargin) / indicatorLabelSize.Width;             indicatorFontSize = (int)Math.Round(indicatorFontSize * ratio, 0, MidpointRounding.ToEven);             indicatorLabel.Font = UIFont.SystemFontOfSize(indicatorFontSize);             indicatorLabel.InvalidateIntrinsicContentSize();             indicatorLabelSize = indicatorLabel.IntrinsicContentSize;             indicatorLabel.Frame = new CGRect((Frame.Width / 2) - (indicatorLabelSize.Width / 2), (Frame.Height / 2) - (indicatorLabelSize.Height / 2), indicatorLabelSize.Width, indicatorLabelSize.Height);             this.AddSubview(indicatorLabel);             animate();         }         private double CalculateValue()         {             double min = Element.Minimun;             double max = Element.Maximun;             double current = Element.Value;             double range = max - min;             return current / range > 1 ? 1 : current / range;
        }

        private void CreateIndicatorLabel()
        {
            indicatorLabel = new UILabel();
            indicatorLabel.AdjustsFontSizeToFitWidth = true;
            indicatorLabel.Font = UIFont.SystemFontOfSize(indicatorFontSize);
            indicatorLabel.Text = Element.Text.ToString();
            indicatorLabel.TextColor = Element.TextColor.ToUIColor();
            indicatorLabel.TextAlignment = UITextAlignment.Center;
            indicatorLabelSize = indicatorLabel.IntrinsicContentSize;
        }

        private void CreateIndicatorCircle()
        {
            indicatorCircle = new CAShapeLayer();
            indicatorCircle.StrokeColor = Element.ForeColor.ToCGColor();
            indicatorCircle.FillColor = UIColor.Clear.CGColor;
            indicatorCircle.LineWidth = new nfloat(Element.BarHeight);
            indicatorCircle.Frame = this.Bounds;
            indicatorCircle.LineCap = CAShapeLayer.CapButt;
            this.Layer.AddSublayer(indicatorCircle);
        }

        private void CreateBackgroundCircle()
        {
            backgroundCircle.StrokeColor = Element.BackColor.ToCGColor();
            backgroundCircle.FillColor = UIColor.Clear.CGColor;
            backgroundCircle.LineWidth = new nfloat(Element.BarHeight);
            backgroundCircle.Frame = this.Bounds;
            this.Layer.AddSublayer(backgroundCircle);
        }

        private double CreatePathAndReturnRadius()
        {
            var radius = (Math.Min(Frame.Size.Width, Frame.Size.Height) - backgroundCircle.LineWidth - 2) / 2;
            var circlePath = new UIBezierPath();
            circlePath.AddArc(new CGPoint(Frame.Size.Width / 2, Frame.Size.Height / 2), (nfloat)radius, (nfloat)startAngle, (nfloat)(startAngle + 2 * Math.PI), true);
            backgroundCircle.Path = circlePath.CGPath;
            indicatorCircle.Path = circlePath.CGPath;
            backgroundCircle.StrokeEnd = new nfloat(1.5);
            indicatorCircle.StrokeEnd = new nfloat(1.5);//new nfloat(CalculateValue());
            return radius;
        }

        private void animate()
        {
            var animation = new CABasicAnimation();
            animation.KeyPath = "strokeEnd";
            animation.Duration = Element.AnimationDuration / 1000;
            animation.From = new NSNumber(0.0);
            animation.To = new NSNumber(CalculateValue());
            animation.TimingFunction = CAMediaTimingFunction.FromName(CAMediaTimingFunction.EaseOut);
            indicatorCircle.StrokeStart = new nfloat(0.0);
            indicatorCircle.StrokeEnd = new nfloat(CalculateValue());
            indicatorCircle.AddAnimation(animation, "appear");
        }

        private void animateBackwards(double val)
        {
            var animation = new CABasicAnimation();
            animation.KeyPath = "strokeEnd";
            animation.Duration = Element.AnimationDuration / 1000;
            animation.From = new NSNumber(val);
            animation.To = new NSNumber(val - 0.00185);
            animation.TimingFunction = CAMediaTimingFunction.FromName(CAMediaTimingFunction.EaseOut);
            indicatorCircle.StrokeStart = new nfloat(0.0);
            indicatorCircle.StrokeEnd = new nfloat(1.5);
            indicatorCircle.AddAnimation(animation, "appear");
        }
    }
}

iOS tarafında CAShapeLayer sınıfı ekranda birşeyler çizmek için kullanacak olduğunuz namespace.

Biz de ekran xamarin forms tarafında renk, font size vs gibi bazı propertyleri alarak, bir yuvarlak simit oluşturuyoruz. Tam ortasına denk gelecek şekilde hesaplayarak bir adet UILabel i simitin ortasına yerleştiriyoruz.

Sonrasında iki farklı animasyon olarak, birincisi ekran ilk açıldığında sıfırdan maksimum noktasına gelecek şekilde animate edip, daha sonra her bir saniye geçtiğinde çizilmesi gereken kısmı hesaplayıp ters yönde bir animasyon çalıştırıyoruz.

OnElementPropertyChanged olayında da hem UILabel ın text ini hem de geri şekilde yapılacak olan anismayonumuzu oynatıyoruz. OnElementPropertyChanged olayı bildiğiniz gibi bir custom renderer yazarken, forms tarafında oluşturmuş olduğunuz bindable propertylerın değerlerinin değişmesi sonucu platform spesifik taraflarda bu olayı tetikliyor. Dolayısı ile bir custom renderer yazdığınız zaman, oluşturmuş olduğunuz componentin bazı değerleri runtime içerisinde değişiyor ve buna göre aksiyonlar almanız gerekiyorsa bu event içerisinde ilgili aksiyonlarınızı tetikleyebilirsiniz.

Android tarafında da durum şöyle.

[assembly: ExportRenderer(typeof(CircleProgressBar), typeof(CircleProgressBarRenderer))]

namespace ...Droid.CustomRenderers
{
    public class CircleProgressBarRenderer : ViewRenderer<CircleProgressBar, ProgressBar>
    {
        private ProgressBar pBar;
        private Drawable pBarBackDrawable;
        private Drawable pBarForeDrawable;
        public CircleProgressBarRenderer(Context context) : base(context)
        {
            SetWillNotDraw(false);
        }

        protected override void OnElementChanged(ElementChangedEventArgs e)
        {
            base.OnElementChanged(e);
            if (Control == null)
            {
                pBar = CreateNativeControl();
                SetNativeControl(pBar);
                CreateAnimation();
            }
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == "Text")
            {
                if (Element.BindingContext is MainPageViewModel model)
                {
                    CreateAnimationCountDown(model.RemainingTime);
                    Draw(new Canvas());
                }
            }
        }

        protected override ProgressBar CreateNativeControl()
        {
            pBarBackDrawable = DrawableCompat.Wrap(Resources.GetDrawable("CircularProgress_background"));
            pBarForeDrawable = DrawableCompat.Wrap(Resources.GetDrawable("CircularProgress_drawable"));

            DrawableCompat.SetTint(pBarBackDrawable, Element.BackColor.ToAndroid());
            DrawableCompat.SetTint(pBarForeDrawable, Element.ForeColor.ToAndroid());

            var nativeControl = new ProgressBar(Context, null, Android.Resource.Attribute.ProgressBarStyleHorizontal)
            {
                Indeterminate = false,
                Max = Element.Maximun,
                ProgressDrawable = pBarForeDrawable,
                Rotation = -90f,
                LayoutParameters = new LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent),
            };

            nativeControl.SetBackground(pBarBackDrawable);

            return nativeControl;
        }

        public override void Draw(Canvas canvas)
        {
            base.Draw(canvas);
        }

        protected override void OnDraw(Canvas canvas)
        {
            base.Invalidate();
            base.OnDraw(canvas);

            Rect bounds = new Rect();
            TextPaint paint = new TextPaint();
            paint.Color = Element.TextColor.ToAndroid();
            paint.TextSize = Element.TextSize;
            paint.GetTextBounds(Element.Text.ToString(), 0, Element.Text.ToString().Length, bounds);
            if (((this.Width / 2) - (Element.TextMargin * 4)) < bounds.Width())
            {
                float ratio = (float)((this.Width / 2) - Element.TextMargin * 4) / (float)bounds.Width();
                paint.TextSize = paint.TextSize * ratio;
                paint.GetTextBounds(Element.Text.ToString(), 0, Element.Text.ToString().Length, bounds);
            }

            int x = this.Width / 2 - bounds.CenterX();
            int y = this.Height / 2 - bounds.CenterY();
            canvas.DrawText(Element.Text.ToString(), x, y, paint);
        }

        private void CreateAnimation()
        {
            ObjectAnimator anim = ObjectAnimator.OfInt(pBar, "progress", Element.Minimun, Element.Value);
            anim.SetDuration(Element.AnimationDuration);
            anim.SetInterpolator(new DecelerateInterpolator());
            anim.Start();
        }

        private void CreateAnimationCountDown(int val)
        {
            ObjectAnimator anim = ObjectAnimator.OfInt(pBar, "progress", val, val - 1);
            anim.SetDuration(Element.AnimationDuration);
            anim.SetInterpolator(new DecelerateInterpolator());
            anim.Start();
        }
    }
}

Android tarafında ise işler kısmen daha kolay çünkü zaten bir ProgressBar widget ı var. Bu tarafta ise bu widget ın ortasına Text i basmak biraz zor oldu, OnDraw metodunu override edip tam ortaya gelecek şekilde bir canvas çizerek devam ettik.

Nihayetinde yukarıdaki resimde ki gibi, renkleri gibi bazı özellikleri ile oynayabildiğimiz ileri geri animasyonla hareke edebilen, güzel bir circle progress bar ımız olmuş oldu her iki platformda da.

Gelelim Grid View ın kendisine. Burada en çok dikkat çeken şey benim için şu oldu.

xamarin.forms.platform.ios içerisinde ki GetControl metodu ve bu metodun android tarafında olmayışı.

Çünkü ilerleyişe şu şekilde başladık;

  • Xamarin.Forms tarafında normal Grid den miras alan bir component yazıp, istediğimiz ekstra özellikleri buraya ekleyelim
  • Zaten harf dizilimlerini de burada yaptıktan sonra, Platform spesifik taraflara geçerek, Touch eventleri ezip ilgili harf e denk gelen o touch ile ilgili işlemlerimizi yapalım

Bu işleyiş iOS tarafında çok güzel çalıştı. Forms tarafında yaptığımız CustomGrid, içi dolu bir şekilde iOS tarafında GetControl dediğimizde elimizdeydi. Ekrandaki tüm UIView lar içerisinde dönüp istediğimiz işlemi istediğimiz şekilde yapabildik. Her şey çok güzeldi.

taa ki android tarafında ki renderer kısmına geçene kadar.

İki taraftada ViewRenderer kullanmamıza rağmen, Android tarafta nereden miras alırsak alalım, elimize Forms tarafında içini butonlarla doldurduğumuz halde bir grid gelmedi. Bir şekilde her ne denediysek, forms taraftaki içi dolu Grid e ulaşamadık.

Bu yüzden tüm GridView baştan oluşturup ekrana sıfırdan bir komponent yazıyormuş eklemek zorunda kaldık.

GridView ın forms tarafı şu şekilde.

 public class CustomGrid : Grid
    {
        public CustomGrid()
        {
            SwipedButtonList = new List();
        }

        public List SwipedButtonList { get; set; }

        public static readonly BindableProperty IsFilledProperty = BindableProperty.Create(nameof(IsFilled),
           typeof(bool),
           typeof(CustomGrid),
           false,
           BindingMode.TwoWay,
           null,
           null,
           null,
           null,
           null);


        public bool IsFilled
        {
            get { return (bool)this.GetValue(IsFilledProperty); }
            set { this.SetValue(IsFilledProperty, (object)value); }
        }


        public static readonly BindableProperty SelectWordProperty = BindableProperty.Create(nameof(BackgroundColor),
               typeof(string),
               typeof(CustomGrid),
               string.Empty,
               BindingMode.TwoWay,
               null,
               (obj, oldValue, newValue) => {
                   var old = oldValue;
                   var n = newValue;
                   var grid = obj as CustomGrid;

                   (grid.BindingContext as MainPageViewModel).SelectedWord = n.ToString();
               },
               null,
               null,
               null);


        public string SelectedWord
        {
            get { return (string)this.GetValue(SelectWordProperty); }
            set { this.SetValue(SelectWordProperty, (object)value); }
        }
    }

iOS tarafındaki CustomRenderer; 

public class CustomGridRenderer : ViewRenderer
    {
        UIView myGrid;
        List selectedButtonList = new List();

        protected override void OnElementChanged(ElementChangedEventArgs e)
        {
            base.OnElementChanged(e);

            if (Control != null)
            {
                Control.BackgroundColor = UIColor.White;
            }
            else
            {
                myGrid = GetControl();
               
                if (myGrid != null)
                {
                    foreach (UIView sview in myGrid.Subviews)
                    {
                        var ctrl = (sview as ButtonRenderer).Control;
                        if (ctrl != null)
                        {
                            ctrl.UserInteractionEnabled = false;
                        }
                    }
                }
            }
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (Control != null)
            {
                Control.BackgroundColor = UIColor.White;
            }
            else
            {
                myGrid = GetControl();
               
               ...
                }
            }
        }

        public override void TouchesBegan(NSSet touches, UIEvent evt)
        {
            base.TouchesBegan(touches, evt);
        }

        public override void TouchesMoved(NSSet touches, UIEvent evt)
        {
            base.TouchesMoved(touches, evt);
            var grid = Element as CustomGrid;
            int totalSwipedButtonCount = grid.SwipedButtonList.Count;

           for (int i = 0; i < myGrid.Subviews.Length; i++)            
           {
             foreach (UITouch touch in touches)                 
                {                     
                   var ctrl = (myGrid.Subviews[i] as ButtonRenderer).Control;                     
                    if (ctrl.PointInside(touch.LocationInView(ctrl), evt) && !(ctrl.AccessibilityValue == "blue"))                  
                    {                         
                       ...
                    }
                    else if (ctrl.PointInside(touch.LocationInView(ctrl), evt))
                    {
                       ...
                    }
                }
            }
        }

        public override async void TouchesEnded(NSSet touches, UIEvent evt)
        {
            base.TouchesEnded(touches, evt);

            var grid = Element as CustomGrid;
            var context = Element.BindingContext as MainPageViewModel;

            var foundWord = context.Words.FirstOrDefault(x => x.Word == grid.SelectedWord);

            if (Element != null)
            {
                if (Element is CustomGrid _grid)
                {
                   ...
                }
            }

            ...
           
        }
    }

Buradaki çok gerekli olmayan kodları kaldırdım, zira en önemli nokta yukarıda da kalın olarak işaretlediğim gibi, o anki touch noktasının Grid içerisinde ki hangi butona denk geldiğini bulmak oldu sadece.

Bunu da

var ctrl = (myGrid.Subviews[i] as ButtonRenderer).Control;

if (ctrl.PointInside(touch.LocationInView(ctrl), evt) && !(ctrl.AccessibilityValue == “blue”)) { …. }

control üzerindeki PointInside metodunu ve touch dan gelen LocationInView metodu nın birlikte kullanımı ile çok basit bir şekilde yapabiliyorsunuz.

Yani sizde içerisinde birden çok ui element i olan ve parmak ile gezildikçe o anki element üzerinde bir şey yapmanız gerekirse iOS tarafında, özet işleyiş şu şekilde oluyor;

TouchesMoved eventi içerisinde tüm UIView larda gezerken yukarıdaki iki metodu kullanarak bu işi hallediyorsunuz.

Gelelim Android tarafına. Yukarıda bahsetiğim gibi android tarafında sıfırdan bir GridView oluşturup, kendi Adapter i ile içerisini doldurup, onun üzerinden yürüdük.

iOS dan farklı olarak burada ayrı ayrı gelen touch eventleri yerine tek bir DispatchTouchEvent event i mevcut. Buraya geçilen MotionEvent ile o anki motion ı yakalayıp işlemlerinizi istediğiniz hareket altında yazabilirsiniz.

        GridView mainView;
        

        public CustomGridRenderer(Context context)
            : base(context)
        {

        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (Control != null)
            {
                var count = Control;
            }

            if (e.PropertyName == "IsFilled")
            {
                var linearLayout = new LinearLayout(Context);
                linearLayout.LayoutParameters = new LayoutParams(100, 100)
                {
                    Width = LayoutParams.MatchParent,
                    Height = LayoutParams.MatchParent
                };

                var density = Resources.DisplayMetrics.Density;

                mainView = new GridView(Context);
                mainView.SetNumColumns(4);
                mainView.SetHorizontalSpacing((int)(10 * density));
                mainView.SetVerticalSpacing((int)(10 * density));

                var adapter = new ButtonAdapter(Context);
                foreach (var item in Element.Children)
                {
                    //add buttons to grid.
                    var customButton = item as CustomButton;
                    adapter.CustomButtons.Add(customButton);
                    var btn = new Button(Context)
                    {
                        Text = customButton.Text,
                        Tag = customButton.WordIndex,
                        TextSize = 30,
                        Clickable = false
                    };

                    Typeface tf = Typeface.CreateFromAsset(Context.Assets, "HVD_Comic_Serif_Pro.ttf");
                    btn.SetTypeface(tf, TypefaceStyle.Normal);

                    btn.LayoutParameters = new LayoutParams(30, 30)
                    {
                        Width = LayoutParams.MatchParent,
                        Height = LayoutParams.MatchParent
                    };
                    btn.SetHeight(60);
                    btn.SetBackgroundResource(Resource.Drawable.Button_Border);
                    btn.SetTextColor(Android.Graphics.Color.Black);
                    adapter.Buttons.Add(btn);

                  ...
                }

                adapter.SecreenDensity = density;
                adapter.FormsGrid = Element;
                mainView.Adapter = adapter;
                linearLayout.AddView(mainView);

                SetNativeControl(linearLayout);
            }
        }

        public override bool DispatchTouchEvent(MotionEvent e)
        {
            var grid = Element as CustomGrid;
            int totalSwipedButtonCount = grid.SwipedButtonList.Count;
            var insideGrid = Control.GetChildAt(0) as GridView;

            switch (e.Action)
            {
                case MotionEventActions.Up:
                    
                     ...
                    break;
                case MotionEventActions.Move:
                    int x = (int)Math.Round(e.GetX());
                    int y = (int)Math.Round(e.GetY());

                    for (int i = 0; i < insideGrid.ChildCount; i++)
                     {                         
                       var child = insideGrid.GetChildAt(i) as Button;                         
                         if (child != null)         
                             {               
                                
                             if (x > child.Left && x < child.Right && y > child.Top && y < child.Bottom && child.ContentDescription != "done")                             
                                                               
                                   ...
                        }
                    }

                    break;
                default:
                    break;
            }

            return base.DispatchTouchEvent(e);
        }

        public class ButtonAdapter : BaseAdapter
        {
           
          ...
        }
    }

Burada da önce bir LinearLayout oluşturuyoruz. Bunun içerisine bir GridView atıyoruz. Bu Grid in içerisini de butonlarla dolduruyoruz.

iOS tarafında yaptığımız gibi ilgili touch ın hangi ui elemente denk geldiğini bulmak içinse bu tarafta şu şekilde ilerliyoruz.

for (int i = 0; i < insideGrid.ChildCount; i++) {

var child = insideGrid.GetChildAt(i) as Button;

if (x > child.Left && x < child.Right && y > child.Top && y < child.Bottom …)

Grid içerisindeki tüm ChildCount sayısı kadar bir for döngüsü içerisinde dönüp, İçerisinde döndüğümüz grid in o anki “i” index li elemanını yakalayıp, bu elemanın Left-Right-Top-Bottom ı, touch ın x ve y değerleri içerisinde mi buna bakıyoruz.

Yani aslında iOS da bir iki metod ile yaptığımız işlemi burada daha gözler önünde yapıyoruz.

Bu yazımda da bahsetmek istediklerim bu kadar

Bir sonraki yazımda görüşmek üzere.

Resilient Network Services – Bölüm 5 – Akavache

Selamlar,

Bu yazımda sizlere bu seri kapsamındaki en değerli kütüphanelerden biri olan Akavache den bahsedeceğim.

Akavache async çalışan ve key-value şeklinde data saklamanızı sağlayan, SQLite3 kullanan ve dataları persistant(kalıcı) şekilde tutan bir kütüphane. Desktop ve mobil uygulamalar için eğer lokal veritabanı kullanmak durumunuz var ise biçilmiş kaftan diyebilirim.

Günümüz de neredeyse local storage kullanmayan bir uygulama mevcut değil. Eğer saklamanız gereken dataları da file olarak saklamak gibi bir zorunluluğunuz yok ise kullanacağınız çözüm SQLite olacaktır. Bazı firmalar SQLite üzerinde çalışacak kendi katmanlarını zaten yazmış yada farklı yardımcı kütüphaneler ile ilerliyorlar. Fakat SQLite üzerinde sorgu çalıştırmaki, bilmeden çok fazla hata ve eksik yapmanıza müsait bir durum. En basit örneği verecek olursak, sqlite tarafına attığınız bir sorgu eğer transaction içine alınmadı ise o bunu kendi yapmaya çalışacaktır, bu da sizin sorgunuzun performans kaybetmesi demektir.

Peki hemen öncelikle bu kütüphaneyi nerelerde kullanabiliriz buna bakalım.

Aşağıdaki tüm dotnet platformlarında bu kütüphaneyi kullanabilirsiniz:

  • Xamarin.iOS / Xamarin.Mac
  • Xamarin.Android
  • .NET 4.5 Desktop (WPF)
  • Windows Phone 8.1 Universal Apps
  • Windows 10 (Universal Windows Platform)
  • Tizen 4.0

 

Akavache yi kullandıktan sonra, zaten hali hazırda kendileri de SQLite kullanmalarına rağmen uygulama performanslarında artış olduğunu söyleyen çok firma var. Peki herkes SQLite kullanmasına rağme bu fark neden oluşuyor?

Sebebi şu; SQLite ın nasıl kullanılmaması gerektiğini bilen kişiler tarafından geliştirildi bu kütüphane, ve yılların birikimi open-source bir proje olarak karşımıza çıktı. Yani eğer “Bende yıllardır kullanıyorum ve hali hazırda yazdığım çok iyi çalışan bir kütüphanem var.” diyor olsanız bile yine de kıyaslama yapmanızı öneririm.

Akavache hem gizlilik değeri olan dataları hemde komplex objeleri (imaj, api response, json data) kolayca cihazda saklamınızı sağlıyor.

Temelinde bir core bir key-value byte array store olarak yazılmış (Dictionary<string, byte[]> gibi düşünebiliriz.) ve bunun üzerine inşa ettikleri çok yardımcı ve kullanımı kolay extension metodları mevcut.

Kullanımına bir bakalım.

Akavache öncelikle BlobCache  denen özel bir sınıf ile kullanılıyor diyebiliriz.uygulamanın startup tarafında sadece uyulamanın adını set edeceğiniz tek satır kod ile başlamaya hazır oluyorsunuz.

BlobCache.ApplicationName = “MyApp” veya

Akavache.Registrations.Start(“MyApp”) ve artık hazırsınız.

Cİhazda verilerinizi saklamanız için 4 farklı seçenek mevcut akavache kullanırken.

  • BlobCache.LocalMachine – Normal cache lenmiş data. Bu işletim sistemi farklarına göre sizin haberiniz olmadan, işletim sistemi tarafından uçurulmuş olabiliyor. Bunun garantisini vermiyor, veremezde zaten.
  • BlobCache.UserAccount – User bilgileri. Bazı sistemler bu datayı otomatik olarak cloud ortamlarına yedekleyebiliyor.
  • BlobCache.Secure – password, iban vb hassas dataları saklamanız için kullanmanız gereken seçenek bu.
  • BlobCache.InMemory – Adında da anlaşılacağı üzere sadece uygulamanın lifetime ı süresince datayı saklamak için kullanacağınız seçenek de bu olucaktır.

Xamarin.iOSBlobCache.LocalMachine  seçeneği ile saklanmış dataları, diskte yer boşaltmak için silebilir (tabii uygulamanız çalışmıyor ise o an). Ama  UserAccount ve Secure seçenekleri ile sakladığınız datalar iCloud ve iTunes da yedeklenecektir.

Xamarin.Android  de aynı şekilde LocalMachine üzerinde saklanmış dataları yine diski boşaltmak amaçlı uçurabilir.

Windows 10 (UWP) nin yaptığı bir güzellik ise şu; UserAccount ve Secure tarafında saklanmış dataları cloud a atıyor ve tüm yüklü cihazlar ile senkronize ediyor.

Akavache yi async kullanmak için System.Reactive.Linq api si önemli. async-await bu şekilde çalışır oluyor.

using System.Reactive.Linq;    

Akavache.Registrations.Start("AkavacheExperiment")

var myToaster = new Toaster();
await BlobCache.UserAccount.InsertObject("toaster", myToaster);


var toaster = await BlobCache.UserAccount.GetObject<Toaster>("toaster");


Toaster toaster;
BlobCache.UserAccount.GetObject<Toaster>("toaster")
    .Subscribe(x => toaster = x, ex => Console.WriteLine("No Key!"));

Yukarıda görüceğiniz üzere, using olarak System.Reactive.Linq sayesinde aşağıda UserAccount üzerinden GetObject yaparken  await işlemi yapabiliyoruz.

Hepsinden önce ilk olarak yukarıda da belirttiğim gibi uygulama adını register ettikten sonra akavache kullanıma hazır hale geliyor.

Async-Await ile kullanmak istemiyorsak, aşağıda yazdığımı gibi Subscribe metodu ile cache aldığımız data üzerinden işlemimizi yapabiliyoruz.

Xamarin Linker Önlemi

Bazılarınız uygulama boyutunu düşürmek için Xamarin Linker ı kullanmıştır. Proje taranıp kullanılmadığı düşünülen dll ler uçurulabilir.

Akavache.Sqlite3 dll inin xamarin build tooları ile projeden uçurulmasını önlemenin(ki bu çok öenmli =) iki yolu var

1). İlk yönetim aşağıdaki gibi, dll deki type ları referance alıcak bir dosya eklemek projeye.

public static class LinkerPreserve
{
  static LinkerPreserve()
  {
    var persistentName = typeof(SQLitePersistentBlobCache).FullName;
    var encryptedName = typeof(SQLiteEncryptedBlobCache).FullName;
  }
}

2) İkinci yöntem ise, ilk belirttiğim gibi;

Sadece uygulamanın adını Akavahce.Registrations.Start metodu ile vermek.

Akavache.Registrations.Start("ApplicationName")

ShutDown

Akavacheyi kullanırken unutmamanız gereken birşey de uygulamanın shut down olayında BlobCache.ShutDown() metodunu çağırmamız. Hatta .Wait() etmemiz. Bunu yapmaz isek, queue ya alınmış datalarımız, önbellekten uçmadan kalabilir.

Bu kadar Akavache dn bahsettikten sonra bir sonraki yazımızda daha detaylı kullanımından ve extension metodlarından bahsedeceğim.

Görüşmek üzere.

Xamarin.Forms’da Kod Paylaşımı İçin .NET Standard 2.0 Kullanımı

Herkese Merhaba,

Bu yazımda önceden oluşturduğumuz veya yeni oluşturacağımız Xamarin.Forms projemizde, PCL (Portable Class Library) yerine nasıl .Net Standart 2.0 kullanabileceğimizden bahsedeceğim.

Bildiğiniz üzere, Visual Studio ile sıfırdan bir Xamarin.Forms projesi oluşturulduğunda bize iki opsiyon sunar. Bunlardan biri PCL diğeri de Shared Project. Shared Project, aslında File Linking gibi çalışan yazdığınız kodların tüm projelerin içine ayrı ayrı kopyalandığı ve paketlendiği bir kod paylaşım şeklidir yani SP ayrı bir proje olarak derlenip size farklı bir dll sunmamaktadır. Küçük projelerde ve tek kişilik ekiplerle yapılan projelerde mantıklı gibi gözükse de proje ve ekip büyüdükçe sıkıntılar çıkarmaya başlar. Ayrıca aşağıdaki gibi Compiler Directive’ler kullanarak, platform spesifik kodlarınızı yazabilirsiniz. Bu ilk bakışta kolay ve kullanışlı gibi gözükmesine rağmen kodlar çoğaldığında, ortalık biraz karışacaktır ve kodun okunabilirliği düşecektir.

  var path = string.Empty;
  #if WINDOWS_PHONE
  path = "windowsphone";
  #else
  
  #if __SILVERLIGHT__
  path = "silverlight";
  #else
  
  #if __ANDROID__
  path = "android";
  #else
  
  #if __IOS__
  path = "iOS";
  #else
  
  #if __TVOS__
  path = "tv";
  #else
  
  #if __WATCHOS__
  path = "watch";
  #endif
  

Gerek Unit Test yazılabilirliği olsun, gerekse daha temiz bir kod paylaşımı sağlaması vb  gibi farklı sebeplerden PCL birçok Xamarin projesinde kod paylaşımı stratejilerinin en başında gelmektedir.

Fakat bundan sonra, Xamarin.Forms 2.4.xx versiyonları ile beraber .NetStandard 2.0‘a tam destek gelmeye başladı. Microsoft’un da bundan sonra .NetCore‘a ve Standard Library ile yoluna devam edeceğini düşünürsek Xamarin projelerimizde kod paylaşımı için .NetStandard’a geçmenin vakti geldi.

netstandard

NET Standard 2.0‘da şimdiden yazılımcıların hayatını kolaylaştırmak için 20000’den fazla API, en çok kullanılan nuget paketlerinin %70’inden fazlasına uyumluluk mevcut bile. Desteğe dahil olan platformlara UWP desteği ile beraber artık Xamarin de eklendi.

.NET Standard 2.0 kütüphanesini Xamarin.iOS 10.14, Xamarin.Android 7.5, Xamarin.Mac 3.8, and Mono 5.4 versiyonları ve sonrası ile kullanabilirsiniz.

Bilgisayarınızda .Net Core ilgili eksikleriniz varsa şuradan indirebilirsiniz.

Projemde Kullanacağım .NetStandard Versiyonunu Seçerken Neleri Düşünmeliyim?

  • Ne kadar yüksek versiyon seçerseniz, o kadar çok API desteği alırsınız.
  • Ne kadar düşük versiyon seçerseniz, o kadar çok platform tarafından implement edilmiş olan versiyonu kullanıyor olursunuz.

Aslında çok da fazla düşünmenize gerek yok çünkü .Net Standard 2.0 neredeyse tüm platformlar tarafından destekleniyor.

  • NET Core 2.0
  • .NET Framework 4.6.1
  • Mono
  • Xamarin.iOS
  • Xamarin.Android
  • Xamarin.Mac
  • UWP

Tüm bu platformlar hali hazırda Net Standard 2.0′ı implement etmiş durumda.

Peki projemizdeki kod paylaşımını .Net Standard 2.0 olacak şekilde değiştirmekten bahsedeyim. Sıfırdan bir proje oluşturarak işe başlayacağım. Henüz kod paylaşımı için ilk başta Visual Studio bize seçenek olarak .Net Standard 2.0 sunmadığı için PCL seçimi yapacağım ve akabinde bunu .Net Standard 2.0 olarak değiştireceğim.

pclselect

pcl

 

Yukarıdaki seçeneklerle sıfırdan bir Xamarin.Forms projesi oluşturuyorum ve bana yandaki gibi bir Solution veriyor (Windows seçeneği için “İptal” tuşuna bastım. Şu an için onunla ilgilenmiyorum).

Projede hiçbir değişiklik yapmadan önce, solution’ı derliyorum. Tüm solution’ın sorunsuz derlendiğine emin olduktan sonra Solution’a sağ tıklayıp Add New Project diyorum.

 

Aşağıdaki gibi sol taraftaki menüden .Net Standard’ ı seçiyorum ve isimde hiçbir değişiklik yapmadan ClassLibrary1 olarak projemi solution’ a ekliyorum.

netstandartadd

Solution’a eklediğim bu SCL’min versiyonu default olarak (Eğer pc’nizde yüklü ise) 2.0 olarak gelmektedir. ClassLibrary1 projesine sağ tıklandığında en altta Properties’e tıklarsam karşıma gelen pencerenin Target framework kısmında SCL versyionunu görüp bu versiyonda değişiklik yapabilirim.

sclsel

Şimdi ClassLibrary1 projemde Dependencies’e sağ tıklayıp Manage Nuget Packages.. ‘a tıklayıp Xamarin.Forms kütüphanesini ekleyeceğim.

versxam

Yukarıda gördüğünüz gibi, ClassLibrary1 projeme Xamarin.Forms 2.4.0.282 (bu yazıyı yazarken ki son stabil versiyon) kütüphanesini ekleyip derliyorum. Projenin derlendiğine emin olduktan sonra, Pcl projesindeki App.xaml ve MainPage.xaml dosyalarını CTRL tuşuna basarak seçiyorum ve mouse yardımı ile iki dosyayı SCL projeme sürükleyip bırakıyorum (yani kopyalamış oluyorum). Bunu yaptıktan sonra artık PCL projemi solution’dan silebilirim. PCL projesini silmeden önce eğer projenizde daha fazla dosya varsa tüm dosyaları SCL’e kopyaladığınıza emin olun mutlaka.

ClassLibrary1 olarak eklediğim SCL’nin adını, bu proje mouse ile seçili iken F2 tuşuna basarak,  App1 olarak değiştiriyorum (ilk oluşturduğumda projeme verilen default isim bu olduğu için App1 yaptım. Eğer sizin önceden oluşturduğunuz bir projeniz varsa ve PCL in adı örneğin MyApp ise SCL’e de App1 değil bu  MyApp ismini veriniz.).

sclselected

Solution’ımın son hali yandaki gibidir. Gördüğünüz gibi App1 projesi artık bir .NetStandart2.0 projesidir. Dependencies altındaki Xamarin.Forms paketinin yanında sanki sorunluymuş gibi bir ünlem işareti var ama bunu şimdilik dikkate almayın. Projede sorun olmamasına rağmen Visual Studio bunu şimdilik yapıyor ancak Visual Studio’yu açıp kapattığınızda sorun düzeliyor. Fonksiyonel olarak bir sıkıntı olmadığına emin olmak için SCL projemizi derliyoruz. Projenin derlendiğine emin olduktan sonra Android ve iOS projeme bu App1 projemin referansını ekliyorum.

İlk başta gelen App1 ismindeki Portable Class Library projemi Solution’dan sildiğimde, referanslar Android ve iOS projesinde kalkmış oldu. Bu yüzden bu yeni oluşturduğum SCL kütüphanemin referansını, iOS ve Android projeme tekrar ekliyorum. Şimdi yeniden Android ve iOS projelerimi derlediğimde sorunsuz derlendiğini görüyorum. Bu noktadan itibaren Xamarin.Forms projeme .NetStandard 2.0 ile devam edebilirim.

 

Bu yazımda kısaca, var olan veya yeni oluşturduğum bir Xamarin.Forms projemin kod paylaşım stratejisi olarak nasıl PCL’den .NetStandard’a geçebileceğimizi anlatmaya çalıştım.

Özetlemek gerekirse:

  • Solution’a yeni bir .NetStandard 2.0 ClassLibrary’si ekliyorum.
  • Bu yeni eklediğim SCL’e Xamarin.Forms nuget Package’ını ekliyorum.
  • Önceden oluşan PCL’deki dosyaları (xaml, .cs vs) bu yeni oluşturduğum SCL içerisine taşıyorum.
  • PCL projesini solution’dan kaldırıyorum.
  • SCL’in ismini projeden kaldırdığım PCL’in ismi olacak şekilde değiştiriyorum.
  • Son olarak bu yeni oluşturduğum SCL’in referansını Android ve iOS projelerime ekliyorum.

 

Bir sonraki yazımda görüşmek üzere…