Android懸浮窗的簡單實現


概述

原理

Android的界面繪制,都是通過 WindowManager 的服務來實現的。 WindowManager 實現了 ViewManager 接口,可以通過獲取 WINDOW_SERVICE 系統服務得到。而 ViewManager 接口有 addView 方法,我們就是通過這個方法將懸浮窗控件加入到屏幕中去。

為了讓懸浮窗與Activity脫離,使其在應用處於后台時懸浮窗仍然可以正常運行,使用Service來啟動懸浮窗並做為其背后邏輯支撐。

權限

在 API Level >= 23 的時候,需要在AndroidManefest.xml文件中聲明權限 SYSTEM_ALERT_WINDOW 才能在其他應用上繪制控件。

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

除了這個權限外,我們還需要在系統設置里面對本應用進行設置懸浮窗權限。該權限在應用中需要啟動 Settings.ACTION_MANAGE_OVERLAY_PERMISSION 來讓用戶手動設置權限。

if (!Settings.canDrawOverlays(this)) {
    showError("當前無權限,請授權");
    startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), 0);
} else {
    startService(new Intent(MainActivity.this, FloatingService.class));
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == 0) {
        if (!Settings.canDrawOverlays(this)) {
            showError("授權失敗");
        } else {
            showMsg("授權成功");
            startService(new Intent(MainActivity.this, FloatingService.class));
        }
    }
}

LayoutParam

WindowManager 的 addView 方法有兩個參數,一個是需要加入的控件對象,另一個參數是 WindowManager.LayoutParam 對象。
  這里需要着重說明的是 LayoutParam 里的 type 變量。這個變量是用來指定窗口類型的。在設置這個變量時,需要注意一個坑,那就是需要對不同版本的Android系統進行適配。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
    layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}

實例

AndroidManifest.xml

添加權限

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

添加Service組件

<service android:name=".MediaFloatService"/>

MyApplication

public class PubApplication extends Application {
    //設置一個全局變量來判斷懸浮窗是否已經開啟
    public static Boolean isMediaFloatShow = false;
}

MediaFloatService

public class MediaFloatService extends Service {
    private WindowManager windowManager;
    private WindowManager.LayoutParams layoutParams;
    private View mView;
    private ViewFloatMediaBinding floatView;
    private ArrayList<String> lstFilePaths;
    private int currentIndex;
    private int screenWidth;
    private int screenHeight;

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //加載窗口布局
        initWindow(intent);
        return super.onStartCommand(intent, flags, startId);
    }
    
    @Override
    public void onDestroy() {
        //移除窗口布局
        windowManager.removeView(mView);
        super.onDestroy();
    }
}

加載窗口布局

private void initWindow(Intent intent) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        if (Settings.canDrawOverlays(this)) {
            //獲取WindowManager服務
            windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);

            //取得屏幕尺寸
            DisplayMetrics dm = new DisplayMetrics();
            windowManager.getDefaultDisplay().getMetrics(dm);
            screenWidth = dm.widthPixels;
            screenHeight = dm.heightPixels;

            //設置LayoutParams
            layoutParams = new WindowManager.LayoutParams();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
            } else {
                layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
            }
            layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_FULLSCREEN
                    | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
            layoutParams.format = PixelFormat.RGBA_8888; //背景透明效果
            layoutParams.width = 512; //懸浮窗口長寬值,單位為 px 而非 dp
            layoutParams.height = 450;
            layoutParams.gravity = 51; //想要x,y生效,一定要指定Gravity為top和left //Gravity.TOP | Gravity.LEFT
            layoutParams.x = 100; //啟動位置
            layoutParams.y = 100;

            //加載懸浮窗布局
            floatView = ViewFloatMediaBinding.inflate(LayoutInflater.from(MediaFloatService.this));
            mView = floatView.getRoot();
            //mView.setAlpha((float) 0.9);

            //設定懸浮窗控件
            floatView.ivFloatMediaClose.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    MyApplication.isMediaFloatShow = false;
                    //切記添加關閉服務按鈕事件,調用 stopSelf() 方法以關閉懸浮窗。
                    stopSelf();
                }
            });
            //接收傳值
            Bundle bundle = intent.getExtras();
            lstFilePaths = bundle.getStringArrayList("lstFilePaths");
            currentIndex = bundle.getInt("currentIndex");
            floatView.ivFloatMediaPrev.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    //判斷是否是第一個,如果是則跳到最后循環播放
                    if (currentIndex == 0) currentIndex = lstFilePaths.size() - 1;
                    else currentIndex = currentIndex - 1;
                    showImage();
                    //Glide.with(MediaFloatService.this).load(lstFilePaths.get(currentIndex)).into(floatView.ivFloatMediaShow);
                }
            });
            floatView.ivFloatMediaNext.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    //判斷是否是最后一個,如果是則跳到最前循環播放
                    if (currentIndex == lstFilePaths.size() - 1) currentIndex = 0;
                    else currentIndex = currentIndex + 1;
                    showImage();
                    //Glide.with(MediaFloatService.this).load(lstFilePaths.get(currentIndex)).into(floatView.ivFloatMediaShow);
                }
            });
            BitmapFactory.Options options = getBitmapOptions(lstFilePaths.get(currentIndex));
            layoutParams.width = Math.min(options.outWidth, screenWidth); //Math.min取得兩個數據中的最小值
            layoutParams.height = Math.min(options.outHeight, screenHeight);
            Glide.with(this).load(lstFilePaths.get(currentIndex)).into(floatView.ivFloatMediaShow);
            //單擊事件是否顯示控制按鈕
            floatView.ivFloatMediaShow.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    if (floatView.grpFloatMediaControl.getVisibility() == View.INVISIBLE)
                        floatView.grpFloatMediaControl.setVisibility(View.VISIBLE);
                    else floatView.grpFloatMediaControl.setVisibility(View.INVISIBLE);
                }
            });
            
			//提交布局
            windowManager.addView(mView, layoutParams);
        }
    }
}

