Tap & Drag on ScrollView (2)

Keywords: Xamarin.Forms, Gesture, Tap, Pan, Pinch, Drag, ScrollView, Invoking Events from Effects

I redeveloped "Tap & Drag on ScrollView (1)" with some techniques I have learned, and it also has graph data management function that I tried to develop in "Drawing (1)."  So the title of this post can be also called "Drawing (2)."

In "Tap & Drag on ScrollView (1)," I developed tap and drag interaction with reference to Xamarin Official Site "Invoking Events from Effects."  At that time, I needed to take a somewhat strange way (reduce the ScrollView size to disable scrolling) because tap gesture was stolen by ScrollView.  But I found that Xamarin.Forms Gestures make it possible to tap and drag on ScrollView, so I modified "Drawing (1)" and "Tap & Drag on ScrollView (1)," and merged the two Apps.

A screenshot displaying a correlation diagram of novel characters same as "Drawing (1)."

As shown in the screenshot above, small memos can be stick in the wide area at the center of this App screen.  The memos can be linked with arrows which indicate the relation of the memos.  This App can display any information whose data type is graph, so maze data can also be created like the figure as below (This maze data were used in the previous post "Reinforcement Learning: Maze Solving") as well as correlation diagrams.

Moving on to the source code.

MainPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:DragOnScrollView"
             x:Class="DragOnScrollView.MainPage">
    
    <Grid RowSpacing="0">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <!-- Title -->
        <StackLayout Grid.Row="0" BackgroundColor="Gray" 
                     Padding="0,0,0,1" HeightRequest="50">
            <StackLayout BackgroundColor="White" HeightRequest="50">
                <Label Text="Tap and Drag on ScrollView Demo" 
                       Grid.Row="0" VerticalOptions="EndAndExpand"/>
            </StackLayout>
        </StackLayout>

        <!-- Buttons to control this App -->
        <StackLayout Grid.Row="1" Orientation="Horizontal">
            <Button x:Name="StickMemoButton"
                    Text="+" Clicked="StickMemo"
                    WidthRequest="50" HeightRequest="25"
                    BorderWidth="1" Margin="3,3,0,3"/>
            <Button x:Name="LinkMemoButton"
                    Text="->" Clicked="LinkMemo"
                    IsEnabled="False" 
                    WidthRequest="50" HeightRequest="25"
                    BorderWidth="1" Margin="3,3,0,3"/>
            <Button x:Name="DeleteObjectButton"
                    Image="Trash.png" Clicked="DeleteObject"
                    IsEnabled="False" 
                    WidthRequest="50" HeightRequest="25"
                    BorderWidth="1" Margin="3,3,0,3"/>
            <Label x:Name="StatusLabel"
                   HorizontalOptions="EndAndExpand" VerticalOptions="Center"/>
        </StackLayout>

        <!-- ScrollView and AbsoluteLayout for Main Board -->
        <StackLayout Grid.Row="2" BackgroundColor="Gray" Padding="0,1,0,1" 
                     VerticalOptions="FillAndExpand">            
            <ScrollView x:Name="ScrollBoard" Orientation="Both"
                        SizeChanged="GetScrollBoardSize" Scrolled="IsScrolled"
                        BackgroundColor="LightGray" VerticalOptions="FillAndExpand">
                <AbsoluteLayout x:Name="MemoBoard" BackgroundColor="White">
                    <AbsoluteLayout.GestureRecognizers>
                        <!--
                        <PanGestureRecognizer PanUpdated="OnPanMemoBoard"/>
                        -->
                        <PinchGestureRecognizer PinchUpdated="OnPinchMemoBoard"/>
                        <TapGestureRecognizer Tapped="OnTapMemoBoard"/>
                    </AbsoluteLayout.GestureRecognizers>
                </AbsoluteLayout>
            </ScrollView>
        </StackLayout>

        <!-- Slider to Pinch MemoBoard -->
        <StackLayout Grid.Row="3" BackgroundColor="Gray" Padding="0, 0, 0, 1">
            <StackLayout BackgroundColor="White">
                <Slider x:Name="ScaleSlider" ValueChanged="ScaleChanged"
                        Maximum="2" Minimum="0.1" Value="1"/>
            </StackLayout>
        </StackLayout>

        <!-- Monitor some variables for Debug -->
        <StackLayout x:Name="Debug" Grid.Row="4" Orientation="Horizontal">
            <StackLayout>
                <Label x:Name="Label1" Text=" "/>
                <Label x:Name="Label2" Text=" "/>
                <Label x:Name="Label3" Text=" "/>
            </StackLayout>
        </StackLayout>

    </Grid>
</ContentPage>
The source code is also here.

The screen layout is designed with a five-rows Grid. In the each row, title, Buttons for memo and link manipulation, main area to display memos linked each other with arrows, scale slider to scale the main area displaying, and variables display area for debug. The main area contains the ScrollView "ScrollBoard" surrounding the AbsoluteLayout "MemoBoard" with Tap, Pan, and Pinch Gesture.


MemoAndArrowClass.cs
using System;
using System.Collections.Generic;
using Xamarin.Forms;

