Flutter 实现安卓原生系统级悬浮窗

这篇具有很好参考价值的文章主要介绍了Flutter 实现安卓原生系统级悬浮窗。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Flutter实现安卓原生系统级悬浮窗

原创:@As.Kai
博客地址:https://blog.csdn.net/qq_42362997
如果以下内容对您有帮助,点赞点赞点赞~

最近碰到了一个需求 使用Flutter实现悬浮窗效果
想来想去只能使用原生代码实现 需求整理:

应用移动到后台 -> 显示系统级悬浮窗口
应用移动到前台 -> 关闭系统级悬浮窗口
点击悬浮窗 显示占比30%的窗口 并且监听剪贴板
获取剪贴板内容请求调用后端接口
显示下半布局 整个窗口改为占80%高度 显示相应内容

效果图:

Flutter 实现安卓原生系统级悬浮窗Flutter 实现安卓原生系统级悬浮窗Flutter 实现安卓原生系统级悬浮窗

效果图大概是上面三张的效果:

点击议价小圈->获取剪贴板内容并且set到文本框上->利用获取到的内容请求接口获取识别内容set到文本框下方TextView上

有数据时点击议价->显示下面内容布局 下面数据布局使用的 横向 RecyclerView 竖向RecyclerView

点击右上角折叠按钮 缩小窗口到议价小圈

代码思路展示:

首先 找到目录文件…/android/app/src/main/java/xx/xx/xx/MainActivity.java文件
xx/xx/xx为您的项目名称 通常使用项目域名倒序命名

在MainActivity.java中重写configureFlutterEngine()方法
并在其中注册FlutterEngine
添加Method建立Flutter与原生通信通道检查悬浮窗权限内容
如果没有权限引导用户到系统设置页面 手动打开

Method在这里我就不细说了 有需要的可以看看我之前写的文章

public static Context mContext;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mContext = this;
}

@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
    GeneratedPluginRegistrant.registerWith(flutterEngine);
    setTokenChannel = new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), channelKey);
    setTokenChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
        @Override
        public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
            switch (call.method) {
                case "checkWindowPermission":
                    if(canShowOnce == 0){
                        canShowOnce++;
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(MainActivity.this)) {
                            //没有权限,需要申请权限,因为是打开一个授权页面,所以拿不到返回状态的,所以建议是在onResume方法中从新执行一次校验
                            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
                            intent.setData(Uri.parse("package:" + getPackageName()));
                            startActivityForResult(intent, 100);
                        }
                    }


                    break;
            }
        }
    });
}

重写onActivityResult方法 获取用户是否开启权限
并且在onstop中打开悬浮窗
onResume关闭悬浮窗服务

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == 0) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (!Settings.canDrawOverlays(this)) {
                Toast.makeText(this, "授权失败", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(this, "授权成功", Toast.LENGTH_SHORT).show();
            }
        }
    }
}

@Override
protected void onResume() {
    SharedPreferences sp = getSharedPreferences("token", MODE_PRIVATE);
    if (sp.getString("haveToken", "default value") != null) {
        stopService(new Intent(MainActivity.this, FloatingService.class));
    }
    super.onResume();
}

@Override
protected void onStop() {
    SharedPreferences sp = getSharedPreferences("token", MODE_PRIVATE);
    if (sp.getString("haveToken", "default value") != null) {
        startFloatingButtonService();
    }
    super.onStop();
}

public void startFloatingButtonService() {
    startService(new Intent(MainActivity.this, FloatingService.class));
}

接着创建悬浮窗服务文件:FloatingService.java继承Service
在onCreate方法中拿悬浮窗服务初始化
updateLayoutParams()方法是封装出来 调整悬浮窗宽高度/xy轴定位以及类型之类的
大家感兴趣可以看看源码

private WindowManager.LayoutParams mainParams;
private WindowManager.LayoutParams floatWindowLayoutParam;
private WindowManager windowManager;

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

@Override
public void onCreate() {
    super.onCreate();
    windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
    mainParams = new WindowManager.LayoutParams();
    updateLayoutParams(mainParams);
}