取得屏幕尺寸

DisplayMetrics dm = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(dm);
screenWidth = dm.widthPixels;
screenHeight = dm.heightPixels;

根據路徑取得圖片尺寸

private BitmapFactory.Options getBitmapOptions(String filepath) {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(filepath, options);
    return options;
}

變更顯示圖片

變更圖片時懸浮窗口也要根據變更后的圖片再做調整,不然窗口大小還是上一圖片的尺寸比例

private void showImage() {
    BitmapFactory.Options options = getBitmapOptions(lstFilePaths.get(currentIndex));
    layoutParams.width = Math.min(options.outWidth, screenWidth); //Math.min取得兩個數據中的最小值
    layoutParams.height = Math.min(options.outHeight, screenHeight);
    Glide.with(this).load(lstFilePaths.get(currentIndex)).into(floatView.ivFloatMediaShow);
    windowManager.updateViewLayout(mView, layoutParams);
}

窗口拖動與縮放

窗口拖動

floatView.ivFloatMediaShow.setOnTouchListener(new View.OnTouchListener() {
    private int dX;
    private int dY;

    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        switch (motionEvent.getAction()) {
            case MotionEvent.ACTION_DOWN:
                dX = (int) motionEvent.getRawX();
                dY = (int) motionEvent.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                int nX = (int) motionEvent.getRawX();
                int nY = (int) motionEvent.getRawY();
                int cW = nX - dX;
                int cH = nY - dY;
                dX = nX;
                dY = nY;
                layoutParams.x = layoutParams.x + cW;
                layoutParams.y = layoutParams.y + cH;
                windowManager.updateViewLayout(mView, layoutParams);
                break;
            default:
                break;
        }
        return true;
    }
});

單擊雙擊

floatView.ivFloatMediaShow.setOnTouchListener(new View.OnTouchListener() {
    private int sX;
    private int sY;

    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        switch (motionEvent.getAction()) {
            case MotionEvent.ACTION_DOWN:
                sX = (int) motionEvent.getRawX();
                sY = (int) motionEvent.getRawY();
                break;
            case MotionEvent.ACTION_UP:
                //如果抬起時的位置和按下時的位置大致相同視作單擊事件
                int nX2 = (int) motionEvent.getRawX();
                int nY2 = (int) motionEvent.getRawY();
                int cW2 = nX2 - sX;
                int cH2 = nY2 - sY;
                //間隔值可能為負值,所以要取絕對值進行比較
                if (Math.abs(cW2) < 3 && Math.abs(cH2) < 3) view.performClick();
                break;
            default:
                break;
        }
        return true;
    }
});

雙指縮放

floatView.ivFloatMediaShow.setOnTouchListener(new View.OnTouchListener() {
    private ScaleGestureDetector scaleGestureDetector = new ScaleGestureDetector(MediaFloatService.this, new myScale());

    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        switch (motionEvent.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isP2Down = true; //雙指按下
                break;
            case MotionEvent.ACTION_MOVE:
                if (motionEvent.getPointerCount() == 2) {
                    //雙指縮放
                    scaleGestureDetector.onTouchEvent(motionEvent);
                }
                break;
            case MotionEvent.ACTION_UP:
                isP2Down = false; //雙指抬起
                break;
            default:
                break;
        }
        return true;
    }
});
ScaleGestureDetector
private float initSapcing = 0;
private int initWidth;
private int initHeight;
private int initX;
private int initY;
private boolean isP2Down = false;

