|
@@ -0,0 +1,370 @@
|
|
|
+package com.yyrh.ui.floatview;
|
|
|
+
|
|
|
+import android.annotation.SuppressLint;
|
|
|
+import android.app.Activity;
|
|
|
+import android.content.Context;
|
|
|
+import android.graphics.PixelFormat;
|
|
|
+import android.graphics.drawable.ColorDrawable;
|
|
|
+import android.os.Build;
|
|
|
+import android.os.Handler;
|
|
|
+import android.os.Looper;
|
|
|
+import android.os.Message;
|
|
|
+import android.support.annotation.NonNull;
|
|
|
+import android.view.Gravity;
|
|
|
+import android.view.MotionEvent;
|
|
|
+import android.view.View;
|
|
|
+import android.view.ViewGroup;
|
|
|
+import android.view.WindowManager;
|
|
|
+import android.widget.FrameLayout;
|
|
|
+import android.widget.ImageView;
|
|
|
+import android.widget.LinearLayout;
|
|
|
+import android.widget.PopupWindow;
|
|
|
+
|
|
|
+import java.lang.reflect.Field;
|
|
|
+import java.lang.reflect.Method;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+
|
|
|
+import cn.yyxx.support.DensityUtils;
|
|
|
+import cn.yyxx.support.scheduler.ScheduledWorker;
|
|
|
+
|
|
|
+/**
|
|
|
+ * @author #Suyghur.
|
|
|
+ * Created on 2021/10/21
|
|
|
+ */
|
|
|
+public class FloatingBall extends FrameLayout implements View.OnTouchListener {
|
|
|
+
|
|
|
+ private Activity activity;
|
|
|
+ private boolean isLandscape = false;
|
|
|
+ private IFloatingBallCallback callback;
|
|
|
+ private WindowManager wm;
|
|
|
+ private WindowManager.LayoutParams wlp;
|
|
|
+ private LinearLayout rootLinearLayout;
|
|
|
+ private FrameLayout frameLayout;
|
|
|
+ private ImageView ballView;
|
|
|
+ private FloatingBallMenu ballMenu;
|
|
|
+ private PopupWindow menu;
|
|
|
+
|
|
|
+ // 浮标是否移动
|
|
|
+ private boolean isMove = false;
|
|
|
+
|
|
|
+ // 手按下时坐标
|
|
|
+ private float mTouchStartX = 0f;
|
|
|
+ private float mTouchStartY = 0f;
|
|
|
+
|
|
|
+ // 是否展开
|
|
|
+ private boolean hasShowContent = false;
|
|
|
+
|
|
|
+ // 当前贴边状态
|
|
|
+ private boolean isHide = false;
|
|
|
+
|
|
|
+ // 默认吸附在左边
|
|
|
+ private boolean isLeftLocation = true;
|
|
|
+
|
|
|
+ // 菜单menu的长度
|
|
|
+ private int mSize = 0;
|
|
|
+
|
|
|
+ private boolean hasStatusBar;
|
|
|
+
|
|
|
+ private ScheduledWorker displayWorker = null;
|
|
|
+
|
|
|
+ private Handler timeHandler = new Handler(Looper.getMainLooper()) {
|
|
|
+ @Override
|
|
|
+ public void handleMessage(@NonNull Message msg) {
|
|
|
+ if (msg.what == 1000) {
|
|
|
+// invokeBallFullOrHalt(true);
|
|
|
+ callback.onUpdateBallView(ballView, isLeftLocation, true);
|
|
|
+ // 把worker取消掉防止一直在执行
|
|
|
+ if (displayWorker != null) {
|
|
|
+ displayWorker.cancel();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ public FloatingBall(Activity activity, boolean isLandscape, IFloatingBallCallback callback) {
|
|
|
+ super(activity);
|
|
|
+ this.activity = activity;
|
|
|
+ this.isLandscape = isLandscape;
|
|
|
+ this.callback = callback;
|
|
|
+ this.hasStatusBar = (activity.getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) != WindowManager.LayoutParams.FLAG_FULLSCREEN;
|
|
|
+ createWM();
|
|
|
+ createView();
|
|
|
+ addView(rootLinearLayout);
|
|
|
+ wm.addView(this, wlp);
|
|
|
+ displayWorker = new ScheduledWorker(1);
|
|
|
+ invokeDisplayTimerWork();
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressLint("WrongConstant")
|
|
|
+ private void createWM() {
|
|
|
+ this.wm = activity.getWindowManager();
|
|
|
+ wlp = new WindowManager.LayoutParams();
|
|
|
+ wlp.type = WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
|
|
|
+ wlp.format = PixelFormat.RGBA_8888;
|
|
|
+ wlp.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
|
|
|
+ wlp.gravity = Gravity.TOP | Gravity.LEFT;
|
|
|
+ wlp.x = 0;
|
|
|
+ wlp.y = DensityUtils.getHeigthAndWidth(activity)[1] / 2;
|
|
|
+ wlp.width = WindowManager.LayoutParams.WRAP_CONTENT;
|
|
|
+ wlp.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void createView() {
|
|
|
+ rootLinearLayout = new LinearLayout(activity);
|
|
|
+ rootLinearLayout.setLayoutParams(new ViewGroup.LayoutParams(DensityUtils.dip2px(activity, 45f), DensityUtils.dip2px(activity, 45f)));
|
|
|
+
|
|
|
+ frameLayout = new FrameLayout(activity);
|
|
|
+ frameLayout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
|
|
+
|
|
|
+ ballView = new ImageView(activity);
|
|
|
+ ballView.setLayoutParams(new ViewGroup.LayoutParams(DensityUtils.dip2px(activity, 45f), DensityUtils.dip2px(activity, 45f)));
|
|
|
+ callback.onUpdateBallView(ballView, isLeftLocation, false);
|
|
|
+ ballView.setScaleType(ImageView.ScaleType.FIT_XY);
|
|
|
+ ballView.setOnTouchListener(this);
|
|
|
+ frameLayout.addView(ballView);
|
|
|
+
|
|
|
+ ballMenu = new FloatingBallMenu(activity, new FloatingBallMenu.IFloatingBallMenuCallback() {
|
|
|
+ @Override
|
|
|
+ public ArrayList<FloatingBallMenu.Item> onInitMenuData() {
|
|
|
+ mSize = callback.onInitMenuData().size();
|
|
|
+ return callback.onInitMenuData();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onDismissMenu() {
|
|
|
+ callback.onDismissMenu();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onMenuItemCLick(FloatingBallMenu.Item item, int pos) {
|
|
|
+ callback.onMenuItemClick(item, pos);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ menu = new PopupWindow(activity);
|
|
|
+ menu.setContentView(ballMenu.contentView);
|
|
|
+ menu.setWidth(DensityUtils.dip2px(activity, 45f + mSize * 45f));
|
|
|
+ menu.setHeight(DensityUtils.dip2px(activity, 45f));
|
|
|
+ menu.setFocusable(true);
|
|
|
+ menu.setBackgroundDrawable(new ColorDrawable());
|
|
|
+ menu.setOutsideTouchable(true);
|
|
|
+ menu.setOnDismissListener(new PopupWindow.OnDismissListener() {
|
|
|
+ @Override
|
|
|
+ public void onDismiss() {
|
|
|
+ hasShowContent = false;
|
|
|
+ invokeDisplayTimerWork();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (isLandscape) {
|
|
|
+ handleFullScreenModel();
|
|
|
+ }
|
|
|
+ rootLinearLayout.addView(frameLayout);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void dismissMenu() {
|
|
|
+ hasShowContent = false;
|
|
|
+ menu.dismiss();
|
|
|
+ callback.onUpdateBallView(ballView, isLeftLocation, false);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void invokeBallFullOrHalt(boolean isHalf) {
|
|
|
+ if (hasShowContent && !isHide) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.isHide = isHalf;
|
|
|
+ if (isHalf) {
|
|
|
+ rootLinearLayout.setLayoutParams(new ViewGroup.LayoutParams(DensityUtils.dip2px(activity, 22.5f), DensityUtils.dip2px(activity, 45f)));
|
|
|
+ setLayoutParams(new ViewGroup.LayoutParams(DensityUtils.dip2px(activity, 22.5f), DensityUtils.dip2px(activity, 45f)));
|
|
|
+ } else {
|
|
|
+ rootLinearLayout.setLayoutParams(new ViewGroup.LayoutParams(DensityUtils.dip2px(activity, 45f), DensityUtils.dip2px(activity, 45f)));
|
|
|
+ setLayoutParams(new ViewGroup.LayoutParams(DensityUtils.dip2px(activity, 45f), DensityUtils.dip2px(activity, 45f)));
|
|
|
+ }
|
|
|
+ frameLayout.updateViewLayout(ballView, getLayoutParams());
|
|
|
+ }
|
|
|
+
|
|
|
+ private void invokeMenuShowOrDismiss() {
|
|
|
+ if (hasShowContent) {
|
|
|
+ hasShowContent = false;
|
|
|
+ menu.dismiss();
|
|
|
+ } else {
|
|
|
+ hasShowContent = true;
|
|
|
+ callback.onExpandMenu(false);
|
|
|
+ ballMenu.updateLayout(isLeftLocation);
|
|
|
+ if (hasStatusBar) {
|
|
|
+ menu.showAtLocation(ballMenu.contentView, Gravity.NO_GRAVITY, wlp.x + 10, wlp.y + getStatusBarHeight(activity));
|
|
|
+ } else {
|
|
|
+ if (isNotch()) {
|
|
|
+ menu.showAtLocation(ballMenu.contentView, Gravity.NO_GRAVITY, wlp.x + 10, wlp.y + getStatusBarHeight(activity));
|
|
|
+ } else if (isDisplayCutout(activity)) {
|
|
|
+ menu.showAtLocation(ballMenu.contentView, Gravity.NO_GRAVITY, wlp.x + 10, wlp.y + getStatusBarHeight(activity));
|
|
|
+ } else {
|
|
|
+ menu.showAtLocation(ballMenu.contentView, Gravity.NO_GRAVITY, wlp.x + 10, wlp.y);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void invokeDisplayTimerWork() {
|
|
|
+ if (displayWorker != null) {
|
|
|
+ displayWorker.invokeAtFixedRate(new Runnable() {
|
|
|
+ @Override
|
|
|
+ public void run() {
|
|
|
+ timeHandler.sendEmptyMessage(1000);
|
|
|
+ }
|
|
|
+ }, 10, 10, TimeUnit.SECONDS);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void attach() {
|
|
|
+ if (getVisibility() == GONE) {
|
|
|
+ invokeDisplayTimerWork();
|
|
|
+ ballView.setVisibility(VISIBLE);
|
|
|
+ frameLayout.setVisibility(VISIBLE);
|
|
|
+ rootLinearLayout.setVisibility(VISIBLE);
|
|
|
+ setVisibility(VISIBLE);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void detach() {
|
|
|
+ if (getVisibility() == VISIBLE) {
|
|
|
+ if (hasShowContent) {
|
|
|
+ hasShowContent = false;
|
|
|
+ menu.dismiss();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (displayWorker != null) {
|
|
|
+ displayWorker.cancel();
|
|
|
+ }
|
|
|
+ ballView.setVisibility(GONE);
|
|
|
+ frameLayout.setVisibility(GONE);
|
|
|
+ rootLinearLayout.setVisibility(GONE);
|
|
|
+ setVisibility(GONE);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void release() {
|
|
|
+ if (displayWorker != null) {
|
|
|
+ displayWorker.cancel();
|
|
|
+ displayWorker = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ frameLayout.removeAllViews();
|
|
|
+ rootLinearLayout.removeAllViews();
|
|
|
+ removeAllViews();
|
|
|
+ wm.removeView(this);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void handleFullScreenModel() {
|
|
|
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
|
+ try {
|
|
|
+ Field mLayoutInScreen = PopupWindow.class.getDeclaredField("mLayoutInScreen");
|
|
|
+ mLayoutInScreen.setAccessible(true);
|
|
|
+ mLayoutInScreen.set(menu, true);
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private int getStatusBarHeight(Context context) {
|
|
|
+ int id = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
|
|
|
+ return context.getResources().getDimensionPixelSize(id);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 适配小米Android O设备 判断是否是刘海屏
|
|
|
+ */
|
|
|
+ private boolean isNotch() {
|
|
|
+ try {
|
|
|
+ Method method = Class.forName("android.os.SystemProperties").getMethod("getInt", String.class, Integer.class);
|
|
|
+ return ((int) method.invoke(null, "ro.miui.notch", 0)) == 1;
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断是否为挖孔屏幕
|
|
|
+ */
|
|
|
+ private boolean isDisplayCutout(Activity activity) {
|
|
|
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
|
+ try {
|
|
|
+ return activity.getWindow().getDecorView().getRootWindowInsets().getDisplayCutout() != null;
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean onTouch(View v, MotionEvent event) {
|
|
|
+ if (displayWorker != null) {
|
|
|
+ displayWorker.cancel();
|
|
|
+ }
|
|
|
+ float x = event.getRawX();
|
|
|
+ float y = event.getRawY();
|
|
|
+
|
|
|
+ switch (event.getAction()) {
|
|
|
+ case MotionEvent.ACTION_DOWN:
|
|
|
+ isMove = false;
|
|
|
+ mTouchStartX = event.getX();
|
|
|
+ mTouchStartY = event.getY();
|
|
|
+ break;
|
|
|
+ case MotionEvent.ACTION_MOVE:
|
|
|
+ float moveStartX = event.getX();
|
|
|
+ float moveStartY = event.getY();
|
|
|
+ // 移动量大于3才判定移动
|
|
|
+ if (Math.abs(mTouchStartX - moveStartX) > 3 && Math.abs(mTouchStartY - moveStartY) > 3) {
|
|
|
+ wlp.x = (int) (x - mTouchStartX);
|
|
|
+ wlp.y = (int) (y - mTouchStartY);
|
|
|
+ // 移动时收起菜单
|
|
|
+ wm.updateViewLayout(this, wlp);
|
|
|
+ isMove = true;
|
|
|
+ if (menu.isShowing()) {
|
|
|
+ menu.dismiss();
|
|
|
+ hasShowContent = false;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case MotionEvent.ACTION_UP:
|
|
|
+ // 大于屏幕宽度一半是吸附在那一边
|
|
|
+ v.performClick();
|
|
|
+ if (wlp.x <= DensityUtils.getHeigthAndWidth(activity)[0] / 2) {
|
|
|
+ wlp.x = 0;
|
|
|
+ isLeftLocation = true;
|
|
|
+ } else {
|
|
|
+ wlp.x = DensityUtils.getHeigthAndWidth(activity)[0];
|
|
|
+ isLeftLocation = false;
|
|
|
+ }
|
|
|
+ wm.updateViewLayout(this, wlp);
|
|
|
+ if (isMove) {
|
|
|
+ invokeDisplayTimerWork();
|
|
|
+ } else {
|
|
|
+ invokeMenuShowOrDismiss();
|
|
|
+ }
|
|
|
+ // 还原贴边的浮球
|
|
|
+// invokeBallFullOrHalt(false);
|
|
|
+ callback.onUpdateBallView(ballView, isLeftLocation, false);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ public interface IFloatingBallCallback {
|
|
|
+
|
|
|
+ void onUpdateBallView(ImageView ballView, boolean isLeftLocation, boolean isHide);
|
|
|
+
|
|
|
+ ArrayList<FloatingBallMenu.Item> onInitMenuData();
|
|
|
+
|
|
|
+ void onMenuItemClick(FloatingBallMenu.Item item, int pos);
|
|
|
+
|
|
|
+ void onExpandMenu(boolean hasRedDot);
|
|
|
+
|
|
|
+ void onDismissMenu();
|
|
|
+ }
|
|
|
+}
|