在手机阅读的时候,如果读到精彩的句子想把它记录下来过后慢慢品位;可能突然想到某件重要的事情,而又觉得划屏找便签类应用不太方便;又或者,你突然需要很方便的记录一些重要的数据,像电话号码......这个时候我们即将要开发的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
欢迎大家讨论交流。