Tap & Drag on ScrollView (1)

In the previous post, Drawing (1), I developed an Xamarin.Forms App that draws correlation diagrams of novel characters.  In other word, the App can display graph data, which nodes are ContextView corresponding to novel characters and linked each other.  At that time, I tried to use ScrollView to be able to show a large correlation diagram including many novel characters.  But it's not so easy, because tap & drag are stolen by ScrollVIew.

I googled and googled, but nothing seem to make sense.  So I made it possible to Tap & Drag on ScrollView in a pseudo way.

The point is content size of ScrollView.  In ScrollView, scroll is enable if content size is larger than window (display) size.  But, on the contrary, if content size is smaller than window size, scroll is disable (Naturally!).  Besides, while scroll is disable, Tap & Drag is not stolen by ScrollView.  So, the following simple procedure,

  • Reduce content size of ScrollView to window (display) size when ContextView is tapped,
  • Enlarge the reduced content size to the original size when ContextView is released (tap & drag is finished), 
enabled to Tap & Drag on ScrollView.  The source code and details are below.


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" 
    x:Class="DragOnScrollView.MainPage">

    <StackLayout Margin="10, 30, 0, 0">
        <StackLayout BackgroundColor="Gray" Padding="0,0,0,1">
            <Label Text="Drag Items on ScrollView Demo:" BackgroundColor="White" />
        </StackLayout>
        <StackLayout BackgroundColor="Gray" Padding="0,0,0,1" >
            <StackLayout Orientation="Horizontal" BackgroundColor="White" Padding="0,0,0,3">
                <Button Text="Add ContentView" Clicked="AddContentViewButton"
                        WidthRequest="130" HeightRequest="30" BorderColor="Blue" BorderWidth="1" />
            </StackLayout>
        </StackLayout>

        
        <Grid>
            <ScrollView x:Name="scrollView" Orientation="Both" >
                <AbsoluteLayout x:Name="absoluteLayout" />
            </ScrollView>
        </Grid>

        <!-- Data Display Area -->
        <StackLayout BackgroundColor="Gray" VerticalOptions="EndAndExpand" Padding="0,1,0,1">
            <StackLayout BackgroundColor="White" >
                <Label x:Name="Label1" Text="Scroll X =    , Y = " />
                <Label x:Name="Label2" Text="Dragged Position X =    , Y = " />
                <Label x:Name="Label3" Text="Display Size of ScrollView: " />
                <Label x:Name="Label4" Text="The number of ContentView: " />
            </StackLayout>
        </StackLayout>
    </StackLayout>
    
</ContentPage>
(The source code is also here.)

Set a Button to create and add a ContextView in order to display two more ContextViews on ScrollView.  (This is not necessary (just one ContextView is enough) if only to test Tap & Drag on ScrollView, but the App is lively and it's fun!)  In a Grid below the Button, place a ScrollView including an AbsoluteLayout. (Grid is necessary? I don't know.)  ContextViews are created on the AbsoluteLayout, and those are content of the ScrollView.  As I mentioned above, scroll On/Off control are available by changing the size of the AbsoluteLayout.  Details are described below.
At the bottom of the display, some Labels are placed to show some variables' values, so it seems to be Debug Monitor.  I often use DisplayAlert to do it, but this method might be more convenient, if there is enough space on display.  Four Labels are placed at fixed position this time, I will try to show many values scrolling above like command prompt of MS-DOS.


MainPage.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;

using Xamarin.Forms;

using TouchTracking;

namespace DragOnScrollView
{
    public partial class MainPage : ContentPage
    {
        public class Position
        {
            public double XY;
        }
        public class Scroll
        {
            public double XY;
        }

        Dictionary<ContentView, DragInfo> dragDictionary = new Dictionary<ContentView, DragInfo>();

        int NoCV = 0;           // The number of the created contentView
        int drag = 0;           // Identification Number of the dragged contentView
        static int max = 20;    // Maximum number of contentViews
        ContentView[] contentViews = new ContentView[max];
        Position[] positions = new Position[max];       // contentViews[i]'s position in scrollView
        Position[] localpositions = new Position[max];  // contentViews[i]'s position in absoluteLayout

        Scroll scroll = new Scroll();   // scroll amount when scroll ON to OFF
                                        //   = absoluteLayout's postion in scroll View
        Scroll ScrollSize = new Scroll { X = 700Y = 900 }; // Size of ScrollView


