最近在体验实习的时候做了一个TV的内存管家,其中有个要求是实现一个悬浮球,模拟TV控制器的按键,实现上下左右,back,menu,home等效果,并且做一个火箭升空的效果。这时候才发现网上有关tv开发的资料十分少,不像手机端,一搜堆博客。一怒之下决定讲讲其实现
1、悬浮球实现
在Android中实现悬浮球效果比较简单,只要调用windowManager的addView方法即可。有这样一个需求:默认是一个小的view,只显示内存使用情况;当其被点击的时候会切换为大的view,即模拟遥控器的控制界面,点击外部又会切换为小的view,当移动悬浮球的时候会显示一个火箭,同时中下部会显示一个发射台,当移动火箭到发射台的时候放手,火箭升空,否则移动到最近的屏幕边缘。
显然需要三个view:
- 显示内存和小火箭的FloatBallSmallView
- 显示控制界面的FloatBallBigView
- 显示发射台的RocketLauncher
主要实现思路
(1)分别创建FloatBallBigView、FloatBallSmallView、RocketLauncher继承RelativeLayout,并重写View:
FloatBallBigView类主要是注册各个button的点击事件
RocketLauncher类主要是创建一个ImageView 加载发射台的图片资源
FloatBallSmallView类比较重要,这里需要着重讲解一下:分为两个组件textView (显示内存使用情况)和ImageView(加载小火箭,默认为GONE),FloatBallSmallView 实现OnClickListener , OnTouchListener接口。在onClick方法中主要做一个view的切换,调用windowManager的removeView方法将FloatBallSmallView remove掉,并将FloatBallBigView添加到WindowsManager中
—————– * 划重点!!!!! * ————————
在onTouch方法中我们监听view获得的触摸事件
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isPressed = true;
mStartX = event.getRawX();
mStartY = event.getRawY();
mTempX = event.getRawX();
mTempY = event.getRawY();
updateViewStatus();//更新视图
break;
case MotionEvent.ACTION_MOVE:
float x = event.getRawX() - mStartX;
float y = event.getRawY() - mStartY;
//计算偏移量,刷新视图
mParams.x += x;
mParams.y += y;
// 手指移动的时候更新小悬浮窗的状态和位置
updateViewPosition();
mStartX = event.getRawX();
mStartY = event.getRawY();
break;
case MotionEvent.ACTION_UP:
float endX = event.getRawX();
float endY = event.getRawY();
isPressed = false;
if (Math.abs(endX - mTempX) < 6 && Math.abs(endY - mTempY) < 6) {
updateViewStatus();//更新视图
return false;//判断为点击事件,不做处理
}
boolean b = mFloatBallManager.isReadyToLaunch();//判断小火箭是否到达发射台
if (b) {
launchRocket();//发射火箭
} else {
updateViewStatus();//更新视图
}
return true;
default:
break;
}
return false;
}
在ACTION_DOWN 的时候,需要记录下点击的位置,便于后面判断是否该拦截该事件。mTempX 和mTempY 是用来记录每次移动时上一个点的位置。注意,这里采用event.getRawX() 而不是event.getX()。查看getRawX方法可以知道,返回值是屏幕的绝对位置,而getX方法是获取view内部的相对值
(2) 创建一个FloatBallManager类来管理悬浮球的创建,删除工作。在创建悬浮球的时候有一个比较重要的对象LayoutParams,我们可以通过他来设置view的位置,在更新view的位置时候再调用windowManager类的updateViewLayout方法即可。在判断小火箭是否到达发送台时候,我们可以比较两者的LayoutParams的X和Y值。
2、模拟按键的实现
在这之前,需要简单介绍一下Android的按键代码,在KeyEvent对象中封存了Android所有物理按键对应的key code,其中用到的code 对应的key如下:
- menu按键: KEYCODE_MENU
- back按键:KEYCODE_BACK
- home按键:KEYCODE_HOME
- 上按键:KEYCODE_DPAD_UP
- 下按键:KEYCODE_DPAD_DOWN
- 左按键:KEYCODE_DPAD_LEFT
- 右按键:KEYCODE_DPAD_RIGHT
现在我们知道各个物理按键对应的code ,那么我们应该如何模拟物理按键呢? 我们可以通过以下代码实现:
public void simulateKeystroke(final int KeyCode) {
new Thread(new Runnable() {
public void run() {
// TODO Auto-generated method stub
try {
Instrumentation inst = new Instrumentation();
inst.sendCharacterSync(KeyCode);
} catch (Exception e) {
// TODO: handle exception
}
}
}).start();
}
是不是很简单呢?上面是一个比较简单的方法,还有一个方法如下,也能达到目的:
private void execAdbCode(int code) {
Runtime runtime = Runtime.getRuntime();
try {
runtime.exec("input keyevent " + code);
} catch (IOException e) { // TODO Auto-generated catch block
e.printStackTrace();
}
}
其主要通过adb的方法去执行相应的代码,间接达到模拟物理按键的效果。到这里看似完美模拟物理按键了,获取对应的code,执行代码,系统自动反馈,一气呵成。然而,当你高兴地运行的时候,你会高兴不起来,为什么呢?你疯狂点击button 执行相应的代码,但是系统就是不买账,根本没有任何反馈,你开始怀疑解决方案,疯狂去找其他答案,发现找来找去,没有更好的办法。那咋办?需求依旧得按dateline完成,既然达不到效果,不妨换个思路想一下,物理按键的点击事件是不是由系统发出的?既然是系统层的,会不会处于安全问题有所限制?那我们应该如何才能使系统认可我们的应用,并调用系统级别的代码呢?先想想平常我们是如何打包应用,下发到手机(TV )的?是不是通过Android studio直接打包安装的?但这样安装的app处于应用级别,也就是说一些系统级别的权限它无法获取。那么怎么才能让我们开发的APP处于系统级别呢?
3、系统应用打包的实现
为了解决上述方案无效的问题,我们需要将自己的APP打包为系统应用,这样就可以愉快的玩耍了,具体过程我就不详细讲了,这里有一篇博客讲的比较详细,附上传送门 将APP打包到系统应用中
4、开机启动的实现
项目中有一个需求需要实现开机自启动,一般我们会注册一个静态广播接收器,监听开机广播,但有些手机在Framework层将广播开机事件给去掉了,这个方案不一定奏效。这时候需要同时监听网络状态的改变,代码如下:
- 先在Androidmanifest中配置
<receiver
android:name="com.example.user.broadcast.InterNetBroadCast"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
- 然后创建接收器,这里的intent需要setflag为FLAG_ACTIVITY_NEW_TASK,因为任务栈可能还没有创建,我们需要新创建一个任务栈
public class InterNetBroadCast extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Intent intent1;
intent1 = new Intent(context,FloatBallService.class);
intent1.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startService(intent1);
}
}
- 然后在service的oncreate方法创建一个悬浮球
@Override
public void onCreate() {
super.onCreate();
mFloatBallManager = FloatBallManager.
getFloatBallManager(this) ;
mFloatBallManager.createBigFloatView();
}
5、关于一些坑
(1)在设置view的layoutParma的type参数设置为TYPE_PHONE与TYPE_TOAST的区别:
- TYPE_PHONE :需要获取android.permission.SYSTEM_ALERT_WINDOW权限,可以获取触摸事件,但是悬浮球跟随手指的效果不好
- TYPE_TOAST:不需要权限,在一些手机机型中无法获取触摸事件,但是在TV中设置为该值可以获取
(2)layoutParma的flags参数:
- 悬浮球外部获取触摸事件:可以设置为 LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_NOT_TOUCH_MODAL,让悬浮球外部获取触摸事件