专栏名称: CSDN
CSDN精彩内容每日推荐。我们关注IT产品研发背后的那些人、技术和故事。
目录
相关文章推荐
凤凰网科技  ·  国补 3999 元起,最便宜的 AI ... ·  3 天前  
51好读  ›  专栏  ›  CSDN

手机游戏无障碍设计——猜地鼠之Android篇

CSDN  · 公众号  · 科技媒体  · 2017-02-02 11:58

正文

作者简介: 何金源,腾讯Android手Q开发工程师,2011年本科毕业,负责Android手Q无障碍优化工作,对Android无障碍系统原理及开发技术有深入了解。

责编: 何永灿 [email protected]

本文为 《程序员》 原创文章,未经允许不得转载,更多精彩文章请订阅2017年《程序员》


手机应用无障碍化逐渐受到重视,这项技术为盲人或者视力有障碍的人士带来了很大便利。那么对于手机游戏,同样也应该进行无障碍化,本文将以盲人猜地鼠游戏为例,讲解如何对手机游戏进行无障碍化设计,如何让原本无法操作变成可在无障碍模式下正常使用,最后总结手机游戏无障碍化的大体思路。

前言


目前市场上针对盲人进行无障碍化的手机游戏几乎没有,这对于障碍用户来讲,是一大遗憾。实际上,他们对游戏的渴望跟对应用无障碍化的渴望一样强烈,他们也希望能在手机上体验一把游戏的乐趣。下面,我们来探讨手机游戏无障碍化。

由于是首次针对手机游戏进行无障碍化,所以这里挑选了一款较为简单的游戏——猜地鼠。猜地鼠是基于MasterMind游戏的玩法(如图1)。原本玩法是两个人对玩,其中一人是出谜者,摆好不同颜色的球的位置;另一个人是猜谜者,每个回合猜各个位置应该放什么颜色的球。每回合结束后会有结果提示。

图1 MasterMind游戏


而猜地鼠则是会有不同颜色的地鼠,用户每个回合要猜地鼠的颜色排列顺序。这是使用Cocos2d-x引擎开发的,引擎并没对无障碍模式做出优化,所以游戏开发完成后只有一个大焦点在界面上,当用户点击这个焦点时,没法进行下一步操作,怎么点也进不了游戏界面。如图2所示。

图2 优化前的游戏界面


构造无障碍虚拟节点


Cocos2d-x引擎是支持跨平台的,所以我们可以针对不同平台区分处理无障碍。首先看下Android平台,在游戏中,Cocos2d-x是把游戏里的界面、图片和文字等素材画到GLSurfaceView上,而GLSurfaceView是添加到Cocos2dxActivity的布局中。Cocos2dxActivity是Cocos2d-x引擎在Android平台上最主要的Activity,我们要对游戏进行无障碍化,就需要让这个Activity支持无障碍。

在Android平台,可以对自定义View进行无障碍化支持。具体的原理可以参考官网上的介绍,或者《程序员》8月发布的《Android无障碍宝典》。所以我们第一步,就是为Cocos2dxActivity增加一个自定义View。

public class MasterMind extends Cocos2dxActivity{


private AccessibilityGameView mGameView;


protected void onCreate(Bundle savedInstanceState){

super.onCreate(savedInstanceState);


Rect rectangle = new Rect();

getWindow().getDecorView().getWindowVisibleDisplayFrame(rectangle);

AccessibilityHelper.setScreen(rectangle.width(), rectangle.height());


@SuppressWarnings("deprecation")

LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(

getWindowManager().getDefaultDisplay().getWidth(),

getWindowManager().getDefaultDisplay().getHeight());

mGameView = new AccessibilityGameView(this);

addContentView(mGameView, params);


AccessibilityHelper.setGameView(mGameView);


ViewCompat.setImportantForAccessibility(mGLSurfaceView, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);

}


static {

System.loadLibrary("game");

}

}


AccessibilityGameView是个简单透明的自定义View,直接通过addContentView方法加入到布局中,同时把GLSurfaceView设置为不需无障碍焦点。


