安卓方案類-應用內懸浮窗適配方案實戰

做者

你們好,我叫小鑫,也能夠叫我蠟筆小鑫😊;java

本人17年畢業於中山大學,於2018年7月加入37手遊安卓團隊,曾經就任於久邦數碼擔任安卓開發工程師;android

目前是37手遊安卓團隊的海外負責人,負責相關業務開發;同時兼顧一些基礎建設相關工做。windows

背景

遊戲內的懸浮窗一般狀況下只出如今遊戲內,用作切換帳號、客服中心等功能的快速入口。本文將介紹幾種實現方案,以及咱們踩過的坑markdown

一、方案一:應用外懸浮窗+棧頂權限/生命週期回調

一般實現懸浮窗,首先考慮到的會是要使用懸浮窗權限,用WindowManager在設備界面上addView實現(UI層級較高,應用外顯示)app

一、彈出懸浮窗須要用到懸浮窗權限ide

<!--懸浮窗權限-->
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
複製代碼

二、判斷懸浮窗遊戲內外顯示動畫

方式一:使用棧頂權限獲取當前ui

//須要聲明權限
<uses-permission android:name="android.permission.GET_TASKS" />

//判斷當前是否在後臺
private boolean isAppIsInBackground(Context context) {
		boolean isInBackground = true;
		ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
		if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT_WATCH) {
			List<ActivityManager.RunningAppProcessInfo> runningProcesses = am.getRunningAppProcesses();
			for (ActivityManager.RunningAppProcessInfo processInfo : runningProcesses) {
				//前臺程序
				if (processInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
					for (String activeProcess : processInfo.pkgList) {
						if (activeProcess.equals(context.getPackageName())) {
							isInBackground = false;
						}
					}
				}
			}
		} else {
			List<ActivityManager.RunningTaskInfo> taskInfo = am.getRunningTasks(1);
			ComponentName componentInfo = taskInfo.get(0).topActivity;
			if (componentInfo.getPackageName().equals(context.getPackageName())) {
				isInBackground = false;
			}
		}

		return isInBackground;
	}
複製代碼

這裏考慮到這種方案網上有不少具體案例,在這裏就不實現了。可是這種方案有以下缺點:this

一、適配問題,懸浮窗權限在不一樣設備上因爲不一樣產商實現不一樣,適配難。spa

二、向用戶申請權限,打開率較低,體驗較差

二、方案二:addContentView實現

原理:Activity的接口中除了咱們經常使用的setContentView接口外,還有addContentView接口。利用該接口能夠在Activity上添加View。

這裏你可能會問:

一、那隻能在一個Activity上添加吧?

沒錯,是隻能在當前Activity上添加,可是因爲遊戲一般也就在一個Activity跑,所以基本上是能夠接受的。

二、只add一個view,那拖動怎麼實現?

LayoutParams params = new LayoutParams(mWidth, mHeight);
params.setMargins(mLeft, mTop, 0, 0);
setLayoutParams(params);
複製代碼

經過更新LayoutParams調整子View在父View中的位置就能實現

具體代碼以下:

/** * @author zhuxiaoxin * 可拖拽貼邊的view */
public class DragViewLayout extends RelativeLayout {

    //手指拖拽獲得的位置
    int mLeft, mRight, mTop, mBottom;

    //view所在的位置
    int mLastX, mLastY;

    /** * 屏幕寬度|高度 */
    int mScreenWidth, mScreenHeight;

    /** * view的寬度|高度 */
    int mWidth, mHeight;


    /** * 是否在拖拽過程當中 */
    boolean isDrag = false;

    /** * 系統最小滑動距離 * @param context */
    int mTouchSlop = 0;

    public DragViewLayout(Context context) {
        this(context, null);
    }