namespace DragOnScrollView
{
    public partial class MainPage : ContentPage
    {
        class Memo
        {
            // Main ContentView surrounding an Editor
            private ContentView body = new ContentView()
            {                
                WidthRequest = 120,
                HeightRequest = 30,
                BackgroundColor = Color.Gray,
                Padding = new Thickness(1, 1, 1, 1),
                Content = new Editor
                {
                    FontSize = 14,
                    InputTransparent = true,
                    Keyboard = Keyboard.Plain
                }
            };
            public ContentView Body { get { return this.body; } }
            
            public Point Offset;   // Position when Tap/Pan/Pinch gesture starts

            public double X { get { return Body.TranslationX; } }
            public double Y { get { return Body.TranslationY; } }
            public double W { get { return Body.WidthRequest; } }
            public double H { get { return Body.HeightRequest; } }

            // Arrows linked this Memo
            public List<Arrow> Arrows = new List<Arrow>();

            // Pinch Gesture Scale Origin
            public Point ScaleOrigin;
        }
        List<Memo> Memos = new List<Memo>();


        // Arrow from Memo "From" to Memo "To"
        class Arrow //: INotifyPropertyChanged
        {
            // This Arrow links the Memo "From" and "To"
            public Memo From, To;            

            // Contents of the Arrow : Body, Line, Title, TitleInput
            // Body : Background to detect Tap Gesture
            private static double ArrowWidth = 20;
            private static double ArrowHeight = 80;
            private ContentView body = new ContentView()
            {
                WidthRequest = ArrowWidth,
                HeightRequest = ArrowHeight,
                Rotation = 90,
                //BackgroundColor = Color.Yellow,
                Content = new AbsoluteLayout
                {
                    BackgroundColor = Color.Transparent,
                    InputTransparent = true
                }
            };
            public ContentView Body { get { return this.body; } }


            // Main Body of the Arrow
            private BoxView line = new BoxView()
            {
                WidthRequest = 2,
                HeightRequest = ArrowHeight,
                TranslationX = ArrowWidth / 2 - 1,
                //TranslationY = NewArrow.Body.HeightRequest / 2,
                BackgroundColor = Color.Black,
                InputTransparent = true
            };
            public BoxView Line { get { return this.line; } }

            // Create Arrow Head
            private Label arrowHead = new Label()
            {
                Text = "▲",
                FontSize = 15,
                WidthRequest = ArrowWidth,
                HeightRequest = ArrowWidth,
                HorizontalTextAlignment = TextAlignment.Center,
                TranslationY = - ArrowWidth / 2
            };
            public Label ArrowHead { get { return this.arrowHead; } }


            // Title Label
            private Label title = new Label()
            {
                Text = "Title",
                WidthRequest = ArrowHeight,
                HeightRequest = 20,
                //BackgroundColor = Color.Blue,
                TranslationX = - ArrowHeight / 2,
                TranslationY = ArrowHeight / 2 - 20 / 2,
                HorizontalTextAlignment = TextAlignment.Center,
                Rotation = -90
            };
            public Label Title { get { return this.title; } }

            // Entry to change the title
            private Entry titleInput = new Entry()
            {
                HorizontalTextAlignment = TextAlignment.Center,
                WidthRequest = ArrowHeight,
                HeightRequest = 20,
                //BackgroundColor = Color.Red,
                TranslationX = - ArrowHeight / 2,
                TranslationY = ArrowHeight / 2 - 20 / 2,
                Rotation = -90,
                IsVisible = false,
                Keyboard = Keyboard.Plain
            };
            public Entry TitleInput { get { return this.titleInput; } }


            // Calculate Positions of A(Start) and B(End) points of the Arrow 
            List<Point> Points()
            {
                List<Point> points = new List<Point>();
                Point a = new Point();
                Point b = new Point();
                double space = 10.0; // space between two arrows with opposite direction
                double gap = 8.0;   // Gap between Memo and ArrowHead

                // Optimize Start and End points position
                if (From.X + From.W < To.X){
                    a.X = From.X + From.W; a.Y = From.Y + From.H / 2 - space;
                    b.X = To.X - gap; b.Y = To.Y + To.H / 2 - space;
                }
                else if (To.X + To.W < From.X){
                    a.X = From.X; a.Y = From.Y + From.H / 2 + space;
                    b.X = To.X + To.W + gap; b.Y = To.Y + To.H / 2 + space;
                }
                else if (From.Y + From.H < To.Y){
                    a.X = From.X + From.W / 2 + space; a.Y = From.Y + From.H;
                    b.X = To.X + To.W / 2 + space; b.Y = To.Y - gap;
                }
                else{
                    a.X = From.X + From.W / 2 - space; a.Y = From.Y;
                    b.X = To.X + To.W / 2 - space; b.Y = To.Y + To.H + gap;
                }

                // When "To" Memo has not been selected yet:
                if (To == From)
                {
                    a.X = From.X + From.W;  a.Y = From.Y + From.H / 2;
                    b.X = a.X + 80;         b.Y = a.Y + 0.001;
                }

                points.Add(a);      points.Add(b);
                return points;
            }
            public Point A { get { return Points()[0]; } }  // Startpoint of this Arrow
            public Point B { get { return Points()[1]; } }  // Endpoint of this Arrow