        public MainPage()
        {
            InitializeComponent();

            // Initialize positions
            for (int i = 0i < maxi++)
            {
                positions[i] = new Position { X = 0Y = 0 };
                localpositions[i] = new Position { X = 0Y = 0 };
            }

            AddContentView();
            OnScroll();

        }


        // Scroll Off to On
        void OnScroll()
        {
            // Enable Scroll by enlarging absoluteLayout size
            absoluteLayout.WidthRequest  = ScrollSize.X;
            absoluteLayout.HeightRequest = ScrollSize.Y;
            absoluteLayout.BackgroundColor = Color.LightGray;

            // Scroll Back to the position when scroll ON to OFF
            scrollView.ScrollToAsync( scroll.Xscroll.Yfalse );

            // Update dragged contentView's positions[drag]
            positions[drag].X = scroll.X + localpositions[drag].X;
            positions[drag].Y = scroll.Y + localpositions[drag].Y;

            // re-locate all contentViews in "large" absoluteLayout
            for (int i = 0i < NoCVi++)
            {
                Rectangle rect = AbsoluteLayout.GetLayoutBounds(contentViews[i]);
                rect.X = positions[i].X;
                rect.Y = positions[i].Y;
                AbsoluteLayout.SetLayoutBounds(contentViews[i], rect);
            }

            Label3.Text = "Display Size of ScrollView: " + scrollView.Width.ToString() +
                           " x " + scrollView.Height.ToString() + "  Label3";
            //DisplayAlert("", "Scroll X = " + scrollView.Width.ToString() +
            // ", Y = " + scrollView.Height.ToString(), "OK");
        }

        // Scroll On to Off
        void OffScroll()
        {
            // Disable Scroll by reducing absoluteLayout size
            absoluteLayout.WidthRequest = scrollView.Width;
            absoluteLayout.HeightRequest = scrollView.Height;
            absoluteLayout.BackgroundColor = Color.White;

            // get scroll amount when scroll On to Off
            scroll.X = scrollView.ScrollX;
            scroll.Y = scrollView.ScrollY;

            // locate contentView in "small" absoluteLayout
            for (int i = 0i < NoCVi++)
            {
                Rectangle rect = AbsoluteLayout.GetLayoutBounds(contentViews[i]);
                rect.X = positions[i].X - scroll.X;
                rect.Y = positions[i].Y - scroll.Y;
                AbsoluteLayout.SetLayoutBounds(contentViews[i], rect);
            }

            Label1.Text = "Scroll X = " + scroll.X.ToString() + 
                               ", Y = " + scroll.Y.ToString();
            Label3.Text = "Display Size of ScrollView: " + scrollView.Width.ToString() +
                                                   " x " + scrollView.Height.ToString();
        }



        void AddContentViewButton(object senderEventArgs args)
        {
            AddContentView();
        }

        /*
        void Button2(object sender, EventArgs args)
        {
            scrollView.ScrollToAsync( 100, 100, false );
        }
        */

        //void AddBoxViewToLayout(AbsoluteLayout absLayout)

        void AddContentView()
        {
            ContentView contentView = new ContentView
            {
                WidthRequest = 200,
                HeightRequest = 100,
                BackgroundColor = Color.Black,
                Padding = new Thickness(2222),
                Content = new Editor
                {
                    FontSize = 20,
                    InputTransparent = true,    // Disable to edit
                    Text = "contentViews[" + NoCV + "]"
                }
            };

            contentViews[NoCV] = contentView;
            NoCV++;
            Label4.Text = "The number of ContentView: " + NoCV.ToString();

            TouchEffect touchEffect = new TouchEffect();
            touchEffect.TouchAction += OnTouchEffectAction;
            contentView.Effects.Add(touchEffect);
            absoluteLayout.Children.Add(contentView);
        }


