Android开发笔记(一百一十八)自定义悬浮窗

WindowManager

在前面《Android开发笔记(六十六)自定义对话框》中,我们提到每个页面都是一个Window窗口,许多的Window对象需要一个管家来打理,这个管家我们称之为WindowManager窗口管理。在手机屏幕上新增或删除页面窗口,都可以归结为WindowManager的操作,下面是该管理类的常用方法说明:

getDefaultDisplay : 获取默认的显示屏信息。通常用该方法获取屏幕分辨率,详情参见《Android开发笔记(三)屏幕分辨率》。

addView : 往窗口添加视图。第二个参数为WindowManager.LayoutParams对象。

updateViewLayout : 更新指定视图的布局参数。第二个参数为WindowManager.LayoutParams对象。

removeView : 往窗口移除指定视图。

下面是窗口布局参数WindowManager.LayoutParams的常用属性说明:

format : 窗口的像素点格式。取值见PixelFormat类中的常量定义,一般取值PixelFormat.RGBA_8888。

type : 窗口的显示类型,常用的类型说明如下:

--TYPE_SYSTEM_ALERT : 系统警告提示。

--TYPE_SYSTEM_ERROR : 系统错误提示。

--TYPE_SYSTEM_OVERLAY : 页面顶层提示。

--TYPE_SYSTEM_DIALOG : 系统对话框。

--TYPE_STATUS_BAR : 状态栏

--TYPE_TOAST : 短暂通知Toast

flags : 窗口的行为准则,常用的标志位如下说明(对于悬浮窗来说,一般只需设置FLAG_NOT_FOCUSABLE):

--FLAG_NOT_FOCUSABLE : 不能抢占焦点,即不接受任何按键或按钮事件。

--FLAG_NOT_TOUCHABLE : 不接受触摸屏事件。悬浮窗一般不设置该标志,因为一旦设置该标志,将无法拖动悬浮窗。

--FLAG_NOT_TOUCH_MODAL : 当窗口允许获得焦点时(即没有设置FLAG_NOT_FOCUSALBE标志),仍然将窗口之外的按键事件发送给后面的窗口处理。否则它将独占所有的按键事件,而不管它们是不是发生在窗口范围之内。

-- :

--FLAG_LAYOUT_IN_SCREEN : 允许窗口占满整个屏幕。

--FLAG_LAYOUT_NO_LIMITS : 允许窗口扩展到屏幕之外。

--FLAG_WATCH_OUTSIDE_TOUCH : 如果设置了FLAG_NOT_TOUCH_MODAL标志,则当按键动作发生在窗口之外时,将接收到一个MotionEvent.ACTION_OUTSIDE事件。

alpha : 窗口的透明度,取值为0-1。

gravity : 取值同View的setGravity方法。

x : 窗口左上角的X坐标。

y : 窗口左上角的Y坐标。

width : 窗口的宽度。

height : 窗口的高度。

静态悬浮窗

悬浮窗有点类似对话框,它们都是独立于Activity页面的窗口,但是悬浮窗又有一些与众不同的特性,例如:

1、悬浮窗是可以拖动的,对话框则不能;

2、悬浮窗不妨碍用户触摸窗外的区域,对话框则不让用户框外的控件;

3、悬浮窗独立于Activity页面,即当页面退出后,悬浮窗仍停留在屏幕上;而对话框与Activity页面是共存关系,一旦页面退出则对话框也消失了;

基于悬浮窗的以上特性,我们要实现窗口的悬浮效果,就不仅仅是调用WindowManager的addView方法那么简单了,而是需要做一系列的自定义处理,具体步骤如下:

1、在AndroidManifest.xml中声明系统窗口权限,即增加下面这句:

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

2、在自定义的悬浮窗控件中,要设置触摸监听器,并根据用户的手势滑动来相应调整窗口位置,以实现悬浮窗的拖动功能;

3、合理设置悬浮窗的窗口参数,主要是把窗口参数的显示类型设置为TYPE_SYSTEM_ALERT或者TYPE_SYSTEM_ERROR,另外要设置标志位FLAG_NOT_FOCUSABLE;

4、在构造悬浮窗实例时,要传入Application的上下文Context,这是为了保证即使退出Activity,也不会关闭悬浮窗。因为Application对象在app运行过程中是始终存在着的,而Activity对象只在打开页面时有效,一旦退出页面则Activity的上下文就立刻回收(这会导致依赖于该上下文的悬浮窗也一块被回收了)。

下面是一个静态悬浮窗的效果截图:

下面是自定义悬浮窗的示例代码:

import android.content.Context;
import android.graphics.PixelFormat;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;

