转-浮动窗口Floating Window 详解

 

 

一,原理

1,全局悬浮

floating view可以悬浮在应用的各个页面。floating view是放在一个单独的window中。 对于每个app而言,它所在的window在floating view所在的window之下,这样,就可以悬浮在其至上。window可以设置相应的层级。比如,通知栏,就是在一个级别很高的window中。如果 想要清晰的看清楚相应的结构,可以通过hierarchyviewer的工具,看view的层级关系。

2,全局移动

通过1,全局移动的关键其实是更改window的位置。关键是坐标的计算。一个坐标是绝对坐标,一个相对坐标。参看示意图,理解以下的计算公式即可。

1
2
3
4
5
privatevoidupdateViewPosition() {
        mWindowLayoutParams.x = (int) (mRawX - mTouchStartX);
        mWindowLayoutParams.y = (int) (mRawY - mTouchStartY);
        mWindowManager.updateViewLayout(this, mWindowLayoutParams);
    }

3,运行状态监控。

floating view所在的显示层级高于app,所以如何控制其显示或者消失,比如类似360手机助手的显示机制。我们关注最多的有四种状态:

3-1 home(通过所有的launcher相应的包名筛选)

2-2 应用内 (通过指定包名)

3-3 应用某个特定页面 (通过指定的完整类名)

3-3 其他状态(其他应用,etc)(排除之后,剩下的情况)

所以,其实对于状态的监控,就是对当前运行的task进行检测判断的过程。相应的代码如下,很清晰,参照文档看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
privatebooleanisInner() {
        booleanisInner = false;
        ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        List<RunningTaskInfo> info = manager
                .getRunningTasks(Integer.MAX_VALUE);
        String pkgName = info.get(0).topActivity.getPackageName();
        isInner = pkgName.startsWith(PKG_NAME_BASE);
        Log.d(TAG, "isInner() isInner = "+ isInner);
        returnisInner;
    }
    privatebooleanisHome() {
        booleanisHome = false;
        ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        List<RunningTaskInfo> info = manager
                .getRunningTasks(Integer.MAX_VALUE);
        isHome = homeLists.contains(info.get(0).topActivity.getPackageName());
        Log.d(TAG, "isHome() isHome = "+ isHome);
        returnisHome;
    }
    privateList<String> getHomes() {
        List<String> packages = newArrayList<String>();
        PackageManager packageManager = mServices.getPackageManager();
        Intent intent = newIntent(Intent.ACTION_MAIN);
        intent.addCategory(Intent.CATEGORY_HOME);
        List<ResolveInfo> resolveInfo = packageManager
                .queryIntentActivities(intent,
                        PackageManager.MATCH_DEFAULT_ONLY);
        for(ResolveInfo info : resolveInfo) {
            packages.add(info.activityInfo.packageName);
        }
        returnpackages;
    }

二,注意事项

此部分,将列举出开发中所遇到的问题。

2-1 window的显示大小

假使window中添加的view为mContentView,影响window大小的最终参数只有:

1
2
mWindowLayoutParams.width = LinearLayout.LayoutParams.FILL_PARENT;
mWindowLayoutParams.height = LinearLayout.LayoutParams.WRAP_CONTENT;

2-2 window的显示位置

定了坐标系之后,以下两个参数决定window的显示位置(以mContentView左上角为准)

1
2
mWindowLayoutParams.x = 0;
mWindowLayoutParams.y = mScreenHeight;// at the position of bottom

2-3 TouchEvent 和ClickEvent的冲突处理

核心是定义touch事件,滑动超过指定值时,才被识别为touch事件,否则则识别为click事件。这里需要深入的理解touch事件。

参看之前的日志。《Android Touch事件分析》

http://mikewang.blog.51cto.com/3826268/1204944

2-4 Service导致的Asynctask不能执行的问题。

在网上没有找到真正的原因,但是找到了解决方案。api level 11之后,Asynctask的默认模式从并行改为串行。即默认情况下,如果前一个task没有执行完,后一个task将会被阻塞。

可以通过手动设置Asynctask的模式来解决这个问题。

1
2
3
4
5
6
LoginAsyncTask task = newLoginAsyncTask(AccountManagementActivity.this);
                if(Utils.isHoneycombOrHigher()) {
                    task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, sb.toString());
                } else{
                    task.execute(sb.toString());
                }

