Android悬浮框应用--悬浮笔记

本文介绍了一个Android悬浮笔记应用的开发过程,包括入口悬浮框、输入悬浮框、后台Service和MainActivity的实现。讨论了如何通过Service监听屏幕状态,并探讨了应用的优化方向,如降低资源消耗、增加功能和存储优化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

             悬浮框,顾名思义就是悬浮在手机界面的View,它可以存在于相对手机屏幕的某个位置,而与手机界面呈现第几屏,呈现什么内容无关。这就使得 悬浮框应用为我们的app使用方便了不少,比如LAS。下面以一个应用为例,来记录与大家分享悬浮框的基本开发方法。

在手机阅读的时候,如果读到精彩的句子想把它记录下来过后慢慢品位;可能突然想到某件重要的事情,而又觉得划屏找便签类应用不太方便;又或者,你突然需要很方便的记录一些重要的数据,像电话号码......这个时候我们即将要开发的app或许可以派上用场,那就是一个简易的悬浮笔记。首先我们实现悬浮笔记的主要功能:记录内容、处理内容。先看下效果图:


它的结构比较简单,由如下四个部分构成:


1.入口悬浮框:悬浮于界面上的占用空间比较小的View,命名为FloatSmallView,点击即可开启输入悬浮框;

2.输入悬浮框:跟小悬浮框同级的FloatBigView,当点击FloatSmallView的时候弹出FloatBigView,即可进行记录;

3.后台Service:用于管理悬浮框的位置、大小悬浮框的切换、以及相应的逻辑处理;

4.MainActivity:我们在FloatBigView中记录的内容就会被保存,可以通过该Activity来查看、编辑,启动Service。

从程序的入口MainActivity开始,先贴代码:

<span style="font-size:18px;">public class MainActivity extends Activity implements View.OnClickListener{

	private EditText content;                           //记录内容
	private Button firstButton,secondButton,thirdButton;//悬浮窗开启模式选择
	private SharedPreferences sp;			    //保存记录的内容
	private int type = 0;				    //记录模式,默认为0
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		firstButton = (Button) findViewById(R.id.only_home);
		secondButton = (Button) findViewById(R.id.only_weixin);
		thirdButton = (Button) findViewById(R.id.all);
		content = (EditText) findViewById(R.id.content);
		
		firstButton.setOnClickListener(this);
		secondButton.setOnClickListener(this);
		thirdButton.setOnClickListener(this);
		
		sp = getSharedPreferences("FloatNote", MODE_PRIVATE);
		content.setText(sp.getString("note", ""));
		content.setSelection(content.getText().toString().length());//设置光标在文字末尾
		
	}

	@Override
	public void onBackPressed() {  //退出时保存编辑的内容
		Editor editor = sp.edit();
		editor.putString("note", content.getText().toString());
		editor.commit();
		super.onBackPressed();
	}

	@Override
	public void onClick(View v) {
		switch(v.getId()){
		case R.id.only_home:
			type = Constant.ONLY_HOME;
			break;
		case R.id.only_weixin:
			type = Constant.ONLY_WEIXIN;
			break;
		case R.id.all:
			type = Constant.ALL;
			break;
		}
		Intent intent = new Intent(MainActivity.this, FloatWindowService.class); 
		intent.putExtra("type", type);
		startService(intent);  
		finish();  
	}
	
}</span>

从中可以看出来,MainActivity的布局很简单,这里为了方便操作,内容是存入SharedPreference的,读出来放入EditText,查看编辑处理。可以采用SQLite存储内容,可以加上分类、提醒等功能。上面三个按钮分别对应于三种模式的悬浮框,这里我设计的三种悬浮框:1.仅仅是Home的时候出现悬浮框,当开启任何应用,悬浮框就会消失;2.仅仅微信浏览内容的界面开启,这只是一个例子,你也可以根据自己的情况开发,选择该模式,仅进入微信的浏览内容界面的时候才会出现悬浮框,其它任何情况悬浮框都消失;3.全部开启模式,即悬浮框一直出现在界面上,在任何应用程序之上。MainActivity的功能就两个:启动Service,查看编辑保存的笔记。这里设置为退出时保存EditText的内容。

