Android创建应用全局小窗口

需求描述

类似微信视频、语音时点击返回会形成一个App小窗口浮动在界面上,点击继续是通通话,如下图:


微信小窗口

效果展示

效果演示图

技术分析

其实实现这个功能只需要你细心分析一下就有思路了:首先这个小窗口是浮动在app最上层的视图,其次所有触屏事件需先由该小窗口处理,还有就是小窗口的生命周期和Application也能虽可能不能同生,但是确是可以共死。所以可以在Application中创建一个view添加到WindowManage,这里将视图为view的window的type设置成系统级别的窗口,这样这个window可以在在全局呈现。另外,还需要让这个window可以随手指拖动而滑动,手指释放后会回弹到距离这个释放点最近的屏幕侧边,所以需要重写view 的OnTouch事件。

代码细节实现

  • 创建全局Application,在Application创建的时候初始化一个view,以及一个WindowManager.LayoutParams,并设置get方法,方便外部调用:
private SmallWindowView windowView;
    private WindowManager wm;
    private WindowManager.LayoutParams mLayoutParams;
    public SmallWindowView getWindowView() {
        return windowView;
    }
    public WindowManager getWm() {
        return wm;
    }
    public WindowManager.LayoutParams getmLayoutParams() {
        return mLayoutParams;
    }
    @Override
    public void onCreate() {
        super.onCreate();
        initSmallViewLayout();
    }
    public void initSmallViewLayout() {
        windowView = (SmallWindowView) LayoutInflater.from(this).inflate(R.layout.small_window, null);
        wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
        mLayoutParams = new WindowManager.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
                PixelFormat.TRANSLUCENT);
        mLayoutParams.gravity = Gravity.NO_GRAVITY;
        //使用非CENTER时,可以通过设置XY的值来改变View的位置
        windowView.setWm(wm);
        windowView.setWmParams(mLayoutParams);
    }
  • 编写一个BaseActivity 实现可供子类来显示隐藏窗口的方法:
  private WindowManager wm;
    private SmallWindowView windowView;
    private WindowManager.LayoutParams mLayoutParams;
    private int OVERLAY_PERMISSION_REQ_CODE = 2;
    private boolean isRange = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        wm = ((MyApplication)getApplication()).getWm();
        windowView = ((MyApplication)getApplication()).getWindowView();
        mLayoutParams = ((MyApplication)getApplication()).getmLayoutParams();
    }


    public void alertWindow() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // 7.0 以上需要引导用去设置开启窗口浮动权限
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 8.0 以上type需要设置成这个
                mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
            }
            requestDrawOverLays();
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // 6.0 动态申请
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.SYSTEM_ALERT_WINDOW}, 1);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            if (wm != null && windowView.getWm() == null) {
                wm.addView(windowView, mLayoutParams);
            }
        } else {
            Toast.makeText(this, "权限申请失败", Toast.LENGTH_SHORT).show();
        }
    }


    private int[] location = new int[2]; // 小窗口位置坐标

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            isRange = calcPointRange(event);
        }
        if (isRange) {
            windowView.dispatchTouchEvent(event);
        }
        return super.onTouchEvent(event);
    }

    /**
     *  计算当前点击事件坐标是否在小窗口内
     * @param event
     * @return
     */
    private boolean calcPointRange(MotionEvent event) {
        windowView.getLocationOnScreen(location);
        int width = windowView.getMeasuredWidth();
        int height = windowView.getMeasuredHeight();
        float curX = event.getRawX();
        float curY = event.getRawY();
        if (curX >= location[0] && curX <= location[0] + width && curY >= location[1] && curY <= location[1] + height) {
            return true;
        }
        return false;
    }

    private static final String TAG = "BaseActivity";

    // android 23 以上先引导用户开启这个权限 该权限动态申请不了
    @TargetApi(Build.VERSION_CODES.M)
    public void requestDrawOverLays() {
        if (!Settings.canDrawOverlays(BaseActivity.this)) {
            Toast.makeText(this, "can not DrawOverlays", Toast.LENGTH_SHORT).show();
            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + BaseActivity.this.getPackageName()));
            startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE);
        } else {
            if (wm != null && windowView.getWindowId() == null) {
                wm.addView(windowView, mLayoutParams);
            }
            Toast.makeText(this, "权限已经授予", Toast.LENGTH_SHORT).show();
        }
    }

    @TargetApi(Build.VERSION_CODES.M)
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == OVERLAY_PERMISSION_REQ_CODE) {
            if (!Settings.canDrawOverlays(this)) {
                Toast.makeText(this, "设置权限拒绝", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(this, "设置权限成功", Toast.LENGTH_SHORT).show();
            }
        }
    }
    // 移除window
    public void dismissWindow() {
        if (wm != null && windowView != null && windowView.getWindowId() != null) {
            wm.removeView(windowView);
        }
    }
  • 自定义一个处理Window内滑动事件的ViewGroup
