Android实现一个通用的PopupWindow
在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.Activityimport android.content.Contextimport android.graphics.drawable.ColorDrawableimport android.graphics.drawable.Drawableimport android.support.annotation.FloatRangeimport android.view.LayoutInflaterimport android.view.Viewimport android.view.ViewGroupimport android.widget.PopupWindowclass 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.Activityimport android.support.annotation.FloatRangeclass 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.IdResimport android.util.SparseArrayimport android.util.TypedValueimport android.view.Viewimport android.view.ViewGroupimport android.widget.ImageViewimport 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);
对应的弹窗布局:
<?xml version="1.0" encoding="utf-8"?>
效果图如下:
最后,附上整个项目的 github地址 ,里面同时包含了Java和Kotlin版本。
更多相关文章
- android 系统属性
- Android基类BaseActivity简单封装
- 布局及视图(一)
- Android(安卓)获取OnItemClick事件中组件的内容
- Android语音转文字一识别语音
- Android(安卓)Studio设置,减少对C盘空间的占用
- android之Android(安卓)Studio下自定义属性的定义和使用
- 解决给一组Button设置Background导致点击效果错乱问题
- 让应用程序具体相应权限