private void updateLayoutParams(WindowManager.LayoutParams 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.format = PixelFormat.RGBA_8888;
    layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
    layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
    layoutParams.width = ScreenUtils.dp2px(66);
    layoutParams.height = ScreenUtils.dp2px(66);
    layoutParams.x = ScreenUtils.getRealWidth() - ScreenUtils.dp2px(60);
    layoutParams.y = ScreenUtils.deviceHeight() / 10 * 2;
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    showFloatingWindow();
    return super.onStartCommand(intent, flags, startId);
}

onStartCommand也就是显示悬浮窗的地方 里面放的一般是悬浮窗布局/样式
这里的button就是效果图中议价小圆圈的样式了 drawable样式文件我就不放出来
可以根据自己的实现效果自定义效果

通过点击议价小圆圈 显示上半部分布局 并且隐藏小圆圈布局
button.setVisibility(View.GONE);

windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
LayoutInflater inflater = (LayoutInflater) getBaseContext().getSystemService(LAYOUT_INFLATER_SERVICE);
floatView = (ViewGroup) inflater.inflate(R.layout.floating_layout, null);
利用ViewGroup绑定上半+下半布局 layout文件

private Button button;
private ViewGroup floatView;
private LinearLayout bodyLinear;
private ClipboardManager manager;