            // Calculate Arrow Length
            double Distance()
            {
                double x1 = Points()[0].X; double y1 = Points()[0].Y;
                double x2 = Points()[1].X; double y2 = Points()[1].Y;

                return Math.Sqrt(Math.Pow(x1 - x2, 2) + Math.Pow(y1 - y2, 2));
            }
            public double Length { get { return Distance(); } }


            // Calculate Arrow Angle
            double Angle()
            {                
                double x1 = Points()[0].X; double y1 = Points()[0].Y;
                double x2 = Points()[1].X; double y2 = Points()[1].Y;
                double l = Length;

                return 90.0 + Math.Acos((x2 - x1) / l)
                                 * Math.Abs(y2 - y1) / (y2 - y1 + 0.0001)
                                 * 180.0 / Math.PI;
            }
            public double Theta { get { return Angle(); } }
        }


        // Class for AbsoluteLayout "MemoBoard"
        class Board
        {
            public Point Offset;
            public Point OffsetMax;
            public Point Display;

            public double OriginalW { get; set; }
            public double OriginalH { get; set; }
        }
    }
}

The properties for memo, link, and "MemoBoard" are defined as class "Memo", "Arrow" and "Board" respectively. Class "Memo" creates a ContentView including an Editor which displays a memo on "MemoBoard." "Arrows" linked this memo, positions etc. are also the properties of "Memo" class.

Since a link contains a lot of parts and variables, class "Arrow" has a lot of properties. "Memo" and "To" are memos linked by this arrow. The ContentView "Body" surrounding an AbsoluteLayout as the Content is the container of all this arrow's parts, and its background color is transparent, so this view is invisible to users. "Line" etc. are this Arrow's parts. "Points" returns the positions on "From" and "To" memo where this arrow should be linked. The class "Arrow" make it simplify arrows display update pretty much more than the previous post, "Drawing (1)."

Finally, class "Board" has some properties for the "MemoBoard."


MainPage.xaml.cs
#define iOS
// #define UWP

//#define MemoBoardPan

//#define BoxDisplay    // Set some BoxViews for Debug

using System;
using Xamarin.Forms;

namespace DragOnScrollView
{
    public partial class MainPage : ContentPage
    {
        ContentView TappedObject, CreatedArrow;   // Lastly Tapped ContentView
        Board memoBoard = new Board();

#if BoxDisplay
        // BoxViews to check some positions of the parts
        BoxView box1 = new BoxView()
        {
            TranslationX = 0,
            TranslationY = 0,
            WidthRequest = 20,
            HeightRequest = 20,
            BackgroundColor = Color.Red
        };
        BoxView box2 = new BoxView()
        {
            TranslationX = 0,
            TranslationY = 0,
            WidthRequest = 15,
            HeightRequest = 15,
            BackgroundColor = Color.Blue
        };
        BoxView box3 = new BoxView()
        {
            TranslationX = 0,
            TranslationY = 0,
            WidthRequest = 10,
            HeightRequest = 10,
            BackgroundColor = Color.Green
        };
#endif

        public MainPage()
        {
            InitializeComponent();

#if MemoBoardPan
            // Pan Gesture Event on MemoBoard
            var panGesture = new PanGestureRecognizer();
            panGesture.PanUpdated += OnPanMemoBoard;
            MemoBoard.GestureRecognizers.Add(panGesture);
#endif
        }


        // Get "ScrollBoard" size properly and set some variables
        void GetScrollBoardSize(object sender, EventArgs args)
        {
            memoBoard.OriginalW = ScrollBoard.Width;
            memoBoard.OriginalH = ScrollBoard.Height;

            Label1.Text = string.Format("ScrollBoard Size = ( {0}, {1} )",
                                        ScrollBoard.Width, ScrollBoard.Height);

#if BoxDisplay
            MemoBoard.Children.Add(box1);
            MemoBoard.Children.Add(box2);
            MemoBoard.Children.Add(box3);
#endif
            StatusLabel.Text = "Get ScrollBoard Size";
        }


        // Create a new Memo and stick it onto "ScrollBoard"
        void StickMemo(object sender, EventArgs args)
        {
            Memo NewMemo = new Memo();

            // Set the initial position and offsets
#if MemoBoardPan
            NewMemo.Body.TranslationX = memoBoard.Display.X;
            NewMemo.Body.TranslationY = memoBoard.Display.Y;
            NewMemo.Offset = new Point()
            {
                X = memoBoard.Display.X,
                Y = memoBoard.Display.Y
            };
#else
            NewMemo.Body.TranslationX = ScrollBoard.ScrollX;
            NewMemo.Body.TranslationY = ScrollBoard.ScrollY;
            NewMemo.Offset = new Point()
            {
                X = ScrollBoard.ScrollX,
                Y = ScrollBoard.ScrollY
            };
#endif

            // Add Tap, Pan, Pinch Gesture Event
            var tapGesture = new TapGestureRecognizer();
            tapGesture.Tapped += OnTapMemo;
            NewMemo.Body.GestureRecognizers.Add(tapGesture);

            var panGesture = new PanGestureRecognizer();
            panGesture.PanUpdated += OnPanMemo;
            NewMemo.Body.GestureRecognizers.Add(panGesture);

            var pinchGesture = new PinchGestureRecognizer();
            pinchGesture.PinchUpdated += OnPinchMemo;
            NewMemo.Body.GestureRecognizers.Add(pinchGesture);


            // Add Editor Event
            Editor editor = (Editor)NewMemo.Body.Content;
            editor.TextChanged  += EditorTextChanged;
            editor.Focused      += EditorFocused;
            editor.Unfocused    += EditorUnfocused;

            // Add this new Memo to the Memos collection
            //               and "MemoBoard" in "ScrollBoard"
            Memos.Add(NewMemo);
            MemoBoard.Children.Add(NewMemo.Body);

            StatusLabel.Text = "Memo Sticked";
        }


