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()