A common gesture for manipulating objects on the screen is to use the twist or rotate gesture. This is often more natural than entering a rotation value or dragging a cursor.
Another multifinger gesture that is very common is the rotate gesture. For a two-finger rotate, we can do something very similar to what we would do when implementing the ScaleGestureDetector
instance. However, we do have to create a RotateGestureDetector
instance ourselves as this is not currently provided by the framework. We will create RotateGestureDetector
by preforming the following steps:
public interface IOnRotateGestureListener { bool OnRotateBegin(RotateGestureDetector detector); void OnRotate(RotateGestureDetector detector); void OnRotateEnd(RotateGestureDetector detector); }
public class RotateGestureDetector { private float oldX2, oldY2, oldX1, oldY1; private int pointerId1, pointerId2; private IOnRotateGestureListener listener; public RotateGestureDetector( IOnRotateGestureListener listener) { this.listener = listener; pointerId1 = -1; pointerId2 = -1; } public bool IsInProgress { get; private set; } public float Angle { get; private set; } public float FocusX { get; private set; } public float FocusY { get; private set; } public bool OnTouchEvent(MotionEvent e) { return true; } }
Once we have the structure in place, we can now implement the logic that will process the touch events from the view. It is important to ensure that we return the correct bool
value when we handle the event to let any other gesture detectors know that it has been handled:
OnTouchEvent()
method so that we can process the rotate gesture:public bool OnTouchEvent(MotionEvent e) { var pointerIndex = e.ActionIndex; var pointerId = e.GetPointerId(pointerIndex); switch (e.ActionMasked) { case MotionEventActions.Down: pointerId1 = pointerId; pointerId2 = -1; IsInProgress = false; break; case MotionEventActions.PointerDown: if (pointerId1 != -1 && pointerId2 == -1) { pointerId2 = pointerId; var index1 = e.FindPointerIndex(pointerId1); var index2 = e.FindPointerIndex(pointerId2); oldX1 = e.GetX(index1); oldY1 = e.GetY(index1); oldX2 = e.GetX(index2); oldY2 = e.GetY(index2); FocusX = (oldX1 + oldX2) / 2f; FocusY = (oldY1 + oldY2) / 2f; Angle = 0; if (listener != null) { IsInProgress = listener.OnRotateBegin(this); } } break; case MotionEventActions.Move: if (IsInProgress) { float newX2, newY2, newX1, newY1; var index1 = e.FindPointerIndex(pointerId1); var index2 = e.FindPointerIndex(pointerId2); newX1 = e.GetX(index1); newY1 = e.GetY(index1); newX2 = e.GetX(index2); newY2 = e.GetY(index2); FocusX = (newX1 + newX2) / 2f; FocusY = (newY1 + newY2) / 2f; var oldA = Math.Atan2(oldY1 - oldY2, oldX1 - oldX2); var newA = Math.Atan2(newY1 - newY2, newX1 - newX2); var angle = (float)(oldA - newA); Angle = angle * 180.0f / (float)Math.PI; oldX1 = newX1; oldY1 = newY1; oldX2 = newX2; oldY2 = newY2; if (listener != null) { listener.OnRotate(this); } } break; case MotionEventActions.Up: case MotionEventActions.PointerUp: if (IsInProgress) { if (pointerId == pointerId1) { pointerId1 = pointerId2; pointerId2 = -1; } else if (pointerId == pointerId2) { pointerId2 = -1; } if (pointerId1 == -1 || pointerId2 == -1) { IsInProgress = false; if (listener != null) { listener.OnRotateEnd(this); } } } break; } return true; }
Now that we have completed our rotate gesture detector, we can use it to perform a rotation:
IOnRotateGestureListener
interface:public class MyView : View, IOnRotateGestureListener { public bool OnRotateBegin(RotateGestureDetector detector) { return true; } public void OnRotate(RotateGestureDetector detector) { } public void OnRotateEnd(RotateGestureDetector detector) { } }
RotateGestureDetector
:rotateDetector = new RotateGestureDetector(this);
OnTouchEvent()
method to pass the event to the rotate gesture detector:public override bool OnTouchEvent(MotionEvent e) { var handled = rotateDetector.OnTouchEvent(e); return handled || base.OnTouchEvent(e); }
OnRotate()
method, we can read the rotation angle since the last update:public void OnRotate(RotateGestureDetector detector) { rotation = (rotation + detector.Angle) % 360f; }
Using touch as input is one of the easiest forms of input. Whether it be a stretch, twist, or fling, the user will always find it easier to actually interact with the objects on the screen.
Android provides many of the common forms of gesture detection, but not everything that may be needed has been added to the framework. However, there is no reason why we cannot create custom gesture detectors to make it easier for the user to work with our app.
We can implement touch event processing in many ways, but following the patterns of the Android framework makes it easier to reuse the code in other apps as well as to make it easier to maintain in the future.
Custom gesture detectors are simple to create, requiring an interface that defines the events of the gesture detector, and the gesture detector itself. The gesture detector receives all the touch events from the view, which are processed. When the gesture actually occurs, the appropriate method on the interface is invoked.
In the case of our rotate gesture detector, as soon as the user places two fingers on the screen, we initiate the rotate gesture. As the user moves their fingers across the screen, we calculate the size of the angle that the fingers have moved in. The angle is calculated from the lines formed from the location of the two fingers on the screen. When the user lifts a finger off the screen, we complete the rotate gesture.
Like with the scale gesture detector, we implement the interface on our view and create an instance of the RotateGetureDetector
instance. Then, we override the OnTouchEvent()
method, passing the touch event data to the gesture detector. This will result in the interface methods being invoked when a rotate gesture is detected.
We can prevent the rotate gesture from being performed for some reason by returning false
from the OnRotateBegin()
method. This is useful when we want to allow objects to be rotated on the screen, but only when both fingers are actually touching the object.