从主程序选择一种模式后就对启动对悬浮窗进行管理的Service,先看Service代码:
public class FloatWindowService extends Service {

	private Timer timer;
	private int type;
	private MyWindowManager myWindowManager;
	
	public final int CREATE_SMALL_WINDOW = 0;
	public final int CREATE_BIG_WINDOW = 1;
	public final int REMOVE_SMALL_WINDOW = 2;
	public final int REMOVE_BIG_WINDOW = 3;
	
	private Handler handler = new Handler(){

		@Override
		public void handleMessage(Message msg) {
			switch(msg.what){
			case CREATE_SMALL_WINDOW:
				myWindowManager.createSmallWindow(getApplicationContext());
				break;
			case CREATE_BIG_WINDOW:
				myWindowManager.createBigWindow(getApplicationContext());
				break;
			case REMOVE_SMALL_WINDOW:
				myWindowManager.removeSmallWindow(getApplicationContext());
				break;
			case REMOVE_BIG_WINDOW:
				myWindowManager.removeBigWindow(getApplicationContext());
				break;
			}
		}
		
	};
	@Override
	public IBinder onBind(Intent intent) {
		return null;
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		myWindowManager = MyWindowManager.getMyWindowManager();
		type = intent.getIntExtra("type",0);
		// 开启定时器,每隔2秒刷新一次
		if (timer == null) {
			timer = new Timer();
			timer.scheduleAtFixedRate(new RefreshTask(), 0, 2000);
		}
		return super.onStartCommand(intent, flags, startId);
	}

	@Override
	public void onDestroy() {
		super.onDestroy();
		timer.cancel();
		timer = null;
	}

	class RefreshTask extends TimerTask {

		@Override
		public void run() {
			switch(type)
			{
			case Constant.ONLY_HOME: 
				if (isHome() && !myWindowManager.isWindowShowing()) {
					handler.sendEmptyMessage(CREATE_SMALL_WINDOW);
				}
				// 当前界面不是桌面,且有悬浮窗显示,则移除悬浮窗。
				else if (!isHome() && myWindowManager.isWindowShowing()) {
					handler.sendEmptyMessage(REMOVE_SMALL_WINDOW);
					handler.sendEmptyMessage(REMOVE_BIG_WINDOW);
				}
				break;
			case Constant.ONLY_WEIXIN:
				//如果当前界面为微信
				if (isWeixin() && !myWindowManager.isWindowShowing()) {
					handler.sendEmptyMessage(CREATE_SMALL_WINDOW);
				}
				else if (!isWeixin() && myWindowManager.isWindowShowing()) {
					handler.sendEmptyMessage(REMOVE_SMALL_WINDOW);
					handler.sendEmptyMessage(REMOVE_BIG_WINDOW);
				}
				break;
			case Constant.ALL:
				if (!myWindowManager.isWindowShowing()) {
					handler.sendEmptyMessage(CREATE_SMALL_WINDOW);
				}
				break;
			}
		}

	}

	/**
	 * 判断当前界面是否是桌面
	 */
	private boolean isHome() {
		ActivityManager mActivityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
		List<RunningTaskInfo> rti = mActivityManager.getRunningTasks(1);
		return getHomes().contains(rti.get(0).topActivity.getPackageName());
	}
	//是否是微信的浏览内容的界面
	private boolean isWeixin(){
		ActivityManager mActivityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
		List<RunningTaskInfo> rti = mActivityManager.getRunningTasks(1);
		return rti.get(0).topActivity.getClassName().equals(Constant.WEIXIN_CONTENT);
	}

