android仿苹果悬浮窗(自动停靠、随手指滑动、返回主屏幕)

说明:本人写博客一来是为了方便日后查看项目,二来是希望能够和广大的程序猿相互交流学习,文章布局简单,如有嫌弃,请绕行,如有错误,请指出,谢谢。

实验环境:安卓6.0 魅族手机

悬浮窗主要有以下几个功能:

1、跟随手指的滑动而滑动(也可以用鼠标滑动)
2、在手指弹起的时候,悬浮窗会自动停靠在左右两侧
3、点击悬浮窗按钮可以返回到桌面

MainActivity中添加6.0访问权限

6.0权限问题:Google在6.0时加入权限管理机制,6.0之后,android需要动态获取权限,要使用权限,不仅要在manifest文件中定义,还要在代码中动态获取。点我了解权限问题

manifest中添加权限声明

    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />

MainActivity中代码如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (Settings.canDrawOverlays(MainActivity.this)) {
            Intent intent = new Intent(MainActivity.this, FloatViewService.class);
            Toast.makeText(MainActivity.this, "已开启悬浮窗", Toast.LENGTH_SHORT).show();
            startService(intent);
            finish();
        } else {
            //若没有权限,提示获取.
            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
            intent.setData(Uri.parse("package:" + getPackageName()));
            Toast.makeText(MainActivity.this, "需要取得权限以使用悬浮窗", Toast.LENGTH_SHORT).show();
            startActivity(intent);
            finish();
        }
    }
}

代码说明:如果手机已授予该app使用悬浮窗的功能,界面会自动开启悬浮窗,MainActivity被finish,否则直接跳转到本手机开启悬浮窗权限的界面,亲测有效,比如魅族手机开启权限的界面如下图所示:
亲测可以字节
问题:只有在第一次安装app的时候才会跳转到打开权限的界面,之后打开app则不会跳转,这部分不太理解,有知悉的评论区见。

悬浮窗界面的绘制

Android的窗口是基于WindowManager实现的,它面向的对象一端是屏幕,另一端就是View,比如我们之前使用的setContentView(R.layout.activity_main), 就是将view显示在屏幕上,代码的底层都是经过WindowManager实现的,整个系统只有一个WindowManager。点我了解界面绘制详解

Service实现后台运行

当app没有被关闭时,悬浮窗同样可以运行,这时候就需要Service来实现后台运行。这里可自行百度Service具体实现的过程,本篇不做解释。

跟随手指的滑动而滑动

说明:需要监听手势,所以设置了setOnTouchListener,识别按下、移动、弹起三个动作,移动的过程需要动态获取触摸的坐标,所以首先要在按下的过程中获取按下的坐标,rawX = event.getRawX(); rawY = event.getRawY(),在移动的过程中进行刷新, wmParams.x = wmParams.x - distanceX;wmParams.y = wmParams.y - distanceY。

        // 设置监听浮动窗口的触摸移动
        go_mainhome.setOnTouchListener(new View.OnTouchListener() {

            private float rawX;
            private float rawY;

            @SuppressLint("ClickableViewAccessibility")
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
//                        Log.i("qqq", "onTouch------------------------------ACTION_DOWN: ");
                        mFloatLayout.setAlpha(1.0f);//设置其透明度
                        myCountDownTimer.cancel();//取消计时
                        rawX = event.getRawX();
                        rawY = event.getRawY();
                        break;
                    case MotionEvent.ACTION_MOVE:
//                        Log.i("qqq", "onTouch------------------------------ACTION_MOVE: ");
                        // getRawX是触摸位置相对于屏幕的坐标,getX是相对于按钮的坐标
                        int distanceX = (int) (event.getRawX() - rawX);
                        int distanceY = (int) (event.getRawY() - rawY);
                        //mFloatView.getMeasuredWidth()和mFloatView.getMeasuredHeight()都是100
                        wmParams.x = wmParams.x - distanceX;
                        wmParams.y = wmParams.y - distanceY;
                        // 刷新
                        mWindowManager.updateViewLayout(mFloatLayout, wmParams);
                        rawX = event.getRawX();
                        rawY = event.getRawY();
                        break;
                    case MotionEvent.ACTION_UP:
                        myCountDownTimer.start();//重新开始计时
                        if (wmParams.x < screenWidth / 2) {
                            //在屏幕右侧
                            wmParams.x = 0;
                            wmParams.y = wmParams.y - 0;
                        } else {
                            //在屏幕左侧
                            wmParams.x = screenWidth;
                            wmParams.y = wmParams.y - 0;
                        }
                        mWindowManager.updateViewLayout(mFloatLayout, wmParams);
                        break;
                }
                return false;//此处必须返回false,否则OnClickListener获取不到监听
            }
        });