/**
 * Author   : luweicheng on 2018/8/24 11:22
 * E-mail   :1769005961@qq.com
 * GitHub   : https://github.com/luweicheng24
 * function:
 **/

public class SmallWindowView extends LinearLayout {
    private final int screenHeight;
    private final int screenWidth;
    private int statusHeight;
    private float mTouchStartX;
    private float mTouchStartY;
    private float x;
    private float y;

    private WindowManager wm;
    public WindowManager.LayoutParams wmParams;


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

    public WindowManager getWm() {
        return wm;
    }

    public void setWm(WindowManager wm) {
        this.wm = wm;
    }

    public WindowManager.LayoutParams getWmParams() {
        return wmParams;
    }

    public void setWmParams(WindowManager.LayoutParams wmParams) {
        this.wmParams = wmParams;
        this.wmParams.x = screenWidth; // 窗口先贴附在右边
    }

    public SmallWindowView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SmallWindowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        statusHeight = getStatusHeight(context);
        DisplayMetrics dm = getResources().getDisplayMetrics();
        screenHeight = dm.heightPixels;
        screenWidth = dm.widthPixels;

    }


    /**
     * 获得状态栏的高度
     *
     * @param context
     * @return
     */
    public static int getStatusHeight(Context context) {
        int statusHeight = -1;
        try {
            Class clazz = Class.forName("com.android.internal.R$dimen");
            Object object = clazz.newInstance();
            int height = Integer.parseInt(clazz.getField("status_bar_height")
                    .get(object).toString());
            statusHeight = context.getResources().getDimensionPixelSize(height);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return statusHeight;
    }

    boolean isRight = true;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        x = event.getRawX(); // 触摸点相对屏幕的x坐标
        y = event.getRawY() - statusHeight; // 触摸点相对于屏幕的y坐标
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (wmParams.x > 0) {
                    isRight = true;
                }
                if (wmParams.x < 0) {
                    isRight = false;
                }
                mTouchStartX = event.getX();// 触摸点在View内的相对x坐标
                mTouchStartY = event.getY();// 触摸点在View内的相对Y坐标
                Log.i("startP", "startX" + mTouchStartX + "====startY" + mTouchStartY);
                break;

            case MotionEvent.ACTION_MOVE:
                updateViewPosition(); //  跟新window布局参数
                break;
            case MotionEvent.ACTION_UP:
                if (wmParams.x <= 0) {  //窗口贴附在左边
                    wmParams.x = Math.abs(wmParams.x) <= screenWidth / 2 ? -screenWidth : screenWidth;
                } else {  // 窗口贴附在右边
                    wmParams.x = wmParams.x <= screenWidth / 2 ? screenWidth : -screenWidth;
                }

                // wmParams.x = screenWidth;
                wmParams.y = (int) (y - screenHeight / 2);// 跟新y坐标
                wm.updateViewLayout(this, wmParams);
                break;
        }
        return true;
    }
    private void updateViewPosition() {
        wmParams.gravity = Gravity.NO_GRAVITY;
        //更新浮动窗口位置参数
        int dx = (int) (mTouchStartX - x);
        int dy = (int) (y-screenHeight / 2);
        if (isRight) {
            wmParams.x = screenWidth / 2 - dx;
        } else {
            wmParams.x = -dx - screenWidth / 2;
        }
        wmParams.y = dy;
        Log.i("winParams", "x : " + wmParams.x + "y :" + wmParams.y + "  dy :" + dy);
        wm.updateViewLayout(this, wmParams);
        //刷新显示
    }
}

以上就能实现一个应用内小窗口了,这里windowManager的布局参数有坑要踩:

  • 不设置Gravity属性,window的坐标是以屏幕左上角为(0,0)原点,而当第一次接受到触摸事件之后就会以默认原点更改为屏幕中心,Github源码,给个小星星
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,333评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,812评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,016评论 0 246
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,293评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,650评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,788评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,003评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,741评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,462评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,681评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,168评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,528评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,169评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,119评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,902评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,846评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,719评论 2 274

推荐阅读更多精彩内容