700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > Flutter 实现安卓原生系统级悬浮窗

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

时间:2024-01-31 07:31:09

相关推荐

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

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

原创:@As.Kai

博客地址:/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;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);mContext = this;}@Overridepublic void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {GeneratedPluginRegistrant.registerWith(flutterEngine);setTokenChannel = new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), channelKey);setTokenChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {@Overridepublic 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关闭悬浮窗服务

@Overrideprotected 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();}}}}@Overrideprotected void onResume() {SharedPreferences sp = getSharedPreferences("token", MODE_PRIVATE);if (sp.getString("haveToken", "default value") != null) {stopService(new Intent(MainActivity.this, FloatingService.class));}super.onResume();}@Overrideprotected 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@Overridepublic IBinder onBind(Intent intent) {return null;}@Overridepublic 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;}@Overridepublic 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() {///议价按钮点击@Overridepublic 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() {@Overridepublic 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() {@Overridepublic void run() {descEditArea.setText(addedTextString);}});}}}//监听剪贴板 如果剪贴板内容有改变 重新赋值到文本框内manager.addPrimaryClipChangedListener(new ClipboardManager.OnPrimaryClipChangedListener() {@Overridepublic void onPrimaryClipChanged() {CharSequence addedText = manager.getPrimaryClip().getItemAt(0).getText();String addedTextString = String.valueOf(addedText);descEditArea.post(new Runnable() {@Overridepublic void run() {descEditArea.setText(addedTextString);}});}});//折叠上半+下半布局 回到议价小圆圈hintButton.setOnClickListener(new View.OnClickListener() {@Overridepublic 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() {@Overridepublic 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();//下半布局竖向滚动布局RecyclerViewLinearLayoutManager 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() {@Overridepublic 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() {@Overridepublic void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {//Not Necessary}@Overridepublic void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {///调用内容if (!descEditArea.getText().toString().equals("")) {queryData(descEditArea.getText().toString());}}@Overridepublic void afterTextChanged(Editable editable) {//Not Necessary}});floatView.setOnTouchListener(new View.OnTouchListener() {final WindowManager.LayoutParams floatWindowLayoutUpdateParam = floatWindowLayoutParam;double x;double y;double px;double py;@Overridepublic 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 retrievedcase MotionEvent.ACTION_DOWN:x = floatWindowLayoutUpdateParam.x;y = floatWindowLayoutUpdateParam.y;//returns the original raw X coordinate of this eventpx = event.getRawX();//returns the original raw Y coordinate of this eventpy = event.getRawY();break;//When the window will be dragged around, it will update the x, y of the Window Layout Parametercase MotionEvent.ACTION_MOVE:floatWindowLayoutUpdateParam.x = (int) ((x + event.getRawX()) - px);floatWindowLayoutUpdateParam.y = (int) ((y + event.getRawY()) - py);//updated parameter is applied to the WindowManagerwindowManager.updateViewLayout(floatView, floatWindowLayoutUpdateParam);break;}return false;}});descEditArea.setOnTouchListener(new View.OnTouchListener() {@Overridepublic 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());}}}@Overridepublic void onDestroy() {super.onDestroy();stopSelf();//Window is removed from the screenif (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;@Overridepublic 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" />……….<serviceandroid:name=".FloatingService"tools:ignore="Instantiatable" />

在写完代码之后有遇到一个问题,在应用后台显示悬浮窗拿不到焦点

最后查阅文章时在找到解决办法

是因为之前设置WindowManager.LayoutParams属性时设置为了FLAG_NOT_FOCUSABLE后改为FLAG_LAYOUT_IN_SCREEN后解决问题

使用机型:HONOR 20 Android 10 SDK29

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

关注我,一起成长!

@As.Kai

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。