一個簡單的網速顯示懸浮窗

2016轉眼就要過去了,剛剛參加完學院舉辦的元旦晚會,看了看系里的大牛的各種事跡,內心感慨萬分。回來繼續安心做我的小碼農,順便更一下將近一個月沒有更新的博客。

這次帶來的是一個懸浮窗網速顯示計,先看下效果:

demo

這里主要是在桌面上顯示一個懸浮窗,利用了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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內容