Android 自定義彈窗框架

前言

在項目開發中,我們經常需要創建各種各樣的自定義彈窗,爲了簡約代碼和統一管理,我初步搭建了這麼一個簡單的框架,待日後在新項目中使用後再不斷維護。

------ 更新 2019/11/27 ------

  • 增加單選彈窗
  • 修復魅族手機出現編輯框清空崩潰的問題
  • 增加彈窗初始化佈局寬高方向代碼

樣式圖

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
這只是初始的想法搭建的框架,本文只說明思維方式,並不提供其他類型彈窗代碼,有需要的朋友根據以下代碼,進行自定義開發

另外本文也是提供一種在每個彈窗上都帶有進度條的代碼方法,供大家參考

寫一個Base類 BaseDialog

/**
 * 基礎彈窗
 *
 * @author D10NG
 * @date on 2019-10-25 14:49
 */
open class BaseDialog<T> constructor(
    private val context: Context
) {
    /** 彈窗創建器 */
    protected val builder = AlertDialog.Builder(context)
    /** 最終的彈窗實例 */
    protected var alert: AlertDialog? = null

    /** 基礎彈窗佈局 */
    val binding: DialogDefLoadingViewBinding = DataBindingUtil.inflate(
        LayoutInflater.from(context),
        R.layout.dialog_def_loading_view, null, false)

    /**
     * 創建
     */
    open fun create() : T {
        binding.loadIndeterminate = true
        binding.loadVisible = false
        builder.setView(binding.root)
        builder.setCancelable(false)
        alert = builder.create()
        return this as T
    }

    /**
     * 移除所有button
     */
    open fun removeAllButtons() {
        binding.buttonLayout.removeAllViews()
    }

    /**
     * 移除content內容
     */
    open fun removeContent() {
        binding.contentLayout.removeAllViews()
    }

    /**
     * 設置標題
     */
    open fun setTittle(tittle: String) : T {
        binding.tittle = tittle
        return this as T
    }

    /**
     * 設置二級文本
     */
    open fun setMsg(msg: String) : T {
        binding.message = msg
        return this as T
    }

    /**
     * 設置圖標
     */
    open fun setIcon(resId: Int) : T {
        binding.image.setImageResource(resId)
        return this as T
    }

    /**
     * 設置圖標
     */
    open fun setIcon(bitmap: Bitmap) : T {
        binding.image.setImageBitmap(bitmap)
        return this as T
    }

    /**
     * 開始加載中
     */
    open fun startLoad(indeterminate: Boolean, progress: Int, max: Int) {
        binding.loadIndeterminate = indeterminate
        binding.loadProgress = progress
        binding.loadMax = max
        binding.loadVisible = true
    }

    /**
     * 停止加載中
     */
    open fun stopLoad() {
        binding.loadVisible = false
    }

    /**
     * 添加button事件
     */
    open fun addAction(text: String, style: Int, onBtnClick: OnBtnClick?) : T {
        binding.buttonLayout.addView(createButton(text, style, onBtnClick))
        return this as T
    }

    /**
     * 顯示
     */
    open fun show() {
        alert?.show()
    }

    /**
     * 關閉
     */
    open fun dismiss() {
        alert?.dismiss()
    }

    protected fun createButton(text: String, style: Int, onBtnClick: OnBtnClick?) : Button {
        val button = Button(context)
        button.background = ContextCompat.getDrawable(context, R.drawable.button_top_line_bg)
        button.text = text
        when(style) {
            ButtonStyle.THEME -> button.setTextColor(ContextCompat.getColor(context, R.color.colorPrimary))
            ButtonStyle.NORMAL -> button.setTextColor(ContextCompat.getColor(context, R.color.text_hint_color))
            ButtonStyle.ERROR -> button.setTextColor(ContextCompat.getColor(context, R.color.text_wrong_color))
        }
        button.setOnClickListener {
            if (null == onBtnClick) {
                dismiss()
            } else {
                onBtnClick.click(this, text)
            }
        }
        return button
    }
}

建立基本彈窗佈局 dialog_def_loading_view.xml

此處用了 DataBinding 使用方法參考我的另一篇文章 :
Android Kotlin學習 Jitpack 組件之DataBinding

