实现辅助(外挂)
参考地址:http://developer.android.com/training/accessibility/index.html我们设计开发的App需要给更广泛的人群使用,有一部分的在视力、手脚等方面可能有残障,这时为了提高他们这部分人的用户体验,我们需要使用辅助服务来帮助他们来完成交互。也就是Android Framework中定义的Accessibility Services。
设计辅助程序
添加android:contentDescription属性
利用Google基于声音的TalkBack服务 ,在UI元素上加 android:contentDescription属性,可以使用这个服务将其读出来,使得视力障碍者可以通过声音访问这些元素。例如:
有一些有状态的UI(比如ToggleButton,CheckBox),就不能通过在布局中设置android:contentDescription来实现,这时可以在代码中动态设置:
String contentDescription = "Select " + strValues[position];label.setContentDescription(contentDescription);
代码很简单,但是却很有用。下载TalkBack服务,然后在 Settings > Accessibility > TalkBack中开启服务即可使用。
Android不仅仅提供了触摸屏的导航方式,还可以通过D-Pad、方向键或轨迹球来操作。后来Android还提供了通过USB或蓝牙连接的外置键盘来操作。要使用这种形式的操作方式,必须让设置操作的元素处于获取焦点状态(Focus),使用View.setFocusable()或在XML布局中设置 android:focusable属性。另外,每个UI控件都有四个属性android:nextFocusUp, android:nextFocusDown, android:nextFocusLeft和android:nextFocusRight,你可以用这些属性定义在某个方向哪个控件将获取焦点,因为系统默认是采用布局临近原则来自动决定顺序的,采用这四个属性可以人工干预。例如:有一个Button和一个TextView,都可以focus,当按下方向键的时候焦点从button跳到TextView,按上焦点返回到button:
最好的验证方式是在模拟器上,操作上下方向键,来查看控件的焦点情况。
发送辅助事件
如果你使用AndroidFramework中的控件,那么不管何时它的选中状态或焦点状态发生变化,都可以发送AccessibilityEvent。这个事件是由accessibility service检查的,可以提供像TTS那样的功能。如果你写一个自定义的View,要保证在适当的时候发送accessibility event。通过调用sendAccessibilityEvent(int)方法,其中参数代表发生的事件类,来创建一个事件。AccessibilityEvent提供了完整的事件类型列表。例如,你想继承imageview以致于在它获取焦点时可以通过键盘输入标题上去,这时需要发生一个 TYPE_VIEW_TEXT_CHANGED事件,尽管这个事件一般没有在内置定义在imageview中。代码如下:
public void onTextChanged(String before, String after) { ... if (AccessibilityManager.getInstance(mContext).isEnabled()) { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); } ...}
开发一个Accessibility Service
创建一个Accessibility Service
package com.example.android.apis.accessibility;import android.accessibilityservice.AccessibilityService;public class MyAccessibilityService extends AccessibilityService {... @Override public void onAccessibilityEvent(AccessibilityEvent event) { } @Override public void onInterrupt() { }...}
在Manifest中注册Service,要特别指定android.accessibilityservice,当应用程序发出一个AccessibilityEvent时可以接收到。
...
. . .
...
配置Accessibility Service
配置Accessibility Service有两种方式,兼容的方式是在代码中配置。在onServiceConnected()中调用setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo)来配置辅助服务:
@Overridepublic void onServiceConnected() { // Set the type of events that this service wants to listen to. Others // won't be passed to this service. info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED | AccessibilityEvent.TYPE_VIEW_FOCUSED; // If you only want this service to work with specific applications, set their // package names here. Otherwise, when the service is activated, it will listen // to events from all applications. info.packageNames = new String[] {"com.example.android.myFirstApp", "com.example.android.mySecondApp"}; // Set the type of feedback your service will provide. info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN; // Default services are invoked only if no package-specific ones are present // for the type of AccessibilityEvent generated. This service is // application-specific, so the flag isn't necessary. If this was a // general-purpose service, it would be worth considering setting the // DEFAULT flag. // info.flags = AccessibilityServiceInfo.DEFAULT; info.notificationTimeout = 100; this.setServiceInfo(info);}
从Android4.0开始,我们可以将配置写在一个XML文件中,一些配置选项比如canRetrieveWindowContent只能在XML中配置。和上面代码同样的配置选项的XML配置如下:
如果使用XML配置,还需要在Manifest文件中配置 属性,指定辅助服务的resource为上面的XML配置文件:
响应AccessibilityEvents
覆盖onAccessibilityEvent(AccessibilityEvent)方法来处理AccessibilityEvents事件:
@Overridepublic void onAccessibilityEvent(AccessibilityEvent event) { final int eventType = event.getEventType(); String eventText = null; switch(eventType) { case AccessibilityEvent.TYPE_VIEW_CLICKED: eventText = "Focused: "; break; case AccessibilityEvent.TYPE_VIEW_FOCUSED: eventText = "Focused: "; break; } eventText = eventText + event.getContentDescription(); // Do something nifty with this text, like speak the composed string // back to the user. speakToUser(eventText); ...}
查询View Heirarchy获取的Context
这个是 Android 4.0 (API Level 14) 上AccessibilityService 才有的能力,这种能力非常有用!我们需要在XML的配置中配置android:canRetrieveWindowContent=”true”。通过getSource()获得AccessibilityNodeInfo对象,如果事件源的窗口仍然是活动窗口,则这个调用返回一个对象;否则返回null。下面的例子是一个代码片段,它接收到一个事件时,作如下事情:1、直接抓住事件源View的父视图2、在父View里,寻找一个label和checkbox作为子View3、如果找到了,创建一个string发给用户,标识checkbox是否被选中4、如果遍历了整个view hierarchy返回null,则默默的放弃
// Alternative onAccessibilityEvent, that uses AccessibilityNodeInfo@Overridepublic void onAccessibilityEvent(AccessibilityEvent event) { AccessibilityNodeInfo source = event.getSource(); if (source == null) { return; } // Grab the parent of the view that fired the event. AccessibilityNodeInfo rowNode = getListItemNodeInfo(source); if (rowNode == null) { return; } // Using this parent, get references to both child nodes, the label and the checkbox. AccessibilityNodeInfo labelNode = rowNode.getChild(0); if (labelNode == null) { rowNode.recycle(); return; } AccessibilityNodeInfo completeNode = rowNode.getChild(1); if (completeNode == null) { rowNode.recycle(); return; } // Determine what the task is and whether or not it's complete, based on // the text inside the label, and the state of the check-box. if (rowNode.getChildCount() < 2 || !rowNode.getChild(1).isCheckable()) { rowNode.recycle(); return; } CharSequence taskLabel = labelNode.getText(); final boolean isComplete = completeNode.isChecked(); String completeStr = null; if (isComplete) { completeStr = getString(R.string.checked); } else { completeStr = getString(R.string.not_checked); } String reportStr = taskLabel + completeStr; speakToUser(reportStr);}
现在,你有一个功能完整的accessibility servicel了。试着配置TTS引擎来更好的与用户交互,或者使用振动提供触摸反馈。
系统状态栏和导航栏
参考地址:http://developer.android.com/training/system-ui/index.html
变暗系统状态栏
在Android 4.0(API14)及以上可以使用SYSTEM_UI_FLAG_LOW_PROFILE这个Flag很容易的变暗状态栏。Android早期版本系统不提供一个内置的API变暗状态栏。
// This example uses decor view, but you can use any visible view.View decorView = getActivity().getWindow().getDecorView();int uiOptions = View.SYSTEM_UI_FLAG_LOW_PROFILE;decorView.setSystemUiVisibility(uiOptions);
当用户触摸状态或导航栏,这个flag就消失了,就恢复了明亮。如果想再次变暗它,就需要重新设置它。如果你要通过代码清除flag,使用setSystemUiVisibility():
View decorView = getActivity().getWindow().getDecorView();// Calling setSystemUiVisibility() with a value of 0 clears// all flags.decorView.setSystemUiVisibility(0);
隐藏状态栏
![](http://upload-images.jianshu.io/upload_images/2761423-5647e863f436313f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)注意:状态栏不可见时,永远不要显示action bar 。![](http://upload-images.jianshu.io/upload_images/2761423-757753de83ded631.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
在Android 4.0及以下版本上隐藏状态栏
...
或者
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // If the Android version is lower than Jellybean, use this call to hide // the status bar. if (Build.VERSION.SDK_INT < 16) { getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); } setContentView(R.layout.activity_main); } ...}
可以使用FLAG_LAYOUT_IN_SCREEN这个flag设置你的Activity使用相同的屏幕区域,这样就不会使状态栏不停的隐藏和显示了。
在Android 4.1隐藏状态栏
View decorView = getWindow().getDecorView();// Hide the status bar.int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN;decorView.setSystemUiVisibility(uiOptions);// Remember that you should never show the action bar if the// status bar is hidden, so hide that too if necessary.ActionBar actionBar = getActionBar();actionBar.hide();
注意:设置UI的flag只是当时生效。比如你在onCreate()中设置隐藏状态栏,点击home回到桌面状态栏显示,再次进入之后onCreate()不会再执行,状态栏就一直显示,就会有问题了。解决方法是:在onResume()或onWindowFocusChanged()中设置flag使其消失。setSystemUiVisibility()方法只对可见的View有效设置过setSystemUiVisibility()的View再导航离开后,flag会消失。
让界面内容显示在状态栏的后面
在Android 4.1及以后,可以设置界面内容在状态栏的后面,这样界面就不会因为状态栏显示和隐藏而resize了。使用SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN这个flag就可以了,还可以使用SYSTEM_UI_FLAG_LAYOUT_STABLE这个flag帮助app维持一个稳定的布局。当你使用这种方法时,你就要对它负责,来确保你的某些UI(比如地图的内置控件)不会被遮住而影响使用。多数情况下你可以在XML布局中设置android:fitsSystemWindows属性为true来处理这种情况,这对大多数应用都适用。某些情况下,可能你需要修改默认的padding值来得到想要的合理布局。要直接操作内容布局相对于状态栏的位置(占据的那部分空间称content insets),需要覆盖fitSystemWindows(Rect insets)方法。fitSystemWindows方法在content insets发生变化被 view hierarchy时调用,允许window调整它的content。通过覆盖这个方法,不管你想不想,你都可以处理这个insets 。
让Actionbar和状态栏同步
在Android 4.1及以上版本中,为避免在actionbar隐藏和显示时resize你的布局,你可以为actionbar开启覆盖(overlay)模式。在覆盖模式中,你的Activity使用尽可能大的空间好像Actionbar不在那儿一样,其实actionbar是在布局的上面,只是布局顶部有一部分变模糊了,但现在actionbar不管显示和隐藏,都不会resize布局了。打开覆盖模式,需要创建一个自定义的主题,继承一个带有actionbar的主题,设置 android:windowActionBarOverlay=true。然后使用上面提到的SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN Flag,设置你的Activity在打开SYSTEM_UI_FLAG_FULLSCREEN flag时使用相同的屏幕区域。当你要隐藏SystemUI时,使用SYSTEM_UI_FLAG_FULLSCREEN的flag。这个也会隐藏action bar(因为android:windowActionBarOverlay=true),而且在隐藏和显示时有一个和谐的动画。
隐藏导航栏
vcC4oaM8L3A+DQo8cHJlIGNsYXNzPQ=="brush:java;"> View decorView = getWindow().getDecorView(); // Hide both the navigation bar and the status bar. // SYSTEM_UI_FLAG_FULLSCREEN is only available on Android 4.1 and higher, but as // a general rule, you should design your app to hide the status bar whenever you // hide the navigation bar. int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN; decorView.setSystemUiVisibility(uiOptions);
使用这个方法,用户点击屏幕任何地方将导致导航栏(和状态栏)都重新显示并保持。这个flag被清除后,需要重新设置它进行隐藏导航栏其他部分都和状态栏的注意部分一样
让界面内容显示在导航栏下面
在Android4.1及以上版本,使用SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION的flag使界面布局显示在导航栏的下面,并使用SYSTEM_UI_FLAG_LAYOUT_STABLE保持布局。其他部分和状态栏部分注意部分相同
使用沉浸式的全屏模式
在Android 4.4 (API Level 19)中为setSystemUiVisibility()新介绍了SYSTEM_UI_FLAG_LAYOUT_STABLE的flag,它让你的app真实的进入“全屏”模式,和SYSTEM_UI_FLAG_HIDE_NAVIGATION以及SYSTEM_UI_FLAG_FULLSCREEN结合起来时,隐藏状态栏和导航栏,app将捕获全屏的触摸事件。当沉浸式全屏模式开启后,你的Activity持续的接收全屏的触摸事件。当用户沿着system bar一般显示的地方向内滑动时会让system bar显示出来。这个动作清除了SYSTEM_UI_FLAG_HIDE_NAVIGATION flag (以及 SYSTEM_UI_FLAG_FULLSCREEN,如果应用的话),于是system bar变得可见,这会触发View.OnSystemUiVisibilityChangeListener。然而,你希望system bar一会儿后再自动隐藏,你可以使用SYSTEM_UI_FLAG_IMMERSIVE_STICKY的flag。注意这个粘性(”sticky” )的版本不会触发任何监听事件,因为system bar在这种模式下只是暂时性的显示。![](http://upload-images.jianshu.io/upload_images/2761423-6b390d66e11494cb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
非沉浸式模式。在app进入沉浸式模式前的状态。它也表示如果你使用沉浸式flag,当用户滑动时清除了SYSTEM_UI_FLAG_HIDE_NAVIGATION 和 SYSTEM_UI_FLAG_FULLSCREEN显示system bar的情况。这是保持UI控件和system bar同步的最好实践,它最小化了屏幕的状态数。这个提供了更加无缝的用户体验,所以这里所有的UI控件和状态栏一起显示。一旦进入沉浸模式,UI控件将随着system bar的隐藏而隐藏。为确保你的UI和system bar保持可见,使用View.OnSystemUiVisibilityChangeListener监听可见性的变化。 提示气泡。当用户第一次进入沉浸模式时,系统将显示一个提示气泡。这个气泡提示用户将怎样显示system bar。注意:如果你想强制性的显示提示气泡用作测试意图,你可以将app进入沉浸模式,然后关闭屏幕,然后在5秒内点亮屏幕。 沉浸模式。app进入沉浸模式,system bars和其他UI控件都隐藏。 粘性Flag。这个UI是你使用IMMERSIVE_STICKY的Flag,然后用户滑动使system bar显示的。半透明的bar临时显示然后会再隐藏。滑动行为不会清除任何flag,所以也不会触发system UI可见性变化的监听。注意:沉浸的FLag只有你使用SYSTEM_UI_FLAG_HIDE_NAVIGATION, SYSTEM_UI_FLAG_FULLSCREEN的Flag或两者都有的时候才会生效。通常情况下当你使用“全屏沉浸”模式会隐藏状态栏和导航栏。
SYSTEM_UI_FLAG_IMMERSIVE 和SYSTEM_UI_FLAG_IMMERSIVE_STICKY可以提供一个差异化的沉浸式的体验。下面是一些情况,你需要使用其中一个,而不是另一个:
当你开发一个阅读app,新闻app或杂志app时,使用沉浸flag需要和SYSTEM_UI_FLAG_FULLSCREEN、 SYSTEM_UI_FLAG_HIDE_NAVIGATION两者结合起来用。 当你开发一个完全沉浸模式的app,期望用户和屏幕的边缘进行交互而不期望用户频繁的和system UI交互,使用粘性沉浸的flag,结合SYSTEM_UI_FLAG_FULLSCREEN 和SYSTEM_UI_FLAG_HIDE_NAVIGATION使用。 如果你开发一个视频播放器或其他很少需要用户交互的app,你可能需要老一点版本的方法了( Android 4.0 (API Level 14)及以上)。因为对于这类app,简单的使用SYSTEM_UI_FLAG_FULLSCREEN和会SYSTEM_UI_FLAG_HIDE_NAVIGATION的Flag就足够了,不需要沉浸的flag。使用非粘性沉浸
这段代码演示了如何隐藏和显示状态栏和导航栏,而不用resize界面的内容。
// This snippet hides the system bars.private void hideSystemUI() { // Set the IMMERSIVE flag. // Set the content to appear under the system bars so that the content // doesn't resize when the system bars hide and show. mDecorView.setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar | View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar | View.SYSTEM_UI_FLAG_IMMERSIVE);}// This snippet shows the system bars. It does this by removing all the flags// except for the ones that make the content appear under the system bars.private void showSystemUI() { mDecorView.setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);}
1、注册一个监听让你的app得到system UI可见性变化的通知。2、实现onWindowFocusChanged()方法。如果你获得window的焦点,你可能想重新隐藏system bar。如果你失去了window的焦点,例如一个对话框或弹出菜单,你可能想取消之前的Handler.postDelayed()或类似方法安排的隐藏操作。3、实现一个GestureDetector ,让它监测onSingleTapUp(MotionEvent),让用户可以通过触摸content手动控制system bar的可见性。简单的click监听不是最好的解决方案因为当用户在屏幕滑动手指都可以触发。
使用粘性沉浸
当你使用SYSTEM_UI_FLAG_IMMERSIVE_STICKY的flag,在system bar内部区域的滑动会导致其半透明状态并暂时性的显示但没有flag被清除,你的system UI的可见性监听没有被触发。system bar会在一会儿以后再次隐藏或用户在content交互下。下图展示了当使用IMMERSIVE_STICKY的flag时半透明的system bar短暂的显示然后隐藏![](http://upload-images.jianshu.io/upload_images/2761423-ddcb0092bb30c425.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)下面是一个简单的方法来使用这个flag:
@Overridepublic void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { decorView.setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);}}
如果你喜欢IMMERSIVE_STICKY的flag的自动隐藏行为,但是需要同时显示你自己的UI控件,使用 IMMERSIVE和Handler.postDelayed()或者其它一些类似的在一会儿之后可以重新进入沉浸模式的方式。
响应UI可见性的变化
要获得UI可见性变化的通知,需要为你的View注册View.OnSystemUiVisibilityChangeListener,例如在你的Activity中的onCreate()中:
View decorView = getWindow().getDecorView();decorView.setOnSystemUiVisibilityChangeListener (new View.OnSystemUiVisibilityChangeListener() { @Override public void onSystemUiVisibilityChange(int visibility) { // Note that system bars will only be "visible" if none of the // LOW_PROFILE, HIDE_NAVIGATION, or FULLSCREEN flags are set. if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { // TODO: The system bars are visible. Make any desired // adjustments to your UI, such as showing the action bar or // other navigational controls. } else { // TODO: The system bars are NOT visible. Make any desired // adjustments to your UI, such as hiding the action bar or // other navigational controls. } }});
通常保持UI与system bar可见性变化的一致性是不错的实践。例如,你可以通过这种方式让action bar和状态栏保持一致的变化状态。