        // Create an new Arrow and Link it to the Memo "From"
        void LinkMemo(object sender, EventArgs args)
        {
            var button = (Button)sender;

            // If pressed Cacel Button, 
            //       Cancel the Arrow Linking to Memo "To"
            if (button.Text == "Cancel")
            {
                MemoBoard.Children.Remove(CreatedArrow);
                StatusLabel.Text = "Cancel Linking";
                button.Text = "->"; // toggle button.Text
                return;             // Exit "LinkMemo" method
            }

            // Create a new Arrow
            Arrow NewArrow = new Arrow();

            // Set Two Memos linked by this Arrow
            NewArrow.From = (Memo)WhichObject(TappedObject);
            NewArrow.To = (Memo)WhichObject(TappedObject);

            // Set the initial position
            NewArrow.Body.TranslationX
                = TappedObject.TranslationX + TappedObject.WidthRequest
                + NewArrow.Body.HeightRequest / 2 - NewArrow.Body.WidthRequest / 2;
            NewArrow.Body.TranslationY
                = TappedObject.TranslationY + TappedObject.HeightRequest / 2
                - NewArrow.Body.HeightRequest / 2;

            // Add Arrow Parts on the Background (ContentView) of the Arrow
            var layout = (AbsoluteLayout)NewArrow.Body.Content;
            layout.Children.Add(NewArrow.Line);
            layout.Children.Add(NewArrow.Title);
            layout.Children.Add(NewArrow.TitleInput);
            layout.Children.Add(NewArrow.ArrowHead);

            // Add Tap Gesture Event
            var tapGesture = new TapGestureRecognizer();
            tapGesture.Tapped += OnTapMemo;
            NewArrow.Body.GestureRecognizers.Add(tapGesture);

            // Add Entry Input Completed Event
            NewArrow.TitleInput.Completed += TitleInputCompleted;

            // Add this new Arrow to the Arrows collection of 
            //      the Memo "From" and "absoluteLayout" in "ScrollBoard"
            NewArrow.From.Arrows.Add(NewArrow);
            MemoBoard.Children.Add(NewArrow.Body);

            //TappedObject = NewArrow.Body;
            CreatedArrow = NewArrow.Body;

            StatusLabel.Text = "Linking Memo";
            button.Text = "Cancel";     // toggle button.Text
        }


        // Delete Object (Memo or Arrow)
        void DeleteObject(object sender, EventArgs args)
        {
            switch (StatusLabel.Text)
            {
                case "Memo Tapped":
                    Memo TappedMemo = (Memo)WhichObject(TappedObject);

                    foreach (Arrow arrow in TappedMemo.Arrows)
                    {
                        // Remove all arrows from Memo and MemoBoard
                        if (arrow.From == TappedMemo) arrow.To.Arrows.Remove(arrow);
                        if (arrow.To == TappedMemo) arrow.From.Arrows.Remove(arrow);
                        MemoBoard.Children.Remove(arrow.Body);
                    }

                    Memos.Remove(TappedMemo);
                    StatusLabel.Text = "Memo Deleted";
                    break;

                case "Arrow Tapped":
                case "Linking Memo":
                    Arrow TappedArrow = (Arrow)WhichObject(TappedObject);

                    Memo from = TappedArrow.From; from.Arrows.Remove(TappedArrow);
                    Memo to = TappedArrow.To; to.Arrows.Remove(TappedArrow);

                    LinkMemoButton.Text = "->"; // change the button label
                    StatusLabel.Text = "Arrow Deleted";
                    break;
            }

            MemoBoard.Children.Remove(TappedObject);
            DeleteObjectButton.IsEnabled = false;
        }