public class FloatView extends View {
	private final static String TAG = "FloatView";

	private Context mContext;
	private WindowManager wm;
	private static WindowManager.LayoutParams wmParams;
	public View mContentView;
	private float mRelativeX;
	private float mRelativeY;
	private float mScreenX;
	private float mScreenY;
	private boolean bShow = false;

	public FloatView(Context context) {
		super(context);
		wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
		if (wmParams == null) {
			wmParams = new WindowManager.LayoutParams();
		}
		mContext = context;
	}

	public void setLayout(int layout_id) {
		mContentView = LayoutInflater.from(mContext).inflate(layout_id, null);
		mContentView.setOnTouchListener(new OnTouchListener() {
			public boolean onTouch(View v, MotionEvent event) {
				mScreenX = event.getRawX();
				mScreenY = event.getRawY();
				switch (event.getAction()) {
				case MotionEvent.ACTION_DOWN:
					mRelativeX = event.getX();
					mRelativeY = event.getY();
					break;
				case MotionEvent.ACTION_MOVE:
					updateViewPosition();
					break;
				case MotionEvent.ACTION_UP:
					updateViewPosition();
					mRelativeX = mRelativeY = 0;
					break;
				}
				return true;
			}
		});
	}

	private void updateViewPosition() {
		wmParams.x = (int) (mScreenX - mRelativeX);
		wmParams.y = (int) (mScreenY - mRelativeY);
		wm.updateViewLayout(mContentView, wmParams);
	}

	public void show() {
		if (mContentView != null) {
			wmParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
			wmParams.format = PixelFormat.RGBA_8888;
			wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
			wmParams.alpha = 1.0f;
			wmParams.gravity = Gravity.LEFT | Gravity.TOP;
			wmParams.x = 0;
			wmParams.y = 0;
			wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
			wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
			// 显示自定义悬浮窗口
			wm.addView(mContentView, wmParams);
			bShow = true;
		}
	}

	public void close() {
		if (mContentView != null) {
			wm.removeView(mContentView);
			bShow = false;
		}
	}

	public boolean isShow() {
		return bShow;
	}

}

下面是打开/关闭悬浮窗的页面代码:

import com.example.exmfloat.widget.FloatView;

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

public class StaticActivity extends Activity implements OnClickListener {

	private FloatView mFloatView;

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

		Button btn_static_open = (Button) findViewById(R.id.btn_static_open);
		Button btn_static_close = (Button) findViewById(R.id.btn_static_close);
		btn_static_open.setOnClickListener(this);
		btn_static_close.setOnClickListener(this);

		mFloatView = new FloatView(MainApplication.getInstance());
		mFloatView.setLayout(R.layout.float_static);
	}

	@Override
	public void onClick(View v) {
		if (v.getId() == R.id.btn_static_open) {
			if (mFloatView!=null && mFloatView.isShow()==false) {
				mFloatView.show();
			}
		} else if (v.getId() == R.id.btn_static_close) {
			if (mFloatView!=null && mFloatView.isShow()==true) {
				mFloatView.close();
			}
		}
	}

}

下面是自定义Application的代码例子:

import android.app.Application;

public class MainApplication extends Application {

	private static MainApplication mApp;

	public static MainApplication getInstance() {
		return mApp;
	}

	@Override
	public void onCreate() {
		super.onCreate();
		mApp = this;
	}

}

动态悬浮窗

在实际开发中,悬浮窗的展示内容是变化的,毕竟一个内容不变的悬浮窗对用户来说没什么用处。具体的应用例子有很多,比如说时钟、天气、实时流量、股市指数等等,下面就以实时流量与股市指数两个例子,来详细说明动态悬浮窗的实际应用。

要想实时刷新悬浮窗,这得通过服务Service来实现,所以动态悬浮窗要在Service服务中创建和更新,页面只负责启动/停止服务。对于手机的实时流量,可以通过TrafficStats类的相关方法计算得到,该类的详细说明参见《Android开发笔记(七十九)资源与权限校验》。

下面是实时流量悬浮窗的效果截图:

下面是实时流量悬浮窗的页面代码:

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

import com.example.exmfloat.service.TrafficService;

public class TrafficActivity extends Activity implements OnClickListener {
	private final static String TAG = "TrafficActivity";

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