<?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>
        <import type="android.view.View"/>
        <variable
            name="loadIndeterminate"
            type="boolean" />
        <variable
            name="loadProgress"
            type="int" />
        <variable
            name="loadMax"
            type="int" />
        <variable
            name="loadVisible"
            type="boolean" />
        <variable
            name="tittle"
            type="String" />
        <variable
            name="message"
            type="String" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/white">

        <TextView
            android:id="@+id/txt_tittle"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="16dp"
            android:gravity="center"
            android:singleLine="true"
            android:text="@{tittle}"
            android:textColor="#ff333333"
            android:textSize="18sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <ImageView
            android:id="@+id/image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="16dp"
            android:adjustViewBounds="true"
            android:scaleType="centerInside"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/txt_tittle" />

        <TextView
            android:id="@+id/txt_message"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="16dp"
            android:ellipsize="end"
            android:lineSpacingExtra="6sp"
            android:maxLines="10"
            android:text="@{message}"
            android:textAlignment="center"
            android:textColor="#ff666666"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/image" />

        <LinearLayout
            android:id="@+id/content_layout"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="16dp"
            android:orientation="vertical"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/txt_message">

        </LinearLayout>

        <ProgressBar
            android:id="@+id/pb_load"
            style="?android:attr/progressBarStyleHorizontal"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="16dp"
            android:indeterminate="@{loadIndeterminate}"
            android:max="@{loadMax}"
            android:progress="@{loadProgress}"
            android:visibility="@{loadVisible? View.VISIBLE : View.INVISIBLE}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/content_layout" />

        <LinearLayout
            android:id="@+id/button_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/pb_load">

        </LinearLayout>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

寫一個按鈕樣式選擇 ButtonStyle

object ButtonStyle {
    const val THEME = 1
    const val ERROR = 2
    const val NORMAL = 3
}

寫一個按鈕點擊監聽 OnBtnClick

interface OnBtnClick {
    fun click(d0: BaseDialog<*>, text: String)
}

自定義彈窗 ButtonDialog

class ButtonDialog constructor(
    context: Context
) : BaseDialog<ButtonDialog>(context)

自定義彈窗 EditDialog

佈局

dialog_edit_view.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/ti_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:errorEnabled="true">

            <androidx.appcompat.widget.AppCompatEditText
                android:id="@+id/edt_text"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="hint" />
        </com.google.android.material.textfield.TextInputLayout>
    </LinearLayout>
</layout>

代碼

/**
 * 帶編輯框的彈窗
 *
 * @author D10NG
 * @date on 2019-11-23 10:05
 */
class EditDialog constructor(
    private val context: Context
) : BaseDialog<EditDialog>(context) {

    init {
        // 改變寬度
        val params = binding.contentLayout.layoutParams
        params.width = LinearLayout.LayoutParams.MATCH_PARENT
        binding.contentLayout.layoutParams = params
    }

    /** 編輯框列表 */
    private val edtMap: MutableMap<String, DialogEditViewBinding> = mutableMapOf()

    /**
     * 添加一個編輯框
     * @param tag 標籤
     * @param text 初始文本
     * @param hint 提示文本
     */
    fun addEdit(tag: String, text: String, hint: String) : EditDialog {
        val viewBinding: DialogEditViewBinding = DataBindingUtil.inflate(
            LayoutInflater.from(context),
            R.layout.dialog_edit_view, null, false
        )
        viewBinding.edtText.setText(text)
        viewBinding.tiLayout.hint = hint
        binding.contentLayout.addView(viewBinding.root)
        edtMap[tag] = viewBinding
        return this
    }

    /**
     * 獲取輸入文本
     * @param tag 標籤
     */
    fun getInputText(tag: String) : String {
        return edtMap[tag]?.edtText?.text.toString().trim()
    }

    /**
     * 顯示錯誤信息
     * @param tag 標籤
     * @param value 信息
     */
    fun setError(tag: String, value: String) : EditDialog {
        edtMap[tag]?.tiLayout?.error = value
        return this
    }
}

自定義彈窗 SingleChooseDialog

佈局

dialog_recycle_view.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="startText"
            type="String" />
        <variable
            name="endText"
            type="String" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/txt_start"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="@{startText}" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rcv"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1">

        </androidx.recyclerview.widget.RecyclerView>

        <TextView
            android:id="@+id/txt_end"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="@{endText}" />

    </LinearLayout>
</layout>

dialog_normal_item_view.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="itemText"
            type="String" />
        <variable
            name="isSelected"
            type="boolean" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="35dp"
        android:gravity="center"
        android:orientation="vertical">

        <TextView
            android:id="@+id/text"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:ellipsize="middle"
            android:gravity="center"
            android:minWidth="100dp"
            android:text="@{itemText}"
            android:textColor="@{isSelected? @color/colorPrimary : @color/text_hint_color}"
            android:textSize="@{isSelected? 80 : 50}" />
    </LinearLayout>
</layout>

代碼

/**
 * 單選彈窗
 *
 * @author D10NG
 * @date on 2019-11-25 16:36
 */
class SingleChooseDialog constructor(
    private val context: Context
) : BaseDialog<SingleChooseDialog>(context) {

    init {
        // 改變內容排列方向
        binding.contentLayout.orientation = LinearLayout.HORIZONTAL
        // 改變固定高度
        val params = binding.contentLayout.layoutParams
        params.height = 600
        binding.contentLayout.layoutParams = params
    }

    /** 選擇項列表 */
    private val recyclerMap: MutableMap<String, DialogRecycleViewBinding> = mutableMapOf()

    /**
     * 添加選擇列表
     * @param tag 標籤
     * @param selectItem 選擇項
     * @param list 全部選項
     * @param start 開始文本
     * @param end 結束文本
     */
    fun addSelectionList(tag: String, selectItem: String, list: List<String>, start: String, end: String) : SingleChooseDialog {
        val viewBinding: DialogRecycleViewBinding = DataBindingUtil.inflate(
            LayoutInflater.from(context),
            R.layout.dialog_recycle_view, null, false
        )
        viewBinding.startText = start
        viewBinding.endText = end
        viewBinding.rcv.layoutManager = LinearLayoutManager(context)
        val adapter = NormalAdapter(selectItem, list)
        viewBinding.rcv.adapter = adapter
        viewBinding.rcv.post {
            viewBinding.rcv.smoothScrollToPosition(list.indexOf(selectItem) + 3)
        }
        binding.contentLayout.addView(viewBinding.root)
        recyclerMap[tag] = viewBinding
        return this
    }

    /**
     * 獲取選中項文本內容
     */
    fun getSelectOnTag(tag: String) : String {
        val viewBinding = recyclerMap[tag]?: return ""
        val adapter = viewBinding.rcv.adapter as NormalAdapter
        return adapter.selectStr
    }
}

使用方法

顯示普通按鍵彈窗
在這裏插入圖片描述

    fun test(v: View) {
        val buttonDialog = ButtonDialog(this)
            .setTittle("標題")
            .setMsg("文本,段落,清晰,強大覺得還u會丟啊就是不對勁啊混合雙打u看見我還大手大腳卡號!「大三大四的」")
            .setIcon(R.mipmap.icon_test)
            .addAction("確定", ButtonStyle.THEME, null)
            .addAction("取消", ButtonStyle.NORMAL, null)
            .addAction("亂來", ButtonStyle.ERROR,
                object : OnBtnClick{
                    override fun click(d0: BaseDialog<*>, text: String) {
                        // 進度條
                        d0.startLoad(true, 50, 100)
                    }
                })
            .create()
        buttonDialog.show()
        
        // 取消進度條
        buttonDialog.stopLoad()
    }

顯示輸入框彈窗
在這裏插入圖片描述

    fun test(v: View) {
        val editDialog = EditDialog(this)
            .setTittle("標題")
            .setMsg("設置文本")
            .addEdit("tag1", "12345678901234567890", "請輸入密碼")
            .addAction("確定", ButtonStyle.THEME,
                object : OnBtnClick{
                    override fun click(d0: BaseDialog<*>, text: String) {
                        val dialog = d0 as EditDialog
                        if (dialog.getInputText("tag1") != "666666") {
                            dialog.setError("tag1", "密碼錯誤")
                        } else {
                            dialog.dismiss()
                        }
                    }
                })
            .create()
        editDialog.show()
    }

顯示單選彈窗
在這裏插入圖片描述

                val list = mutableListOf<String>()
                for (i in 0 .. 100) {
                    list.add("$i")
                }
                SingleChooseDialog(this)
                    .setTittle("提示")
                    .setMsg("設定溫度")
                    .addSelectionList("temp", "50", list,
                        "", "℃")
                    .addAction(resources.getString(R.string.sure), ButtonStyle.THEME, object : OnBtnClick{
                        override fun click(d0: BaseDialog<*>, text: String) {
                            val d = d0 as SingleChooseDialog
                            // 拿到溫度
                            val temp = d.getSelectOnTag("temp").toInt()
                            viewModel.update(temp)
                            d.dismiss()
                        }
                    })
                    .addAction(resources.getString(R.string.cancel), ButtonStyle.NORMAL, null)
                    .create()
                    .show()

完事

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章