Android TV 悬浮球模拟物理按键

  最近在体验实习的时候做了一个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,让悬浮球外部获取触摸事件

附上 TV内存管家github传送门

你可能感兴趣的:(android)