	/**
	 * 获得属于桌面的应用的应用包名称
	 * 
	 * @return 返回包含所有包名的字符串列表
	 */
	private List<String> getHomes() {
		List<String> names = new ArrayList<String>();
		PackageManager packageManager = this.getPackageManager();
		Intent intent = new Intent(Intent.ACTION_MAIN);
		intent.addCategory(Intent.CATEGORY_HOME);
		List<ResolveInfo> resolveInfo = packageManager.queryIntentActivities(intent,
				PackageManager.MATCH_DEFAULT_ONLY);
		for (ResolveInfo ri : resolveInfo) {
			names.add(ri.activityInfo.packageName);
		}
		return names;
	}
}

下面来分析下Service,逻辑很清楚了。Handler作为更新界面的对象。Timer定时器对屏幕进行监听,每隔两秒监听一次。创建、移除大小悬浮窗的方法在自定义的MyWindowManager类里,ActivityManager类可以获取桌面应用、当前(TopActivity)的Activity的名称等信息,将一些常量写入类Constant里,方便修改添加。现在就根据MainActivity里传过来的type来选择开启那种类型的悬浮框,由Service的循环监听判断来控制悬浮窗是否出现。

下面看看MyWindowManager的代码:

public class MyWindowManager {

	private FloatSmallView smallWindow;

	private FloatBigView bigWindow;

	private LayoutParams smallWindowParams;   //小悬浮窗的参数

	private LayoutParams bigWindowParams;     //大悬浮窗的参数

	private WindowManager mWindowManager;
	
	private static MyWindowManager myWindowManager;
	
	private MyWindowManager(){
		
	}
	public static MyWindowManager getMyWindowManager(){
		if(myWindowManager == null)
			myWindowManager = new MyWindowManager();
		return myWindowManager;
	}

	public void createSmallWindow(Context context) {
		WindowManager windowManager = getWindowManager(context);
		int screenWidth = windowManager.getDefaultDisplay().getWidth();
		int screenHeight = windowManager.getDefaultDisplay().getHeight();
		if (smallWindow == null) {
			smallWindow = new FloatSmallView(context);
			if (smallWindowParams == null) {
				smallWindowParams = new LayoutParams();
				smallWindowParams.type = LayoutParams.TYPE_PHONE;
				smallWindowParams.format = PixelFormat.RGBA_8888;
				smallWindowParams.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL
						| LayoutParams.FLAG_NOT_FOCUSABLE;
				smallWindowParams.gravity = Gravity.LEFT | Gravity.TOP;
				smallWindowParams.width = FloatSmallView.smallViewWidth;
				smallWindowParams.height = FloatSmallView.smallViewHeight;
				smallWindowParams.x = screenWidth;
				smallWindowParams.y = screenHeight / 2;
			}
			smallWindow.setParams(smallWindowParams);
			windowManager.addView(smallWindow, smallWindowParams);
		}
	}

	public void removeSmallWindow(Context context) {
		if (smallWindow != null) {
			WindowManager windowManager = getWindowManager(context);
			windowManager.removeView(smallWindow);
			smallWindow = null;
		}
	}

	//创建一个大悬浮窗,位于屏幕顶端
	public void createBigWindow(Context context) {
		WindowManager windowManager = getWindowManager(context);
		if (bigWindow == null) {
			bigWindow = new FloatBigView(context);
			if (bigWindowParams == null) {
				bigWindowParams = new LayoutParams();
				bigWindowParams.x = FloatBigView.viewWidth / 2;
				bigWindowParams.y = FloatBigView.viewHeight / 2;
				bigWindowParams.type = LayoutParams.TYPE_PHONE;
				bigWindowParams.format = PixelFormat.RGBA_8888;
				bigWindowParams.gravity = Gravity.LEFT | Gravity.TOP;
				bigWindowParams.width = FloatBigView.viewWidth;
				bigWindowParams.height = FloatBigView.viewHeight;
			}
			windowManager.addView(bigWindow, bigWindowParams);
		}
	}

