手记

一个无界面的悬浮时钟实现方式


可能有人会疑惑,这样的需求有什么用?接下来,我就先讲讲有什么用!对于那些经常玩准时抢购,抢优惠券的人来说,时间的精确度是非常重要的,手慢1秒就没了。而安卓手机中,却没法准确的看到秒数,并且是悬浮在其他应用之上的时钟就很有必要了!

首先先捋一捋实现本功能的关键所在!

  1. 得实现无界面。

  2. 悬浮在其他应用之上,可任意移动。随时关闭。

  3. 准确的北京时间,每秒、甚至毫秒级的刷新时间。

接下来就来谈谈具体如何实现?

如何实现无界面?

其实这个很简单,现在大家都明白是怎么回事。无界面,那就用Service呗!是的,这是毋庸置疑的,不过我的实现方式,不知道与你们是否有所区别!

首先,程序的入口还是个mainActivity,只是这个活动有点特殊,我们不给它设置view,只在它的oncreate中启动服务,然后finish掉activity。这里还有一点需要注意的,咋看之下,好像已经是没有界面的,其实不然,除此之外,还需要在配置文件中,把app的主题设置为

android:theme="@android:style/Theme.NoDisplay">

这样便实现了没有任何界面,只有一个悬浮窗!这里还有一个要注意的,在安卓6.0(api23)之后,对于危险的权限(是危险还是正常,都是谷歌说的算),需要动态获取权限!在本例子中,就需要用户授予悬浮的权限。代码如下

if (Build.VERSION.SDK_INT >= 23) {
    if (! Settings.canDrawOverlays(this)) {
        Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                Uri.parse("package:" + getPackageName()));
        startActivityForResult(intent,10);
    }
}

    Intent intent = new Intent(MainActivity.this, FxService.class);
    //启动FxService
    startService(intent);
    finish();

到此为止,无界面已经实现了。既然没有界面,那悬浮按钮上显示时间的view就只能依赖于Service了!接下来就看看这个Service是怎么实现的。

其实在Service中添加view也是挺简单的,只要在Service中获取到WindowManager,绑定定义好的layout,然后调用addView添加即可。

mWindowManager = (WindowManager) getApplication().getSystemService(getApplication().WINDOW_SERVICE);
wmParams = new WindowManager.LayoutParams();
//获取浮动窗口视图所在布局
mFloatLayout = (LinearLayout) inflater.inflate(R.layout.float_layout, null);
//添加mFloatLayout
mWindowManager.addView(mFloatLayout, wmParams);
//浮动窗口按钮
tvTime = (TextView) mFloatLayout.findViewById(R.id.tv_time);
closeBtn = (ImageView) mFloatLayout.findViewById(R.id.img_close);

值得注意的是,在安卓8.0(API25)起,添加悬浮窗口的类型从TYPE_PHONE改成TYPE_APPLICATION_OVERLAY,所以还需要通过下方代码进行适配。

if (Build.VERSION.SDK_INT > 25) {
    wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
    wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}

悬浮窗口的移动,点击关闭等也是在Service中来实现。

//设置监听浮动窗口的触摸移动
mFloatLayout.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        // TODO Auto-generated method stub
        //g这里实现自己的逻辑
        //刷新ui
        mWindowManager.updateViewLayout(mFloatLayout, wmParams);
        return false;  //此处必须返回false,否则OnClickListener获取不到监听
    }
});

点击关闭按钮,Service结束掉自身,并强制退出系统。

closeBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        stopSelf();
        System.exit(0);
    }
});

到此为止,所有的准备工作都已经完毕。接下来就是本应用的核心:获取北京时间,并实时刷新ui,显示时间。

本例中,获取网络时间使用的http请求框架是OkHttp,相信大家都不陌生。不清楚的可以查阅

public static long getNetworkTime()  {
    OkHttpClient client = new OkHttpClient();
    Request request = new Request.Builder().url("这里修改成能获取到北京时间的url即可").build();
    try {
        Response response = client.newCall(request).execute();
        if (response.code() == 200) {
            return Long.parseLong(response.body().string());
        }
    }catch (Exception e){
        return 0;
    }
    return 0;
}

由于Service中不能进行耗时操作,所以网络请求需要放在子线程中,而子线程不能更新ui,并且在本例中的Service无法调用runOnUiThread,所以我选择了Handler发送message的方式来刷新ui,然而Handler.sendEmptyMessage可能会存在一定的延时,这对于要求时间精准度非常高的应用有一定的影响。最后是通过了一个小小的技巧来弥补一下。在子线程获取到时间的时候,定义个全局变量来记录获取到北京时间的当前时间戳。然后在准备刷新ui的时候,再获取到时的时间戳,两个相减得到时间差。

new Thread(new Runnable() {
    @Override
    public void run() {
        dl = TimeUtils.getNetworkTime();
        Message msg=new Message();
        start = System.currentTimeMillis();
        refreshHandler.sendEmptyMessage(0);
    }
}).start();

在本例中,获取网络时间,只在打开应用的时候请求了一次,后面都是通过从请求到时间那刻开始,到当前时间的时间差来累计时间的。也算是这小技巧!

refreshHandler.post(new Runnable() {
    @Override
    public void run() {
        long d= System.currentTimeMillis()-start;
        String time= TimeUtils.getBJTime(dl+d);;
        tvTime.setText(time);
        refreshHandler.postDelayed(this, 1);
    }
});

效果图!左边是北京时间,时间几乎是0.1秒是看不到误差。

可以悬浮在任何应用之上!

第一次写。有误之处,还请指教!

1人推荐
随时随地看视频
慕课网APP

热门评论

赞!!!!!!!!!!!

查看全部评论