2016轉眼就要過去了,剛剛參加完學院舉辦的元旦晚會,看了看系里的大牛的各種事跡,內心感慨萬分。回來繼續安心做我的小碼農,順便更一下將近一個月沒有更新的博客。
這次帶來的是一個懸浮窗網速顯示計,先看下效果:
這里主要是在桌面上顯示一個懸浮窗,利用了WindowManager以及Service,接下來看看如何實現這樣一個效果:
首先APP必須獲得在桌面上顯示懸浮窗的機會,很多第三方ROM都限制了這一權限,我們首先就是要申請改權限,代碼如下:
public boolean checkDrawOverlayPermission() {
if (!Settings.canDrawOverlays(this)) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, REQUEST_CODE);
return false;
}
return true;
}
該函數首先檢查APP是否有顯示懸浮窗的權限,如果沒有,就跳轉到該APP設置懸浮窗權限的界面,如下圖所示:
然后先編寫我們的懸浮窗,布局很簡單,就是兩個TextView:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#88000000">
<TextView
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:id="@+id/speed_up"
android:text="upload speed"
android:gravity="left"
android:textSize="10dp"
android:layout_centerHorizontal="true"
android:textColor="@android:color/white"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:id="@+id/speed_down"
android:layout_below="@id/speed_up"
android:text="download speed"
android:gravity="left"
android:textSize="10dp"
android:layout_centerHorizontal="true"
android:textColor="@android:color/white"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
SpeedView:
public class SpeedView extends FrameLayout {
private Context mContext;
public TextView downText;
public TextView upText;
private WindowManager windowManager;
private int statusBarHeight;
private float preX,preY,x,y;
public SpeedView(Context context) {
super(context);
mContext=context;
init();
}
private void init() {
statusBarHeight=WindowUtil.statusBarHeight;
windowManager= (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
//a view inflate itself, that's funny
inflate(mContext,R.layout.speed_layout,this);
downText= (TextView) findViewById(R.id.speed_down);
upText= (TextView) findViewById(R.id.speed_up);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
preX=event.getRawX();preY=event.getRawY()-statusBarHeight;
return true;
case MotionEvent.ACTION_MOVE:
x=event.getRawX();y=event.getRawY()-statusBarHeight;
WindowManager.LayoutParams params= (WindowManager.LayoutParams) getLayoutParams();
params.x+=x-preX;
params.y+=y-preY;
windowManager.updateViewLayout(this,params);
preX=x;preY=y;
return true;
default:
break;
}
return super.onTouchEvent(event);
}
在SpeedView里我們重寫了onTouchEvent事件,這樣就能響應我們的拖拽事件了,注意這里更新SpeedView的位置是通過改變WindowManager.LayoutParam的x和y來實現的,調用windowManager.updateViewLayout(this,params)
來更新位置。
因為我們的網速顯示懸浮窗要脫離于Activity的生命周期而獨立存在,因此需要通過Service來實現:
public class SpeedCalculationService extends Service {
private WindowUtil windowUtil;
private boolean changed=false;
@Override
public void onCreate() {
super.onCreate();
WindowUtil.initX= (int) SharedPreferencesUtils.getFromSpfs(this,INIT_X,0);
WindowUtil.initY= (int) SharedPreferencesUtils.getFromSpfs(this,INIT_Y,0);
windowUtil=new WindowUtil(this);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
changed=intent.getBooleanExtra(MainActivity.CHANGED,false);
if(changed){
windowUtil.onSettingChanged();
}else{
if(!windowUtil.isShowing()){
windowUtil.showSpeedView();
}
SharedPreferencesUtils.putToSpfs(this,MainActivity.IS_SHOWN,true);
}
//return super.onStartCommand(intent, flags, startId);
return START_STICKY;
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
super.onDestroy();
WindowManager.LayoutParams params= (WindowManager.LayoutParams) windowUtil.speedView.getLayoutParams();
SharedPreferencesUtils.putToSpfs(this, INIT_X,params.x);
SharedPreferencesUtils.putToSpfs(this, INIT_Y,params.y);
if(windowUtil.isShowing()){
windowUtil.closeSpeedView();
SharedPreferencesUtils.putToSpfs(this,MainActivity.IS_SHOWN,false);
}
Log.d("yjw","service destroy");
}
}
這里的WindowUtil其實就是一個工具類,幫助我們控制懸浮窗SpeedView的顯示與隱藏:
public class WindowUtil {
public static int statusBarHeight=0;
//記錄懸浮窗的位置
public static int initX,initY;
private WindowManager windowManager;
public SpeedView speedView;
private WindowManager.LayoutParams params;
private Context context;
public boolean isShowing() {
return isShowing;
}
private boolean isShowing=false;
public static final int INTERVAL=2000;
private long preRxBytes,preSeBytes;
private long rxBytes,seBytes;
private Handler handler=new Handler(){
@Override
public void dispatchMessage(Message msg) {
super.dispatchMessage(msg);
calculateNetSpeed();
sendEmptyMessageDelayed(0,INTERVAL);
}
};
public void onSettingChanged(){
String setting= (String) SharedPreferencesUtils.getFromSpfs(context,MainActivity.SETTING,MainActivity.BOTH);
if(setting.equals(MainActivity.BOTH)){
speedView.upText.setVisibility(View.VISIBLE);
speedView.downText.setVisibility(View.VISIBLE);
}else if(setting.equals(MainActivity.UP)){
speedView.upText.setVisibility(View.VISIBLE);
speedView.downText.setVisibility(View.GONE);
}else{
speedView.upText.setVisibility(View.GONE);
speedView.downText.setVisibility(View.VISIBLE);
}
}
private void calculateNetSpeed() {
rxBytes=TrafficStats.getTotalRxBytes();
seBytes=TrafficStats.getTotalTxBytes()-rxBytes;
double downloadSpeed=(rxBytes-preRxBytes)/2;
double uploadSpeed=(seBytes-preSeBytes)/2;
preRxBytes=rxBytes;
preSeBytes=seBytes;
//根據范圍決定顯示單位
String upSpeed=null;
String downSpeed=null;
NumberFormat df= java.text.NumberFormat.getNumberInstance();
df.setMaximumFractionDigits(2);
if(downloadSpeed>1024*1024){
downloadSpeed/=(1024*1024);
downSpeed=df.format(downloadSpeed)+"MB/s";
}else if(downloadSpeed>1024){
downloadSpeed/=(1024);
downSpeed=df.format(downloadSpeed)+"B/s";
}else{
downSpeed=df.format(downloadSpeed)+"B/s";
}
if(uploadSpeed>1024*1024){
uploadSpeed/=(1024*1024);
upSpeed=df.format(uploadSpeed)+"MB/s";
}else if(uploadSpeed>1024){
uploadSpeed/=(1024);
upSpeed=df.format(uploadSpeed)+"kB/s";
}else{
upSpeed=df.format(uploadSpeed)+"B/s";
}
updateSpeed("↓ "+downSpeed,"↑ "+upSpeed);
}
public WindowUtil(Context context) {
this.context = context;
windowManager= (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
speedView=new SpeedView(context);
params=new WindowManager.LayoutParams();
params=new WindowManager.LayoutParams();
params.x=initX;
params.y=initY;
params.width=params.height=WindowManager.LayoutParams.WRAP_CONTENT;
params.type=WindowManager.LayoutParams.TYPE_PHONE;
params.gravity= Gravity.LEFT|Gravity.TOP;
params.format= PixelFormat.RGBA_8888;
params.flags=WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
//設置懸浮窗可以拖拽至狀態欄的位置
// | WindowManager.LayoutParams.FLAG_FULLSCREEN| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
}
public void showSpeedView(){
windowManager.addView(speedView,params);
isShowing=true;
preRxBytes= TrafficStats.getTotalRxBytes();
preSeBytes=TrafficStats.getTotalTxBytes()-preRxBytes;
handler.sendEmptyMessage(0);
}
public void closeSpeedView(){
windowManager.removeView(speedView);
isShowing=false;
}
public void updateSpeed(String downSpeed,String upSpeed){
speedView.upText.setText(upSpeed);
speedView.downText.setText(downSpeed);
}
}
WindowUtil類中也包含了一個很重要的方法,那就是計算網速。這里計算網速的方法很簡單,Android提供了一個類TrafficStats
,這個類里面為我們提供了好多接口,我們用到了其中的兩個:
1.public static long getTotalTxBytes ()
Return number of bytes transmitted since device boot. Counts packets across all network interfaces, and always increases monotonically since device boot. Statistics are measured at the network layer, so they include both TCP and UDP usage.
2.public static long getTotalRxPackets ()
Return number of packets received since device boot. Counts packets across all network interfaces, and always increases monotonically since device boot. Statistics are measured at the network layer, so they include both TCP and UDP usage.
可以看出,getTotalTxBytes()方法返回系統自開機到現在為止所一共傳輸的數據的字節數,包括上傳的數據和下載的數據;而getTotalRxPackets()方法返回的是系統自開機到現在為止所一共接收到也就是下載的數據的字節數,用getTotalTxBytes()-getTotalRxPackets()自然就是系統開機到現在為止所上傳的數據的字節數。
這樣每隔一定時間,我們計算一下系統自開機到目前所接受的數據包的字節數和所發送的數據的字節數的變化量,用變化量除以時間,就是這段時間的平均網速了。
為了每個一段時間計算一下網速,我們利用了一個Handler來實現這個定時任務
private Handler handler=new Handler(){
@Override
public void dispatchMessage(Message msg) {
super.dispatchMessage(msg);
calculateNetSpeed();
sendEmptyMessageDelayed(0,INTERVAL);
}
};
這里要注意將SpeedView添加到屏幕上,也就是添加到WindowManager里的時候,這個WindowManager.LayoutParams十分重要,其參數都是有用的,這里就不細講了,詳細介紹請移步官方文檔.
最后要將我們的懸浮窗設置為開機自啟動的,利用一個BroadcastReceiver就可以了:
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d("yjw","receiver receive boot broadcast");
boolean isShown= (boolean) SharedPreferencesUtils.getFromSpfs(context,MainActivity.IS_SHOWN,false);
if(isShown){
context.startService(new Intent(context,SpeedCalculationService.class));
}
}
}
在Manifest里這樣注冊我們的BroadcastReceiver:
<receiver android:name="me.mrrobot97.netspeed.MyBroadcastReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<category android:name="android.intent.category.HOME" />
</intent-filter>
</receiver>
這樣當系統啟動完成,就會開啟我們的Service。注意這里<intent-filter>中的<category>不可省略,親測省略后BroadcastReceiver無法接收到系統廣播。
最后還有一點,在Manifest的MainActivity條目中加一個屬性:android:excludeFromRecents="true"
這樣我們的ManiActivity就不會顯示在最近任務列表,防止用戶清空任務列表時將我們的Sercvice進程終結了。
完整的項目地址Github