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.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<View>

    init {
        mViewList = SparseArray()
    }

    //查找View中的控件
    fun <T : View> 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<TextView>(viewId)
        view?.text = text
        return this //链式调用
    }

    //设置文本字体颜色
    fun setTextColor(@IdRes viewId: Int, color: Int): ViewHolder {
        val view = getView<TextView>(viewId)
        view?.setTextColor(color)
        return this
    }

    //设置文本字体大小,单位默认为SP,故设置时只需要传递数值就可以,如setTextSize(R.id.xxx,15f)
    fun setTextSize(@IdRes viewId: Int, textSize: Float): ViewHolder {
        val view = getView<TextView>(viewId)
        view?.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize)
        return this
    }

    //设置图片
    fun setImageResource(@IdRes viewId: Int, resId: Int): ViewHolder {
        val iv = getView<ImageView>(viewId)
        iv?.setImageResource(resId)
        return this
    }

    //显示View
    fun setViewVisible(@IdRes viewId: Int): ViewHolder {
        getView<View>(viewId)?.visibility = View.VISIBLE
        return this
    }

    //隐藏View
    fun setViewGone(@IdRes viewId: Int): ViewHolder {
        getView<View>(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<View>(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<View>(viewId)?.setOnClickListener { v -> listener.invoke(v) }
        return this
    }

    //设置长按事件
    fun setOnLongClickListener(@IdRes viewId: Int, listener: (v: View) -> Boolean): ViewHolder {
        getView<View>(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"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@color/white"
    android:elevation="5dp"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="120dp"
        android:layout_height="200dp"
        android:orientation="vertical">

        <TextView
            android:id="@+id/reply_tv"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:gravity="center"
            android:text="回复"
            android:textColor="@color/black"
            android:textSize="18sp" />

        <TextView
            android:id="@+id/share_tv"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:gravity="center"
            android:text="分享"
            android:textColor="@color/black"
            android:textSize="18sp" />

        <TextView
            android:id="@+id/report_tv"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:gravity="center"
            android:text="举报"
            android:textColor="@color/black"
            android:textSize="18sp" />

        <TextView
            android:id="@+id/copy_tv"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:gravity="center"
            android:text="复制"
            android:textColor="@color/black"
            android:textSize="18sp" />

    </LinearLayout>

</LinearLayout>

       效果图如下:

       最后,附上整个项目的 github地址 ,里面同时包含了Java和Kotlin版本。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值