    public DragViewLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DragViewLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mScreenWidth = context.getResources().getDisplayMetrics().widthPixels;
        mScreenHeight = context.getResources().getDisplayMetrics().heightPixels;
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLeft = getLeft();
                mRight = getRight();
                mTop = getTop();
                mBottom = getBottom();
                mLastX = (int) event.getRawX();
                mLastY = (int) event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                int x = (int) event.getRawX();
                int y = (int) event.getRawY();
                int dx = x - mLastX;
                int dy = y - mLastY;
                if (Math.abs(dx) > mTouchSlop) {
                    isDrag = true;
                }
                mLeft += dx;
                mRight += dx;
                mTop += dy;
                mBottom += dy;
                if (mLeft < 0) {
                    mLeft = 0;
                    mRight = mWidth;
                }
                if (mRight >= mScreenWidth) {
                    mRight = mScreenWidth;
                    mLeft = mScreenWidth - mWidth;
                }
                if (mTop < 0) {
                    mTop = 0;
                    mBottom = getHeight();
                }
                if (mBottom > mScreenHeight) {
                    mBottom = mScreenHeight;
                    mTop = mScreenHeight - mHeight;
                }
                mLastX = x;
                mLastY = y;
                //根據拖動舉例設置view的margin參數,實現拖動效果
                LayoutParams params = new LayoutParams(mWidth, mHeight);
                params.setMargins(mLeft, mTop, 0, 0);
                setLayoutParams(params);
                break;
            case MotionEvent.ACTION_UP:
                //手指擡起,執行貼邊動畫
                if (isDrag) {
                    startAnim();
                    isDrag = false;
                }
                break;
        }
        return super.dispatchTouchEvent(event);
    }

    //執行貼邊動畫
    private void startAnim(){
        ValueAnimator valueAnimator;
        if (mLeft < mScreenWidth / 2) {
            valueAnimator = ValueAnimator.ofInt(mLeft, 0);
        } else {
            valueAnimator = ValueAnimator.ofInt(mLeft, mScreenWidth - mWidth);
        }
        //動畫執行時間
        valueAnimator.setDuration(100);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mLeft = (int)animation.getAnimatedValue();
                //動畫執行依然是使用設置margin參數實現
                LayoutParams params = new LayoutParams(mWidth, mHeight);
                params.setMargins(mLeft, getTop(), 0, 0);
                setLayoutParams(params);
            }
        });
        valueAnimator.start();
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (mWidth == 0) {
            //獲取view的高寬
            mWidth = getWidth();
            mHeight = getHeight();
        }
    }

}
複製代碼
/** * @author zhuxiaoxin * 37懸浮窗基礎view */
public class SqAddFloatView extends DragViewLayout {

    private RelativeLayout mFloatContainer;

    public SqAddFloatView(final Context context, final int floatImgId) {
        super(context);
        setClickable(true);
        final ImageView floatView = new ImageView(context);
        floatView.setImageResource(floatImgId);
        floatView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(context, "點擊了懸浮球", Toast.LENGTH_SHORT).show();
            }
        });
        LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        addView(floatView, params);
    }

    public void show(Activity activity) {
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
        if(mFloatContainer == null) {
            mFloatContainer = new RelativeLayout(activity);
        }
        RelativeLayout.LayoutParams floatViewParams = new RelativeLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT);
        floatViewParams.setMargins(0, (int) (mScreenHeight * 0.4), 0, 0);
        mFloatContainer.addView(this, floatViewParams);
        activity.addContentView(mFloatContainer, params);

    }
}
複製代碼

在Activity中使用

SqAddFloatView(this, R.mipmap.ic_launcher).show(this)
複製代碼

三、方案三:WindowManager+應用內層級實現

WindowManger中的層級有以下兩個(實際上是同樣的~)能夠實如今Activity上增長View

/** * Start of types of sub-windows. The {@link #token} of these windows * must be set to the window they are attached to. These types of * windows are kept next to their attached window in Z-order, and their * coordinate space is relative to their attached window. */
        public static final int FIRST_SUB_WINDOW = 1000;

        /** * Window type: a panel on top of an application window. These windows * appear on top of their attached window. */
        public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;
複製代碼

具體實現時,WindowManger相關的核心代碼以下:

public void show() {
        floatLayoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT, 
//最最重要的一句 WindowManager.LayoutParams.FIRST_SUB_WINDOW,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                        | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
                PixelFormat.RGBA_8888);
        floatLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
        floatLayoutParams.x = mMinWidth;
        floatLayoutParams.y = (int)(mScreenHeight * 0.4);
        mWindowManager.addView(this, floatLayoutParams);
    }
複製代碼

添加完view如何更新位置?

使用WindowManager的updateViewLayout方法

mWindowManager.updateViewLayout(DragViewLayout.this, floatLayoutParams);
複製代碼

完整代碼以下:

DragViewLayout:

public class DragViewLayout extends RelativeLayout {

    //view所在位置
    int mLastX, mLastY;

    //屏幕高寬
    int mScreenWidth, mScreenHeight;

    //view高寬
    int mWidth, mHeight;