        // Panning Memo, and Update the Memo and the Arrows linked it
        void OnPanMemo(object sender, PanUpdatedEventArgs e)
        {
            ContentView PannedObject = (ContentView)sender;
            Memo PannedMemo = (Memo)WhichObject(PannedObject);

            switch (e.StatusType)
            {
                case GestureStatus.Started:
                    PannedObject.BackgroundColor = Color.Black;
                    StatusLabel.Text = "Panning Memo";
                    break;

                case GestureStatus.Running:
                    PannedObject.TranslationX
                        = PannedMemo.Offset.X + e.TotalX * PannedObject.Scale;
                    PannedObject.TranslationY
                        = PannedMemo.Offset.Y + e.TotalY * PannedObject.Scale;

                    foreach (Arrow arrow in PannedMemo.Arrows)
                        UpdateArrow(arrow);

                    Label1.Text = string.Format("Memo Position (X, Y) = ( {0:0.0} , {1:0.0} )",
                                                PannedObject.TranslationX, PannedObject.TranslationY);
                    Label2.Text = string.Format("Memo Moved (dX, dY) = ( {0:0.0} , {1:0.0} )",
                                                e.TotalX, e.TotalY);
                    break;

                case GestureStatus.Completed:
                    PannedObject.BackgroundColor = Color.Gray;

                    // Store the translation applied during the pan
                    PannedMemo.Offset.X = PannedObject.TranslationX;
                    PannedMemo.Offset.Y = PannedObject.TranslationY;

#if !MemoBoardPan
                    // If thie object is moved out of the ScrollBoard, expand the ScrollBoard
                    if (PannedMemo.Offset.X + PannedObject.Width > MemoBoard.Width) // expand to Right
                        MemoBoard.WidthRequest = MemoBoard.Width +
                            PannedMemo.Offset.X + PannedObject.Width - MemoBoard.Width + 10;
                    if (PannedMemo.Offset.Y + PannedObject.Height > MemoBoard.Height)   // expand to 
                        MemoBoard.HeightRequest = MemoBoard.Height +
                            PannedMemo.Offset.Y + PannedObject.Height - MemoBoard.Height + 10;
                    if (PannedMemo.Offset.X < 0)
                    {
                        double dx = (-PannedMemo.Offset.X) + 10;
                        MemoBoard.WidthRequest = MemoBoard.Width + dx;
                        ScrollBoard.ScrollToAsync(dx, ScrollBoard.ScrollY, false);

                        // Shift all Memos and Arrows
                        foreach (Memo memo in Memos)
                        {
                            memo.Body.TranslationX += dx;
                            memo.Offset.X = memo.Body.TranslationX;
                            foreach (Arrow arrow in memo.Arrows)
                            {
                                arrow.Body.TranslationX += dx;
                                UpdateArrow(arrow);
                            }
                        }
                    }
                    if (PannedMemo.Offset.Y < 0)
                    {
                        double dy = (-PannedMemo.Offset.Y) + 10;
                        MemoBoard.HeightRequest = MemoBoard.Height + dy;
                        ScrollBoard.ScrollToAsync(ScrollBoard.ScrollX, dy, false);

                        // Shift all Memos and Arrows
                        foreach (Memo memo in Memos)
                        {
                            memo.Body.TranslationY += dy;
                            memo.Offset.Y = memo.Body.TranslationY;
                            foreach (Arrow arrow in memo.Arrows)
                            {
                                arrow.Body.TranslationY += dy;
                                UpdateArrow(arrow);
                            }
                        }
                    }
#endif
                    StatusLabel.Text = "Pan Completed";
                    break;

                    // If this Memo is removed, Cancel event is raised
                    //case GestureStatus.Canceled:
                    //    DisplayAlert("", "Canceled", "Ok");
                    //    break;
            }
        }


        // Pinching Memo and Update the Memo and the Arrows linked it
        void OnPinchMemo(object sender, PinchGestureUpdatedEventArgs e)
        {
            ContentView PinchedObject = (ContentView)sender;
            Memo PinchedMemo = (Memo)WhichObject(PinchedObject);

            switch (e.Status)
            {
                case GestureStatus.Started:
                    PinchedMemo.ScaleOrigin = e.ScaleOrigin;
                    StatusLabel.Text = "Pinching Memo";
                    break;

                case GestureStatus.Running:
                    double dx = e.ScaleOrigin.X - PinchedMemo.ScaleOrigin.X;

                    // Horizontal Pinch by 2-finger Drag
                    PinchedObject.WidthRequest *= (1 + dx);

                    // Vertical Pinch by 2-finger Pinch
                    PinchedObject.HeightRequest *= (1.0 + (e.Scale - 1.0) * 0.5);

                    // Store ScaleOrigin
                    PinchedMemo.ScaleOrigin = e.ScaleOrigin;

                    Label1.Text = string.Format("ScaleOrigin = ( {0:0.00} , {1:0.00} )",
                                                e.ScaleOrigin.X, e.ScaleOrigin.Y);
                    Label2.Text = string.Format("Scale (X, Y) = ({0:0.00}, {1:0.00}", dx, e.Scale);
                    break;

                case GestureStatus.Completed:
                    // Store the translation applied during the pinch
                    PinchedMemo.Offset.X = PinchedObject.TranslationX;
                    PinchedMemo.Offset.Y = PinchedObject.TranslationY;

                    StatusLabel.Text = "Pinch Completed";
                    break;
            }

        }