private void showFloatingWindow() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {//判断系统版本
            if (Settings.canDrawOverlays(this)) {
                button = new Button(getApplicationContext());
                button.setText("询价");
                button.setBackgroundResource(R.drawable.button_style);
                windowManager.addView(button, mainParams);
                button.setOnClickListener(new View.OnClickListener() {
                    ///议价按钮点击
                    @Override
                    public void onClick(View view) {
                        button.setVisibility(View.GONE);
                        windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
                        LayoutInflater inflater = (LayoutInflater) getBaseContext().getSystemService(LAYOUT_INFLATER_SERVICE);
                        floatView = (ViewGroup) inflater.inflate(R.layout.floating_layout, null);
                        bodyLinear = floatView.findViewById(R.id.body_dialog);
                        descEditArea = floatView.findViewById(R.id.float_edit);
                        descEditArea.setSelection(descEditArea.getText().toString().length());
                        descEditArea.setCursorVisible(false);
                        bottomWidget = floatView.findViewById(R.id.bottom_widget);
                        bottomRecyclerView = floatView.findViewById(R.id.listview_horizontial);
                        planRecyclerView = floatView.findViewById(R.id.plan_recyclerView);
                        bottomLinear = floatView.findViewById(R.id.recycler_view_linear);
                        systemIden = floatView.findViewById(R.id.system_iden);
                        hintButton = floatView.findViewById(R.id.hint_button);
                        returnAppText = floatView.findViewById(R.id.return_app_text);
                        floatView.requestFocus();
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                            LAYOUT_TYPE = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
                        } else {
                            LAYOUT_TYPE = WindowManager.LayoutParams.TYPE_TOAST;
                        }

                        //这里用来控制上半布局属性
                        floatWindowLayoutParam = new WindowManager.LayoutParams(
                                ScreenUtils.getRealWidth() / 10 * 9,
                                ScreenUtils.deviceHeight() / 10 * 3,
                                LAYOUT_TYPE,
                                WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
                                PixelFormat.TRANSLUCENT
                        );


                        floatWindowLayoutParam.gravity = Gravity.CENTER;
                        floatWindowLayoutParam.x = 0;
                        floatWindowLayoutParam.y = -ScreenUtils.deviceHeight() / 10 * 2;

                        //添加到windowManager中
                        windowManager.addView(floatView, floatWindowLayoutParam);

                        //点击TextView回到App中 xx.xx.xx为您的包名
                        returnAppText.setOnClickListener(new View.OnClickListener() {
                            @Override
                            public void onClick(View view) {
                                Intent intent = getPackageManager().getLaunchIntentForPackage(“xx.xx.xx");
                                startActivity(intent);
                            }
                        });

                        //获取剪贴板内容
                        manager = (ClipboardManager) getSystemService(getApplicationContext().CLIPBOARD_SERVICE);
                        if (manager != null) {
                            if (manager.hasPrimaryClip()) {
                                if (manager.getPrimaryClip().getItemCount() > 0) {
                                    CharSequence addedText = manager.getPrimaryClip().getItemAt(0).getText();
                                    String addedTextString = String.valueOf(addedText);
                                    //拿到剪贴板内容 setText 并且使用Runnable刷新控件
                                    descEditArea.post(new Runnable() {
                                        @Override
                                        public void run() {
                                            descEditArea.setText(addedTextString);
                                        }
                                    });
                                }
                            }
                        }
                        //监听剪贴板 如果剪贴板内容有改变 重新赋值到文本框内
                        manager.addPrimaryClipChangedListener(new ClipboardManager.OnPrimaryClipChangedListener() {
                            @Override
                            public void onPrimaryClipChanged() {
                                CharSequence addedText = manager.getPrimaryClip().getItemAt(0).getText();
                                String addedTextString = String.valueOf(addedText);
                                descEditArea.post(new Runnable() {
                                    @Override
                                    public void run() {
                                        descEditArea.setText(addedTextString);
                                    }
                                });
                            }
                        });
                        //折叠上半+下半布局 回到议价小圆圈
                        hintButton.setOnClickListener(new View.OnClickListener() {
                            @Override
                            public void onClick(View view) {
                                button.setVisibility(View.VISIBLE);
                                bodyLinear.setVisibility(View.GONE);
                                windowManager.updateViewLayout(floatView, floatWindowLayoutParam);
                            }
                        });


                        clickShowBottom = floatView.findViewById(R.id.click_show_bottom_text);
                        ///点击询价
                        clickShowBottom.setOnClickListener(new View.OnClickListener() {
                            @Override
                            public void onClick(View view) {
                                //如果请求接口获取到的数据不为空 显示下半布局
                                if (data != null && data.size() > 0) {
                                    if (bottomWidget.getVisibility() != View.VISIBLE) {
                                        bottomWidget.setVisibility(View.VISIBLE);
                                        floatWindowLayoutParam.height = ScreenUtils.deviceHeight() / 10 * 7;
                                        floatWindowLayoutParam.gravity = Gravity.CENTER;
                                        floatWindowLayoutParam.x = 0;
                                        floatWindowLayoutParam.y = ScreenUtils.dp2px(10);
                                        LinearLayoutManager layoutManager = new LinearLayoutManager(getApplicationContext());
                                        layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
                                        //下半底部横向滚动布局 RecyclerView 
                                        bottomRecyclerView.setLayoutManager(layoutManager);
                                        adapter = new RecyclerAdapter(bargains);
                                        bottomRecyclerView.setAdapter(adapter);
                                        adapter.notifyDataSetChanged();

                                        //下半布局竖向滚动布局RecyclerView
                                        LinearLayoutManager planLayoutManager = new LinearLayoutManager(getApplicationContext());
                                        planLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
                                        planRecyclerView.setLayoutManager(planLayoutManager);
                                        PlanAdapter planAdapter = new PlanAdapter(plans);
                                        planRecyclerView.setAdapter(planAdapter);


                                        windowManager.updateViewLayout(floatView, floatWindowLayoutParam);
                                        adapter.setOnItemClickListener(new RecyclerAdapter.OnItemClickListener() {
                                            @Override
                                            public void onItemClick(View view, int position) {
                                                Intent intent = getPackageManager().getLaunchIntentForPackage("com.shibida.flutter_purchase");
                                                startActivity(intent);
                                            }
                                        });
                                    }
                                } else {
                                    Toast.makeText(getApplicationContext(), "请先粘贴内容识别", Toast.LENGTH_SHORT).show();
                                }




                            }
                        });


                        descEditArea.addTextChangedListener(new TextWatcher() {
                            @Override
                            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
                                //Not Necessary
                            }


                            @Override
                            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
                                ///调用内容
                                if (!descEditArea.getText().toString().equals("")) {
                                    queryData(descEditArea.getText().toString());
                                }


                            }


                            @Override
                            public void afterTextChanged(Editable editable) {
                                //Not Necessary
                            }
                        });


                        floatView.setOnTouchListener(new View.OnTouchListener() {
                            final WindowManager.LayoutParams floatWindowLayoutUpdateParam = floatWindowLayoutParam;
                            double x;
                            double y;
                            double px;
                            double py;


                            @Override
                            public boolean onTouch(View v, MotionEvent event) {


                                switch (event.getAction()) {
                                    //When the window will be touched, the x and y position of that position will be retrieved
                                    case MotionEvent.ACTION_DOWN:
                                        x = floatWindowLayoutUpdateParam.x;
                                        y = floatWindowLayoutUpdateParam.y;
                                        //returns the original raw X coordinate of this event
                                        px = event.getRawX();
                                        //returns the original raw Y coordinate of this event
                                        py = event.getRawY();
                                        break;
                                    //When the window will be dragged around, it will update the x, y of the Window Layout Parameter
                                    case MotionEvent.ACTION_MOVE:
                                        floatWindowLayoutUpdateParam.x = (int) ((x + event.getRawX()) - px);
                                        floatWindowLayoutUpdateParam.y = (int) ((y + event.getRawY()) - py);


                                        //updated parameter is applied to the WindowManager
                                        windowManager.updateViewLayout(floatView, floatWindowLayoutUpdateParam);
                                        break;
                                }


                                return false;
                            }
                        });


                        descEditArea.setOnTouchListener(new View.OnTouchListener() {
                            @Override
                            public boolean onTouch(View v, MotionEvent event) {
//                                ClipboardManager manager = getApplicationContext().getSystemService()
                                descEditArea.setCursorVisible(true);
                                WindowManager.LayoutParams floatWindowLayoutParamUpdateFlag = floatWindowLayoutParam;
                                floatWindowLayoutParamUpdateFlag.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
                                windowManager.updateViewLayout(floatView, floatWindowLayoutParamUpdateFlag);
                                return false;
                            }
                        });
                    }
                });
                button.setOnTouchListener(new FloatingOnTouchListener());
            }
        }
    }