		Button btn_traffic_open = (Button) findViewById(R.id.btn_traffic_open);
		Button btn_traffic_close = (Button) findViewById(R.id.btn_traffic_close);
		btn_traffic_open.setOnClickListener(this);
		btn_traffic_close.setOnClickListener(this);
	}

	@Override
	public void onClick(View v) {
		if (v.getId() == R.id.btn_traffic_open) {
			Intent intent = new Intent(this, TrafficService.class);
			intent.putExtra("type", TrafficService.OPEN);
			startService(intent);
		} else if (v.getId() == R.id.btn_traffic_close) {
			Intent intent = new Intent(this, TrafficService.class);
			intent.putExtra("type", TrafficService.CLOSE);
			startService(intent);
		}
	}

}

下面是实时流量悬浮窗的服务代码:

import com.example.exmfloat.MainApplication;
import com.example.exmfloat.R;
import com.example.exmfloat.util.FlowUtil;
import com.example.exmfloat.widget.FloatView;

import android.app.Service;
import android.content.Intent;
import android.net.TrafficStats;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;
import android.widget.TextView;

public class TrafficService extends Service {
	private final static String TAG = "TrafficService";
	public static int OPEN = 0;
	public static int CLOSE = 1;

	private long curRx;
	private long curTx;
	private FloatView mFloatView;
	private TextView tv_traffic;
	private final int delayTime = 2000;

	private Handler mHandler = new Handler();
	private Runnable mRefresh = new Runnable() {
		public void run() {
			if (mFloatView != null && mFloatView.isShow() == true &&
					(TrafficStats.getTotalRxBytes()>curRx || TrafficStats.getTotalTxBytes()>curTx)) {
				long a = ((TrafficStats.getTotalRxBytes() - curRx) + (TrafficStats
						.getTotalTxBytes() - curTx)) / 2;
				String desc = String.format("当前流量: %s/S", FlowUtil.BToShowStringNoDecimals(a));
				tv_traffic.setText(desc);
				curRx = TrafficStats.getTotalRxBytes();
				curTx = TrafficStats.getTotalTxBytes();
			}
			mHandler.postDelayed(this, delayTime);
		}
	};

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

	@Override
	public void onCreate() {
		super.onCreate();
		if (mFloatView == null) {
			mFloatView = new FloatView(MainApplication.getInstance());
			mFloatView.setLayout(R.layout.float_traffic);
			tv_traffic = (TextView) mFloatView.mContentView.findViewById(R.id.tv_traffic);
		}
		curRx = TrafficStats.getTotalRxBytes();
		curTx = TrafficStats.getTotalTxBytes();
		mHandler.postDelayed(mRefresh, 0);
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		if (intent != null) {
			int type = intent.getIntExtra("type", OPEN);
			if (type == OPEN) {
				if (mFloatView != null && mFloatView.isShow() == false) {
					mFloatView.show();
				}
			} else if (type == CLOSE) {
				if (mFloatView != null && mFloatView.isShow() == true) {
					mFloatView.close();
				}
				stopSelf();
			}
		}

		return super.onStartCommand(intent, flags, startId);
	}

	@Override
	public void onDestroy() {
		super.onDestroy();
		mHandler.removeCallbacks(mRefresh);
	}

}

对于股市指数的展示,可以通过调用财经网站的实时指数查询接口得到,比如新浪财经与腾讯财经均提供了上证指数与深圳成指的查询接口。

下面是实时股指悬浮窗的效果截图:

下面是实时股指悬浮窗的页面代码:

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

import com.example.exmfloat.service.StockService;

public class StockActivity extends Activity implements OnClickListener {
	private final static String TAG = "StockActivity";

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

		Button btn_stock_open = (Button) findViewById(R.id.btn_stock_open);
		Button btn_stock_close = (Button) findViewById(R.id.btn_stock_close);
		btn_stock_open.setOnClickListener(this);
		btn_stock_close.setOnClickListener(this);
	}

	@Override
	public void onClick(View v) {
		if (v.getId() == R.id.btn_stock_open) {
			Intent intent = new Intent(this, StockService.class);
			intent.putExtra("type", StockService.OPEN);
			startService(intent);
		} else if (v.getId() == R.id.btn_stock_close) {
			Intent intent = new Intent(this, StockService.class);
			intent.putExtra("type", StockService.CLOSE);
			startService(intent);
		}
	}

}

下面是实时股指悬浮窗的服务代码:

import com.example.exmfloat.MainApplication;
import com.example.exmfloat.R;
import com.example.exmfloat.http.HttpReqData;
import com.example.exmfloat.http.HttpRespData;
import com.example.exmfloat.http.HttpUrlUtil;
import com.example.exmfloat.widget.FloatView;

import android.app.Service;
import android.content.Intent;
import android.graphics.Color;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.widget.TextView;

public class StockService extends Service {
	private final static String TAG = "StockService";
	public static int OPEN = 0;
	public static int CLOSE = 1;