三,实例

3-1 例子所涵盖的内容:

1
2
3
4
5
6
This demo will finish the functions as followings:
1, display the global floating view on the top of the screen.
2, switchthe floating state(visible or invisible) when the user toggle from two of three state (app inner, other app, home).
3, dynamically change the floating view's size or position
4, let the floating view automatically on the edge of the screen (n/a)
5, handle the conflict between touch event and click event

3-2 demo 源码

github地址:https://github.com/mikewang0326/FloatingViewDemo

四,其他

4-1 开源项目

当自己参考网上的代码完成之后,发现xda上开源项目。但是花时间了解还是值得。以后如果再要做floating window相应的东西,可以直接使用这个开源库。

xda地址:http://forum.xda-developers.com/showthread.php?t=1688531

对应的介绍都有,源码在github上,从上述连接上都可以找到。

4-2 参考资料

主要参考了krislq的相关资料,很有帮助。附件给出文档。

本文出自 “小新专栏” 博客,请务必保留此出处http://mikewang.blog.51cto.com/3826268/1250706

 

 

Android悬浮窗实现 使用WindowManager

 

WindowManager介绍

通过Context.getSystemService(Context.WINDOW_SERVICE)可以获得 WindowManager对象。

每一个WindowManager对象都和一个特定的 Display绑定。

想要获取一个不同的display的WindowManager,可以用 createDisplayContext(Display)来获取那个display的 Context,之后再使用:

Context.getSystemService(Context.WINDOW_SERVICE)来获取WindowManager。

 

使用WindowManager可以在其他应用最上层,甚至手机桌面最上层显示窗口。

调用的是WindowManager继承自基类的addView方法和removeView方法来显示和隐藏窗口。具体见后面的实例。

 

另:API 17推出了Presentation,它将自动获取display的Context和WindowManager,可以方便地在另一个display上显示窗口。

 

WindowManager实现悬浮窗例子

声明权限

首先在manifest中添加如下权限:

<!-- 显示顶层浮窗 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

注意:在MIUI上需要在设置中打开本应用的”显示悬浮窗”开关,并且重启应用,否则悬浮窗只能显示在本应用界面内,不能显示在手机桌面上。

 

服务获取和基本参数设置

        // 获取应用的Context
        mContext = context.getApplicationContext();
        // 获取WindowManager
        mWindowManager = (WindowManager) mContext
                .getSystemService(Context.WINDOW_SERVICE);

参数设置:

复制代码
        final WindowManager.LayoutParams params = new WindowManager.LayoutParams();

        // 类型
        params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;

        // WindowManager.LayoutParams.TYPE_SYSTEM_ALERT

        // 设置flag

        int flags = WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
        // | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        // 如果设置了WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,弹出的View收不到Back键的事件
        params.flags = flags;
        // 不设置这个弹出框的透明遮罩显示为黑色
        params.format = PixelFormat.TRANSLUCENT;
        // FLAG_NOT_TOUCH_MODAL不阻塞事件传递到后面的窗口
        // 设置 FLAG_NOT_FOCUSABLE 悬浮窗口较小时,后面的应用图标由不可长按变为可长按
        // 不设置这个flag的话,home页的划屏会有问题

        params.width = LayoutParams.MATCH_PARENT;
        params.height = LayoutParams.MATCH_PARENT;

        params.gravity = Gravity.CENTER;
复制代码

 

点击和按键事件

除了View中的各个控件的点击事件之外,弹窗View的消失控制需要一些处理。

点击弹窗外部可隐藏弹窗的效果,首先,悬浮窗是全屏的,只不过最外层的是透明或者半透明的:

布局如下:

