PopupWindow 是什么?
简述:
PopupWindow 是 Android 提供的一种浮层窗口,可以用于展示弹出的内容,比如菜单、提示、广告、输入框等,不会影响 Activity 本身的布局结构。
常见用途:
- 下拉菜单(如 Spinner 的下拉项)
- 广告半屏浮层或全屏浮层
- 输入框提示(AutoCompleteTextView)
- 自定义弹出面板(例如日期选择器)
PopupWindow 核心特性
| 特性 | 说明 |
|---|---|
| 独立于 Activity | 悬浮在界面顶层,不占用布局空间 |
| 可自定义内容 | 内容可以是任意 View(如 LinearLayout、ListView) |
| 灵活控制位置 | 可指定相对于某个 View 或屏幕坐标显示 |
| 可设置动画 | 支持进入/退出动画 |
| 模态/非模态 | 可设置是否拦截背景点击事件 |
PopupWindow 与 Dialog 的区别
| 特性 | PopupWindow | Dialog |
|---|---|---|
| UI 结构 | 不影响 Activity 层级 | 影响 UI 层级,阻塞事件传递 |
| 可定制性 | 灵活,可以任意位置显示 | 居中弹出,结构固定 |
| 背景遮罩 | 默认无,需要手动设置 | 默认带有遮罩(模态) |
| 生命周期 | 不受 Activity 生命周期控制 | 受控于 Activity 生命周期 |
PopupWindow 基础与生命周期管理
PopupWindow 创建与展示基本方法
/**
* 展示 PopupWindow
*/
private fun showPopupWindow(anchorView: View) {
// 对已经展示的 PopupWindow 进行关闭(避免重复创建)
mPopupWindow?.takeIf { it.isShowing }?.dismiss()
// 通过 LayoutInflater 加载自定义弹窗布局
val popupView = LayoutInflater.from(this).inflate(R.layout.popup_content, null)
// 设置关闭按钮
val ivClose = popupView.findViewById<ImageView>(R.id.iv_close)
ivClose.setOnClickListener {
if (mPopupWindow != null && mPopupWindow!!.isShowing) {
mPopupWindow!!.dismiss()
}
}
// 设置弹窗内的文本内容
val tvMessage: TextView = popupView.findViewById<TextView>(R.id.tv_popup_message)
tvMessage.text = "这是一个 PopupWindow 示例"
// 创建 PopupWindow 实例,宽高设为 wrap_content,第三个参数为 focusable:允许获得焦点(影响返回键关闭)
mPopupWindow = PopupWindow(
popupView,
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
true
)
// 设置弹窗背景为透明 Drawable:
// 这是触发外部点击关闭的前提条件(无背景则点击无效)
mPopupWindow?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT));
// 设置可点击弹窗外部区域以关闭弹窗
mPopupWindow?.isOutsideTouchable = true;
// 设置可以响应返回键关闭
mPopupWindow?.isFocusable = true;
// 设置弹窗的进出动画(可自定义样式)
mPopupWindow?.animationStyle = android.R.style.Animation_Dialog;
// 显示 PopupWindow,位置在 anchorView 的下方,y 方向偏移 20 像素
mPopupWindow?.showAsDropDown(anchorView, 0, 20);
}其中:
width,height可设为WRAP_CONTENT,MATCH_PARENT, 或具体数值;focusable为true表示 PopupWindow 可获取焦点,支持返回键关闭。
另一种显示方式:固定屏幕位置
mPopupWindow?.showAtLocation(
anchorView,
Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL, // 底部居中
0, // 水平偏移(0 表示完全居中)
500 // 垂直偏移(从底部向上偏移 100px)
)生命周期管理
避免重复创建,通常我们会使用成员变量来持有 popup 实例:
private var popupWindow: PopupWindow? = null每次点击前判断:
// 对已经展示的 PopupWindow 进行关闭(避免重复创建)
mPopupWindow?.takeIf { it.isShowing }?.dismiss()防止内存泄漏
PopupWindow 持有 View,如果它的 Context 是已销毁的 Activity,会导致泄漏。需要销毁。
override fun onDestroy() {
popupWindow?.dismiss()
super.onDestroy()
}PopupWindow 的布局控制与展示行为详解
聚焦于实际开发中非常关键的内容:布局、位置、尺寸控制、动画与交互行为设计。
显示位置控制
PopupWindow 常用的两个显示方法:
showAsDropDown(View anchor, int xoff, int yoff)
- 将弹窗显示在锚点 View 的正下方;
xoff,yoff是偏移量(相对 anchor 左上角)。
popupWindow.showAsDropDown(anchorView, 0, 10)showAtLocation(View parent, int gravity, int x, int y)
- 更自由,适合全屏遮罩弹窗;
gravity控制显示方向,配合(x, y)指定绝对位置。
popupWindow.showAtLocation(parentView, Gravity.CENTER, 0, 0)尺寸与边界控制
弹窗布局尺寸是常见 Bug 来源:
wrap_content 是相对的!
- 如果你在
PopupWindow设置中用了WRAP_CONTENT,但弹窗内部布局宽高是match_parent,会导致全屏; - 所以弹窗内部的根布局也要正确设置为
wrap_content或加边框。
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/popup_background"
... />动画设置
可用系统默认,也可以自定义:
popupWindow.animationStyle = android.R.style.Animation_Dialog自定义动画:
<!-- res/anim/popup_enter.xml -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300">
<translate android:fromYDelta="100%" android:toYDelta="0%"/>
<alpha android:fromAlpha="0.0" android:toAlpha="1.0"/>
</set>然后在 styles.xml 配上:
<style name="PopupAnim">
<item name="android:windowEnterAnimation">@anim/popup_enter</item>
<item name="android:windowExitAnimation">@anim/popup_exit</item>
</style>背景遮罩与点击关闭
设置背景**(必须要,否则点击外部无效):**
在 Android 中,PopupWindow 的点击外部关闭功能(isOutsideTouchable)必须配合背景设置才能正常工作。如果没有设置背景,即使设置了 isOutsideTouchable = true,点击 PopupWindow 外部也不会触发关闭。
popupWindow.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
popupWindow.isOutsideTouchable = true
popupWindow.isFocusable = true点击外部自动关闭:
- 需要上述设置;
- 如果用的是
showAtLocation,还可自己加遮罩半透明背景并监听点击关闭。
一个例子:全屏 PopupWindow
class DraggableWebPopup(private val context: Activity) : PopupWindow() {
private val TAG = DraggableWebPopup::class.java.simpleName
/** 内容区 */
private lateinit var contentLayout: LinearLayout
private lateinit var inputField: TextView
private lateinit var webView: WebView
/** 全屏高度 */
private val maxHeight by lazy {
val rect = Rect()
context.window.decorView.getWindowVisibleDisplayFrame(rect)
rect.height()
}
/** 半屏高度 */
private val halfHeight: Int by lazy { maxHeight / 2 }
/** 初始圆角 */
private var currentRadius = 100f
/** 圆角 drawable */
private val roundedBg by lazy {
GradientDrawable().apply {
setColor(Color.WHITE)
cornerRadii = floatArrayOf(
currentRadius, currentRadius,
currentRadius, currentRadius,
0f, 0f,
0f, 0f
)
}
}
/**
* 展示 PopupWindow
*/
fun show(url: String = "https://www.baidu.com") {
val rootLayout = LayoutInflater.from(context).inflate(R.layout.draggable_popup_window, null)
rootLayout.findViewById<FrameLayout>(R.id.popup_view).setOnClickListener { dismiss() }
contentLayout = rootLayout.findViewById(R.id.container_content)
// 初始化 WebView(如需)
// webView = rootLayout.findViewById(R.id.webview)
// webView.settings.javaScriptEnabled = true
// webView.loadUrl(url)
// 内容展示区初始为半屏大小
contentLayout.layoutParams.height = halfHeight
contentLayout.background = roundedBg
// 设置 PopupWindow 本身
contentView = rootLayout
width = ViewGroup.LayoutParams.MATCH_PARENT
height = ViewGroup.LayoutParams.MATCH_PARENT
isFocusable = true
isOutsideTouchable = true
animationStyle = android.R.style.Animation_Dialog
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
showAtLocation(context.window.decorView, Gravity.BOTTOM, 0, 0)
setupDrag()
}
private fun setupDrag() {
/** 上次按下时 Y 的坐标 */
var lastY = 0f
contentLayout.setOnTouchListener { v, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> lastY = event.rawY
MotionEvent.ACTION_MOVE -> {
val dy = event.rawY - lastY
lastY = event.rawY
val newHeight = (v.height - dy).coerceIn(halfHeight.toFloat(), maxHeight.toFloat())
v.updateLayoutParams<ViewGroup.LayoutParams> { height = newHeight.toInt() }
val ratio = (newHeight - halfHeight) / (maxHeight - halfHeight).coerceAtLeast(1)
currentRadius = 100f * (1f - ratio.coerceIn(0f, 1f))
(v.background as GradientDrawable).cornerRadii = floatArrayOf(
currentRadius, currentRadius,
currentRadius, currentRadius,
0f, 0f, 0f, 0f
)
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
val currentHeight = v.height
val threshold = (halfHeight + maxHeight) / 2
val targetHeight = if (currentHeight > threshold) maxHeight else halfHeight
animateHeight(v, currentHeight, targetHeight)
}
}
true
}
}
private fun animateHeight(view: View, from: Int, to: Int) {
// 计算本次动画圆角起止
val radiusTo = if (to == maxHeight) 0f else 100f
val animator = ValueAnimator.ofInt(from, to).apply {
duration = 300
interpolator = DecelerateInterpolator()
}
// 直角
val radii = FloatArray(8)
animator.addUpdateListener {
val value = it.animatedValue as Int
view.layoutParams.height = value
view.requestLayout()
val fraction = it.animatedFraction
val radius = currentRadius + (radiusTo - currentRadius) * fraction
// 只改前 4 个角
radii.fill(radius, 0, 4)
radii.fill(0f, 4, 8)
(view.background as GradientDrawable).cornerRadii = radii
}
animator.start()
}
}Info
这里遇到几个小问题:
- 由于刷新的问题导致 popupWindow 频闪。解决方式:在 popupWindow 外面套一层,外层为透明。这样 popupWindow 在拖动的时候不会影响下层,就不会产生频闪的问题。
更新: 2025-08-02 21:20:53
原文: https://www.yuque.com/dongpozhouzi-mshe3/zhm85g/awcu8tx6616qfprp