        // Tap Object (Memo / Arrow)
        void OnTapMemo(object sender, EventArgs args)
        {
            // Reset all Memos
            foreach (Memo memo in Memos)
            {
                memo.Body.BackgroundColor = Color.Gray;
                memo.Body.Content.InputTransparent = true;
            }

            TappedObject = (ContentView)sender;
            object o = WhichObject(TappedObject);
            Arrow arrow = new Arrow();

            switch (o.GetType().Name)
            {
                case "Memo":    // Tapped object is Memo
                    MemoBoard.Children.Remove(TappedObject);
                    MemoBoard.Children.Add(TappedObject);

                    TappedObject.BackgroundColor = Color.Black;
                    TappedObject.Content.InputTransparent = false;

                    LinkMemoButton.IsEnabled = true;
                    DeleteObjectButton.IsEnabled = true;

                    StatusLabel.Text = "Memo Tapped";

                    // A new arrow has been already linked "From" Memo, 
                    //       so the Tapped Memo will be its "To" Memo
                    if (LinkMemoButton.Text == "Cancel")
                    {
                        arrow = (Arrow)WhichObject(CreatedArrow);
                        arrow.To = (Memo)WhichObject(TappedObject);
                        arrow.To.Arrows.Add(arrow);
                        UpdateArrow(arrow);
                        LinkMemoButton.Text = "->";
                        StatusLabel.Text = "Memos Linked";
                    }
                    break;

                case "Arrow":   // Tapped object is Arrow                    
                    arrow = (Arrow)WhichObject(TappedObject);

                    // Enable Title Input
                    arrow.TitleInput.IsVisible = true;
                    arrow.TitleInput.IsEnabled = true;
                    arrow.TitleInput.Focus();

                    // Disable LinkMemoButton
                    LinkMemoButton.IsEnabled = false;

                    StatusLabel.Text = "Arrow Tapped";
                    break;
            }
        }


        // Update all Arrow parts' altitude
        void UpdateArrow(Arrow arrow)
        {
            double length = arrow.Length;

            arrow.Body.HeightRequest = length;
            arrow.Body.TranslationX
                = (arrow.A.X + arrow.B.X - arrow.Body.WidthRequest) / 2;
            arrow.Body.TranslationY
                = (arrow.A.Y + arrow.B.Y - arrow.Body.HeightRequest) / 2;
            arrow.Body.Rotation = arrow.Theta;

            arrow.Line.HeightRequest = length;

            arrow.Title.WidthRequest = length;
            arrow.Title.TranslationX = -length / 2;
            arrow.Title.TranslationY = length / 2;

            arrow.TitleInput.WidthRequest = length;
            arrow.TitleInput.TranslationX = -length / 2;
            arrow.TitleInput.TranslationY = length / 2;
        }


        // If Return Key is detected in the editor, expand its vertical size
        void EditorTextChanged(object sender, TextChangedEventArgs e)
        {
            Editor editor = (Editor)sender;
            Memo memo = (Memo)WhichObject(editor);

#if iOS
            if (e.NewTextValue.EndsWith("\n"))  // for iOS
#elif UWP
            if (e.NewTextValue.EndsWith("\r"))  // for UWP
#endif
                memo.Body.HeightRequest += 20;
            
            StatusLabel.Text = "Editting";
        }

        // Change the border color, when the editor is focused
        void EditorFocused(object sender, EventArgs args)
        {
            Editor editor = (Editor)sender;

            Memo memo = (Memo)WhichObject(editor);
            memo.Body.BackgroundColor = Color.Maroon;

            StatusLabel.Text = "Editor Focused";
        }

        // Reset the border color, when the editor is unfocused
        void EditorUnfocused(object sender, EventArgs args)
        {
            Editor editor = (Editor)sender;

            Memo memo = (Memo)WhichObject(editor);
            memo.Body.BackgroundColor = Color.Gray;
            memo.Body.Content.InputTransparent = true;
            //memo.Body.Content.Unfocus();

            StatusLabel.Text = "Editor Unfocused";
        }

        // When Title Input is completed, change the Title and make the Entry invisible
        void TitleInputCompleted(object sender, EventArgs args)
        {
            Entry entry = (Entry)sender;
            Arrow arrow = (Arrow)WhichObject(TappedObject);

            arrow.Title.Text = entry.Text;
            entry.IsVisible = false;

            StatusLabel.Text = "Link Title Input Completed";
        }


        // Raised when "ScrollBoard" is scrolled
        void IsScrolled(object sender, EventArgs e)
        {
            Label3.Text = string.Format("Scroll (X, Y) = ({0}, {1})",
                                        ScrollBoard.ScrollX, ScrollBoard.ScrollY);
            StatusLabel.Text = "Scrolling";
        }

