使用设计模式封装一个弹窗引擎
前言
最近项目使用的一个第三方库很多线上 Bug 所以考虑换一个第三方库了,结果去除依赖之后导致基本每个页面都要修改,项目全部爆红,人都麻了,悔不该当初图方便没有使用引擎类啊。
第三方库提供基本的各种功能,引擎类封装一层给自己的项目预定特定的效果,上层应用只需要调用引擎即可实现效果,不需要关心底层的实现,不需要引用第三方的依赖和类库。当我们第三方库升级或替换的时候,只需要修改引擎类即可。
下面我以弹窗库的封装为例,封装一个弹窗引擎库,下来就一起来看看吧。
一、工厂模式实现不同弹窗布局的创建
其实作为一上层应用,我只关心弹窗的类型,方向,动画,是否居中,依附在哪,简单生命周期回调,控制展示和关闭即可。
以本文的第三方库 [Xpopup] 为例。它的 Api 是很多的各种方法,但是我们项目UI风格固定,只用得到那么几种。当然你完全可以换成自己的封装库或者Android原生的Dialog,WindowPoppup 都可以。
我们以常用的 底部弹窗,居中弹窗,局部下拉选,全屏下拉选,全屏弹窗这五种弹窗类型,由于传递进来一个布局我不知道是哪一种类型,所以根据弹窗类型我们要动态的返回对应的布局。
所以我们以工厂模式来创建对应的实例。
interface IPopupViewCreator<VB : ViewBinding> {
fun create(viewBinding: ((LayoutInflater) -> VB)?): BasePopupView
}
那么基本的五种类型弹窗的创建器如下:
默认下拉选弹窗:
private open class OpenAttachPopupView<VB : ViewBinding>(
context: Context,
private val viewBinding: ((LayoutInflater) -> VB)?,
private val width: Int,
private val height: Int,
private val maxWidth: Int,
private val maxHeight: Int,
private val onCreateListener: ((VB, IPopupController) -> Unit)?,
private val onDismissListener: (() -> Unit)?,
) : AttachPopupView(context) {
//弹窗控制器-常见的功能
private val closeControl = IPopupController { dismiss() }
lateinit var mBinding: VB
override fun addInnerContent() {
if (viewBinding != null) {
mBinding = viewBinding.invoke(LayoutInflater.from(context))
attachPopupContainer.addView(mBinding.root)
} else {
super.addInnerContent()
}
}
override fun getPopupWidth(): Int {
return if (width != 0) width else super.getPopupWidth()
}
override fun getPopupHeight(): Int {
return if (height != 0) height else super.getPopupHeight()
}
// 如果需要覆写 getMaxWidth 和 getMaxHeight,可以提供自定义的最大宽高
override fun getMaxWidth(): Int {
return if (maxWidth != 0) maxWidth else super.getMaxWidth()
}
override fun getMaxHeight(): Int {
return if (maxHeight != 0) maxHeight else super.getMaxHeight()
}
override fun onCreate() {
super.onCreate()
onCreateListener?.invoke(mBinding, closeControl)
}
override fun onDismiss() {
super.onDismiss()
onDismissListener?.invoke()
}
}
class AttachPopupViewCreator<VB : ViewBinding>(
private val context: Context,
private val width: Int,
private val height: Int,
private val maxWidth: Int,
private val maxHeight: Int,
private val onCreateListener: ((VB, IPopupController) -> Unit)?,
private val onDismissListener: (() -> Unit)?,
) : IPopupViewCreator<VB> {
override fun create(viewBinding: ((LayoutInflater) -> VB)?): BasePopupView {
return OpenAttachPopupView<VB>(
context,
viewBinding,
width, height,
maxWidth, maxHeight,
onCreateListener, onDismissListener
)
}
}
全屏弹窗:
private open class OpenFullScreenPopupView<VB : ViewBinding>(
context: Context,
private val viewBinding: ((LayoutInflater) -> VB)?,
private val onCreateListener: ((VB, IPopupController) -> Unit)?,
private val onDismissListener: (() -> Unit)?,
) : FullScreenPopupView(context) {
//弹窗控制器-常见的功能
private val closeControl = IPopupController { dismiss() }
lateinit var mBinding: VB
override fun addInnerContent() {
if (viewBinding != null) {
mBinding = viewBinding.invoke(LayoutInflater.from(context))
fullPopupContainer.addView(mBinding.root)
} else {
super.addInnerContent()
}
}
override fun onCreate() {
super.onCreate()
onCreateListener?.invoke(mBinding, closeControl)
}
override fun onDismiss() {
super.onDismiss()
onDismissListener?.invoke()
}
}
class FullScreenPopupViewCreator<VB : ViewBinding>(
private val context: Context,
private val onCreateListener: ((VB, IPopupController) -> Unit)?,
private val onDismissListener: (() -> Unit)?,
) : IPopupViewCreator<VB> {
override fun create( viewBinding: ((LayoutInflater) -> VB)?): BasePopupView {
return OpenFullScreenPopupView(
context,
viewBinding,
onCreateListener, onDismissListener
)
}
}
全屏下拉选弹窗:
private open class OpenPartPopupView<VB : ViewBinding>(
context: Context,
private val viewBinding: ((LayoutInflater) -> VB)?,
private val height: Int,
private val maxHeight: Int,
private val onCreateListener: ((VB, IPopupController) -> Unit)?,
private val onDismissListener: (() -> Unit)?,
) : PartShadowPopupView(context) {
//弹窗控制器-常见的功能
private val closeControl = IPopupController { dismiss() }
lateinit var mBinding: VB
override fun addInnerContent() {
if (viewBinding != null) {
mBinding = viewBinding.invoke(LayoutInflater.from(context))
attachPopupContainer.addView(mBinding.root)
} else {
super.addInnerContent()
}
}
override fun getPopupHeight(): Int {
return if (height != 0) height else super.getPopupHeight()
}
override fun getMaxHeight(): Int {
return if (maxHeight != 0) maxHeight else super.getMaxHeight()
}
override fun onCreate() {
super.onCreate()
onCreateListener?.invoke(mBinding, closeControl)
}
override fun onDismiss() {
super.onDismiss()
onDismissListener?.invoke()
}
}
class PartPopupViewCreator<VB : ViewBinding>(
private val context: Context,
private val height: Int,
private val maxHeight: Int,
private val onCreateListener: ((VB, IPopupController) -> Unit)?,
private val onDismissListener: (() -> Unit)?,
) : IPopupViewCreator<VB> {
override fun create(viewBinding: ((LayoutInflater) -> VB)?): BasePopupView {
return OpenPartPopupView(
context,
viewBinding,
height,
maxHeight,
onCreateListener, onDismissListener
)
}
}
居中弹窗:
private open class OpenCenterPopupView<VB : ViewBinding>(
context: Context,
private val viewBinding: ((LayoutInflater) -> VB)?,
private val width: Int,
private val height: Int,
private val maxWidth: Int,
private val maxHeight: Int,
private val onCreateListener: ((VB, IPopupController) -> Unit)?,
private val onDismissListener: (() -> Unit)?,
) : CenterPopupView(context) {
//弹窗控制器-常见的功能
private val closeControl = IPopupController { dismiss() }
lateinit var mBinding: VB
override fun addInnerContent() {
if (viewBinding != null) {
mBinding = viewBinding.invoke(LayoutInflater.from(context))
centerPopupContainer.addView(mBinding.root)
} else {
super.addInnerContent()
}
}
override fun getPopupWidth(): Int {
return if (width != 0) width else super.getPopupWidth()
}
override fun getPopupHeight(): Int {
return if (height != 0) height else super.getPopupHeight()
}
// 如果需要覆写 getMaxWidth 和 getMaxHeight,可以提供自定义的最大宽高
override fun getMaxWidth(): Int {
return if (maxWidth != 0) maxWidth else super.getMaxWidth()
}
override fun getMaxHeight(): Int {
return if (maxHeight != 0) maxHeight else super.getMaxHeight()
}
override fun onCreate() {
super.onCreate()
onCreateListener?.invoke(mBinding, closeControl)
}
override fun onDismiss() {
super.onDismiss()
onDismissListener?.invoke()
}
}
class CenterPopupViewCreator<VB : ViewBinding>(
private val context: Context,
private val width: Int,
private val height: Int,
private val maxWidth: Int,
private val maxHeight: Int,
private val onCreateListener: ((VB, IPopupController) -> Unit)?,
private val onDismissListener: (() -> Unit)?,
) : IPopupViewCreator<VB> {
override fun create( viewBinding: ((LayoutInflater) -> VB)?): BasePopupView {
return OpenCenterPopupView(
context,
viewBinding,
width, height,
maxWidth, maxHeight,
onCreateListener, onDismissListener
)
}
}
底部弹窗:
private open class OpenBottomPopupView<VB : ViewBinding>(
context: Context,
private val viewBinding: ((LayoutInflater) -> VB)?,
private val width: Int,
private val height: Int,
private val maxWidth: Int,
private val maxHeight: Int,
private val onCreateListener: ((VB, IPopupController) -> Unit)?,
private val onDismissListener: (() -> Unit)?,
) : BottomPopupView(context) {
//弹窗控制器-常见的功能
private val closeControl = IPopupController { dismiss() }
lateinit var mBinding: VB
override fun addInnerContent() {
if (viewBinding != null) {
mBinding = viewBinding.invoke(LayoutInflater.from(context))
bottomPopupContainer.addView(mBinding.root)
} else {
super.addInnerContent()
}
}
override fun getPopupWidth(): Int {
return if (width != 0) width else super.getPopupWidth()
}
override fun getPopupHeight(): Int {
return if (height != 0) height else super.getPopupHeight()
}
// 如果需要覆写 getMaxWidth 和 getMaxHeight,可以提供自定义的最大宽高
override fun getMaxWidth(): Int {
return if (maxWidth != 0) maxWidth else super.getMaxWidth()
}
override fun getMaxHeight(): Int {
return if (maxHeight != 0) maxHeight else super.getMaxHeight()
}
override fun onCreate() {
super.onCreate()
onCreateListener?.invoke(mBinding, closeControl)
}
override fun onDismiss() {
super.onDismiss()
onDismissListener?.invoke()
}
}
class BottomPopupViewCreator<VB : ViewBinding>(
private val context: Context,
private val width: Int,
private val height: Int,
private val maxWidth: Int,
private val maxHeight: Int,
private val onCreateListener: ((VB, IPopupController) -> Unit)?,
private val onDismissListener: (() -> Unit)?,
) : IPopupViewCreator<VB> {
override fun create(viewBinding: ((LayoutInflater) -> VB)?): BasePopupView {
return OpenBottomPopupView(
context,
viewBinding,
width, height,
maxWidth, maxHeight,
onCreateListener, onDismissListener
)
}
}
最后我们创建一个布局实例工厂来提供对应的布局:
object PopupViewCreatorFactory {
fun <VB : ViewBinding> getCreator(
popupType: PopupType,
context: Context,
width: Int = 0,
height: Int = 0,
maxWidth: Int = 0,
maxHeight: Int = 0,
onCreateListener: ((VB, IPopupController) -> Unit)? = null,
onDismissListener: (() -> Unit)? = null,
): IPopupViewCreator<VB> {
return when (popupType) {
PopupType.BOTTOM -> BottomPopupViewCreator(context, width, height, maxWidth, maxHeight, onCreateListener, onDismissListener)
PopupType.KEYBOARD -> BottomPopupViewCreator(context, width, height, maxWidth, maxHeight, onCreateListener, onDismissListener)
PopupType.CENTER -> CenterPopupViewCreator(context, width, height, maxWidth, maxHeight, onCreateListener, onDismissListener)
PopupType.FULL_SCREEN -> FullScreenPopupViewCreator(context, onCreateListener, onDismissListener)
PopupType.PART_SCREEN -> PartPopupViewCreator(context, height, maxHeight, onCreateListener, onDismissListener)
PopupType.ATTACH -> AttachPopupViewCreator(context, width, height, maxWidth, maxHeight, onCreateListener, onDismissListener)
}
}
}
在入口中我们就能对Xpopup的调用做封装:
private fun <VB : ViewBinding> FragmentActivity.showPopup(
popupType: PopupType, //必须指定类型
viewBinding: (LayoutInflater) -> VB,
popupAnimationType: PopupAnimationType? = null, //动画类型
targetView: View? = null, //如果是Attach则一定要指定在哪个View的基础上弹窗
width: Int = 0,
height: Int = 0, //宽高一般不需要额外设置,自动适配布局的宽高
maxWidth: Int = 0, //如果想要指定宽高,可以自定义宽高
maxHeight: Int = 0,
offsetX: Int = 0, //弹框在 X,Y 方向的偏移值
offsetY: Int = 0,
isCenterHorizontal: Boolean = false, //布局是否水平居中
onCreateListener: ((VB, IPopupController) -> Unit)? = null, //创建的回调
onDismissListener: (() -> Unit)? = null, //消失的回调
) {
//根据类型转换对应的PopupView继承
val creator = PopupViewCreatorFactory.getCreator<VB>(popupType, this, width, height, maxWidth, maxHeight, onCreateListener, onDismissListener)
val popupView = creator.create(viewBinding)
val builder = XPopup.Builder(this)
.hasShadowBg(popupType == PopupType.BOTTOM || popupType == PopupType.CENTER) //默认底部弹窗和中间弹窗展示灰色背景遮罩
.isDestroyOnDismiss(true) //消失的时候是否释放资源
.offsetX(offsetX)
.offsetY(offsetY)
.isCenterHorizontal(isCenterHorizontal)
if (targetView != null) {
builder.atView(targetView)
}
if (popupType == PopupType.KEYBOARD) {
builder.autoOpenSoftInput(true)
.moveUpToKeyboard(true)
}
builder.asCustom(popupView)
.show()
}
二、策略+工厂实现动画类型的设置
接下来就是如何处理动画的类型,Xpopup 预置的动画类型很多,我们项目用到的就那么几种,所以就只使用自己用到的动画类型。
我选择使用策略模式来优化动画处理,首先你需要为每种动画类型定义一个策略接口,然后实现各自的策略类。这样,动画处理将从硬编码的枚举判断转变为动态的策略选择,增强了代码的扩展性和灵活性。
interface IPopupAnimationStrategy {
fun getAnimation(): PopupAnimation?
}
实现:
class ScaleAlphaFromCenterStrategy : IPopupAnimationStrategy {
override fun getAnimation(): PopupAnimation {
return PopupAnimation.ScaleAlphaFromCenter
}
}
class ScaleAlphaFromLeftTopStrategy : IPopupAnimationStrategy {
override fun getAnimation(): PopupAnimation {
return PopupAnimation.ScaleAlphaFromLeftTop
}
}
class ScaleAlphaFromRightTopStrategy : IPopupAnimationStrategy {
override fun getAnimation(): PopupAnimation {
return PopupAnimation.ScaleAlphaFromRightTop
}
}
class ScaleAlphaFromLeftBottomStrategy : IPopupAnimationStrategy {
override fun getAnimation(): PopupAnimation {
return PopupAnimation.ScaleAlphaFromLeftBottom
}
}
class ScaleAlphaFromRightBottomStrategy : IPopupAnimationStrategy {
override fun getAnimation(): PopupAnimation {
return PopupAnimation.ScaleAlphaFromRightBottom
}
}
class TranslateAlphaFromLeftStrategy : IPopupAnimationStrategy {
override fun getAnimation(): PopupAnimation {
return PopupAnimation.TranslateAlphaFromLeft
}
}
class TranslateAlphaFromRightStrategy : IPopupAnimationStrategy {
override fun getAnimation(): PopupAnimation {
return PopupAnimation.TranslateAlphaFromRight
}
}
class TranslateAlphaFromTopStrategy : IPopupAnimationStrategy {
override fun getAnimation(): PopupAnimation {
return PopupAnimation.TranslateAlphaFromTop
}
}
class TranslateAlphaFromBottomStrategy : IPopupAnimationStrategy {
override fun getAnimation(): PopupAnimation {
return PopupAnimation.TranslateAlphaFromBottom
}
}
class TranslateFromLeftStrategy : IPopupAnimationStrategy {
override fun getAnimation(): PopupAnimation {
return PopupAnimation.TranslateFromLeft
}
}
class TranslateFromRightStrategy : IPopupAnimationStrategy {
override fun getAnimation(): PopupAnimation {
return PopupAnimation.TranslateFromRight
}
}
class TranslateFromTopStrategy : IPopupAnimationStrategy {
override fun getAnimation(): PopupAnimation {
return PopupAnimation.TranslateFromTop
}
}
class TranslateFromBottomStrategy : IPopupAnimationStrategy {
override fun getAnimation(): PopupAnimation {
return PopupAnimation.TranslateFromBottom
}
}
class NoAnimationStrategy : IPopupAnimationStrategy {
override fun getAnimation(): PopupAnimation {
return PopupAnimation.NoAnimation
}
}
关于动画策略工厂,为了方便创建具体的策略实例,实现了一个工厂类。
object PopupAnimationStrategyFactory {
fun getStrategy(animationType: PopupAnimationType?): IPopupAnimationStrategy? {
return when (animationType) {
PopupAnimationType.ScaleAlphaFromCenter -> ScaleAlphaFromCenterStrategy()
PopupAnimationType.ScaleAlphaFromLeftTop -> ScaleAlphaFromLeftTopStrategy()
PopupAnimationType.ScaleAlphaFromRightTop -> ScaleAlphaFromRightTopStrategy()
PopupAnimationType.ScaleAlphaFromLeftBottom -> ScaleAlphaFromLeftBottomStrategy()
PopupAnimationType.ScaleAlphaFromRightBottom -> ScaleAlphaFromRightBottomStrategy()
PopupAnimationType.TranslateAlphaFromLeft -> TranslateAlphaFromLeftStrategy()
PopupAnimationType.TranslateAlphaFromRight -> TranslateAlphaFromRightStrategy()
PopupAnimationType.TranslateAlphaFromTop -> TranslateAlphaFromTopStrategy()
PopupAnimationType.TranslateAlphaFromBottom -> TranslateAlphaFromBottomStrategy()
PopupAnimationType.TranslateFromLeft -> TranslateFromLeftStrategy()
PopupAnimationType.TranslateFromRight -> TranslateFromRightStrategy()
PopupAnimationType.TranslateFromTop -> TranslateFromTopStrategy()
PopupAnimationType.TranslateFromBottom -> TranslateFromBottomStrategy()
PopupAnimationType.NoAnimation -> NoAnimationStrategy()
else -> null
}
}
}
修改上面的入口代码:
private fun <VB : ViewBinding> FragmentActivity.showPopup(
popupType: PopupType, //必须指定类型
viewBinding: (LayoutInflater) -> VB,
popupAnimationType: PopupAnimationType? = null, //动画类型
targetView: View? = null, //如果是Attach则一定要指定在哪个View的基础上弹窗
width: Int = 0,
height: Int = 0, //宽高一般不需要额外设置,自动适配布局的宽高
maxWidth: Int = 0, //如果想要指定宽高,可以自定义宽高
maxHeight: Int = 0,
offsetX: Int = 0, //弹框在 X,Y 方向的偏移值
offsetY: Int = 0,
isCenterHorizontal: Boolean = false, //布局是否水平居中
onCreateListener: ((VB, IPopupController) -> Unit)? = null, //创建的回调
onDismissListener: (() -> Unit)? = null, //消失的回调
) {
//根据类型转换对应的PopupView继承
val creator = PopupViewCreatorFactory.getCreator<VB>(popupType, this, width, height, maxWidth, maxHeight, onCreateListener, onDismissListener)
val popupView = creator.create(viewBinding)
val builder = XPopup.Builder(this)
.hasShadowBg(popupType == PopupType.BOTTOM || popupType == PopupType.CENTER) //默认底部弹窗和中间弹窗展示灰色背景遮罩
.isDestroyOnDismiss(true) //消失的时候是否释放资源
.offsetX(offsetX)
.offsetY(offsetY)
.isCenterHorizontal(isCenterHorizontal)
if (targetView != null) {
builder.atView(targetView)
}
if (popupType == PopupType.KEYBOARD) {
builder.autoOpenSoftInput(true)
.moveUpToKeyboard(true)
}
builder.asCustom(popupView)
.show()
}
//获取动画类型
fun transformAnimationType(animationType: PopupAnimationType?): PopupAnimation? {
val strategy = PopupAnimationStrategyFactory.getStrategy(animationType)
return strategy?.getAnimation()
}
三、建造者模式创建入参
建造者模式可以非常适合用于进一步提升弹窗创建的灵活性和可读性。建造者模式能让你构建一个复杂对象的多个版本(例如不同配置的弹窗),而无需创建多个构造器,这样代码会更加清晰。
设置一些配置选项:
//配置选项
data class PopupConfig<VB : ViewBinding>(
val popupType: PopupType,
val viewBinding: (LayoutInflater) -> VB,
val popupAnimationType: PopupAnimationType? = null,
val targetView: View? = null,
val width: Int = 0,
val height: Int = 0,
val maxWidth: Int = 0,
val maxHeight: Int = 0,
val offsetX: Int = 0,
val offsetY: Int = 0,
val isCenterHorizontal: Boolean = false,
val onCreateListener: ((VB, IPopupController) -> Unit)? = null,
val onDismissListener: (() -> Unit)? = null
)
设置构建者模式:
class DialogEngine<VB : ViewBinding>(private val activity: FragmentActivity) {
private var config = PopupConfig<VB>(
popupType = PopupType.CENTER,
viewBinding = { throw IllegalArgumentException("ViewBinding must be set.") }
)
//默认居中弹窗,需要指定类型
fun setPopupType(popupType: PopupType) = apply { config = config.copy(popupType = popupType) }
//一定要设置布局
fun setViewBinding(viewBinding: (LayoutInflater) -> VB) = apply { config = config.copy(viewBinding = viewBinding) }
//选填弹窗动画方式
fun setPopupAnimationType(popupAnimationType: PopupAnimationType?) = apply { config = config.copy(popupAnimationType = popupAnimationType) }
//依附布局必须指定
fun setTargetView(targetView: View?) = apply { config = config.copy(targetView = targetView) }
//其他选填项
fun setWidth(width: Int) = apply { config = config.copy(width = width) }
fun setHeight(height: Int) = apply { config = config.copy(height = height) }
fun setMaxWidth(maxWidth: Int) = apply { config = config.copy(maxWidth = maxWidth) }
fun setMaxHeight(maxHeight: Int) = apply { config = config.copy(maxHeight = maxHeight) }
fun setOffsetX(offsetX: Int) = apply { config = config.copy(offsetX = offsetX) }
fun setOffsetY(offsetY: Int) = apply { config = config.copy(offsetY = offsetY) }
fun isCenterHorizontal(isCenterHorizontal: Boolean) = apply { config = config.copy(isCenterHorizontal = isCenterHorizontal) }
fun setOnCreateListener(listener: (VB, IPopupController) -> Unit) = apply { config = config.copy(onCreateListener = listener) }
fun setOnDismissListener(listener: () -> Unit) = apply { config = config.copy(onDismissListener = listener) }
fun show() {
//根据类型转换对应的PopupView继承
val creator = PopupViewCreatorFactory.getCreator<VB>(popupType, this, width, height, maxWidth, maxHeight, onCreateListener, onDismissListener)
val popupView = creator.create(viewBinding)
val builder = XPopup.Builder(this)
.hasShadowBg(popupType == PopupType.BOTTOM || popupType == PopupType.CENTER) //默认底部弹窗和中间弹窗展示灰色背景遮罩
.isDestroyOnDismiss(true) //消失的时候是否释放资源
.offsetX(offsetX)
.offsetY(offsetY)
.isCenterHorizontal(isCenterHorizontal)
if (targetView != null) {
builder.atView(targetView)
}
if (popupType == PopupType.KEYBOARD) {
builder.autoOpenSoftInput(true)
.moveUpToKeyboard(true)
}
//转换动画效果,是否需要添加动画
if (transformAnimationType(popupAnimationType) != null) {
builder.popupAnimation(transformAnimationType(popupAnimationType))
}
builder.asCustom(popupView)
.show()
}
}
这样我们就可以使用啦
mBinding.btnBottomPopup.click {
DialogEngine<PopopBottomCardBinding>(mActivity)
.setPopupType(PopupType.BOTTOM)
.setViewBinding(PopopBottomCardBinding::inflate)
.setOnCreateListener { binding, control ->
binding.tvClearAll.click {
toast("点击清理")
control.dismiss()
}
}
.show()
}
mBinding.btnCenterPopup.click {
//同样的布局,居中弹窗展示
DialogEngine<PopopBottomCardBinding>(mActivity)
.setViewBinding(PopopBottomCardBinding::inflate)
.setOnCreateListener { binding, control ->
binding.tvClearAll.click {
toast("点击清理")
control.dismiss()
}
}
.show()
}
四、扩展方法
如果说建造者模式比较适合 Java 项目,那么在 Kotlin 语言中天然就支持构建者的方式,可以给定默认值,必填值与选填值。
简单的概况,如果你想 API 能更好的支持 Java 语言调用,那么使用建造者是不错的方案,如果你的项目就是自用的并且是 Kotlin 项目,直接使用扩展方法就可以达成目的了。
例如我们直接对Activity做扩展,设置弹窗类型和弹窗布局是必选的,再设置默认值的可选参数:
fun <VB : ViewBinding> FragmentActivity.showPopup(
popupType: PopupType, //必须指定类型
viewBinding: (LayoutInflater) -> VB,
popupAnimationType: PopupAnimationType? = null, //动画类型
targetView: View? = null, //如果是Attach则一定要指定在哪个View的基础上弹窗
width: Int = 0,
height: Int = 0, //宽高一般不需要额外设置,自动适配布局的宽高
maxWidth: Int = 0, //如果想要指定宽高,可以自定义宽高
maxHeight: Int = 0,
offsetX: Int = 0, //弹框在 X,Y 方向的偏移值
offsetY: Int = 0,
isCenterHorizontal: Boolean = false, //布局是否水平居中
onCreateListener: ((VB, IPopupController) -> Unit)? = null, //创建的回调
onDismissListener: (() -> Unit)? = null, //消失的回调
) {
//根据类型转换对应的PopupView继承
val creator = PopupViewCreatorFactory.getCreator<VB>(popupType, this, width, height, maxWidth, maxHeight, onCreateListener, onDismissListener)
val popupView = creator.create(viewBinding)
val builder = XPopup.Builder(this)
.hasShadowBg(popupType == PopupType.BOTTOM || popupType == PopupType.CENTER) //默认底部弹窗和中间弹窗展示灰色背景遮罩
.isDestroyOnDismiss(true) //消失的时候是否释放资源
.offsetX(offsetX)
.offsetY(offsetY)
.isCenterHorizontal(isCenterHorizontal)
if (targetView != null) {
builder.atView(targetView)
}
if (popupType == PopupType.KEYBOARD) {
builder.autoOpenSoftInput(true)
.moveUpToKeyboard(true)
}
//转换动画效果,是否需要添加动画
if (transformAnimationType(popupAnimationType) != null) {
builder.popupAnimation(transformAnimationType(popupAnimationType))
}
builder.asCustom(popupView)
.show()
}
//获取动画类型
fun transformAnimationType(animationType: PopupAnimationType?): PopupAnimation? {
val strategy = PopupAnimationStrategyFactory.getStrategy(animationType)
return strategy?.getAnimation()
}
使用:
mBinding.btnFullscreenPopup.click {
//同样的布局,全屏弹窗展示
showPopup(
PopupType.FULL_SCREEN,
PopopBottomCardBinding::inflate,
onCreateListener = { binding, control ->
binding.tvClearAll.click {
toast("点击清理")
control.dismiss()
}
}
)
}
五、使用与效果
我们把构建者模式与扩展方法模式的两种方案同时使用,就可以看到效果
mBinding.btnBottomPopup.click {
DialogEngine<PopopBottomCardBinding>(mActivity)
.setPopupType(PopupType.BOTTOM)
.setViewBinding(PopopBottomCardBinding::inflate)
.setOnCreateListener { binding, control ->
binding.tvClearAll.click {
toast("点击清理")
control.dismiss()
}
}
.show()
}
mBinding.btnCenterPopup.click {
//同样的布局,居中弹窗展示
DialogEngine<PopopBottomCardBinding>(mActivity)
.setViewBinding(PopopBottomCardBinding::inflate)
.setOnCreateListener { binding, control ->
binding.tvClearAll.click {
toast("点击清理")
control.dismiss()
}
}
.show()
}
mBinding.btnFullscreenPopup.click {
//同样的布局,全屏弹窗展示
showPopup(
PopupType.FULL_SCREEN,
PopopBottomCardBinding::inflate,
onCreateListener = { binding, control ->
binding.tvClearAll.click {
toast("点击清理")
control.dismiss()
}
}
)
}
mBinding.btnFilterPopup.click {
val data = listOf(
"Rewards", "Promotion", "Mall", "Cook", "Delivery", "Queue", "Running", "CV", "Resume", "Me", "Profile",
"News", "Moment", "Feeds", "Jobs", "Cleaner"
)
showPopup(
PopupType.PART_SCREEN,
PopupCategoryFilterBinding::inflate,
targetView = it,
onCreateListener = { binding, control ->
binding.recyclerView.apply {
vertical()
bindData(data, R.layout.item_filter_category) { holder, item, position ->
holder.setText(R.id.tv_category_name, item)
}
}
}
)
}
mBinding.btnDropdownPopup.click {
val data = listOf(
"Rewards", "Promotion", "Mall", "Cook", "Delivery", "Queue", "Running", "CV", "Resume", "Me", "Profile",
"News", "Moment", "Feeds", "Jobs", "Cleaner"
)
showPopup(
PopupType.ATTACH,
PopupCategoryFilterBinding::inflate,
targetView = it,
maxHeight = dp2px(250f),
isCenterHorizontal = true,
onCreateListener = { binding, control ->
binding.recyclerView.apply {
vertical()
bindData(data, R.layout.item_filter_category) { holder, item, position ->
holder.setText(R.id.tv_category_name, item)
}
}
}
)
}
mBinding.btnKeyboardPopup.click {
showPopup(
PopupType.BOTTOM,
PopupEditViewBinding::inflate,
onCreateListener = { binding, control ->
binding.btnFinish.click {
control.dismiss()
}
}
)
}
最终的效果是:
后记
本文是以自定义布局弹窗为示例演示如何在基类组件中定义功能引擎,方便在其他上层组件中使用。收紧权限之后可以方便引擎内部升级与替换。
由于是自用的,在第三方库的基础上做了自己项目的专有配置,比如我们使用的 ViewBinding 来填充的布局,如果你不想使用 ViewBinding 可以直接设置布局id的参数,然后在加载布局的时候添加到弹窗容器中即可。
除了布局弹窗引擎,常见的我们需要做一些类似相机、相册、文件、权限、图片加载、网络请求等常用功能的引擎,在引擎的基础上我们就能方便做各个功能的 UseCase 逻辑,例如多媒体选择UseCase、文件选择UseCase,权限申请UseCase,图片下载UseCase,图片预览UseCase,生物识别UseCase等等。我们在App开发的时候在上层直接调用引擎类或者对应的UseCase即可。
那本文源码在文中已经全部展出作为参考,最后惯例,如果有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。如果我的文章有错别字,不通顺的,或者代码/注释有错漏的地方,同学们都可以指出修正。
如果感觉本文对你有一点的启发和帮助,还望你能点赞
支持一下,你的支持对我真的很重要。
如果你看到了这里,觉得文章写得不错就给个赞呗?
更多Android进阶指南 可以扫码 解锁更多Android进阶资料
敲代码不易,关注一下吧。ღ( ´・ᴗ・` )