private class myScale extends ScaleGestureDetector.SimpleOnScaleGestureListener {
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
//            detector.getCurrentSpan();//兩點間的距離跨度
//            detector.getCurrentSpanX();//兩點間的x距離
//            detector.getCurrentSpanY();//兩點間的y距離
//            detector.getFocusX();       //
//            detector.getFocusY();       //
//            detector.getPreviousSpan(); //上次
//            detector.getPreviousSpanX();//上次
//            detector.getPreviousSpanY();//上次
//            detector.getEventTime();    //當前事件的事件
//            detector.getTimeDelta();    //兩次事件間的時間差
//            detector.getScaleFactor();  //與上次事件相比,得到的比例因子

        if (isP2Down) {
            //雙指按下時最初間距
            initSapcing = detector.getCurrentSpan();
            //雙指按下時窗口大小
            initWidth = layoutParams.width;
            initHeight = layoutParams.height;
            //雙指按下時窗口左頂點位置
            initX = layoutParams.x;
            initY = layoutParams.y;
            isP2Down = false;
        }
        float scale = detector.getCurrentSpan() / initSapcing; //取得縮放比
        int newWidth = (int) (initWidth * scale);
        int newHeight = (int) (initHeight * scale);
        //判斷窗口縮放后是否超出屏幕大小
        if (newWidth < screenWidth && newHeight < screenHeight) {
            layoutParams.width = newWidth;
            layoutParams.height = newHeight;
            layoutParams.x = initX;
            layoutParams.y = initY;
            //縮放后圖片會失真重新載入圖片
            Glide.with(MediaFloatService.this)
                .load(lstFilePaths.get(currentIndex))
                .into(floatView.ivFloatMediaShow);
            //提交更新布局
            windowManager.updateViewLayout(mView, layoutParams);
        }
        return true;
        //return super.onScale(detector);
    }
}

完整代碼

floatView.ivFloatMediaShow.setOnTouchListener(new View.OnTouchListener() {
    private int dX;
    private int dY;
    private int sX;
    private int sY;
    private ScaleGestureDetector scaleGestureDetector = new ScaleGestureDetector(MediaFloatService.this, new myScale());

    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        switch (motionEvent.getAction()) {
            case MotionEvent.ACTION_DOWN:
                dX = (int) motionEvent.getRawX();
                dY = (int) motionEvent.getRawY();
                sX = (int) motionEvent.getRawX();
                sY = (int) motionEvent.getRawY();
                isP2Down = true; //雙指按下
                break;
            case MotionEvent.ACTION_MOVE:
                if (motionEvent.getPointerCount() == 1) {
                    //單指拖動
                    int nX = (int) motionEvent.getRawX();
                    int nY = (int) motionEvent.getRawY();
                    int cW = nX - dX;
                    int cH = nY - dY;
                    dX = nX;
                    dY = nY;
                    layoutParams.x = layoutParams.x + cW;
                    layoutParams.y = layoutParams.y + cH;
                    windowManager.updateViewLayout(mView, layoutParams);
                } else if (motionEvent.getPointerCount() == 2) {
                    //雙指縮放
                    scaleGestureDetector.onTouchEvent(motionEvent);
                }
                break;
            case MotionEvent.ACTION_UP:
                isP2Down = false; //雙指抬起
                //如果抬起時的位置和按下時的位置大致相同視作單擊事件
                int nX2 = (int) motionEvent.getRawX();
                int nY2 = (int) motionEvent.getRawY();
                int cW2 = nX2 - sX;
                int cH2 = nY2 - sY;
                //間隔值可能為負值,所以要取絕對值進行比較
                if (Math.abs(cW2) < 3 && Math.abs(cH2) < 3) view.performClick();
                break;
            default:
                break;
        }
        return true;
    }
});

實例2

private WindowManager windowManager;
private WindowManager.LayoutParams layoutParams;
private View floatView;
if(Settings.canDrawOverlays(this)){
    windowManager = (WindowManager)getSystemService(WINDOW_SERVICE);
    layoutParams = new WindowManager.LayoutParams();
    
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
        layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
    } else {
        layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
    }
    layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
    layoutParams.format = PixelFormat.RGBA_8888; //背景透明效果
    layoutParams.width = 512; //懸浮窗口長寬值,單位為 px 而非 dp
    layoutParams.height = 450;
    layoutParams.gravity = Gravity.TOP | Gravity.LEFT; //啟動位置
    layoutParams.x = 100; 
    layoutParams.y = 100;
    
    floatView = LayoutInflater.from(this).inflate(R.layout.note_float_view, null);
    floatView.setAlpha((float) 0.9);
    
    windowManager.addView(floatView, layoutParams);
}

常見問題

起始位置設置無效

想要x,y生效,一定要指定Gravity為top和left,想要居中就設置為 Center

layoutParams.gravity = Gravity.TOP | Gravity.LEFT; 
layoutParams.x = 100; //啟動位置
layoutParams.y = 100;

獲取狀態欄高度

private int statusBarHeight = -1;
//獲取狀態欄高度
private void getStatusBarHeight(){
    mView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
    int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
    if(resourceId > 0){
        statusBarHeight = getResources().getDimensionPixelSize(resourceId);
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM