【工具类】BaseDialog的封装(Kotlin+Binding+spannableBuilder的应用)

581 阅读6分钟

一、自定义Dialog

已加入:样品工具库:kirikaTowa/AndroidUtils: Common tool library (github.com)

1、 参考文章:[android]我是这样写自定义Dialog的

  1. 改版原因:缺少部分代码,进行补齐,代码精简;使用kotlion+lambda进行yes,no选项的优化,使用Databinding与stateCanceled(可否点击外部关闭)。

2、 相关代码

  1. BaseDialog
abstract class BaseDialog<VB : ViewDataBinding>(
    var context: Context
) {
    val DIALOG_COMMON_STYLE:Int= R.style.common_dialog_style
    private val display: Display
    private var dialog : Dialog? = null
    abstract val layoutId: Int


    protected abstract val dialogStyleId: Int
    protected abstract val stateCanceled: Boolean
    protected lateinit var binding: VB


    init {
        binding = DataBindingUtil.inflate(LayoutInflater.from(context),layoutId, null, false)
        val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        display = windowManager.defaultDisplay
        dialog = if (dialogStyleId == 0) {
            Dialog(context, DIALOG_COMMON_STYLE)
        } else {
            Dialog(context, dialogStyleId)
        }
        // 调整dialog背景大小
        binding.root.layoutParams = FrameLayout.LayoutParams(
            (display.width * 0.8).toInt(),
            LinearLayout.LayoutParams.WRAP_CONTENT
        )

        dialog?.setContentView(binding.root)
        //隐藏系统输入盘
        dialog?.setCanceledOnTouchOutside(stateCanceled)
        dialog?.window!!
            .setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
        initViewEvent()
    }
    protected open fun initViewEvent() {}

    fun show() {
        dialog!!.show()
    }

    fun dismiss() {
        dialog!!.dismiss()
    }

    val isShowing: Boolean
        get() = dialog!!.isShowing
}
  1. Theme
<style name="common_dialog_style" parent="@android:style/Theme.Dialog">
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:windowFrame">@null</item>
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowIsFloating">true</item>
    <item name="android:windowIsTranslucent">true</item>
    <item name="android:windowContentOverlay">@null</item>
    <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
    <item name="android:backgroundDimEnabled">true</item>
</style>
  1. 子类Demo(附带了常规的string设置颜色和相应点击事件:spannableBuilder.setSpan)