	public void removeBigWindow(Context context) {
		if (bigWindow != null) {
			WindowManager windowManager = getWindowManager(context);
			windowManager.removeView(bigWindow);
			bigWindow = null;
		}
	}

	public boolean isWindowShowing() {
		return smallWindow != null || bigWindow != null;
	}

	public WindowManager getWindowManager(Context context) {
		if (mWindowManager == null) {
			mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
		}
		return mWindowManager;
	}
}

将Android系统的WindowManager组合到MyWindowManager类里,将创建大小悬浮窗的方法封装,来实现对悬浮窗大小位置属性设置、显示状态的判断以及新建和移除的操作。悬浮框的一些属性设置见代码,就不具体说了。这里要对屏幕进行操作就必须获取Android的系统服务WINDOW_SERVICE。

接着看小悬浮窗的设计代码:

public class FloatSmallView extends LinearLayout {

	public static int smallViewWidth;

	public static int smallViewHeight;

	private static int statusBarHeight;  //系统状态栏的高度

	private WindowManager windowManager;
	
	private MyWindowManager myWindowManager;

	private WindowManager.LayoutParams mParams; //小悬浮窗的参数

	private float xTouchInScreen;     //记录当前手指位置在屏幕上的横坐标值

	private float yTouchInScreen;    //记录当前手指位置在屏幕上的纵坐标值

	private float xDownInScreen;     //记录手指按下时在屏幕上的横坐标的值

	private float yDownInScreen;     //记录手指按下时在屏幕上的纵坐标的值

	private float xInView;           //记录手指按下时在小悬浮窗的View上的横坐标的值

	private float yInView;           //记录手指按下时在小悬浮窗的View上的纵坐标的值