获取屏幕大小
尝试了好几种获取屏幕大小的代码,此方法亲测有效。

        Display display = mWindowManager.getDefaultDisplay();
        Point point = new Point();
        display.getRealSize(point);
        screenWidth = point.x;
        screenHeight = point.y;
        Log.i("qqq", "screenWidth------: " + screenWidth + "\n" + "screenHeight---" + screenHeight);

停靠功能

说明:当手指滑动到屏幕中央右侧时,比如在图中的A点(x,y),最终悬浮窗将会停靠在图中的B点,A点向右平移到B点,纵坐标不变,横坐标为0,在屏幕左侧同理,可详见代码case MotionEvent.ACTION_UP部分,前提是需要设置 wmParams.gravity = Gravity.RIGHT | Gravity.BOTTOM,可以滑动最下面看详细代码。
在这里插入图片描述

点击悬浮窗按钮可以返回到桌面

其实就是对按钮设置了监听setOnClickListener,点击之后跳转到桌面的main。

Service中的代码

package com.lightingcontour.toucher;

import android.annotation.SuppressLint;
import android.app.Service;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.os.CountDownTimer;
import android.os.IBinder;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.Toast;

public class FloatViewService extends Service {

    private static final String TAG = "FloatViewService";
    // 定义浮动窗口布局
    private LinearLayout mFloatLayout;
    private WindowManager.LayoutParams wmParams;
    // 创建浮动窗口设置布局参数的对象
    private WindowManager mWindowManager;

    private ImageButton go_mainhome;
    private ImageButton go_back;
//    private LinearLayout toucher_layout;

    private int screenHeight;
    private int screenWidth;
    private MyCountDownTimer myCountDownTimer;

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(TAG, "onCreate");

