这一周又遇到个麻烦事儿,公司对接了一个老外客户,要求在现有的USDK密码键盘界面自定义密码界面风格,但密码按键的处理以及获取密码数据需要USDK去实现,也就是说老外只负责画界面,把绘制出的界面中的控件坐标传下来(控件的左上角和右下即角坐标),不处理点击事件,让USDK把click事件屏蔽并返回密码数据给他们(这需求也是没谁了~~无力吐槽),so开动吧!谁让我是苦逼的程序猿呢~
首先我们先整理下需要做的事儿:
针对上面问题1,我首先想到的是setOnTouchListener方法,获取点击的位置不正合适吗?可问题来了,不是一个进程的如何获取到界面的MotionEvent方法呢?那我可以在USDK中添加一个空白的View上去,这样不久可以获取了吗?而且通过设置View的属性也正好可以消耗点击事件,一举两得呀~至于问题2,这个修改系统就OK了,so方案有了:
这里我定义了一个空的View,layout文件如下:代码块
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/offlinepin_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@android:color/transparent">
<!--显示透明View可见 方便调试,后续调试OK屏蔽即可-->
<TextView
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="test"/>
</LinearLayout>
这里通过设置android:background="@android:color/transparent" 将其设置为透明;然后在USDK中添加本View,主要代码如下:代码块
LinearLayout mLayout = (LinearLayout) LayoutInflater.from(TopwiseApplication.getContext()).
inflate(R.layout.view_offlinepin, null);
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
// 设置框口属性为系统提示级别,属于最上层显示
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
// 设置View颜色支持属性
params.format = PixelFormat.RGBA_8888;
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
//宽 高显示限制
params.width = WindowManager.LayoutParams.MATCH_PARENT;
params.height = WindowManager.LayoutParams.MATCH_PARENT;
SDKLog.d(TAG, "addView");
//if (manager != null) {
// manager.addView(layout, params);
// isAddView = true;
//}
mHandler = new ViewHandler(mWindowManager, mLayout, params);
此处先获取layout实例,通过WindowManager服务去addView,WindowManager可通过mWindowManager = (WindowManager) TopwiseApplication.getContext().
getSystemService(Context.WINDOW_SERVICE);获取;
解释下WindowManager.LayoutParams布局属性,此属性用于设置当前添加的View的显示,活动包括事件处理等相关配置,其中flags值比较重要,介绍几个常用的:
FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
//Constant Value: 1 (0x00000001)
只要这个window对用户是可见的,则允许在屏幕开启的时候锁定屏幕
这个flag可以单独的使用,也可以配合FLAG_KEEP_SCREEN_ON和(或者) FLAG_SHOW_WHEN_LOCKED使用
FLAG_DIM_BEHIND
//Constant Value: 2 (0x00000002)
所有在这个window之后的会变暗,
使用dimAmount属性来控制变暗的程度(1.0不透明,0.0完全透明)
FLAG_NOT_FOCUSABLE
//Constant Value: 8 (0x00000008)
设置之后window永远不会获取焦点,所以用户不能给此window发送点击事件
焦点会传递给在其下面的可获取焦点的window
这个flag同时会启用 FLAG_NOT_TOUCH_MODAL flag , 不管你有没有手动设置
设置这个flag同时表明了这个window不会和软键盘交互
FLAG_NOT_TOUCHABLE
//Constant Value: 16 (0x00000010)
这个window永远无法获取点击事件
FLAG_NOT_TOUCH_MODAL
//Constant Value: 32 (0x00000020)
即使这个window是可获取焦点的,
也允许window之外点击事件传递给其他在其之后的window
如果不设置这个值,则window消费掉所有点击事件,不管这些点击事件是不是在window的范围之内
这个flag简而言之就是说,当前window区域以外的点击事件传递给下层window,当前window区域以内的点击事件自己处理
FLAG_KEEP_SCREEN_ON
//Constant Value: 128 (0x00000080)
当这个window对用户是可见状态,则保持设备屏幕不关闭且不变暗
所有的flags值详细介绍可参考链接:https://www.jianshu.com/p/c91448e1c7d1;
针对params属性说明介绍可参考链接:https://blog.csdn.net/zhangbijun1230/article/details/80140946;
ps:这里我踩了一个小坑,本来我以为客户在调用我这个接口的时候是在主线程中调用的,所以刚开始直接如注释代码那样写了,后面发现addView的时候没成功,catch了一下addView的异常,才发现是非主线程,so自定义了一个ViewHandler,代码如下:代码块
private static class ViewHandler extends Handler {
private WindowManager manager;
private LinearLayout layout;
private WindowManager.LayoutParams params;
private boolean isAddView;
ViewHandler(WindowManager manager, LinearLayout layout, WindowManager.LayoutParams params) {
//保证为主线程处理,如果在主线程中创建Handler,可不加此行代码
super(Looper.getMainLooper());
this.manager = manager;
this.layout = layout;
this.params = params;
isAddView = false;
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case ADD_VIEW_TAG:
if (manager != null) {
manager.addView(layout, params);
isAddView = true;
}
break;
case DELETE_VIEW_TAG:
try {
if (manager != null && isAddView) {
isAddView = false;
manager.removeView(layout);
}
} catch (Exception e) {
e.printStackTrace();
SDKLog.d(TAG, "remove view fail!");
}
break;
default:
break;
}
}
}
添加好View以后设置监听事件就简单了,代码块
mLayout.setOnTouchListener(this);
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
SDKLog.d(TAG, "touch x: " + event.getX() + ";y: " + event.getY());
}
return true;
}
这里有个小点需要提一下,如果这里单单实现onTouch或者onClick点击事件的话,会提示你缺少事件分配方法,大概提示如下:
onTouch should call View#performClick when a click is detected less… (Ctrl+F1)
…
如果覆盖onTouchEvent或使用OnTouchListener的View没有实现performClick方法,并且在检测到click事件时调用它,则View可能无法正确地处理可访问性操作。处理单击操作的逻辑理想情况下应该放在View#performClick中,因为某些可访问性服务在应该发生单击操作时调用performClick。
如果想细查问题原因,就得看Android的View事件分发机制了,这里不多讲,只告诉你添加*@SuppressLint(“ClickableViewAccessibility”)*这行注解就可以;
这里是使用SystemProperties类去设置读取自定义字段值,在功能实现代码处进行判断屏蔽,其实只要找到各个按键的功能实现位置,基本就没什么问题;
SystemProperties.set("persist.sys.usdk.home.enable", String.valueOf(flag));
SystemProperties.getBoolean("persist.sys.usdk.home.enable",false);
此处屏蔽的是虚拟按键,实体键通过KeyEvent可获取,不需要改系统代码,源码:MTK7.0;
PhoneWindowManager类中的interceptKeyBeforeDispatching方法,此方法为按键事件分发前拦截,部分代码如下:代码块
@Override
public long interceptKeyBeforeDispatching(WindowState win, KeyEvent event, int policyFlags) {
final boolean keyguardOn = keyguardOn();
final int keyCode = event.getKeyCode();
final int repeatCount = event.getRepeatCount();
final int metaState = event.getMetaState();
final int flags = event.getFlags();
final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
final boolean canceled = event.isCanceled();
// 拦截Home按键代码块 --start--
if(keyCode == KeyEvent.KEYCODE_HOME &&
SystemProperties.getBoolean("persist.sys.usdk.home.enable",false)){
String pkgName = win.getAttrs().packageName;
if (!"com.centerm.frame".equals(pkgName)) {
return -1;
}
}
// -- end --
/ ...
...
... /
}
SystemUI中的PanelBar类中的onTouchEvent方法,部分代码如下:代码块
@Override
public boolean onTouchEvent(MotionEvent event) {
/ ...
...
... /
if(SystemProperties.getBoolean("persist.sys.usdk.drop.enable",false)){
return false;
}
}
PhoneStatusBar类中相关代码:代码块
// init clickListenter
private void prepareNavigationBarView() {
mNavigationBarView.reorient();
ButtonDispatcher recentsButton = mNavigationBarView.getRecentsButton();
recentsButton.setOnClickListener(mRecentsClickListener);
recentsButton.setOnTouchListener(mRecentsPreloadOnTouchListener);
recentsButton.setLongClickable(true);
recentsButton.setOnLongClickListener(mRecentsLongClickListener);
ButtonDispatcher backButton = mNavigationBarView.getBackButton();
backButton.setLongClickable(true);
backButton.setOnLongClickListener(mLongPressBackListener);
ButtonDispatcher homeButton = mNavigationBarView.getHomeButton();
homeButton.setOnTouchListener(mHomeActionListener);
homeButton.setOnLongClickListener(mLongPressHomeListener);
ButtonDispatcher hideButton = mNavigationBarView.getHideButton();
hideButton.setOnClickListener(mHideClickListener);
/// M: BMW restore button @{
if (MultiWindowManager.isSupported()) {
ButtonDispatcher restoreButton = mNavigationBarView.getRestoreButton();
restoreButton.setOnClickListener(mRestoreClickListener);
}
/// @}
mAssistManager.onConfigurationChanged();
}
private View.OnClickListener mRecentsClickListener = new View.OnClickListener() {
public void onClick(View v) {
/ ...
...
... /
boolean isEnable = true;
try{
isEnable = mWindowManagerService.isRecentAppsKeyEnable();
}catch(RemoteException e) {
e.printStackTrace();
}
if(isEnable && SystemProperties.getBoolean("persist.sys.usdk.appbtn.enable",false)){
Log.d(TAG, "disable recentApp btn");
return;
}
}
};
关于系统代码这块的讲解就不多写了,不然写起来很多,后面学习到相关的再统一归类整理;
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
// 设置框口属性为系统提示级别,属于最上层显示
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
关于这个type设置,中间还搞了我一手,客户在实现自定义密码键盘界面的时候,使用的是继承Dialog的自定义View,调用USDK接口的位置如下:代码块
@Override
@Override
public void onWindowFocusChanged(boolean hasFocus) {
// TODO Auto-generated method stub
super.onWindowFocusChanged(hasFocus);
// 1
if (!hasFocus) {
return;
}
try {
pinpad.getPin(param, new GetPinListener.Stub(){
@Override
public void onInputKey(int i, String s) throws RemoteException {
listener.onInputKey(i,s);
}
@Override
public void onError(int i) throws RemoteException {
listener.onError(i);
onClose();
}
@Override
public void onConfirmInput(byte[] bytes) throws RemoteException {
listener.onConfirmInput(bytes);
onClose();
}
@Override
public void onCancelKeyPress() throws RemoteException {
listener.onCancelKeyPress();
onClose();
}
@Override
public void onStopGetPin() throws RemoteException {
listener.onStopGetPin();
onClose();
}
});
} catch (RemoteException e) {
// TODO Auto-generated catch block
e.printStackTrace();
onClose();
}
}
之前是没有注释1的判断条件的,所以在创建这个Dialog的时候会走onWindowFocusChanged方法,调用USDK的getPin方法,然后USDK中的透明View起来的时候又会走onWindowFocusChanged方法,结果又调用了一次getPin方法,然后程序崩溃;
onWindowFocusChanged方法在这里稍微解释一下,这个方法是在生命周期过程中调用的一个方法,其作用是:从onWindowFocusChanged被执行起,用户可以与应用进行交互了,而这之前,对用户的操作需要做一点限制;具体在生命周期的位置为:
1 entry: onStart---->onResume---->onAttachedToWindow----------->onWindowVisibilityChanged–visibility=0---------->onWindowFocusChanged(true)-------> …
2 exit: onPause---->onStop---->onWindowFocusChanged(false) ---------------------- (lockscreen)
3 exit : onPause----->onWindowFocusChanged(false)-------->onWindowVisibilityChanged–visibility=8------------>onStop(to another activity)
复写这个方法的时候一定注意hasFocus值的判断!
OK,到这里此功能基本就可以满足客户要求了!开心撒花~~~
也不知道之前是写代码少了手生还是自己基础知识不牢固,有些基础知识的使用总感觉不难,知道其原理,但写起来就是漏这漏那,感觉后面还是多看一些基础,有必要多写一些基础性的文章,巩固知识,备战面试!!^ - ^
本文到此结束,欢迎同行(xing)大佬们评论指正,觉得写的还不错滴麻烦点个赞哟~~