	public FloatSmallView(Context context) {
		super(context);
		myWindowManager = MyWindowManager.getMyWindowManager();
		windowManager = myWindowManager.getWindowManager(context);
		LayoutInflater.from(context).inflate(R.layout.small_view, this);
		View view = findViewById(R.id.float_bg);
		smallViewWidth = view.getLayoutParams().width;
		smallViewHeight = view.getLayoutParams().height;
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
		// 手指按下时记录必要数据,纵坐标的值都需要减去状态栏高度
			xInView = event.getX();
			yInView = event.getY();
			xDownInScreen = event.getRawX();
			yDownInScreen = event.getRawY() - getStatusBarHeight();
			xTouchInScreen = event.getRawX();
			yTouchInScreen = event.getRawY() - getStatusBarHeight();
			break;
		// 手指移动的时候更新小悬浮窗的位置
		case MotionEvent.ACTION_MOVE:
			xTouchInScreen = event.getRawX();
			yTouchInScreen = event.getRawY() - getStatusBarHeight();
			updateViewPosition();
			break;
		case MotionEvent.ACTION_UP:
			// 如果手指离开屏幕时,则视为点击事件,开启大悬浮窗
			if (Math.abs(xDownInScreen-xTouchInScreen)<5 ||
				Math.abs(yDownInScreen-yTouchInScreen)<5 ) {
				myWindowManager.createBigWindow(getContext());
				myWindowManager.removeSmallWindow(getContext());
			}
			break;
		}
		return true;
	}

	//设置小悬浮窗的参数
	public void setParams(WindowManager.LayoutParams params) {
		mParams = params;
	}

	//更新小悬浮窗在屏幕中的位置
	private void updateViewPosition() {
		mParams.x = (int) (xTouchInScreen - xInView);
		mParams.y = (int) (yTouchInScreen - yInView);
		windowManager.updateViewLayout(this, mParams);
	}

	//获取状态栏的高度
	private int getStatusBarHeight() {
		if (statusBarHeight == 0) {
			try {
				Class<?> c = Class.forName("com.android.internal.R$dimen");
				Object o = c.newInstance();
				Field field = c.getField("status_bar_height");
				int x = (Integer) field.get(o);
				statusBarHeight = getResources().getDimensionPixelSize(x);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return statusBarHeight;
	}
}
这里的设计参考自网上的方法,注释很清楚,但是有一点细节必须得说下,就是在判断是否是点击事件的时候,手触摸的位置不是很精确,如果是以View的坐标完全没有改变来作为是否点击的标准,我自己测试过不点个至少3次都没办法开启大悬浮窗,而且还要很小心翼翼的点,所以为了提高体验就设计为5个单位以内均被判断为点击事件,开启大悬浮窗。

最后看看输入悬浮框的代码:

public class FloatBigView extends LinearLayout {

	public static int viewWidth;
	public static int viewHeight;
	public EditText content;
	public Button submitButton;
	public SharedPreferences sp;
	public StringBuffer sb;
	public Context context;
	private MyWindowManager myWindowManager;
	
	public FloatBigView(final Context context) {
		super(context);
		this.context = context;
		myWindowManager = MyWindowManager.getMyWindowManager();
		LayoutInflater.from(context).inflate(R.layout.big_view, this);
		View view = findViewById(R.id.float_layout);
		viewWidth = view.getLayoutParams().width;
		viewHeight = view.getLayoutParams().height;
		sp = context.getSharedPreferences("FloatNote", Context.MODE_PRIVATE);
		
		submitButton = (Button) view.findViewById(R.id.submit);
		content = (EditText) view.findViewById(R.id.content);
		//监听输入框内容变化
		content.addTextChangedListener(new TextWatcher() {
			@Override
			public void onTextChanged(CharSequence s, int start, int before, int count) {
				if(content.getText().toString().equals(""))
					submitButton.setText(getResources().getString(R.string.cancel));
				else
					submitButton.setText(getResources().getString(R.string.save));
			}
			
			@Override
			public void beforeTextChanged(CharSequence s, int start, int count,
					int after) {
			}
			
			@Override
			public void afterTextChanged(Editable s) {
			}
		});
		submitButton.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				if(getResources().getString(R.string.save).equals(submitButton.getText().toString()))
					saveData();
				myWindowManager.removeBigWindow(context);
				myWindowManager.createSmallWindow(context);
			}
		});
	}
	
	//将内容保存
	public void saveData(){
		Editor edit = sp.edit();
		sb = new StringBuffer();
		sb.append(sp.getString("note", ""));
		sb.append('\n');
		sb.append(content.getText().toString());
		edit.putString("note", sb.toString());
		edit.commit();
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		if(event.getRawY() > viewHeight){
			myWindowManager.removeBigWindow(context);
			myWindowManager.createSmallWindow(context);
		}
		return super.onTouchEvent(event);
	}
	
	
	
}
输入悬浮窗的UI初始化类似于小悬浮窗。这里使用了TextWatcher对象来监听EditText输入框的内容变化,动态的判断是否有内容来显示按钮,并设计不同的按钮提示,如果有内容输入并且点击了保存按钮,前面已经说了将内容暂时保存到SharedPreference。如果没有内容,就是取消的按钮,返回小悬浮窗。最后的onTouchEvent方法必须实现,输入悬浮窗的位置在屏幕顶端,当点击屏幕其他位置时自动返回小悬浮窗,如果不实现该方法,必须清空内容点击按钮才会移除,这也是为了提高体验设计的。

       一个简单的悬浮窗应用就实现了,这个只是作为悬浮窗应用开发的一个小小的例子。我们还可以想想有哪些地方可以添加优化:Service的扫描频率会影响应用所占资源大小、耗电量等,手机资源如此有限,尽量将其消耗降低到最小;是否根据开启不同的模式需要来改变扫描的频率;悬浮窗的背景应该设置为半透明;添加提醒等功能,选择SQLite来存储数据;是否设置开机启动......等等有很多可以扩展优化的地方。

       附上完整的应用项目下载地址,有需要的朋友可以自己下载下来自己设计:

http://download.csdn.net/download/zcdreaming/8160899

       欢迎大家讨论交流。






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值