复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center"
    android:background="@color/darken_background"
    android:gravity="center"
    android:orientation="vertical" >

    <RelativeLayout
        android:id="@+id/popup_window"
        android:layout_width="@dimen/dialog_window_width"
        android:layout_height="@dimen/dialog_window_height"
        android:background="@color/white"
        android:orientation="vertical" >

        <TextView
            android:id="@+id/title"
            android:layout_width="match_parent"
            android:layout_height="@dimen/dialog_title_height"
            android:gravity="center"
            android:text="@string/default_title"
            android:textColor="@color/dialog_title_text_color"
            android:textSize="@dimen/dialog_title_text_size" />

        <View
            android:id="@+id/title_divider"
            android:layout_width="match_parent"
            android:layout_height="2dp"
            android:layout_below="@id/title"
            android:background="@drawable/dialog_title_divider" />

        <TextView
            android:id="@+id/content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/title_divider"
            android:gravity="center"
            android:padding="@dimen/dialog_content_padding_side"
            android:text="@string/default_content"
            android:textColor="@color/dialog_content_text_color"
            android:textSize="@dimen/dialog_content_text_size" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:orientation="horizontal"
            android:paddingBottom="@dimen/dialog_content_padding_bottom"
            android:paddingLeft="@dimen/dialog_content_padding_side"
            android:paddingRight="@dimen/dialog_content_padding_side" >

            <Button
                android:id="@+id/negativeBtn"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:background="@drawable/promote_window_negative_btn_selector"
                android:focusable="true"
                android:padding="@dimen/dialog_button_padding"
                android:text="@string/default_btn_cancel"
                android:textColor="@color/dialog_negative_btn_text_color"
                android:textSize="@dimen/dialog_button_text_size" />

            <Button
                android:id="@+id/positiveBtn"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="18dp"
                android:layout_weight="1"
                android:background="@drawable/promote_window_positive_btn_selector"
                android:focusable="true"
                android:padding="@dimen/dialog_button_padding"
                android:text="@string/default_btn_ok"
                android:textColor="@color/dialog_positive_btn_text_color"
                android:textSize="@dimen/dialog_button_text_size" />
        </LinearLayout>
    </RelativeLayout>

</LinearLayout>
复制代码

 

点击外部可消除设置:

复制代码
        // 点击窗口外部区域可消除
        // 这点的实现主要将悬浮窗设置为全屏大小,外层有个透明背景,中间一部分视为内容区域
        // 所以点击内容区域外部视为点击悬浮窗外部
        final View popupWindowView = view.findViewById(R.id.popup_window);// 非透明的内容区域

        view.setOnTouchListener(new OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {

                LogUtil.i(LOG_TAG, "onTouch");
                int x = (int) event.getX();
                int y = (int) event.getY();
                Rect rect = new Rect();
                popupWindowView.getGlobalVisibleRect(rect);
                if (!rect.contains(x, y)) {
                    WindowUtils.hidePopupWindow();
                }

                LogUtil.i(LOG_TAG, "onTouch : " + x + ", " + y + ", rect: "
                        + rect);
                return false;
            }
        });
复制代码

 

点击Back键可隐藏弹窗:

注意Flag不能设置WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE。

复制代码
        // 点击back键可消除
        view.setOnKeyListener(new OnKeyListener() {

            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                switch (keyCode) {
                case KeyEvent.KEYCODE_BACK:
                    WindowUtils.hidePopupWindow();
                    return true;
                default:
                    return false;
                }
            }
        });
复制代码

 

完整效果

完整代码:

复制代码
package com.example.hellowindow;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

public class MainActivity extends Activity {

    private Handler mHandler = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mHandler = new Handler();

        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {

                mHandler.postDelayed(new Runnable() {

                    @Override
                    public void run() {
                        WindowUtils.showPopupWindow(MainActivity.this);

                    }
                }, 1000 * 3);

            }
        });
    }
}
复制代码

复制代码
package com.example.hellowindow;

import android.content.Context;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnKeyListener;
import android.view.View.OnTouchListener;
import android.view.WindowManager;
import android.view.View.OnClickListener;
import android.view.WindowManager.LayoutParams;
import android.widget.Button;

/**
 * 弹窗辅助类
 *
 * @ClassName WindowUtils
 *
 *
 */
public class WindowUtils {

    private static final String LOG_TAG = "WindowUtils";
    private static View mView = null;
    private static WindowManager mWindowManager = null;
    private static Context mContext = null;

    public static Boolean isShown = false;