	private FloatView mFloatView;
	private TextView tv_sh_stock, tv_sz_stock;
	private final int delayTime = 5000;

	private Handler mHandler = new Handler() {
		@Override
		public void handleMessage(Message msg) {
			//上证指数,3019.9873,-5.6932,-0.19,1348069,14969598
			String desc = (String) msg.obj;
			String[] array = desc.split(",");
			String stock = array[1];
			float distance = Float.parseFloat(array[2]);
			String range = array[3];
			String text = String.format("%s  %s%%", stock, range);
			int type = msg.what;
			if (type == SHANGHAI) {
				tv_sh_stock.setText(text);
				if (distance > 0) {
					tv_sh_stock.setTextColor(Color.RED);
				} else {
					tv_sh_stock.setTextColor(Color.GREEN);
				}
			} else if (type == SHENZHEN) {
				tv_sz_stock.setText(text);
				if (distance > 0) {
					tv_sz_stock.setTextColor(Color.RED);
				} else {
					tv_sz_stock.setTextColor(Color.GREEN);
				}
			}
		}
	};

	private Runnable mRefresh = new Runnable() {
		@Override
		public void run() {
			if (mFloatView != null && mFloatView.isShow() == true ) {
				new StockThread(SHANGHAI).start();
				new StockThread(SHENZHEN).start();
			}
			mHandler.postDelayed(this, delayTime);
		}
	};

	private static int SHANGHAI = 0;
	private static int SHENZHEN = 1;
	private class StockThread extends Thread {
		private int mType;
		public StockThread(int type) {
			mType = type;
		}

		@Override
		public void run() {
			HttpReqData req_data = new HttpReqData();
			if (mType == SHANGHAI) {
				req_data.url = "http://hq.sinajs.cn/list=s_sh000001";
			} else if (mType == SHENZHEN) {
				req_data.url = "http://hq.sinajs.cn/list=s_sz399001";
			}
			HttpRespData resp_data = HttpUrlUtil.getData(req_data);
			//var hq_str_s_sh000001="上证指数,3019.9873,-5.6932,-0.19,1348069,14969598";
			String desc = resp_data.content;
			Message msg = Message.obtain();
			msg.what = mType;
			msg.obj = desc.substring(desc.indexOf("\"")+1, desc.lastIndexOf("\""));
			mHandler.sendMessage(msg);
		}
	}

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

	@Override
	public void onCreate() {
		super.onCreate();
		if (mFloatView == null) {
			mFloatView = new FloatView(MainApplication.getInstance());
			mFloatView.setLayout(R.layout.float_stock);
			tv_sh_stock = (TextView) mFloatView.mContentView.findViewById(R.id.tv_sh_stock);
			tv_sz_stock = (TextView) mFloatView.mContentView.findViewById(R.id.tv_sz_stock);
		}
		mHandler.postDelayed(mRefresh, 0);
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		if (intent != null) {
			int type = intent.getIntExtra("type", OPEN);
			if (type == OPEN) {
				if (mFloatView != null && mFloatView.isShow() == false) {
					mFloatView.show();
				}
			} else if (type == CLOSE) {
				if (mFloatView != null && mFloatView.isShow() == true) {
					mFloatView.close();
				}
				stopSelf();
			}
		}

		return super.onStartCommand(intent, flags, startId);
	}

	@Override
	public void onDestroy() {
		super.onDestroy();
		mHandler.removeCallbacks(mRefresh);
	}

}

点此查看Android开发笔记的完整目录

时间: 2024-10-11 00:02:32

Android开发笔记(一百一十八)自定义悬浮窗的相关文章

Android学习笔记(二十)——自定义内容提供器

//此系列博文是<第一行Android代码>的学习笔记,如有错漏,欢迎指正! 如果我们想要实现跨程序共享数据的功能,官方推荐的方式就是使用内容提供器,可以通过新建一个类去继承 ContentProvider 的方式来创建一个自己的内容提供器: 一.继承ContentProvider的六个方法: ContentProvider 类中有六个抽象方法,我们需要使用子类去继承它,并重写六个方法,我们先来认识这六个类.新建 MyProvider继承自 ContentProvide,代码如下所示: 1 p

Android开发系列(二十八):使用SubMenu创建选项菜单

大部分手机上边都会有一个"MENU"键,在一个应用安装到手机上之后,可以通过"MENU"显示该应用关联的菜单. 但是,从Android 3.0开始,Android不再要求手机设备上必须提供MENU案件,虽然现在还有很多手机都会提供MENU按键,但是有一部分已经不再提供.在这种情况下,Android推荐使用ActionBar来代替菜单.在以后的博文中我们会介绍Android对ActionBar的支持 Menu接口是实现这个功能的按键之一,其中SubMenu继承自Men