        // Scale change by the Slider
        void ScaleChanged(object sender, ValueChangedEventArgs args)
        {
            double value = args.NewValue;
            Label3.Text = "ScaleValue = " + value.ToString();
            MemoBoard.Scale = value;

            StatusLabel.Text = "Scale Changing";
        }


#if MemoBoardPan
        // Panning MemoBoard
        void OnPanMemoBoard(object sender, PanUpdatedEventArgs e)
        {
            AbsoluteLayout PannedObject = (AbsoluteLayout)sender;

            switch (e.StatusType)
            {
                case GestureStatus.Started:
                    StatusLabel.Text = "Panning MemoBoard";
                    break;

                case GestureStatus.Running:
                    PannedObject.TranslationX
                                = memoBoard.Offset.X + e.TotalX * PannedObject.Scale;
                    PannedObject.TranslationY
                                = memoBoard.Offset.Y + e.TotalY * PannedObject.Scale;
                    break;

                case GestureStatus.Completed:
                    // Store the translation applied during the pan
                    memoBoard.Offset.X = PannedObject.TranslationX;
                    memoBoard.Offset.Y = PannedObject.TranslationY;

                    // Update Display Position
                    memoBoard.Display.X = memoBoard.OffsetMax.X - PannedObject.TranslationX;
                    memoBoard.Display.Y = memoBoard.OffsetMax.Y - PannedObject.TranslationY;

                    // If dx1, dy1 > 0, memoBoard should be expanded to Left and Up
                    double dx1 = PannedObject.TranslationX - memoBoard.OffsetMax.X;
                    double dy1 = PannedObject.TranslationY - memoBoard.OffsetMax.Y;

                    // If dx2, dy2 > 0, memoBoard should be expanded to Right and Down
                    double dx2 = memoBoard.Display.X + memoBoard.OriginalW
                                          - PannedObject.WidthRequest;
                    double dy2 = memoBoard.Display.Y + memoBoard.OriginalH
                                          - PannedObject.HeightRequest;
                    //- (PannedObject.TranslationY + PannedObject.Height);


                    Label2.Text = PannedObject.TranslationY.ToString();
                    //Label3.Text = string.Format("(dx, dy) = ( {0:0.00} , {1:0.00} )", dx, dy);
                    //Label2.Text = string.Format("e.Totoal X, Y = ( {0:0.00} , {1:0.00} )",
                    //                            e.TotalX, e.TotalY);


                    // Expand MemoBoard
                    if (dx1 > 0)  // expand to Left
                    {
                        PannedObject.WidthRequest = PannedObject.Width + dx1;
                        memoBoard.Display.X = 0;

#if BoxDisplay
                        // Green BoxView
                        box3.TranslationX = PannedObject.WidthRequest - 10;
#endif
                    }
                    else  // dx1 <= 0
                    {
#if BoxDisplay
                        // Green BoxView
                        box3.TranslationX = PannedObject.Width - 10;
#endif
                        if (dx2 > 0)  // expand to Right
                        {
                            PannedObject.WidthRequest =
                                memoBoard.Display.X + memoBoard.OriginalW;
                        }
                    }

                    if (dy1 > 0)  // expand to Up
                    {
                        PannedObject.HeightRequest = PannedObject.Height + dy1;
                        memoBoard.Display.Y = 0;

#if BoxDisplay
                        // Green BoxView
                        box3.TranslationY = PannedObject.HeightRequest - 10;
#endif
                    }
                    else  // dy1 <= 0
                    {
#if BoxDisplay
                        // Green BoxView
                        box3.TranslationY = PannedObject.Height - 10;
#endif

                        //if (PannedObject.TranslationY + PannedObject.Height
                        //< memoBoard.Display.Y + memoBoard.OriginalH)
                        if (dy2 > 0)  // expand to Down
                        {
                            PannedObject.HeightRequest =
                                memoBoard.Display.Y + memoBoard.OriginalH;
                        }
                    }


#if BoxDisplay
                    // Red BoxView
                    box1.TranslationX = memoBoard.Display.X;
                    box1.TranslationY = memoBoard.Display.Y;
                    // Blue BoxView
                    box2.TranslationX = memoBoard.Display.X + memoBoard.OriginalW - 15;
                    box2.TranslationY = memoBoard.Display.Y + memoBoard.OriginalH - 15;
#endif


                    //Label2.Text = string.Format("MemoBoard Size = ( {0:0.0} , {1:0.0} )",
                    //                    PannedObject.Width, PannedObject.Height);
                    Label3.Text = string.Format("Display X, Y = ( {0:0.0} , {1:0.0} )",
                                                memoBoard.Display.X, memoBoard.Display.Y);

                    // Shift MemoBoard to Left and Up
                    if (dx1 > 0)
                    {
                        memoBoard.OffsetMax.X = PannedObject.TranslationX;
                        ScrollBoard.ScrollToAsync(PannedObject.TranslationX,
                                                      ScrollBoard.ScrollY, false);
                    }
                    if (dy1 > 0)
                    {
                        memoBoard.OffsetMax.Y = PannedObject.TranslationY;
                        ScrollBoard.ScrollToAsync(ScrollBoard.ScrollX,
                                                      PannedObject.TranslationY, false);
                    }


                    //Label2.Text = string.Format("OffsetMax = ( {0:0.0} , {1:0.0} )",
                    //                            memoBoard.OffsetMax.X, memoBoard.OffsetMax.Y);


                    // Shift all Memos and Arrows
                    foreach (Memo memo in Memos)
                    {
                        if (dx1 > 0) memo.Body.TranslationX += dx1;
                        if (dy1 > 0) memo.Body.TranslationY += dy1;
                        memo.Offset.X = memo.Body.TranslationX;
                        memo.Offset.Y = memo.Body.TranslationY;

                        foreach (Arrow arrow in memo.Arrows)
                        {
                            if (dx1 > 0) arrow.Body.TranslationX += dx1;
                            if (dy1 > 0) arrow.Body.TranslationY += dy1;
                            UpdateArrow(arrow);
                        }
                    }

                    StatusLabel.Text = "";
                    break;
            }

            Label1.Text = string.Format("(X, Y) = ( {0:0.00} , {1:0.00} )",
                                        PannedObject.TranslationX, PannedObject.TranslationY);
            //Label2.Text = string.Format("e.Total = ( {0:0.00} , {1:0.00} )",
            //                            e.TotalX, e.TotalY);

        }
#endif