        createFloatView();
        myCountDownTimer = new MyCountDownTimer(2500, 1000); //设置计时2.5s
        myCountDownTimer.start();
    }

    @SuppressWarnings("static-access")
    @SuppressLint("InflateParams")
    private void createFloatView() {
        wmParams = new WindowManager.LayoutParams();
        // 通过getApplication获取的是WindowManagerImpl.CompatModeWrapper
        mWindowManager = (WindowManager) getApplication().getSystemService(getApplication().WINDOW_SERVICE);

        Display display = mWindowManager.getDefaultDisplay();
        Point point = new Point();
        display.getRealSize(point);
        screenWidth = point.x;
        screenHeight = point.y;
        Log.i("qqq", "screenWidth------: " + screenWidth + "\n" + "screenHeight---" + screenHeight);

        // 设置window type
        wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
        // 设置图片格式,效果为背景透明
        wmParams.format = PixelFormat.RGBA_8888;
        // 设置浮动窗口不可聚焦(实现操作除浮动窗口外的其他可见窗口的操作)
        wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        // 调整悬浮窗显示的停靠位置为右侧底部
        wmParams.gravity = Gravity.RIGHT | Gravity.BOTTOM;
        // 以屏幕左上角为原点,设置x、y初始值,相对于gravity
        wmParams.x = 0;
        wmParams.y = 0;
        // 设置悬浮窗口长宽数据
        wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;

        LayoutInflater inflater = LayoutInflater.from(getApplication());
        // 获取浮动窗口视图所在布局
        mFloatLayout = (LinearLayout) inflater.inflate(R.layout.toucherlayout, null);
        // 添加mFloatLayout
        mWindowManager.addView(mFloatLayout, wmParams);
        // 浮动窗口按钮

        go_mainhome = (ImageButton) mFloatLayout.findViewById(R.id.go_mainhome);
        go_back = (ImageButton) mFloatLayout.findViewById(R.id.go_back);

        //UNSPECIFIED是未指定模式
        mFloatLayout.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
                View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));

        // 设置监听浮动窗口的触摸移动
        go_mainhome.setOnTouchListener(new View.OnTouchListener() {

            private float rawX;
            private float rawY;

            @SuppressLint("ClickableViewAccessibility")
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
//                        Log.i("qqq", "onTouch------------------------------ACTION_DOWN: ");
                        mFloatLayout.setAlpha(1.0f);//设置其透明度
                        myCountDownTimer.cancel();//取消计时
                        rawX = event.getRawX();
                        rawY = event.getRawY();
                        break;
                    case MotionEvent.ACTION_MOVE:
//                        Log.i("qqq", "onTouch------------------------------ACTION_MOVE: ");
                        // getRawX是触摸位置相对于屏幕的坐标,getX是相对于按钮的坐标
                        int distanceX = (int) (event.getRawX() - rawX);
                        int distanceY = (int) (event.getRawY() - rawY);
                        //mFloatView.getMeasuredWidth()和mFloatView.getMeasuredHeight()都是100
                        wmParams.x = wmParams.x - distanceX;
                        wmParams.y = wmParams.y - distanceY;
                        // 刷新
                        mWindowManager.updateViewLayout(mFloatLayout, wmParams);
                        rawX = event.getRawX();
                        rawY = event.getRawY();
                        break;
                    case MotionEvent.ACTION_UP:
                        myCountDownTimer.start();//重新开始计时
                        if (wmParams.x < screenWidth / 2) {
                            //在屏幕右侧
                            wmParams.x = 0;
                            wmParams.y = wmParams.y - 0;
                        } else {
                            wmParams.x = screenWidth;
                            wmParams.y = wmParams.y - 0;
                        }
                        mWindowManager.updateViewLayout(mFloatLayout, wmParams);
                        break;
                }
                return false;//此处必须返回false,否则OnClickListener获取不到监听
            }
        });

        // 设置监听浮动窗口的触摸移动
        go_back.setOnTouchListener(new View.OnTouchListener() {

            private float rawX;
            private float rawY;

            @SuppressLint("ClickableViewAccessibility")
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
//                        Log.i("qqq", "onTouch------------------------------ACTION_DOWN: ");
                        mFloatLayout.setAlpha(1.0f);//设置其透明度
                        myCountDownTimer.cancel();//取消计时
                        rawX = event.getRawX();
                        rawY = event.getRawY();
                        break;
                    case MotionEvent.ACTION_MOVE:
//                        Log.i("qqq", "onTouch------------------------------ACTION_MOVE: ");
                        // getRawX是触摸位置相对于屏幕的坐标,getX是相对于按钮的坐标
                        int distanceX = (int) (event.getRawX() - rawX);
                        int distanceY = (int) (event.getRawY() - rawY);
                        //mFloatView.getMeasuredWidth()和mFloatView.getMeasuredHeight()都是100
                        wmParams.x = wmParams.x - distanceX;
                        wmParams.y = wmParams.y - distanceY;
                        // 刷新
                        mWindowManager.updateViewLayout(mFloatLayout, wmParams);
                        rawX = event.getRawX();
                        rawY = event.getRawY();
                        break;
                    case MotionEvent.ACTION_UP:
                        myCountDownTimer.start();//重新开始计时
                        if (wmParams.x < screenWidth / 2) {
                            //在屏幕右侧
                            wmParams.x = 0;
                            wmParams.y = wmParams.y - 0;
                        } else {
                            wmParams.x = screenWidth;
                            wmParams.y = wmParams.y - 0;
                        }
                        mWindowManager.updateViewLayout(mFloatLayout, wmParams);
                        break;
                }
                return false;//此处必须返回false,否则OnClickListener获取不到监听
            }
        });


        go_mainhome.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(FloatViewService.this, "返回到桌面",
                        Toast.LENGTH_SHORT).show();
                Intent intent = new Intent();
                // 为Intent设置Action、Category属性
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                intent.setAction(Intent.ACTION_MAIN);// "android.intent.action.MAIN"
                intent.addCategory(Intent.CATEGORY_HOME); //"android.intent.category.HOME"CATEGORY_HOME  目标Activity是HOME Activity,即手机开机启动后显示的Activity,或按下HOME键后显示的Activity
                startActivity(intent);
            }
        });

        go_back.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {

                Toast.makeText(FloatViewService.this, "返回",
                        Toast.LENGTH_SHORT).show();

            }
        });


    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mFloatLayout != null) {
            // 移除悬浮窗口
            mWindowManager.removeView(mFloatLayout);
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    public class MyCountDownTimer extends CountDownTimer {

        public MyCountDownTimer(long millisInFuture, long countDownInterval) {
            super(millisInFuture, countDownInterval);
        }

        @Override
        public void onTick(long millisUntilFinished) {
        }

        @Override
        public void onFinish() {
            mFloatLayout.setAlpha(0.4f);
        }
    }

}

manifest中的代码

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.lightingcontour.toucher">

    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service android:name=".FloatViewService" />
    </application>

</manifest>

布局中的代码

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


    <LinearLayout
        android:id="@+id/toucher_layout"
        android:orientation="vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <ImageButton
            android:id="@+id/go_mainhome"
            android:layout_marginBottom="10dp"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:background="@drawable/go_mainhome" />

        <ImageButton
            android:id="@+id/go_back"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:background="@drawable/go_back" />

    </LinearLayout>



</LinearLayout>

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值