Android开发笔记(一百一十一)聊天室中的Socket通信

Socket通信 基本概念 对于程序开发来说,网络通信的基础就是Socket,但因为是基础,所以用起来不容易,今天我们就来谈谈Socket通信.计算机网络有个大名鼎鼎的TCP/IP协议,普通用户在电脑上设置本地连接的ip时,便经常看到下图的弹窗,注意红框部分已经很好地描述了TCP/IP协议的作用. TCP/IP是个协议组,它分为三个层次:网络层.传输层和应用层: 网络层包括:IP协议.ICMP协议.ARP协议.RARP协议和BOOTP协议. 传输层包括:TCP协议.UDP协议. 应用层包括:HT

Android开发笔记(一百一十七)app省电方略

电源管理PowerManager PowerManager是Android的电源管理类,用于管理电源操作如睡眠.唤醒.重启以及调节屏幕亮度等等. PowerManager的对象从系统服务POWER_SERVICE中获取,它的主要方法如下: goToSleep : 睡眠,即锁屏. wakeUp : 唤醒,即解锁. reboot : 重启. 另有下列几个隐藏的方法: getMinimumScreenBrightnessSetting : 获取屏幕亮度的最小值. getMaximumScreenBri

【转】Android开发笔记(序)写在前面的目录

原文:http://blog.csdn.net/aqi00/article/details/50012511 知识点分类 一方面写写自己走过的弯路掉进去的坑,避免以后再犯:另一方面希望通过分享自己的经验教训,与网友互相切磋,从而去芜存菁进一步提升自己的水平.因此博主就想,入门的东西咱就不写了,人不能老停留在入门上:其次是想拾缺补漏,写写虽然小众却又用得着的东西:另外就是想以实用为主,不求大而全,但求小而精:还有就是有的知识点是java的,只是Android开发也会经常遇上,所以蛮记下来.个人的经

Android开发笔记(一百一十九)工具栏ToolBar

Toolbar 在前面的博文<Android开发笔记(二十)顶部导航栏>中,我们学习了ActionBar的用法,可是ActionBar着实是不怎么好用,比如文字风格不能定制.图标不能定制,而且还存在低版本的兼容性问题,所以实际开发中大家还是不倾向使用ActionBar.为此,Android提供了加强版的工具栏控件即Toolbar,因为Toolbar继承自ViewGroup,而且可在布局文件中像其它布局视图一样使用,所以灵活性大大的提高了.既然Android都与时俱进了,那我们也不能落后,现在就

Android开发笔记(一百一十三)测试工具

单元测试TestCase Android的sdk提供了对项目进行单元测试的功能,开发包的android.test下面便是专门用来单元测试的类.单元测试的作用是通过模拟文本输入和手势输入(如点击操作),从而让app自动执行一系列的操作,这样就能够检查程序是否运行正常. 下面是搭建测试工程的具体步骤: 1.首先当然你得有一个待测试的app工程,最简单的如带有一个编辑框的Hello World工程: 2.其次在ADT中创建测试工程,操作步骤为依次选择菜单"File"--"New&q

Android开发笔记(一百零八)智能语音

智能语音技术 如今越来越多的app用到了语音播报功能,例如地图导航.天气预报.文字阅读.口语训练等等.语音技术主要分两块,一块是语音转文字,即语音识别:另一块是文字转语音,即语音合成. 对中文来说,和语音播报相关的一个技术是汉字转拼音,想想看,拼音本身就是音节拼读的标记,每个音节对应一段音频,那么一句的拼音便能用一连串的音频流合成而来.汉字转拼音的说明参见<Android开发笔记(八十三)多语言支持>. 语音合成通常也简称为TTS,即TextToSpeech(从文本到语言).语音合成技术把文字

Android开发笔记(一百零六)支付缴费SDK

第三方支付 第三方支付指的是第三方平台与各银行签约,在买方与卖方之间实现中介担保,从而增强了支付交易的安全性.国内常用的支付平台主要是支付宝和微信支付,其中支付宝的市场份额为71.5%,微信支付的市场份额为15.99%,也就是说这两家垄断了八分之七的支付市场(2015年数据).除此之外,还有几个app开发会用到的支付平台,包括:银联支付,主要用于公共事业缴费,如水电煤.有线电视.移动电信等等的充值:易宝支付,主要用于各种报名考试的缴费,特别是公务员与事业单位招考:快钱,被万达收购,主要用于航空旅