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

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

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

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

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

效果图:

Flutter 实现安卓原生系统级悬浮窗_第1张图片 Flutter 实现安卓原生系统级悬浮窗_第2张图片 Flutter 实现安卓原生系统级悬浮窗_第3张图片

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

点击议价小圈->获取剪贴板内容并且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中添加一下内容:




……….

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

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

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

你可能感兴趣的:(Flutter,flutter,android,java)