演练 - 在 Android 中使用触控

我们来看一看如何在工作应用程序中使用上一部分中的概念。 我们将创建一个包含四个活动的应用程序。 第一个活动将是一个菜单或一个切换板,用于启动其他活动来演示各种 API。 以下屏幕截图显示了主要活动:

Example screenshot with Touch Me button

第一个活动是触控示例,它将演示如何使用事件处理程序来触控视图。 手势识别器活动将演示如何创建子类 Android.View.Views 和处理事件,以及演示如何处理捏合手势。 第三个和最后一个活动是自定义手势,它将显示如何使用自定义手势。 为了便于理解,我们将本演练分为几个部分,其中每个部分将重点介绍其中一个活动。

触控示例活动

  • 打开项目 TouchWalkthrough_Start。 MainActivity 已准备就绪,我们需要实施活动中的触控行为。 如果运行应用程序并单击“触控示例”,则应启动以下活动:

    Screenshot of activity with Touch Begins displayed

  • 现在,我们已确认活动启动,可以打开文件 TouchActivity.cs 并为 ImageViewTouch 事件添加处理程序:

    _touchMeImageView.Touch += TouchMeImageViewOnTouch;
    
  • 接下来,将以下方法添加到 TouchActivity.cs:

    private void TouchMeImageViewOnTouch(object sender, View.TouchEventArgs touchEventArgs)
    {
        string message;
        switch (touchEventArgs.Event.Action & MotionEventActions.Mask)
        {
            case MotionEventActions.Down:
            case MotionEventActions.Move:
            message = "Touch Begins";
            break;
    
            case MotionEventActions.Up:
            message = "Touch Ends";
            break;
    
            default:
            message = string.Empty;
            break;
        }
    
        _touchInfoTextView.Text = message;
    }
    

请注意,在上面的代码中,我们将 MoveDown 视为相同的操作。 这是因为,即使用户可能未将手指抬离 ImageView,它也可能会四处移动,或者用户施加的压力可能会发生改变。 这些类型的更改将生成 Move 操作。

每次用户触摸 ImageView 时,都会引发 Touch 事件,并且处理程序将在屏幕上显示“触控开始”消息,如以下屏幕截图所示:

Screenshot of activity with Touch Begins

只要用户触摸 ImageView,就会在 TextView 中显示“触控开始”。 当用户不再触摸 ImageView 时,TextView 中就会显示消息“触控结束”,如以下屏幕截图所示:

Screenshot of activity with Touch Ends

手势识别器活动