@Override
public void onDestroy() {
    super.onDestroy();
    stopSelf();
    //Window is removed from the screen
    if (button != null) {
        windowManager.removeView(button);
    }
    if (floatView != null) {
        windowManager.removeView(floatView);
    }
}

//悬浮窗移动
private class FloatingOnTouchListener implements View.OnTouchListener {
    private int x;
    private int y;
    private long downTime;
    public int positionX;
    public int positionY;


    @Override
    public boolean onTouch(View view, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downTime = System.currentTimeMillis();
                x = (int) event.getRawX();
                y = (int) event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                int nowX = (int) event.getRawX();
                int nowY = (int) event.getRawY();
                int movedX = nowX - x;
                int movedY = nowY - y;
                x = nowX;
                y = nowY;
                mainParams.x = positionX != 0 ? positionX : mainParams.x + movedX;
                positionX = mainParams.x + movedX;
                mainParams.y = positionY != 0 ? positionY : mainParams.y + movedY;
                positionY = mainParams.y + movedY;
                windowManager.updateViewLayout(view, mainParams);
                break;
            default:
                break;
        }
        return false;
    }
}

这里我就不放okHttp3请求接口的内容了
思路就是在请求接口返回200时
将数据放到Adapter中并且刷新RecyclerView控件
未识别到内容时 将下半部分布局隐藏

最后别忘了在AndroidManifest.xml中添加一下内容:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.REORDER_TASKS" />
……….
<service
    android:name=".FloatingService"
    tools:ignore="Instantiatable" />

在写完代码之后有遇到一个问题,在应用后台显示悬浮窗拿不到焦点
最后查阅文章时在找到解决办法
是因为之前设置WindowManager.LayoutParams属性时设置为了FLAG_NOT_FOCUSABLE后改为FLAG_LAYOUT_IN_SCREEN后解决问题

使用机型:HONOR 20 Android 10 SDK29
大概就是这样 所有内容都放在一个文件中方便大家查阅,如果有遇到哪些问题可以私信给我 或者留言

关注我,一起成长!
@As.Kai文章来源地址https://www.toymoban.com/news/detail-401094.html