public class AccessibilityGameView extends View {


private BaseSceneHelper mTouchHelper;


public AccessibilityGameView(Context context) {

super(context);

}


public void setCurSceneHelper(BaseSceneHelper helper){

mTouchHelper = helper;

}


@SuppressLint("NewApi")

@Override

protected boolean dispatchHoverEvent(MotionEvent event) {

if(mTouchHelper != null && mTouchHelper.dispatchHoverEvent(event)){

return true;

}


return super.dispatchHoverEvent(event);

}


}


AccessibilityGameView中包含BaseSceneHelper类,并将HoverEvent交给BaseSceneHelper处理。BaseSceneHelper负责构造自定义无障碍虚拟节点,提供游戏中必要信息给用户。首先看BaseSceneHelper内部的实现。


public class BaseSceneHelper extends ExploreByTouchHelper {

protected ArrayList mNodeItems;

public BaseSceneHelper(View forView) {

super(forView);

mNodeItems = new ArrayList ();

}

@Override

protected int getVirtualViewAt(float x, float y) {

for (int i = 0; i

Rect rect = mNodeItems.get(i).mRect;

if (rect.contains((int) x, (int) y)) {

return i;

}

}

return -1;

}

@Override

protected void getVisibleVirtualViews(List virtualViewIds) {

for (int i = 0; i

virtualViewIds.add(mNodeItems.get(i).id);

}

}

@Override

protected void onPopulateNodeForVirtualView(int virtualViewId,

AccessibilityNodeInfoCompat node) {

if(mNodeItems.size() > virtualViewId){

node.setContentDescription(getContentDesc(virtualViewId));

setParentRectFor(virtualViewId, node);

}

}

private String getContentDesc(int virtualViewId) {

if (mNodeItems.size() > virtualViewId) {

return mNodeItems.get(virtualViewId).mDesc;

}

return "";

}


…..

}


BaseSceneHelper实现无障碍辅助类ExploreBy TouchHelper,通过维护AccessibilityItem列表,将所需要的无障碍虚拟节点的Rect和Description记录起来。当BaseSceneHelper被调用创建无障碍节点时,实时提供AccessibilityItem。


public class AccessibilityItem {


public Rect mRect;

public String mDesc;

public int id;

}


处理游戏场景切换


到此,就完成了Java层的无障碍化,但是游戏中需要的无障碍焦点,得从游戏代码中触发。对猜地鼠进行无障碍化,一共要处理4个场景。一进入游戏就能看到的菜单场景(如图3),会有两个按钮需要无障碍化;从开始游戏按钮,我们会进入游戏场景(如图4),这个场景相对比较复杂,需要对每个不同颜色的地鼠进行无障碍化,然后需要对当前回合的结果提示进行无障碍化,还要提供一个结束游戏的按钮;游戏共9个回合,如果都猜不中则是输了,如果猜中则赢得游戏,两种情况都会弹出游戏结束场景(如图5)。这个场景需要告诉用户最终结果是什么,花费多长时间,以及提供一个重新开始游戏的操作。最后一个场景是帮助场景(如图6),它其中只需要告诉用户这个游戏的玩法,以及提供返回菜单场景的操作就行。

图3 菜单场景


图4 游戏场景


图5 结束场景


图6 帮助场景


接着来看下如何对场景无障碍化,举个例子,对菜单场景进行无障碍化。首先,需要去掉原来的大焦点;接着,为开始游戏按钮和怎么玩按钮提供无障碍焦点和无障碍信息,也就是说,要构造两个无障碍节点。对AccessibilityGameView添加两个无障碍虚拟节点。这样,用户就能操作到场景中这两个按钮。

进入到游戏代码中,两个按钮是CCMenuItemLabel控件,通过调用rect()方法,可以获得两个按钮在屏幕中的大小。


m_pItemMenu = CCMenu::create();

