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.
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.
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."
"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).
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.<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 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; }
}
}
}
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;
}
}
}
// #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).
コメント
コメントを投稿