        // Pinching MemoBoard
        void OnPinchMemoBoard(object sender, PinchGestureUpdatedEventArgs e)
        {
            AbsoluteLayout PinchedObject = (AbsoluteLayout)sender;

            switch (e.Status)
            {
                case GestureStatus.Started:
                    StatusLabel.Text = "Pinching MemoBoard";
                    break;

                case GestureStatus.Running:
                    PinchedObject.Scale *= e.Scale;

                    Label1.Text = string.Format("ScaleOrigin = ( {0:0.00} , {1:0.00} )",
                                                e.ScaleOrigin.X, e.ScaleOrigin.Y);
                    Label2.Text = string.Format("e.Scale = {0:0.00},  e.Scale Total = {1:0.00}",
                                                e.Scale, PinchedObject.Scale);

                    break;

                case GestureStatus.Completed:
#if BoxDisplay
                    // Red BoxView
                    box1.TranslationX = PinchedObject.TranslationX;
                    box1.TranslationY = PinchedObject.TranslationY;
                    // Blue BoxView
                    box2.TranslationX = PinchedObject.TranslationX + PinchedObject.Width;
                    box2.TranslationY = PinchedObject.TranslationY + PinchedObject.Height;
#endif

                    StatusLabel.Text = "MemoBoard Pinch Completed";
                    break;
            }
        }


        // If tapped MemoBoard, reset MemoBoard Scale
        void OnTapMemoBoard(object sender, EventArgs e)
        {
            AbsoluteLayout tappedObject = (AbsoluteLayout)sender;

            tappedObject.Scale = 1.0;
            ScaleSlider.Value = 1.0;

            StatusLabel.Text = "MemoBoard Tapped";
        }


        // Identify whether the object is ContentView or Editor
        object WhichObject(object o)
        {
            switch(o.GetType().Name)
            {
                case "ContentView":
                    foreach (Memo memo in Memos){                        
                        if (memo.Body == (ContentView)o) return memo;
                        foreach (Arrow arrow in memo.Arrows)
                            if (arrow.Body == (ContentView)o) return arrow;
                    }
                    break;

                case "Editor":
                    foreach (Memo memo in Memos)
                        if (memo.Body.Content == (Editor)o) return memo;
                    break;
            }

            StatusLabel.Text = "Error in WhichObject";
            return null;
        }
    }
}

"GetScrollBoardSize" method get the size of the ScrollVew "ScrollBoard" and store it as the original size of the AbsoluteLayout "MemoBoard."

"StickMemo" method create a new memo and stick it on "MemoBoard."  Tap, Pan, etc. events are added on the memo, and it is added to "MemoBoard" and "Memos."

"LinkeMemo" method is raised by the Button "LinkMemoButton" press, and it creates a new link on the memo tapped lastly.  (If the Text property of the "LinkMemoButton" is 'Cancel,' this method deletes the arrow created lastly.)

The "DeleteObject" method deletes a tapped memo or link.  The method identifies whether the previously tapped object is a memo or a link according to the Text property of the "StatusLabel."  When a memo is deleted, links linked to the memo are also deleted.

The "OnPanMemo" method manages the drag control for memos.  If a dragged memo reaches out bound of the "MemoBoard" when the drag (pan) is finished (GestureStatus.Completed), the "MemoBoard" is expanded and scrolled so as to display all memos and links properly.

The "OnPinchMemo" method scales memos with 2-finger interaction.  This time, memos are vertically expanded by 2-finger pinch, while these are horizontally expanded by 2-finger drag.

The "OnTapMemo" method is raised by tap action to memos.  This method is to select a memo or link to be linked or deleted.

"UpdateArrow" method updates the altitudes of all the links, as the memo which those links are linked are dragged.  In this code which is much better than the previous one "Drawing (1)," this method is simplified pretty much because link positions of memos and links can be calculated in class "Arrow."

The four methods from "EditTextchanged" to "TitleInputCompleted" are for the control of Editor and Entry in memos and links.

"OnPanMemoBoard," "OnPinchMemoBoard," and "OnTapMemoBoard" are for Gesture interaction of the "MemoBoard."
"OnPanMemoBoard" method can drag (pan) the whole "MemoBoard," so it looks that the "MemoBoard" is scrolled.
"OnPinchMemoBoard" method scales the "MemoBoard" simply.  It's convenient when the "MemoBoard" is expanded.
"OnTapMemoBoard" method restore the pinched "MemoBoard" scale to 1.0.

"WhichObject" method identifies the tapped object ContentView or Editor in a memo and a link to choose the following processing properly.


A movie showing this App running.  You can get the original movie file from here (in "movie" folder).


コメント

このブログの人気の投稿

Get the Color Code from an Image Pixel

PCL Storage (1)

Prolog Interpreter