for (int i = 0; i

CCLabelTTF* label;

label = textAddOutline(menuNames[i].c_str(),

"fonts/akaDylan Plain.ttf", 30, ccWHITE, 1);

CCMenuItemLabel* pMenuItem = CCMenuItemLabel::create(label, this,

menu_selector(HelloWorld::menuCallback));

m_pItemMenu->addChild(pMenuItem, i + 10000);

pMenuItem->setPosition(

ccp( VisibleRect::center().x, (VisibleRect::bottom().y + (menuCount - i) * LINE_SPACE) ));

CCRect rect = pMenuItem->rect();

const char * str = GameConstants::getMenuNodeDesc(i);

AccessibilityWrapper::getInstance()->addMenuSceneRect(i, str, rect.getMinX(),rect.getMaxX(),rect.getMinY(),rect.getMaxY());

}


在取得按钮的大小后,将按钮描述str和rect交给AccessibilityWrapper。AccessibilityWrapper将会通过JNI的方法调用Java层代码,通知BaseSceneHelper来构造无障碍虚拟节点。


public class BaseSceneHelper extends ExploreByTouchHelper {


protected ArrayList mNodeItems;


....


public void updateAccessibilityItem(int i, String desc){

if(mNodeItems.size() > i){

AccessibilityItem item = mNodeItems.get(i);

item.mDesc = desc;

}

}

public void addAccessibilityItem(AccessibilityItem item) {

mNodeItems.add(item);

}

public void destroyScene() {

mNodeItems.clear();

}

}


static AccessibilityWrapper * s_Instance = NULL;


AccessibilityWrapper * AccessibilityWrapper::getInstance(){

if(s_Instance == NULL){

s_Instance = new AccessibilityWrapper();

}


return s_Instance;

}


void AccessibilityWrapper::addMenuSceneRect(int i, const char * s, float l, float r, float t, float b){

JniMethodInfo minfo;

bool isHave = JniHelper::getStaticMethodInfo(minfo,

"cn/robust/mastermind/AccessibilityHelper","addMenuSceneRect","(ILjava/lang/String;IIII)V");

if(!isHave){

//CCLog("jni:openURL 函数不存在");

}else{

int left = (int)l;

int right = (int)r;

int top = (int)t;

int bottom = (int) b;

jstring jstr = minfo.env->NewStringUTF(s);

minfo.env->CallStaticVoidMethod(minfo.classID,minfo.methodID, i, jstr, left, right, top, bottom);

}

}


Java层被调用的类是AccessibilityHelper,在addMenuSceneRect方法中构造AccessibilityItem,放到菜单页面的BaseSceneHelper中。


public class AccessibilityHelper {

private static BaseSceneHelper mMenuRef;

private static BaseSceneHelper mPlayRef;

private static BaseSceneHelper mOverRef;

private static BaseSceneHelper mHelpRef;

private static BaseSceneHelper mCurRef;

……

public static void addMenuSceneRect(int i, String d, int l, int r, int t, int b){

int sl = getScreenX(l);

int sr = getScreenX(r);

int st = getScreenY(b);

int sb = getScreenY(t);

AccessibilityItem item = new AccessibilityItem(i, d, sl, sr, st, sb);

if(mMenuRef != null){

mMenuRef.addAccessibilityItem(item);

}

}

}


游戏跟应用的无障碍化不同,它只有一个activity,场景切换并不会改变activity。所以需要在上个场景结束时,把上个场景的无障碍虚拟节点删除,在下个场景出现之前,把下个场景的无障碍虚拟节点构造好。具体代码如下:


public static void onMenuSceneLoad(int scene){

if(mGameViewRef.get() != null && mMenuRef != null &&

mPlayRef != null && mOverRef != null){

switch (scene) {

case 0:

ViewCompat.setAccessibilityDelegate(mGameViewRef.get(), mMenuRef);

handleNewScene(mMenuRef);

break;

case 1:

ViewCompat.setAccessibilityDelegate(mGameViewRef.get(), mPlayRef);







请到「今天看啥」查看全文