可能有人会疑惑,这样的需求有什么用?接下来,我就先讲讲有什么用!对于那些经常玩准时抢购,抢优惠券的人来说,时间的精确度是非常重要的,手慢1秒就没了。而安卓手机中,却没法准确的看到秒数,并且是悬浮在其他应用之上的时钟就很有必要了!
首先先捋一捋实现本功能的关键所在!
得实现无界面。
悬浮在其他应用之上,可任意移动。随时关闭。
准确的北京时间,每秒、甚至毫秒级的刷新时间。
接下来就来谈谈具体如何实现?
如何实现无界面?
其实这个很简单,现在大家都明白是怎么回事。无界面,那就用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秒是看不到误差。
可以悬浮在任何应用之上!
第一次写。有误之处,还请指教!
热门评论
赞!!!!!!!!!!!