在Android中使用PopupWindow,通常都是通过LayoutInflater.from(context).inflate获取View,再通过setContentView设置弹窗布局,如果要处理View上的控件,还需要单独对View进行findViewById和setOnClickListener等等再setContentView,这个过程有点繁琐。如果弹窗布局有多个的话,这样一个一个地去组装PopupWindow就更加繁杂了。所以自己的目的是实现一个PopupWindow类,通过布局id去setContentView,能够匹配各种各样的布局,同时简化PopupWindow的封装过程。
这里先说明一下PopupWindow的一些属性和方法:
方法 | 方法说明 |
setContentView(View contentView) | 设置弹窗的布局 |
setWidth(int width) | 设置弹窗的宽度 |
setHeight(int height) | 设置弹窗的高度 |
setAnimationStyle(int animationStyle) | 设置弹窗出现和消失的动画效果 |
setBackgroundDrawable(Drawable background) | 设置弹窗的背景,但如果弹窗的根布局已经设置了android:background属性,有可能会覆盖整个弹窗的背景导致这个方法看起来无效 |
setOutsideTouchable(boolean touchable) | 设置弹窗外部区域是否可触摸,设为true时当点击外部区域弹窗会消失,false不会消失 |
showAsDropDown(View anchor) | 在指定View的左下角显示弹窗 |
showAsDropDown(View anchor, int xoff, int yoff) | 在指定View的左下角显示弹窗,其中xoff表示相对于View左下角在水平方向上的偏移量,yoff表示相对于View左下角在竖直方向上的偏移量 (Android坐标系的X轴和Y轴的正方向分别是向右和向下的,因此如果xoff为10表示向右偏移10像素,yoff为-10表示向上偏移10像素)。 |
showAtLocation(View parent, int gravity, int x, int y) | 在父控件指定的位置显示弹窗,其中x和y分别表示相对于父控件指定位置在水平和竖直方向上的偏移量,gravity表示在相对于父控件的位置,Gravity.CENTER在父控件正中间显示,Gravity.BOTTOM在父控件底部显示,Gravity.NO_GRAVITY相当于Gravity.LEFT|Gravity.TOP。 |
更多PopupWindow的信息可以到 android developers 上了解。
现在开始封装PopupWindow,完整代码如下:
import android.app.Activity
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.support.annotation.FloatRange
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.PopupWindow
class CommonPopupWindow private constructor(context: Context) : PopupWindow() {
private var mWindowHelper: WindowHelper? = null
init {
if (context is Activity) {
mWindowHelper = WindowHelper(context)
}
}
override fun dismiss() {
super.dismiss()
mWindowHelper?.setBackGroundAlpha(1.0f)
}
class Builder(private var mContext: Context) {
private var mLayoutId: Int = -1 //弹窗的布局id
private var mWidth: Int = 0 //弹窗的宽度
private var mHeight: Int = 0 //弹窗的高度
private var mAlpha: Float = 1.0f //背景透明度
private var mAnimationStyle: Int = -1 //动画
private var mTouchable: Boolean = true //是否可点击
private var mBackgroundDrawable: Drawable = ColorDrawable(0x00000000) //背景drawable
private var mOnViewListener: ((holder: ViewHolder, popupWindow: PopupWindow) -> Unit)? =
null
//通过布局id设置弹窗布局的View
fun setContentView(layoutId: Int): Builder {
mLayoutId = layoutId
return this
}
//设置宽高
fun setViewParams(width: Int, height: Int): Builder {
mWidth = width
mHeight = height
return this
}
//设置外部区域背景透明度,0:完全不透明,1:完全透明
fun setBackGroundAlpha(@FloatRange(from = 0.0, to = 1.0) alpha: Float): Builder {
mAlpha = alpha
return this
}
//设置显示和消失动画
fun setAnimationStyle(animationStyle: Int): Builder {
mAnimationStyle = animationStyle
return this
}
//设置外部区域是否可点击取消对话框
fun setOutsideTouchable(touchable: Boolean): Builder {
mTouchable = touchable
return this
}
//设置弹窗背景
fun setBackgroundDrawable(drawable: Drawable): Builder {
mBackgroundDrawable = drawable
return this
}
//设置事件监听
fun setOnViewListener(listener: (holder: ViewHolder, popupWindow: PopupWindow) -> Unit): Builder {
mOnViewListener = listener
return this
}
fun build(): CommonPopupWindow {
val popupWindow = CommonPopupWindow(mContext)
with(popupWindow) {
//设置contentView
if (mLayoutId != -1) {
val view = LayoutInflater.from(mContext).inflate(mLayoutId, null)
//因为PopupWindow在显示前无法获取准确的宽高值(getWidth和getHeight可能会返回0或-2),
//通过提前测量contentView的宽高就可以通过getMeasuredWidth和getMeasuredHeight获取contentView的宽高
view.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
contentView = view
} else {
throw NullPointerException("The contentView of PopupWindow is null")
}
//设置宽高,没有设置宽高的话默认为ViewGroup.LayoutParams.WRAP_CONTENT
if (mWidth == 0) {
width = ViewGroup.LayoutParams.WRAP_CONTENT
} else {
width = mWidth
}
if (mHeight == 0) {
height = ViewGroup.LayoutParams.WRAP_CONTENT
} else {
height = mHeight
}
mWindowHelper?.setBackGroundAlpha(mAlpha) //设置外部区域的透明度
//设置弹窗显示和消失的动画效果
if (mAnimationStyle != -1) {
animationStyle = mAnimationStyle
}
//设置弹窗背景,如果contentView对应的View已经设置android:background可能会覆盖弹窗背景
setBackgroundDrawable(mBackgroundDrawable)
//设置点击外部区域是否可取消弹窗
isOutsideTouchable = mTouchable
isFocusable = mTouchable
//设置contentView上控件的事件监听
mOnViewListener?.invoke(ViewHolder(contentView), this)
}
return popupWindow
}
}
}
代码是用Kotlin写的,因为都有注释,这里就不再做过多介绍了,主要说一下两个辅助类:WindowHelper和ViewHolder。(这几个类也有用Java语言编写,在最后的demo地址)
WindowHelper主要用于实现弹窗外部区域的阴影效果,代码如下:
import android.app.Activity
import android.support.annotation.FloatRange
class WindowHelper(private var mActivity: Activity) {
//设置外部区域背景透明度,0:完全不透明,1:完全透明
fun setBackGroundAlpha(@FloatRange(from = 0.0, to = 1.0) alpha: Float) {
val window = mActivity.window
val lp = window.attributes
lp.alpha = alpha
window.attributes = lp
}
}
ViewHolder用于简化View的处理,比如findViewById和setOnClickListener等,代码如下:
import android.support.annotation.IdRes
import android.util.SparseArray
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
@Suppress("UNCHECKED_CAST")
class ViewHolder(private var mView: View) {
//缓存View
private var mViewList: SparseArray
init {
mViewList = SparseArray()
}
//查找View中的控件
fun getView(@IdRes viewId: Int): T? {
//对已有的view做缓存
var view: View? = mViewList.get(viewId)
//使用缓存的方式减少findViewById的次数
if (view == null) {
view = mView.findViewById(viewId)
mViewList.put(viewId, view)
}
return view as? T
}
//设置文本
fun setText(@IdRes viewId: Int, text: CharSequence): ViewHolder {
val view = getView(viewId)
view?.text = text
return this //链式调用
}
//设置文本字体颜色
fun setTextColor(@IdRes viewId: Int, color: Int): ViewHolder {
val view = getView(viewId)
view?.setTextColor(color)
return this
}
//设置文本字体大小,单位默认为SP,故设置时只需要传递数值就可以,如setTextSize(R.id.xxx,15f)
fun setTextSize(@IdRes viewId: Int, textSize: Float): ViewHolder {
val view = getView(viewId)
view?.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize)
return this
}
//设置图片
fun setImageResource(@IdRes viewId: Int, resId: Int): ViewHolder {
val iv = getView(viewId)
iv?.setImageResource(resId)
return this
}
//显示View
fun setViewVisible(@IdRes viewId: Int): ViewHolder {
getView(viewId)?.visibility = View.VISIBLE
return this
}
//隐藏View
fun setViewGone(@IdRes viewId: Int): ViewHolder {
getView(viewId)?.visibility = View.GONE
return this
}
//设置View宽度
fun setViewWidth(@IdRes viewId: Int, width: Int): ViewHolder {
return setViewParams(viewId, width, -1)
}
//设置View高度
fun setViewHeight(@IdRes viewId: Int, height: Int): ViewHolder {
return setViewParams(viewId, -1, height)
}
//设置View的宽度和高度
fun setViewParams(@IdRes viewId: Int, width: Int, height: Int): ViewHolder {
getView(viewId)?.let {
val params = it.layoutParams as ViewGroup.MarginLayoutParams
if (width >= 0) {
params.width = width
}
if (height >= 0) {
params.height = height
}
it.layoutParams = params
}
return this
}
//设置点击事件
fun setOnClickListener(@IdRes viewId: Int, listener: (v: View) -> Unit): ViewHolder {
getView(viewId)?.setOnClickListener { v -> listener.invoke(v) }
return this
}
//设置长按事件
fun setOnLongClickListener(@IdRes viewId: Int, listener: (v: View) -> Boolean): ViewHolder {
getView(viewId)?.setOnLongClickListener { v -> listener.invoke(v) }
return this
}
}
ViewHolder使用举例:
holder.setText(R.id.share_tv, "分享")
.setTextSize(R.id.share_tv, 15f)
.setTextColor(R.id.share_tv, Color.BLACK)
.setOnClickListener(R.id.share_tv) {
}.setOnClickListener(R.id.copy_tv) {
}
是不是要比一个一个地findViewById和setText方便多了,至此所有相关的代码已经列举出来了。
CommonPopupWindow使用举例:
CommonPopupWindow.Builder(this)
.setContentView(R.layout.layout_popup_window_to_top)
.setAnimationStyle(R.style.AnimScaleBottom)
.setOnViewListener { holder, popupWindow ->
holder.setOnClickListener(R.id.reply_tv) {
showToast("回复")
popupWindow.dismiss()
}.setOnClickListener(R.id.share_tv) {
showToast("分享")
popupWindow.dismiss()
}.setOnClickListener(R.id.report_tv) {
showToast("举报")
popupWindow.dismiss()
}.setOnClickListener(R.id.copy_tv) {
showToast("复制")
popupWindow.dismiss()
}
}.build()
.showAsDropDown(view);
对应的弹窗布局:
效果图如下:
最后,附上整个项目的 github地址 ,里面同时包含了Java和Kotlin版本。