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;
}
}
});
}
重写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
大概就是这样 所有内容都放在一个文件中方便大家查阅,如果有遇到哪些问题可以私信给我 或者留言文章来源:https://www.toymoban.com/news/detail-401094.html
关注我,一起成长!
@As.Kai文章来源地址https://www.toymoban.com/news/detail-401094.html
到了这里,关于Flutter 实现安卓原生系统级悬浮窗的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!