class StatementDialog  
    (context: Context?, var callback: ((Boolean) -> Unit)?) :
    BaseDialog<DialogStatementBinding>(context!!) {


    override val dialogStyleId: Int
        get() = DIALOG_COMMON_STYLE
    override val stateCanceled: Boolean
        get() = false
    override val layoutId: Int
        get() = R.layout.dialog_statement

    //View的事件
    override fun initViewEvent() {
        //设置对话框那个叉叉的方法,点击关闭对话框
        binding.apply {
            tvYes.setOnClickListener {
                callback?.invoke(true)
            }

            tvNo.setOnClickListener {
                callback?.invoke(false)
            }


            val stringContext = context.resources.getString(R.string.str_statement_content)
          /*  val colorSpan =
                ForegroundColorSpan(ContextCompat.getColor(context, R.color.color_effect_180))*/

            val stringProtocolOne = context.resources.getString(R.string.str_protocol_one)
            val positionOne = stringContext.indexOf(stringProtocolOne)
            val spannableBuilder = SpannableStringBuilder(stringContext)
         /*   // 单独设置字体颜色

            spannableBuilder.setSpan(
                colorSpan,
                positionOne,
                positionOne+stringProtocolOne.length,
                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
            )*/
            // 单独设置点击事件
            val clickableSpanOne: ClickableSpan = object : ClickableSpan() {

                override fun onClick(widget: View) {
                    WebViewActivity.launch(context as Activity,WebViewActivity.URL_PROTOCOL_ONE)
                    (context as Activity).overridePendingTransition(R.anim.anim_in_up, R.anim.anim_origin)
                }

                override fun updateDrawState(ds: TextPaint) {
                    /**set textColor**/
                    ds.color = ds.linkColor
                    /**Remove the underline**/
                    ds.isUnderlineText = false
                }
            }

            spannableBuilder.setSpan(clickableSpanOne,    positionOne,
                positionOne+stringProtocolOne.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

            val stringProtocolTwo = context.resources.getString(R.string.str_protocol_two)
            val positionTwo = stringContext.indexOf(stringProtocolTwo)
            // 单独设置点击事件
            val clickableSpanTwo: ClickableSpan = object : ClickableSpan() {

                override fun onClick(widget: View) {
                   WebViewActivity.launch(context as Activity,WebViewActivity.URL_PROTOCOL_TWO)
                    (context as Activity).overridePendingTransition(R.anim.anim_in_up, R.anim.anim_origin)
                }

                override fun updateDrawState(ds: TextPaint) {
                    /**set textColor**/
                    ds.color = ds.linkColor
                    /**Remove the underline**/
                    ds.isUnderlineText = false
                }
            }

            spannableBuilder.setSpan(clickableSpanOne,    positionOne,
                positionOne+stringProtocolOne.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

            spannableBuilder.setSpan(clickableSpanTwo,    positionTwo,
                positionTwo+stringProtocolTwo.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
            tvContent.movementMethod = LinkMovementMethod.getInstance()
            tvContent.highlightColor = Color.TRANSPARENT
            tvContent.text = spannableBuilder;
        }
    }
}
  1. 子类对应layout
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/rl_dialog_pwd"
        android:layout_width="match_parent"
        android:layout_marginStart="@dimen/dp_50"
        android:layout_marginEnd="@dimen/dp_50"
        android:layout_height="wrap_content"
        android:background="@drawable/shape_confirm_dialog_bg"
        android:orientation="vertical">    <!-- 取消对话框按钮图标 -->

        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/iv_statement"
            android:layout_width="@dimen/dp_48"
            android:layout_height="@dimen/dp_48"
            android:layout_marginTop="@dimen/dp_15"
            android:src="@drawable/ic_statement"
            app:layout_constraintEnd_toStartOf="@id/tv_dialog_statement_title"
            app:layout_constraintHorizontal_chainStyle="packed"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/tv_dialog_statement_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="@dimen/dp_10"
            android:layout_marginTop="@dimen/dp_8"
            android:text="个人信息保护提示"
            android:textColor="@color/black"
            android:textSize="@dimen/dp_20"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="@id/iv_statement"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toEndOf="@+id/iv_statement"
            app:layout_constraintTop_toTopOf="@id/iv_statement" />

        <TextView
            android:id="@+id/tv_content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="@dimen/dp_15"
            android:padding="@dimen/dp_15"
            android:text="@string/str_statement_content"
            app:layout_constraintTop_toBottomOf="@id/iv_statement" />

        <View
            android:id="@+id/view_line"
            app:layout_constraintTop_toBottomOf="@id/tv_content"
            android:layout_width="match_parent"
            android:layout_height="@dimen/dp_1"
            android:background="#aaaaaa" />

        <TextView
            android:id="@+id/tv_no"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="@string/str_statement_no"
            android:paddingTop="@dimen/dp_20"
            app:layout_constraintStart_toStartOf="parent"
            android:gravity="center"
            app:layout_constraintEnd_toStartOf="@id/tv_yes"
            android:paddingBottom="@dimen/dp_20"
            app:layout_constraintTop_toBottomOf="@id/view_line"
            app:layout_constraintBottom_toBottomOf="parent" />

        <View
            android:id="@+id/view_line_guide"
            app:layout_constraintTop_toTopOf="@id/view_line"
            app:layout_constraintBottom_toBottomOf="@id/tv_yes"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_width="@dimen/dp_1"
            android:layout_height="0dp"
            android:background="#aaaaaa" />

        <TextView
            android:id="@+id/tv_yes"
            android:layout_width="0dp"
            android:gravity="center"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_height="wrap_content"
            android:text="@string/str_statement_yes"
            app:layout_constraintStart_toEndOf="@id/tv_no"
            android:paddingTop="@dimen/dp_20"
            android:paddingBottom="@dimen/dp_20"
            app:layout_constraintTop_toBottomOf="@id/view_line"
            app:layout_constraintBottom_toBottomOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
  1. 图例

image.png

  1. Activity中调用方式
var stateDialog:StatementDialog?=null
     stateDialog= StatementDialog(this@MainActivity) {
        if (it) {//点击了同意
            stateDialog?.dismiss()
        } else {//点击了不同意
            finish()
            stateDialog?.dismiss()
        }
    }
    stateDialog.show()
}

二、改版

1、改版原因

1、Dialog类移动到一个老项目里发现宽高受到了限制,像是有padding。

  • Theme.Dialog中确实有listPreferredItemPadd,但是新项目不受影响,老项目受影响;
  • 写了一个继承Dialog后的,代码中手动调整发现ok了,代码已更新,在仓库里就不贴出来水字数了
class DialogComplaintFullScreen(context: Context,var callback: ((Boolean) -> Unit)?) : Dialog(context) {

    private lateinit var binding: DialogComplaintBinding


    fun setIvComplaint(context: Context,url: String?) {
    Glide.with(context).load(url).into(binding.ivComplaint)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.dialog_complaint, null, false)

        setContentView(binding.root )
        setCancelable(true) //按返回可以关闭
        setCanceledOnTouchOutside(true) //按屏幕外面可以关闭
        //设置背景 xml中设置无效
        window?.decorView?.setBackgroundResource(R.color.transparent)
        //设置尺寸 Base可提供个方法供子view实现
        window?.setLayout(DeviceUtil.dip2px(context, 488f), DeviceUtil.dip2px(context, 800f))
        window?.decorView?.setPadding(0, 0, 0, 0)

        binding.btnDismiss.setOnClickListener {
            callback?.invoke(true)
        }
    }
}

三、其他Dialog知识

1、多种Dialog样式

  1. Android常用对话框大全

  2. 安卓AlertDialog弹窗_沐游虞的博客

  3. 关于全屏:三句代码创建全屏Dialog或者DialogFragment:带你从源码角度实现

3、小技巧

4、Dialog对于获取Activity还是蛮严格的:Context转Activity

5、Dialog使用Kotlin队列完成依次弹窗的责任链模式:

lifecycleScope.launch {
    showDialog("签到活动", "签到领10000币") // 直到dialog被关闭, 才会继续运行下一行
}

lifecycleScope.launch {
    showDialog("新手任务", "做任务领20000币") // 直到dialog被关闭, 才会继续运行下一行
    showDialog("首充奖励", "首充6元送神装")
}

四、DialogFragment

1、手把手带你玩转 DialogFragment

2、dialogfragment监听返回键_dialogfragment 返回键监听

  • 在onCreateDialog的Dialog里要dialog.setOnKeyListener(this);

3、Fragment中的Crash排查

  • Fragment必要有个无参构造方法
  • 文章里的instance不是单例,提供个静态方法创建dialog;

4、生命周期 * DialogFragment 不会触发原fragment和Activity的onresume

5、关于全屏

  • DialogFragment自身设定就不是全屏的,宽度受限 *宽度根据内容自适应,但最大不会超过某个限度,可能受屏幕比例影响
  • 正常情况Margin和Padding都是有效的,但效果有限,超过最大限度的宽度则不生效
  • 但有的机子可能只有padding会生效

总体来说DialogFragment还是和普通Fragment很像的 Dialog默认会把View居中,想跳位置的话,margin没啥卵用,加个透明VIEW补视差

  1. 方法一
  • 简单居中
override fun onStart() { super.onStart() dialog?.window?.setLayout(-1,-2) }
  1. 方法二
  • 将view置于底部
 /**
     * 设置弹框宽度全屏
     */
    override fun onStart() {
        super.onStart()
        val win = dialog!!.window
        // 一定要设置Background,如果不设置,window属性设置无效
        win!!.setBackgroundDrawableResource(android.R.color.transparent)
        val dm = DisplayMetrics()
        requireActivity().windowManager.defaultDisplay.getMetrics(dm)
        val params = win.attributes
        params.gravity = Gravity.BOTTOM
        // 使用ViewGroup.LayoutParams,以便Dialog 宽度充满整个屏幕
        params.width = ViewGroup.LayoutParams.MATCH_PARENT
        params.height = ViewGroup.LayoutParams.WRAP_CONTENT
//        params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
        win.attributes = params
    }

6、改变蒙层深度

  override fun onStart() {
        super.onStart()
        dialog?.window?.apply {
            // 设置背景蒙层深度,0.0f = 无蒙层,1.0f = 全黑
            setDimAmount(0.6f)
        }
    }