到了这里,关于Flutter 实现安卓原生系统级悬浮窗的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 所有安卓手机通刷原生系统

    一.手机需解锁bl锁 二.准备好对应手机低包 小米底包下载网址:XiaomiROM.com - 小米 ROM 线刷包, 卡刷包的最新及历史版本下载 三.使用命令刷入谷歌system分区 参考文档1:Redmi K50 刷入类原生系统Pixel Experience及后续优化全流程指南 参考文档2:通用系统映像  |  Android 开发者  | 

    2024年02月06日
    浏览(40)
  • 安卓Android类原生系统官网集合

    由于Miui的平板系统一言难尽,所以一直在使用类原生系统,最近在找网络上有人汇总了原生系统的链接,根据自己的理解和网络介绍,整理成以下方便自己和他人使用。 google套件 https://opengapps.org/ 目前有些系统已经删库了,比如像魔趣等,所以链接是否需要自己去验证了,

    2024年02月02日
    浏览(41)
  • VSCode 开发flutter 实现安卓设备远程调试

    目前只找到了安卓的调试方案😬。 1首先安装 ADB Commanads for VSCode扩展 并且必须确保ADB已经添加到系统环境变量中 如未添加请按照下面的方式添加,如添加请直接跳到下面。 2添加环境变量(windows可参考,mac忽略此项) 我将ADB安装到这个目录下,请查找自己的安装目录。 将

    2024年02月02日
    浏览(43)
  • Flutter悬浮UI的设计Overlay组件

    有时候我们在开发APP的时候会遇到下面这些需求: 在现有页面上添加浮动的悬浮按钮、气泡或菜单。 实现全局的通知或提示弹窗。 创建自定义的导航栏、底部导航或标签栏。 构建模态对话框或底部弹出菜单。 在屏幕上展示悬浮窗,比如 Flutter 版本的 Toast,任意位置的 Pop

    2024年02月09日
    浏览(39)
  • flutter开发实战-MethodChannel实现flutter与原生Android双向通信

    flutter开发实战-MethodChannel实现flutter与原生Android双向通信 最近开发中需要原生Android与flutter实现通信,这里使用的MethodChannel MethodChannel:用于传递方法调用(method invocation)。 通道的客户端和宿主端通过传递给通道构造函数的通道名称进行连接 一个应用中所使用的所有通道名称

    2024年02月13日
    浏览(40)
  • uniapp - [ H5 网页 / App ] 高性能 tabbar 底部菜单凸起效果,原生系统自定义底部菜单不卡顿、切换页面不闪烁、自动缓存页面(底部菜单中间自定义一个图片并悬浮起来)

    网上有很多自定义 tabbar 底部菜单的教程,但终归是组件形式,避免不了切换页面卡顿、闪屏闪烁、各平台不兼容等一系列问题。 本文 基于 uniapp 系统原生 tabbar 底部菜单,植入一个向上凸起的 “图片” 菜单,并支持点击触发事件, 您可以直接复制代码,换个中间凸起的菜

    2024年02月21日
    浏览(55)
  • 编译原生安卓aosp源码,实现硬改以及定位

    第一章 安卓aosp源码编译环境搭建 第二章 手机硬件参数介绍和校验算法 第三章 修改安卓aosp代码更改硬件参数 第四章 编译定制rom并刷机实现硬改(一) 第五章 编译定制rom并刷机实现硬改(二) 第六章 不root不magisk不xposed lsposed frida原生修改定位 第七章 安卓手机环境检测软件分享

    2024年02月03日
    浏览(107)
  • 安卓手机ROOT和刷机基本操作——以红米Note7刷安卓原生系统并Root为例

    学习安卓逆向需要进行调试,虽然之前对测试机root过可以进行一些调试,但是某些软件不能正常运行调试,遂选择刷安卓原生系统(PixelExperience) 软件权限(第三方软件) 权限最低,要向用户请求权限 用户权限 高于第三方软件,可以进行授权 ROOT权限 最高权限 Boot分区 包括了内核(Ker

    2024年02月10日
    浏览(75)
  • Uniapp 调用 原生安卓方法 使用cv 实现图片人脸识别 返回人脸位置和人脸数量

    效果: 安卓方法代码 uniapp代码

    2024年04月17日
    浏览(38)
  • 荣耀平板5鸿蒙降级安卓并刷入原生Android12系统——麒麟659,4+64G,10英寸wifi版本

      在学习Linux时,一边看手册和教程,一边写代码,一边还要远程控制另一台设备进行烧写和操作串口,一个屏幕有些不够用,再买一个又囊中羞涩,扒了扒杂货堆找到一个很老的荣耀平板5,10英寸用来看个手册刚刚好。   使用spacedesk把平板拓展成了个显示器,但是经常

    2024年02月04日
    浏览(115)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包