Android悬浮窗的实现--可以置顶,可以设置优先级的view

本文介绍了如何使用WindowManager和View实现一个可以在任意界面显示的60秒倒计时悬浮窗。通过设置WindowManager.LayoutParams达到置顶效果,并讨论了避免WindowLeaked异常的方法,强调了传递全局Context的重要性。代码示例中展示了布局文件和倒计时逻辑的实现。

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

悬浮窗,顾名思义,显示在window界面之上的一种视图。如今,有这么个需求:设计一个能够在任意界面上显示的60s倒计时弹窗,60s之后执行其他操作。注意哦,这里的任意不仅限于当前的应用,而是所有的界面。效果如下图:



这里,我们用WindowManager + view来实现,代码不多,实现起来也简单,不过会遇到几个坑:


1,项目结构:



2,布局文件:



①activity_main.xml代码:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="show"
        android:text="点击显示弹窗" />
    
    <Button
        android:layout_alignParentBottom="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="other"
        android:text="别的操作" />

</RelativeLayout>

效果图:



②countdown_weight.xml代码:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:background="#ffff00"
        android:gravity="center_horizontal"
        android:orientation="vertical"
        android:padding="10dp" >

        <TextView
            android:id="@+id/tv_countDown"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="关机倒计时"
            android:textSize="20dp"
            android:textColor="#000" />

        <TextView
            android:id="@+id/tv_time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="60"
            android:textColor="#ff0000"
            android:textSize="45dp" />

        <Button
            android:id="@+id/cancle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="取消"
            android:textSize="25dp"
            android:textColor="#000" />
        <!-- android:background="@drawable/xml_sel_button_bg" -->
    </LinearLayout>

</FrameLayout>

效果图见第一张图:


③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.youkuaiyun.com/calvin_zhou/article/details/53009758

http://blog.youkuaiyun.com/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(),就实现了关机倒计时的逻辑。


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值