现在,我们来实现手势识别器活动。 此活动将演示如何在屏幕上拖动视图,并演示一种实现捏合以缩放的方法。

  • 将新活动添加到名为 GestureRecognizer 应用程序。 编辑此活动的代码,已使其类似于以下代码:

    public class GestureRecognizerActivity : Activity
    {
        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);
            View v = new GestureRecognizerView(this);
            SetContentView(v);
        }
    }
    
  • 向项目添加新的 Android 视图,并将其命名为 GestureRecognizerView。 将以下变量添加到此类:

    private static readonly int InvalidPointerId = -1;
    
    private readonly Drawable _icon;
    private readonly ScaleGestureDetector _scaleDetector;
    
    private int _activePointerId = InvalidPointerId;
    private float _lastTouchX;
    private float _lastTouchY;
    private float _posX;
    private float _posY;
    private float _scaleFactor = 1.0f;
    
  • 将下列构造函数添加到 GestureRecognizerView。 此构造函数将向活动添加一个 ImageView。 此时,代码仍然不会编译 - 我们需要创建 MyScaleListener 类,以在用户捏合它时帮助调整 ImageView 的大小:

    public GestureRecognizerView(Context context): base(context, null, 0)
    {
        _icon = context.Resources.GetDrawable(Resource.Drawable.Icon);
        _icon.SetBounds(0, 0, _icon.IntrinsicWidth, _icon.IntrinsicHeight);
        _scaleDetector = new ScaleGestureDetector(context, new MyScaleListener(this));
    }
    
  • 要在活动上绘制图像,我们需要替代 View 类的 OnDraw 方法,如以下代码片段所示。 此代码会将 ImageView 移动到 _posX_posY 指定的位置,并根据缩放系数调整图像大小:

    protected override void OnDraw(Canvas canvas)
    {
        base.OnDraw(canvas);
        canvas.Save();
        canvas.Translate(_posX, _posY);
        canvas.Scale(_scaleFactor, _scaleFactor);
        _icon.Draw(canvas);
        canvas.Restore();
    }
    
  • 接下来,我们需要在用户捏合 ImageView 时更新实例变量 _scaleFactor。 我们将添加一个名为 MyScaleListener 的类。 当用户捏合 ImageView 时,此类将会侦听由 Android 引发的缩放事件。 将以下内部类添加到 GestureRecognizerView。 此类是一个 ScaleGesture.SimpleOnScaleGestureListener。 此类是一个便利类,如果你对一部分手势感兴趣时,侦听器可以创建子类:

    private class MyScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener
    {
        private readonly GestureRecognizerView _view;
    
        public MyScaleListener(GestureRecognizerView view)
        {
            _view = view;
        }
    
        public override bool OnScale(ScaleGestureDetector detector)
        {
            _view._scaleFactor *= detector.ScaleFactor;
    
            // put a limit on how small or big the image can get.
            if (_view._scaleFactor > 5.0f)
            {
                _view._scaleFactor = 5.0f;
            }
            if (_view._scaleFactor < 0.1f)
            {
                _view._scaleFactor = 0.1f;
            }
    
            _view.Invalidate();
            return true;
        }
    }
    
  • 我们需要在 GestureRecognizerView 中替代的下一种方法是 OnTouchEvent。 以下代码列出了此方法的完整实现。 这里有很多代码,所以让我们花一分钟时间看看这里发生了什么。 此方法要执行的第一个操作是根据需要缩放图标 - 这是通过调用 _scaleDetector.OnTouchEvent 来处理的。 接下来,我们尝试找出调用此方法的操作:

    • 如果用户触摸屏幕,我们将记录 X 和 Y 位置以及触摸屏幕的第一个指针的 ID。

    • 如果用户在屏幕上移动了触摸点,则我们将确定用户移动指针的距离。

    • 如果用户已将手指抬离屏幕,我们将停止跟踪手势。

    public override bool OnTouchEvent(MotionEvent ev)
    {
        _scaleDetector.OnTouchEvent(ev);
    
        MotionEventActions action = ev.Action & MotionEventActions.Mask;
        int pointerIndex;
    
        switch (action)
        {
            case MotionEventActions.Down:
            _lastTouchX = ev.GetX();
            _lastTouchY = ev.GetY();
            _activePointerId = ev.GetPointerId(0);
            break;
    
            case MotionEventActions.Move:
            pointerIndex = ev.FindPointerIndex(_activePointerId);
            float x = ev.GetX(pointerIndex);
            float y = ev.GetY(pointerIndex);
            if (!_scaleDetector.IsInProgress)
            {
                // Only move the ScaleGestureDetector isn't already processing a gesture.
                float deltaX = x - _lastTouchX;
                float deltaY = y - _lastTouchY;
                _posX += deltaX;
                _posY += deltaY;
                Invalidate();
            }
    
            _lastTouchX = x;
            _lastTouchY = y;
            break;
    
            case MotionEventActions.Up:
            case MotionEventActions.Cancel:
            // We no longer need to keep track of the active pointer.
            _activePointerId = InvalidPointerId;
            break;
    
            case MotionEventActions.PointerUp:
            // check to make sure that the pointer that went up is for the gesture we're tracking.
            pointerIndex = (int) (ev.Action & MotionEventActions.PointerIndexMask) >> (int) MotionEventActions.PointerIndexShift;
            int pointerId = ev.GetPointerId(pointerIndex);
            if (pointerId == _activePointerId)
            {
                // This was our active pointer going up. Choose a new
                // action pointer and adjust accordingly
                int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                _lastTouchX = ev.GetX(newPointerIndex);
                _lastTouchY = ev.GetY(newPointerIndex);
                _activePointerId = ev.GetPointerId(newPointerIndex);
            }
            break;
    
        }
        return true;
    }
    
  • 现在运行应用程序,并启动手势识别器活动。 当它启动时,屏幕外观应类似于下面的屏幕截图:

    Gesture Recognizer start screen with Android icon

  • 现在,触摸图标,并在屏幕上四处拖动它。 尝试捏合以缩放手势。 在某些时候,屏幕外观可能类似于以下屏幕截图:

    Gestures move icon around the screen

此时,应该奖励一下自己:你刚刚在 Android 应用程序中实现了捏合以缩放设置! 休息片刻后,我们继续进行本演练中的第三个和最后一个活动 - 使用自定义手势。

自定义手势活动

本演练中的最后一个屏幕将使用自定义手势。

