当前位置:   article > 正文

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

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

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

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

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

效果图:

请添加图片描述 请添加图片描述 请添加图片描述

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

点击议价小圈->获取剪贴板内容并且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;
            }
        }
    });
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

重写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));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

接着创建悬浮窗服务文件: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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285

这里我就不放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" />
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

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

关注我,一起成长!
@As.Kai

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/736381
推荐阅读
相关标签
  

闽ICP备14008679号