zoukankan      html  css  js  c++  java
  • Android 悬浮窗 System Alert Window

    悬浮窗能显示在其他应用上方。桌面系统例如Windows,macOS,Ubuntu,打开的程序能以窗口形式显示在屏幕上。
    受限于屏幕大小,安卓系统中主要使用多任务切换的方式和分屏的方式。视频播放,视频对话可能会采用悬浮窗功能(例如手Q,微信的视频通话)。应用留下一个视频(通话)窗口,用户可以返回安卓桌面,或者去其他app的界面操作。
    前面我们探讨了悬浮activity的实现方式,并结合CameraX预览来实现应用内摄像头预览悬浮Activity。这些是在app内实现的悬浮activity效果。

    本文我们用一个例子来展示Android悬浮窗的实现方法和注意事项。

    本文例子启用了dataBinding

        dataBinding {
            enabled = true
        }
    

    SYSTEM_ALERT_WINDOW权限

    manifest里申明权限SYSTEM_ALERT_WINDOW

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

    请求了这个权限后,app的权限管理中会有「显示悬浮窗」的权限选项。后面我们会引导用户去开启这个权限。

    标题中“System Alert Window”即SYSTEM_ALERT_WINDOW

    悬浮窗的界面

    准备layout文件floating_window_1.xml,它作为悬浮窗的界面。

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/f_root"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#00897B"
        android:gravity="center_horizontal"
        android:orientation="vertical"
        android:padding="4dp">
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center_horizontal"
            android:text="悬浮窗\n an.rustfisher.com "
            android:textColor="#FFFFFF"
            android:textSize="16sp" />
    
        <TextView
            android:id="@+id/f_btn1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:background="#00897B"
            android:text="打开控制页面"
            android:textColor="#FFFFFF" />
    
        <View
            android:layout_width="100dp"
            android:layout_height="1dp"
            android:layout_margin="16dp"
            android:background="#eaeaea" />
    
        <TextView
            android:id="@+id/exit_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#00897B"
            android:text="关闭悬浮窗服务"
            android:textColor="#FFFFFF" />
    </LinearLayout>
    

    悬浮窗service

    新建一个Service FloatingWindowService。它来执行创建/销毁悬浮窗的操作。
    完整代码如下。

    // package com.rustfisher.tutorial2020.service.floating;
    import android.app.Service;
    import android.content.Intent;
    import android.graphics.PixelFormat;
    import android.os.Build;
    import android.os.IBinder;
    import android.util.DisplayMetrics;
    import android.util.Log;
    import android.view.Gravity;
    import android.view.LayoutInflater;
    import android.view.MotionEvent;
    import android.view.View;
    import android.view.ViewGroup;
    import android.view.WindowManager;
    
    import androidx.annotation.Nullable;
    
    import com.rustfisher.tutorial2020.R;
    
    /**
     * 悬浮窗的服务
     *
     * @author an.rustfisher.com
     * @date 2022-01-05 23:53
     */
    public class FloatingWindowService extends Service {
        private static final String TAG = "rfDevFloatingService";
    
        private WindowManager windowManager;
        private View floatView;
    
        @Nullable
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
    
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            Log.d(TAG, "onStartCommand , " + startId);
            if (floatView == null) {
                Log.d(TAG, "onStartCommand: 创建悬浮窗");
                initUi();
            }
            return super.onStartCommand(intent, flags, startId);
        }
    
        @Override
        public void onDestroy() {
            Log.d(TAG, "onDestroy");
            super.onDestroy();
        }
    
        private void initUi() {
            DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
            int width = metrics.widthPixels;
            int height = metrics.heightPixels;
    
            windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
            LayoutInflater inflater = (LayoutInflater) getBaseContext().getSystemService(LAYOUT_INFLATER_SERVICE);
            floatView = (ViewGroup) inflater.inflate(R.layout.floating_window_1, null);
    
            int layoutType;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                layoutType = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
            } else {
                layoutType = WindowManager.LayoutParams.TYPE_TOAST;
            }
    
            WindowManager.LayoutParams floatLp = new WindowManager.LayoutParams(
                    (int) (width * (0.4f)),
                    (int) (height * (0.3f)),
                    layoutType,
                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                    PixelFormat.TRANSLUCENT
            );
    
            floatLp.gravity = Gravity.CENTER;
            floatLp.x = 0;
            floatLp.y = 0;
    
            windowManager.addView(floatView, floatLp);
    
            floatView.findViewById(R.id.f_btn1).setOnClickListener(v -> {
                stopSelf();
                windowManager.removeView(floatView);
                Intent backToHome = new Intent(getApplicationContext(), FloatingCmdAct.class);
                backToHome.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                startActivity(backToHome);
            });
            floatView.findViewById(R.id.exit_btn).setOnClickListener(v -> {
                stopSelf();
                windowManager.removeView(floatView);
            });
    
            floatView.setOnTouchListener(new View.OnTouchListener() {
                final WindowManager.LayoutParams floatWindowLayoutUpdateParam = floatLp;
                double x;
                double y;
                double px;
                double py;
    
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    switch (event.getAction()) {
                        case MotionEvent.ACTION_DOWN:
                            x = floatWindowLayoutUpdateParam.x;
                            y = floatWindowLayoutUpdateParam.y;
                            px = event.getRawX();
                            py = event.getRawY();
                            break;
                        case MotionEvent.ACTION_MOVE:
                            floatWindowLayoutUpdateParam.x = (int) ((x + event.getRawX()) - px);
                            floatWindowLayoutUpdateParam.y = (int) ((y + event.getRawY()) - py);
                            windowManager.updateViewLayout(floatView, floatWindowLayoutUpdateParam);
                            break;
                    }
                    return false;
                }
            });
        }
    }
    

    onStartCommand方法中我们可以知道,启动这个服务时,如果没有悬浮窗floatView,则去创建一个。

    WindowManager提供了与窗口管理器(window manager)沟通的接口。
    显示View到窗口(屏幕)上,用的是WindowManager提供的方法addView。移除调用removeView

    layout类型需要指定

    • WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
    • WindowManager.LayoutParams.TYPE_TOAST

    LayoutInflater用来创建floatView

    floatViewOnTouchListener,用来执行拖动操作。

    回到控制activity时,需要flag:Intent.FLAG_ACTIVITY_NEW_TASK,否则报错AndroidRuntimeException

    android.util.AndroidRuntimeException: 
        Calling startActivity() from outside of an Activity  context requires the FLAG_ACTIVITY_NEW_TASK flag.
    

    一个小问题:Service的生命周期方法运行在主线程(UI线程)上吗?

    Service相关概念请参考Service综述

    activity

    提供一个启动服务的地方。

    layout

    <?xml version="1.0" encoding="utf-8"?>
    <layout>
    
        <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center_horizontal"
                android:orientation="vertical">
    
                <Button
                    android:id="@+id/setting_window_btn"
                    style="@style/NormalBtn"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="4dp"
                    android:text="悬浮窗权限" />
    
                <Button
                    android:id="@+id/start_btn"
                    style="@style/NormalBtn"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="4dp"
                    android:text="启动服务" />
    
                <Button
                    android:id="@+id/end_btn"
                    style="@style/NormalBtn"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="4dp"
                    android:text="停止服务" />
    
            </LinearLayout>
    
        </androidx.constraintlayout.widget.ConstraintLayout>
    </layout>
    

    控制

    为了方便从悬浮窗出发跳回这个activity,启动模式设置为singleTop

    <activity
        android:name=".service.floating.FloatingCmdAct"
        android:launchMode="singleTop" />
    

    可根据具体需求选择启动模式。

    以下是FloatingCmdAct的完整代码

    // package com.rustfisher.tutorial2020.service.floating;
    import android.content.Intent;
    import android.net.Uri;
    import android.os.Build;
    import android.os.Bundle;
    import android.provider.Settings;
    import android.util.Log;
    import android.widget.Toast;
    
    import androidx.annotation.Nullable;
    import androidx.appcompat.app.AppCompatActivity;
    import androidx.databinding.DataBindingUtil;
    
    import com.rustfisher.tutorial2020.R;
    import com.rustfisher.tutorial2020.databinding.ActFloatingCmdBinding;
    
    /**
     * 启动服务前的界面
     *
     * @author an.rustfisher.com
     * @date 2022-01-05 14:57
     */
    public class FloatingCmdAct extends AppCompatActivity {
        private static final String TAG = "rfDevFloatingCmd";
    
        private ActFloatingCmdBinding mBinding;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            mBinding = DataBindingUtil.setContentView(this, R.layout.act_floating_cmd);
    
            mBinding.settingWindowBtn.setOnClickListener(v -> {
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
                    startActivity(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())));
                } else {
                    Toast.makeText(getApplicationContext(), "API < " + android.os.Build.VERSION_CODES.M, Toast.LENGTH_SHORT).show();
                }
            });
            mBinding.startBtn.setOnClickListener(v -> {
                startService(new Intent(getApplicationContext(), FloatingWindowService.class));
                Toast.makeText(getApplicationContext(), "启动服务", Toast.LENGTH_SHORT).show();
            });
            mBinding.endBtn.setOnClickListener(v -> {
                stopService(new Intent(getApplicationContext(), FloatingWindowService.class));
                Toast.makeText(getApplicationContext(), "停止服务", Toast.LENGTH_SHORT).show();
            });
    
            if (!checkOverlayDisplayPermission()) {
                Toast.makeText(getApplicationContext(), "请允许应用显示悬浮窗", Toast.LENGTH_SHORT).show();
            }
        }
    
        @Override
        protected void onNewIntent(Intent intent) {
            super.onNewIntent(intent);
            Log.d(TAG, "onNewIntent: 回来了");
        }
    
        private boolean checkOverlayDisplayPermission() {
            // API23以后需要检查权限
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
                return Settings.canDrawOverlays(this);
            } else {
                return true;
            }
        }
    }
    
    

    API23以后,需要检查是否允许显示悬浮窗。如果不允许则弹一个toast。
    跳转去显示悬浮窗权限界面,用Settings.ACTION_MANAGE_OVERLAY_PERMISSION

    Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName()))
    

    代码请参考工程 https://gitee.com/rustfisher/AndroidTutorial

    运行效果

    红米9A(MIUI 12.5.1 Android 10)

    红米9A

    小结

    本文实现了一个简单的悬浮窗功能。有了SYSTEM_ALERT_WINDOW权限后,悬浮窗能显示在其他app上方。
    添加view到窗口上,主要使用android.view.WindowManager的功能。

    参考

    一个软件工程师的记录
  • 相关阅读:
    Java Stream 流(JDK 8 新特性)
    Java EnumMap 实现类
    Java 设计模式
    Java lambda 表达式详解(JDK 8 新特性)
    Java forEach 方式遍历集合(Java 8 新特性)
    Java 单例设计模式
    Java public 和 private 访问修饰符
    == 、equals 、hashcode
    String
    ClassLoader 的分类及加载顺序
  • 原文地址:https://www.cnblogs.com/rustfisher/p/15770016.html
Copyright © 2011-2022 走看看