1

我有一个 Xamarin 应用程序,它不打算处理 android 的对讲功能,因为它必须以特定方式构建才能正常工作。

我的应用程序是一个小订单,我根本无法完成整个事情。

那么,发生了什么?我的 Xamarin 应用程序是使用非本机库制作的,Talkback 不支持这些库,因此,当用户打开 Talkback 功能时,应用程序有效地停止接收 DPAD 事件,因为它们是由系统辅助功能服务处理的。

该服务获取事件,并尝试在我的应用程序中处理它们,但是,由于我的组件是非本地的,系统无法识别它们并且 DPAD 被浪费,因此产生 DPAD 不工作的错觉。

那么,如果您只想在开启 Talkback 的情况下自己处理 DPAD(而不是其他任何东西),您需要做什么?

这篇文章的答案将包含描述以下行为的代码:

1. The talkback wont be able to 'talk' about your components

2. The DPAD events will be handled by an Accessibility Delegate

3. A virtual DPAD will handle the navigation

4. The green rectangle used for focus will be disabled, since you wont need it anyway

5. The app will look exactly the same with Talkback on and off

这篇文章是出于教育目的,因为我很难想出解决方案,希望下一个人觉得它有帮助。

4

1 回答 1

1

第一步是创建一个继承 AccessibilityDelegateCompat 的类,以便创建我们自己的 Accessibility Service。

class MyAccessibilityHelper : AccessibilityDelegateCompat
{
    const string Tag = "MyAccessibilityHelper";
    const int ROOT_NODE = -1;
    const int INVALID_NODE = -1000;
    const string NODE_CLASS_NAME = "My_Node";

    public const int NODE_UP = 1;
    public const int NODE_LEFT = 2;
    public const int NODE_CENTER = 3;
    public const int NODE_RIGHT = 4;
    public const int NODE_DOWN = 5;

    private class MyAccessibilityProvider : AccessibilityNodeProviderCompat
    {
        private readonly MyAccessibilityHelper mHelper;

        public MyAccessibilityProvider(MyAccessibilityHelper helper)
        {
            mHelper = helper;
        }

        public override bool PerformAction(int virtualViewId, int action, Bundle arguments)
        {
            return mHelper.PerformNodeAction(virtualViewId, action, arguments);
        }

        public override AccessibilityNodeInfoCompat CreateAccessibilityNodeInfo(int virtualViewId)
        {
            var node = mHelper.CreateNode(virtualViewId);
            return AccessibilityNodeInfoCompat.Obtain(node);
        }
    }

    private readonly View mView;
    private readonly MyAccessibilityProvider mProvider;
    private Dictionary<int, Rect> mRects = new Dictionary<int, Rect>();
    private int mAccessibilityFocusIndex = INVALID_NODE;

    public MyAccessibilityHelper(View view)
    {
        mView = view;
        mProvider = new MyAccessibilityProvider(this);
    }

    public override AccessibilityNodeProviderCompat GetAccessibilityNodeProvider(View host)
    {
        return mProvider;
    }

    public override void SendAccessibilityEvent(View host, int eventType)
    {
        Android.Util.Log.Debug(Tag, "SendAccessibilityEvent: host={0} eventType={1}", host, eventType);
        base.SendAccessibilityEvent(host, eventType);
    }

    public void AddRect(int id, Rect rect)
    {
        mRects.Add(id, rect);
    }

    public AccessibilityNodeInfoCompat CreateNode(int virtualViewId)
    {
        var node = AccessibilityNodeInfoCompat.Obtain(mView);
        if (virtualViewId == ROOT_NODE)
        {
            node.ContentDescription = "Root node";
            ViewCompat.OnInitializeAccessibilityNodeInfo(mView, node);
            foreach (var r in mRects)
            {
                node.AddChild(mView, r.Key);
            }
        }
        else
        {
            node.ContentDescription = "";
            node.ClassName = NODE_CLASS_NAME;
            node.Enabled = true;
            node.Focusable = true;
            var r = mRects[virtualViewId];
            node.SetBoundsInParent(r);
            int[] offset = new int[2];
            mView.GetLocationOnScreen(offset);
            node.SetBoundsInScreen(new Rect(offset[0] + r.Left, offset[1] + r.Top, offset[0] + r.Right, offset[1] + r.Bottom));
            node.PackageName = mView.Context.PackageName;
            node.SetSource(mView, virtualViewId);
            node.SetParent(mView);
            node.VisibleToUser = true;
            if (virtualViewId == mAccessibilityFocusIndex)
            {
                node.AccessibilityFocused = true;
                node.AddAction(AccessibilityNodeInfoCompat.ActionClearAccessibilityFocus);
            }
            else
            {
                node.AccessibilityFocused = false;
                node.AddAction(AccessibilityNodeInfoCompat.FocusAccessibility);
            }
        }
        return node;
    }

    private AccessibilityEvent CreateEvent(int virtualViewId, EventTypes eventType)
    {
        var e = AccessibilityEvent.Obtain(eventType);
        if (virtualViewId == ROOT_NODE)
        {
            ViewCompat.OnInitializeAccessibilityEvent(mView, e);
        }
        else
        {
            var record = AccessibilityEventCompat.AsRecord(e);
            record.Enabled = true;
            record.SetSource(mView, virtualViewId);
            record.ClassName = NODE_CLASS_NAME;
            e.PackageName = mView.Context.PackageName;
        }
        return e;
    }

    public bool SendEventForVirtualView(int virtualViewId, EventTypes eventType)
    {
        if (mView.Parent == null)
            return false;
        var e = CreateEvent(virtualViewId, eventType);
        return ViewParentCompat.RequestSendAccessibilityEvent(mView.Parent, mView, e);
    }

    public bool PerformNodeAction(int virtualViewId, int action, Bundle arguments)
    {
        if (virtualViewId == ROOT_NODE)
        {
            return ViewCompat.PerformAccessibilityAction(mView, action, arguments);
        }
        else
        {
            switch (action)
            {
                case AccessibilityNodeInfoCompat.ActionAccessibilityFocus:
                    if (virtualViewId != mAccessibilityFocusIndex)
                    {
                        if (mAccessibilityFocusIndex != INVALID_NODE)
                        {
                            SendEventForVirtualView(mAccessibilityFocusIndex, EventTypes.ViewAccessibilityFocusCleared);
                        }
                        mAccessibilityFocusIndex = virtualViewId;
                        mView.Invalidate();
                        SendEventForVirtualView(virtualViewId, EventTypes.ViewAccessibilityFocused);
                        // virtual key event                            
                        switch (virtualViewId)
                        {
                            case NODE_UP:
                                HandleDpadEvent(Keycode.DpadUp);
                                break;
                            case NODE_LEFT:
                                HandleDpadEvent(Keycode.DpadLeft);
                                break;
                            case NODE_RIGHT:
                                HandleDpadEvent(Keycode.DpadRight);
                                break;
                            case NODE_DOWN:
                                HandleDpadEvent(Keycode.DpadDown); 
                                break;
                        }
                        // refocus center
                        SendEventForVirtualView(NODE_CENTER, EventTypes.ViewAccessibilityFocused);
                        return true;
                    }
                    break;
                case AccessibilityNodeInfoCompat.ActionClearAccessibilityFocus:
                    mView.RequestFocus();
                    if (virtualViewId == mAccessibilityFocusIndex)
                    {
                        mAccessibilityFocusIndex = INVALID_NODE;
                        mView.Invalidate();
                        SendEventForVirtualView(virtualViewId, EventTypes.ViewAccessibilityFocusCleared);
                        return true;
                    }
                    break;
            }
        }
        return false;
    }

    private void HandleDpadEvent(Keycode keycode)
    {
       //Here you know what DPAD was pressed
       //You can create your own key event and send it to your app
       //This code depends on your own application, and I wont be providing the code
       //Note, it is important to handle both, the KeyDOWN and the KeyUP event for it to work
    }
}

由于代码有点大,我只解释关键部分。一旦对讲处于活动状态,字典(从我们下面的视图)将用于创建我们虚拟 DPAD 的虚拟树节点。考虑到这一点,PerformNodeAction 函数将是最重要的一个。

一旦虚拟节点被辅助系统聚焦后,它就会处理操作,基于提供的虚拟元素的 id,有两个部分,第一个是 ROOT_NODE,它是包含我们的虚拟 dpad 的视图iteslf,它用于大部分可以忽略,但第二部分是处理完成的地方。

第二部分是处理 ActionAccessibilityFocus 和 ActionClearAccessibilityFocus 的地方。两个女巫都很重要,但第一个是我们最终可以处理虚拟 dpad 的地方。

这里所做的是使用字典中提供的虚拟 ID,我们知道选择了哪个 DPAD (virtualViewId)。根据选择的 DPAD,我们可以在 HandleDpadEvent 函数中执行我们想要的动作。需要注意的重要一点是,在我们处理了 selecteds DPAD 事件之后,我们将重新聚焦我们的 CENTER 节点,以便准备好处理下一个按钮按下。这一点非常重要,因为您不想发现自己处于向下然后向上的情况,只是为了让虚拟 dpad 聚焦 CENTER pad。

所以,我会重复自己,需要在处理上一个 DPAD 事件后重新调整中心垫,以便我们知道在按下下一个 DPAD 按钮后我们将在哪里!

有一个函数我不会在这里发布,因为它的代码对我的应用程序来说非常具体,函数是 HandleDpadEvent,你必须在那里创建一个 keydown 和一个 keyup 事件并将其发送到你的主要活动,其中函数 onKeyDown/Up将被触发。一旦你这样做了,委托就完成了。

一旦委托完成,我们必须使我们的视图像这样:

/**
* SimplestCustomView
*/
public class AccessibilityHelperView : View
{
    private MyAccessibilityHelper mHelper;

    Dictionary<int, Rect> virtualIdRectMap = new Dictionary<int, Rect>();

    public AccessibilityHelperView(Context context) :
        base(context)
    {
        Init();
    }

    public AccessibilityHelperView(Context context, IAttributeSet attrs) :
        base(context, attrs)
    {
        Init();
    }

    public AccessibilityHelperView(Context context, IAttributeSet attrs, int defStyle) :
        base(context, attrs, defStyle)
    {
        Init();
    }

    public void Init()
    {
        this.SetFocusable(ViewFocusability.Focusable);
        this.Focusable = true;
        this.FocusedByDefault = true;

        setRectangle();

        mHelper = new MyAccessibilityHelper(this);
        ViewCompat.SetAccessibilityDelegate(this, mHelper);
        foreach (var r in virtualIdRectMap)
        {
            mHelper.AddRect(r.Key, r.Value);
        }
    }

    private void setRectangle()
    {
        virtualIdRectMap.Add(MRAccessibilityHelper.NODE_CENTER, new Rect(1, 1, 2, 2));
        virtualIdRectMap.Add(MRAccessibilityHelper.NODE_LEFT, new Rect(0, 1, 1, 2));
        virtualIdRectMap.Add(MRAccessibilityHelper.NODE_UP, new Rect(1, 0, 2, 1));
        virtualIdRectMap.Add(MRAccessibilityHelper.NODE_RIGHT, new Rect(2, 1, 3, 2));
        virtualIdRectMap.Add(MRAccessibilityHelper.NODE_DOWN, new Rect(1, 2, 2, 3));
    }

    protected override void OnDraw(Canvas canvas)
    {
        base.OnDraw(canvas);
    }
}

该视图如下所示:

虚拟键盘

有什么要注意的?

  1. 节点焊盘的大小以像素为单位,它们将位于应用程序的左上角。

  2. 它们被设置为单个像素大小,因为对讲功能会选择添加到字典中的第一个节点焊盘,并带有绿色矩形(这是对讲的标准行为)

  3. 视图中的所有矩形都添加到字典中,将在我们自己的 Accessibility Delegate 中使用,这里要提到的是首先添加了 CENTER pad,因此默认情况下激活对讲后将成为焦点

  4. 初始化函数

Init 函数对此至关重要,我们将在那里创建我们的视图,并设置一些对讲参数,以便我们的虚拟 dpad 能够被系统自己的辅助功能服务识别。

此外,我们的 Accessibility Delegate 将被初始化,我们的字典将包含所有创建的 DPAD。

好的,到目前为止,我们制作了一个 Delegate 和一个 View,我将它们放在同一个文件中,这样它们就可以互相看到了。但这不是必须的。

所以现在怎么办?我们必须在 MainActivity.cs 文件中将 AccessibilityHelperView 添加到我们的应用程序中

AccessibilityHelperView mAccessibilityHelperView;

在 OnCreate 函数中,您可以添加以下代码来启动视图:

mAccessibilityHelperView = new AccessibilityHelperView(this);

在 OnResume 函数中,您可以检查对讲是否打开或关闭,根据结果,您可以从 mBackgroundLayout(AddView 和 RemoveView)中添加或删除 mAccessibilityHelperView。

OnResume 函数应如下所示:

 if (TalkbackEnabled && !_isVirtualDPadShown)
 {
     mBackgroundLayout.AddView(mAccessibilityHelperView);
     _isVirtualDPadShown = true;
 }
 else if (!TalkbackEnabled && _isVirtualDPadShown)
 {
      mBackgroundLayout.RemoveView(mAccessibilityHelperView);
      _isVirtualDPadShown = false;
 }

TalkbackEnabled 变量是一个本地变量,用于检查 Talkback 服务是打开还是关闭,如下所示:

 public bool TalkbackEnabled 
 {
        get
        {
            AccessibilityManager am = MyApp.Instance.GetSystemService(Context.AccessibilityService) as AccessibilityManager;
            if (am == null) return false;

            String TALKBACK_SETTING_ACTIVITY_NAME = "com.android.talkback.TalkBackPreferencesActivity";
            var serviceList = am.GetEnabledAccessibilityServiceList(FeedbackFlags.AllMask);
            foreach (AccessibilityServiceInfo serviceInfo in serviceList)
            {
                String name = serviceInfo.SettingsActivityName;
                if (name.Equals(TALKBACK_SETTING_ACTIVITY_NAME))
                {
                    Log.Debug(LogArea, "Talkback is active");
                    return true;
                }
            }
            Log.Debug(LogArea, "Talkback is inactive");
            return false;
        }
 }

这应该是让它工作所需要的一切。

希望我能帮助你。

于 2021-06-07T08:05:34.677 回答