悬浮窗,顾名思义,显示在window界面之上的一种视图。如今,有这么个需求:设计一个能够在任意界面上显示的60s倒计时弹窗,60s之后执行其他操作。注意哦,这里的任意不仅限于当前的应用,而是所有的界面。效果如下图:
这里,我们用WindowManager + view来实现,代码不多,实现起来也简单,不过会遇到几个坑:
1,项目结构:
2,布局文件:
①activity_main.xml代码:
效果图:
②countdown_weight.xml代码:
③new_activity.xml其实就是一张上了颜色的Activity,这里只是为了起演示作用
3,为了让view始终置顶显示,就像手机音乐里的桌面歌词一样,无论怎么切换,view都不会消失。我们用结合WindowManager,让view显示,而且用WindowManager的好处是,如果有多个view需要显示,我们还可以通过设置WindowManager.LayoutParams相关的参数,让view按我们设定的优先级显示,类似层级图一样。这也是为什么,不用PopupWindow的原因所在,它和窗口小部件还是有区别的!
代码如下:
package com.example.floateweight;
import java.util.Timer;
import java.util.TimerTask;
import com.example.countdownweight.R;
import android.os.Bundle;
import android.os.Handler;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
/**
* 倒计时60s悬浮窗
*
* @author zy
*
*/
public class FloateActivity extends Activity {
protected static final String TAG = "CountDownActivity";
protected static final int TIME = 1;
private Context context = FloateActivity.this;
private TextView tv_time;
private Button cancle;
private static Timer countDown = null;
private int mValue = 60;
WindowManager wm;
WindowManager.LayoutParams params;
View countDownView;
Handler post = new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
/**
* 点击显示悬浮窗
*
* @param view
*/
public void show(View v) {
wm = (WindowManager) getApplicationContext().getSystemService(
WINDOW_SERVICE); // 注意:这里必须是全局的context
// 判断UI控件是否存在,存在则移除,确保开启任意次应用都只有一个悬浮窗
if (countDownView != null) {
wm.removeView(countDownView);
}
params = new WindowManager.LayoutParams();
// 系统级别的窗口
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
| WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;
// 居中显示
params.gravity = Gravity.CENTER;
// 设置背景透明
params.format = PixelFormat.TRANSPARENT;
countDownView = new View(getApplicationContext()); // 不依赖activity的生命周期
countDownView = View.inflate(getApplicationContext(),
R.layout.countdown_weight, null);
cancle = (Button) countDownView.findViewById(R.id.cancle);
tv_time = (TextView) countDownView.findViewById(R.id.tv_time);
tv_time.setText("60");
wm.addView(countDownView, params);
// 设置“取消”倒计时按钮的监听
cancle.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
Log.e(TAG, "取消倒计时");
wm.removeView(countDownView);
countDownView = null;
countDown.cancel();
mValue = 60;
}
});
// 添加倒计时功能
countDown = new Timer();
countDown.schedule(new TimerTask() {
@Override
public void run() {
mValue--;
post.post(drawCount);
if (mValue == 0) {
// 执行关机操作(这里可以使任意其他操作,根据自己的需求)
Log.e(TAG, "关机");
wm.removeView(countDownView);
countDownView = null;
// 取消定时
countDown.cancel();
finish();
}
}
}, 0, 1000);
}
/**
* 模拟其他操作
* @param view
*/
public void other(View view) {
Toast.makeText(context, "别的操作", Toast.LENGTH_SHORT).show();
startActivity(new Intent(context, NewActivity.class));
}
Runnable drawCount = new Runnable() {
@Override
public void run() {
tv_time.setText(Integer.toString(mValue));
}
};
@Override
protected void onDestroy() {
super.onDestroy();
Log.e(TAG, "倒计时结束");
};
}
①首先是关于WindowManager.LayoutParams的设置,这里就不赘述了,可以参考以下资料:
http://blog.csdn.net/calvin_zhou/article/details/53009758
http://blog.csdn.net/u012165769/article/details/51907306
这里,我将type设置为系统级别的,这样view的优先级最高,会置顶在所有界面之上。同理,如果将type设置为优先级较低的窗口,那么在在显示时,优先级高的会覆盖掉优先级低的,原理就是这样子。
②创建view的时候,需要传一个上下文进去,这里要注意:一定要传全局的context,如果仅仅是当期的上下文环境,那么在退出当前界面时,会引发“窗体泄露”的bug,那为什么呢?其实,原因很简单:在在创建view的时候,当前view的生命周期依附于当前的Activity,当退出当前Activity时,当前Activity的context就不存在了,我们的view的就没有可依附的窗体,所以就会报android.view.WindowLeaked窗体泄露的异常。所以,在创建view的时候,一定要传一个全局的上下文环境。
③倒计时的逻辑很简单,创建一个定时任务TimerTask,然后每隔一秒执行一次,更新UI,使用了handler去post一个请求,在每一秒执行定时任务的时候去setText(),就实现了关机倒计时的逻辑。