    /** * 是否在拖拽過程當中 */
    boolean isDrag = false;

    /** * 系統最小滑動距離 * @param context */
    int mTouchSlop = 0;

    WindowManager.LayoutParams floatLayoutParams;
    WindowManager mWindowManager;

    //手指觸摸位置
    private float xInScreen;
    private float yInScreen;
    private float xInView;
    public float yInView;


    public DragViewLayout(Context context) {
        this(context, null);
    }

    public DragViewLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DragViewLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mScreenWidth = context.getResources().getDisplayMetrics().widthPixels;
        mScreenHeight = context.getResources().getDisplayMetrics().heightPixels;
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mWindowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastX = (int) event.getRawX();
                mLastY = (int) event.getRawY();
                yInView = event.getY();
                xInView = event.getX();
                xInScreen = event.getRawX();
                yInScreen = event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                int dx = (int) event.getRawX() - mLastX;
                int dy = (int) event.getRawY() - mLastY;
                if (Math.abs(dx) > mTouchSlop || Math.abs(dy) > mTouchSlop) {
                    isDrag = true;
                }
                xInScreen = event.getRawX();
                yInScreen = event.getRawY();
                mLastX = (int) event.getRawX();
                mLastY = (int) event.getRawY();
                //拖拽時調用WindowManager updateViewLayout更新懸浮球位置
                updateFloatPosition(false);
                break;
            case MotionEvent.ACTION_UP:
                if (isDrag) {
                    //執行貼邊
                    startAnim();
                    isDrag = false;
                }
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(event);
    }

    //更新懸浮球位置
    private void updateFloatPosition(boolean isUp) {
        int x = (int) (xInScreen - xInView);
        int y = (int) (yInScreen - yInView);
        if(isUp) {
            x = isRightFloat() ? mScreenWidth : 0;
        }
        if(y < 0) {
            y = 0;
        }
        if(y > mScreenHeight - mHeight) {
            y = mScreenHeight - mHeight;
        }
        floatLayoutParams.x = x;
        floatLayoutParams.y = y;
        //更新位置
        mWindowManager.updateViewLayout(this, floatLayoutParams);
    }

    /** * 是否靠右邊懸浮 * @return */
    boolean isRightFloat() {
        return xInScreen > mScreenWidth / 2;
    }


    //執行貼邊動畫
    private void startAnim(){
        ValueAnimator valueAnimator;
        if (floatLayoutParams.x < mScreenWidth / 2) {
            valueAnimator = ValueAnimator.ofInt(floatLayoutParams.x, 0);
        } else {
            valueAnimator = ValueAnimator.ofInt(floatLayoutParams.x, mScreenWidth - mWidth);
        }
        valueAnimator.setDuration(200);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                floatLayoutParams.x = (int)animation.getAnimatedValue();
                mWindowManager.updateViewLayout(DragViewLayout.this, floatLayoutParams);
            }
        });
        valueAnimator.start();
    }

    //懸浮球顯示
    public void show() {
        floatLayoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                        | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
                PixelFormat.RGBA_8888);
        floatLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
        floatLayoutParams.x = 0;
        floatLayoutParams.y = (int)(mScreenHeight * 0.4);
        mWindowManager.addView(this, floatLayoutParams);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (mWidth == 0) {
            //獲取懸浮球高寬
            mWidth = getWidth();
            mHeight = getHeight();
        }
    }
}
複製代碼

懸浮窗View

public class SqWindowManagerFloatView extends DragViewLayout {


    public SqWindowManagerFloatView(final Context context, final int floatImgId) {
        super(context);
        setClickable(true);
        final ImageView floatView = new ImageView(context);
        floatView.setImageResource(floatImgId);
        floatView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(context, "點擊了懸浮球", Toast.LENGTH_SHORT).show();
            }
        });
        LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        addView(floatView, params);
    }
}
複製代碼

使用:

SqWindowManagerFloatView(this, R.mipmap.float_icon).show()
複製代碼

四、小結

一、方案一須要用到多個權限,顯然是不合適的。

二、方案二簡單方便,可是用到了Activity的addContentView方法,在某些遊戲引擎上使用會有問題。由於有些遊戲引擎不是在Activity上跑的,而是在NativeActivity上跑

三、方案三是咱們當前採用的方案,目前還暫未發現有顯示不出來之類的問題~

四、本文講述的方案只是Demo哈,實際使用還須要考慮劉海屏的問題,本文暫未涉及

相關文章
相關標籤/搜索