        void OnTouchEffectAction(object sender, TouchActionEventArgs args)
        {
            ContentView view = sender as ContentView;

            // identify the dragged contentView
            for (int i = 0i < NoCVi++)
            {
                if (view == contentViews[i]) { drag = ibreak; }
            }

            switch (args.Type)
            {
                case TouchActionType.Pressed:
                    // Don't allow a second touch on an already touched BoxView
                    if (!dragDictionary.ContainsKey(view))
                    {
                        dragDictionary.Add(viewnew DragInfo(args.Id, args.Location));

                        // Set Capture property to true
                        TouchEffect touchEffect = (TouchEffect)view.Effects.FirstOrDefault(e => e is TouchEffect);
                        touchEffect.Capture = true;
                    }

                    OffScroll();

                    break;

                case TouchActionType.Moved:
                    if (dragDictionary.ContainsKey(view) && dragDictionary[view].Id == args.Id)
                    {
                        Rectangle rect = AbsoluteLayout.GetLayoutBounds(view);
                        Point initialLocation = dragDictionary[view].PressPoint;
                        rect.X += args.Location.X - initialLocation.X;
                        rect.Y += args.Location.Y - initialLocation.Y;
                        AbsoluteLayout.SetLayoutBounds(viewrect);
                    }
                    break;

                case TouchActionType.Released:
                    if (dragDictionary.ContainsKey(view) && dragDictionary[view].Id == args.Id)
                    {
                        Rectangle rect = AbsoluteLayout.GetLayoutBounds(view);

                        localpositions[drag].X = rect.X;
                        localpositions[drag].Y = rect.Y;

                        Label2.Text = "Position in absoluteLayout: X = " + rect.X.ToString() +
                             ", Y = " + rect.Y.ToString();
                        
                        dragDictionary.Remove(view);
                    }

                    OnScroll();

                    break;
            }
        }
    }
}

After some variables declaration, in MainPage, initialization and create a ContextView on the AbsoluteLayout by AddContextView method.  Then enable scroll by OnScroll method.  This is the first state just after the App is launched.

AddContextView method create a ContextView including an Editor.  In this version, its own created order of ContextView are shown in the Editor, and set InputTransparent true because it's not necessary to be edited.  And also, if InputTransparent is false, Tap on ContextView put the Editor of the ContextView to editor-mode.  (Tap is stolen not by ScrollView, but by Editor!)  After saving the created ContextView in array data to manage it later, enable TouchEffect on it and place it on the AbsoluteLayout.  This procedure is the same as Invoking Events of Effects of Xamarin Official Site.

OnTouchEffectAction method is almost the same as the official site mentioned above, but there are some following different points.  The "for" sentence at the beginning of this method is to identify tapped ContextView from all created ContextViews.  And scroll On/Off of the ScrollView is controlled in this method.  Scroll On to Off when the ContextView is tapped (pressed), scroll Off to On when Tap & Drag is finished (the ContextView is released).  At the end of Tap & Drag, final position of the dragged ContextView are saved as the variable "localpositions", and they are referred in OnScroll method explained later.

The next two methods are the most important part of this App.  Firstly, I explain OffScroll method which is called when ContextView is tapped.  This method, at first, changes (reduce) the size of the AbsoluteLayout to its window (display) size, which size can be obtained with ScrollView.Width/Height.  This size change disable scroll in the ScrollView.  Save the scroll amount to the variable "scroll" referred later, and then calculate the position, that the ContextView should be displayed, as the variable "rect" from its position in ScrollView, "positions[]" and scroll amount, "scroll", and place the ContextView to the position, "rect", by SetLayoutBounds method.  This process is the coordinate transformation between the original (large) AbsoluteLayout and the reduced (small) AbsoluteLayout.  Since forcibly change the AbsoluteLayout size, the ContextView is not displayed at the correct position without this transformation.

Finally, OnScroll method enlarge the reduced AbsoluteLayout to the original size (1000x1800 in this version), and enable scroll in ScrollView.  Call ScrollToAsync method and scroll back to the scroll position when the ContextView was tapped, because the display position (scroll amount) of the ScrollView become zero when the AbsoluteLayout size return to the original one.  Then, as OffScroll method, coordinate transformation is necessary in also this method.  Calculate the dragged ContextView position, "positions[drag]", from the scroll amount, "scroll", and the final position in display, "localpositions[drag]."  After that, place all the ContextViews in the original (large) size ScrollView.

As I described above, combining OffScroll(), OnScroll(), and the modified OnTouchEffectAction(), although it is pseudo way, Tap & Drag became available in ScrollVIew.

App.xaml.cs, TouchTracking.cs, TouchEffect.cs, and TouchRecognizer.cs are the same as the previous post, Drawing (1).  SkiaSharp is not used this time.

OK, run this App.

Sorry for poor resolution.  You can get the original movie file from here ("iOS Simulator Movie" folder).


コメント

このブログの人気の投稿

Get the Color Code from an Image Pixel

PCL Storage (1)

Prolog Interpreter