为了进行本次演练,已使用手势工具创建了手势库,并已将其添加到文件 Resources/raw/gestures 中的项目。 这些准备工作已经完成,我们开始完成演练的最后一个活动。

  • 将名为 custom_gesture_layout.axml 的布局文件添加到包含以下内容的项目中。 项目已包含 Resources 文件夹中的所有图像:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1" />
        <ImageView
            android:src="@drawable/check_me"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="3"
            android:id="@+id/imageView1"
            android:layout_gravity="center_vertical" />
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1" />
    </LinearLayout>
    
  • 接下来,向项目添加新的活动并将其命名为 CustomGestureRecognizerActivity.cs。 将两个实例变量添加到该类,如以下两行代码所示:

    private GestureLibrary _gestureLibrary;
    private ImageView _imageView;
    
  • 编辑此活动的 OnCreate 方法,以将其修改为类似于以下代码。 我们来花一分钟时间解释以下此代码中发生的情况。 我们要做的第一件事是将 GestureOverlayView 实例化并将其设置为活动的根视图。 我们还将事件处理程序分配给 GestureOverlayViewGesturePerformed 事件。 接下来,我们将扩充之前创建的布局文件,并将其添加为 GestureOverlayView 的子视图。 最后一步是初始化变量 _gestureLibrary,并从应用程序资源加载手势文件。 如果由于某种原因无法加载手势文件,则此活动会无事可做,因此会被关闭:

    protected override void OnCreate(Bundle bundle)
    {
        base.OnCreate(bundle);
    
        GestureOverlayView gestureOverlayView = new GestureOverlayView(this);
        SetContentView(gestureOverlayView);
        gestureOverlayView.GesturePerformed += GestureOverlayViewOnGesturePerformed;
    
        View view = LayoutInflater.Inflate(Resource.Layout.custom_gesture_layout, null);
        _imageView = view.FindViewById<ImageView>(Resource.Id.imageView1);
        gestureOverlayView.AddView(view);
    
        _gestureLibrary = GestureLibraries.FromRawResource(this, Resource.Raw.gestures);
        if (!_gestureLibrary.Load())
        {
            Log.Wtf(GetType().FullName, "There was a problem loading the gesture library.");
            Finish();
        }
    }
    
  • 我们需要执行最后一个操作来实现方法 GestureOverlayViewOnGesturePerformed,如以下代码片段所示。 当 GestureOverlayView 检测到手势时,它会回调此方法。 我们首先尝试通过调用 _gestureLibrary.Recognize() 来获取与手势匹配的 IList<Prediction> 对象。 使用一些 LINQ 获取具有该手势最高分数的 Prediction

    如果没有具有足够高分数的匹配手势,则事件处理程序将退出而不执行任何操作。 否则,我们将检查预测的名称,并根据手势的名称更改所显示的图像:

    private void GestureOverlayViewOnGesturePerformed(object sender, GestureOverlayView.GesturePerformedEventArgs gesturePerformedEventArgs)
    {
        IEnumerable<Prediction> predictions = from p in _gestureLibrary.Recognize(gesturePerformedEventArgs.Gesture)
        orderby p.Score descending
        where p.Score > 1.0
        select p;
        Prediction prediction = predictions.FirstOrDefault();
    
        if (prediction == null)
        {
            Log.Debug(GetType().FullName, "Nothing seemed to match the user's gesture, so don't do anything.");
            return;
        }
    
        Log.Debug(GetType().FullName, "Using the prediction named {0} with a score of {1}.", prediction.Name, prediction.Score);
    
        if (prediction.Name.StartsWith("checkmark"))
        {
            _imageView.SetImageResource(Resource.Drawable.checked_me);
        }
        else if (prediction.Name.StartsWith("erase", StringComparison.OrdinalIgnoreCase))
        {
            // Match one of our "erase" gestures
            _imageView.SetImageResource(Resource.Drawable.check_me);
        }
    }
    
  • 运行应用程序并启动自定义手势识别器活动。 它应类似于以下屏幕截图:

    Screenshot with Check Me image

    现在,在屏幕上绘制复选标记,所显示的位图应类似于下一屏幕截图所示:

    Drawn checkmark, checkmark is recognized

    最后,在屏幕上绘制一个涂鸦。 该复选框应更改回其原始图像,如以下屏幕截图所示:

    Scribble on the screen, original image is displayed

现在,你已了解如何使用 Xamarin.Android 在 Android 应用程序中集成触控和手势。