    /**
     * 显示弹出框
     *
     * @param context
     * @param view
     */
    public static void showPopupWindow(final Context context) {
        if (isShown) {
            LogUtil.i(LOG_TAG, "return cause already shown");
            return;
        }

        isShown = true;
        LogUtil.i(LOG_TAG, "showPopupWindow");

        // 获取应用的Context
        mContext = context.getApplicationContext();
        // 获取WindowManager
        mWindowManager = (WindowManager) mContext
                .getSystemService(Context.WINDOW_SERVICE);

        mView = setUpView(context);

        final WindowManager.LayoutParams params = new WindowManager.LayoutParams();

        // 类型
        params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;

        // WindowManager.LayoutParams.TYPE_SYSTEM_ALERT

        // 设置flag

        int flags = WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
        // | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        // 如果设置了WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,弹出的View收不到Back键的事件
        params.flags = flags;
        // 不设置这个弹出框的透明遮罩显示为黑色
        params.format = PixelFormat.TRANSLUCENT;
        // FLAG_NOT_TOUCH_MODAL不阻塞事件传递到后面的窗口
        // 设置 FLAG_NOT_FOCUSABLE 悬浮窗口较小时,后面的应用图标由不可长按变为可长按
        // 不设置这个flag的话,home页的划屏会有问题

        params.width = LayoutParams.MATCH_PARENT;
        params.height = LayoutParams.MATCH_PARENT;

        params.gravity = Gravity.CENTER;

        mWindowManager.addView(mView, params);

        LogUtil.i(LOG_TAG, "add view");

    }

    /**
     * 隐藏弹出框
     */
    public static void hidePopupWindow() {
        LogUtil.i(LOG_TAG, "hide " + isShown + ", " + mView);
        if (isShown && null != mView) {
            LogUtil.i(LOG_TAG, "hidePopupWindow");
            mWindowManager.removeView(mView);
            isShown = false;
        }

    }

    private static View setUpView(final Context context) {

        LogUtil.i(LOG_TAG, "setUp view");

        View view = LayoutInflater.from(context).inflate(R.layout.popupwindow,
                null);
        Button positiveBtn = (Button) view.findViewById(R.id.positiveBtn);
        positiveBtn.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {

                LogUtil.i(LOG_TAG, "ok on click");
                // 打开安装包
                // 隐藏弹窗
                WindowUtils.hidePopupWindow();

            }
        });

        Button negativeBtn = (Button) view.findViewById(R.id.negativeBtn);
        negativeBtn.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                LogUtil.i(LOG_TAG, "cancel on click");
                WindowUtils.hidePopupWindow();

            }
        });

        // 点击窗口外部区域可消除
        // 这点的实现主要将悬浮窗设置为全屏大小,外层有个透明背景,中间一部分视为内容区域
        // 所以点击内容区域外部视为点击悬浮窗外部
        final View popupWindowView = view.findViewById(R.id.popup_window);// 非透明的内容区域

        view.setOnTouchListener(new OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {

                LogUtil.i(LOG_TAG, "onTouch");
                int x = (int) event.getX();
                int y = (int) event.getY();
                Rect rect = new Rect();
                popupWindowView.getGlobalVisibleRect(rect);
                if (!rect.contains(x, y)) {
                    WindowUtils.hidePopupWindow();
                }

                LogUtil.i(LOG_TAG, "onTouch : " + x + ", " + y + ", rect: "
                        + rect);
                return false;
            }
        });

        // 点击back键可消除
        view.setOnKeyListener(new OnKeyListener() {

            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                switch (keyCode) {
                case KeyEvent.KEYCODE_BACK:
                    WindowUtils.hidePopupWindow();
                    return true;
                default:
                    return false;
                }
            }
        });

        return view;

    }
}
复制代码

 

 

 

参考资料

WindowManager:

http://developer.android.com/reference/android/view/WindowManager.html

 

参考实例:

http://blog.csdn.net/deng0zhaotai/article/details/16827719

http://www.xsmile.net/?p=538

http://blog.csdn.net/guolin_blog/article/details/8689140

 

简单说明:

Android之Window、WindowManager与窗口管理:

http://blog.csdn.net/xieqibao/article/details/6567814

 

Android系统服务-WindowManager:

http://blog.csdn.net/chenyafei617/article/details/6577940

 

进一步的学习:

老罗的Android之旅:

Android Activity的窗口对象Window的创建过程分析:

http://blog.csdn.net/luoshengyang/article/details/8223770

窗口管理服务WindowManagerService的简要介绍和学习计划:

http://blog.csdn.net/luoshengyang/article/details/8462738

 

Android核心分析之窗口管理:

http://blog.csdn.net/maxleng/article/details/5557758

 

分类: Android
标签: Android