Android实战场景 - 用Kotlin写个能让我进步的Dialog

去年接到过一个小需求,一个简简单单的弹框,不论是用dialog、popupWindow都能很轻松的实现;之所以记录是因为每一次都感觉好像进步了一点点…

弹框相关Blog

一个小小需求,我使用了俩种实现方式,其一为我的惯性常规实现,其二为项目中所学的进阶实现,用到的新技术也多一点

      • 效果分析
      • 常规实现
        • 弹框样式
        • 按钮样式
        • 调用方式
      • 进阶实现
        • 弹框样式
        • 扩展函数
          • View、TextView (基本UI)
          • SpannableStringBuilder 自身(富文本)
          • SpannableStringBuilder 引用
        • 调用方式

效果分析

效果图

在这里插入图片描述

记录原因

  • kotlin 写弹框
  • 命令式UI、声明式UI 自定义弹框视图
  • 富文本样式
  • SpannableStringBuilder 扩展函数
  • 典型的弹框场景

功能分析

常规实现

按照我的开发习惯,我一般是先画 dialog 的 UI...

弹框样式

提示TypefaceTextViewsplitties 库提供的控件,其继承自 AppCompatTextView 的视图类,如不使用可自行用TextView,加粗用自带的即可

dialog_layout_discount


<splitties.widgets.TypefaceTextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/mp_20"
android:layout_marginRight="@dimen/mp_20"
android:layout_marginBottom="@dimen/mp_15"
android:ellipsize="end"
android:gravity="center"
android:text="温馨提示"
android:textColor="@color/font_33"
android:textSize="@dimen/textSize_17" />





该处重写主要在其内部添加了设置字体粗度的功能

package xxx

import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView

open class TypefaceTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
AppCompatTextView(context, attrs, defStyleAttr) {

private var mTypefaceScale: Float = 0.0f

enum class TypefaceScale {
MEDIUM, MEDIUM_SMALL, DEFAULT,
}

override fun onDraw(canvas: Canvas?) {
if (mTypefaceScale == 0f) {
return super.onDraw(canvas)
}
val strokeWidth = paint.strokeWidth
val style = paint.style
paint.strokeWidth = mTypefaceScale
paint.style = Paint.Style.FILL_AND_STROKE
super.onDraw(canvas)
paint.strokeWidth = strokeWidth
paint.style = style
}

internal fun setTypefaceScale(scale: TypefaceScale = TypefaceScale.DEFAULT) {
mTypefaceScale = when (scale) {
TypefaceScale.DEFAULT -> 0.0f
TypefaceScale.MEDIUM_SMALL -> 0.6f
TypefaceScale.MEDIUM -> 1.1f
}
invalidate()
}

}

按钮样式

我直接copy项目已有的shape,原始是支持按压和平时的俩种显示状态,因为我没有这项功能,所以我注释掉了…

shape_dialog_discount_confirm_bg















shape_dialog_discount_cancel_bg

















调用方式

var view = LayoutInflater.from(activity).inflate(R.layout.dialog_layout_discount, null)
val alertDialog = AlertDialog.Builder(activity).setCancelable(false).setView(view).create()
alertDialog.window?.setBackgroundDrawable(ColorDrawable(0))
var title = view.findViewById(R.id.title)
title.typefaceScale = TypefaceScale.MEDIUM_SMALL
//富文本
var content = view.findViewById(R.id.content)
val contentText = SpannableStringBuilder("您还未领取0折购基打折卡,无法享受\n0折购基的优惠,是否现在去领取?")
contentText.setSpan(AbsoluteSizeSpan(13, true), 0, 4, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
contentText.setSpan(AbsoluteSizeSpan(17, true), 5, 9, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
contentText.setSpan(AbsoluteSizeSpan(13, true), 9, contentText.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
content.text = contentText

view.findViewById(R.id.cancel).onClick { alertDialog.dismiss() }
view.findViewById(R.id.confirm).onClick { alertDialog.dismiss() }
alertDialog.show()


进阶实现

项目中用到的新技术稍微多一些,这里稍微做下说明

  • 命令式UI用到了 splitties 视图框架
  • 部分控件用到了 扩展函数

弹框样式

package xx

import android.content.Context
import android.graphics.Color
import android.text.SpannedString
import android.text.method.LinkMovementMethod
import android.widget.FrameLayout
import androidx.core.graphics.toColorInt
import splitties.dimensions.dp
import splitties.views.dsl.core.*
import splitties.views.*

internal class DiscountAlertDialogUi(context: Context) : FrameLayout(context) {

private lateinit var messageView: TypefaceTextView
private lateinit var negativeView: TypefaceTextView
private lateinit var positiveView: TypefaceTextView

init {
horizontalPadding = 25.dp
bottomPadding = 25.dp
createView()
}

private fun createView() {
add(lParams(width = matchParent) { topMargin = 27.dp }, verticalLayout {
backgroundResource = R.drawable.icon_dialog_discount_bg
horizontalPadding = 20.dp
add(lParams(width = matchParent) { topMargin = 35.dp }, typefaceTextView {
typefaceScale = TypefaceScale.MEDIUM_SMALL
gravity = gravityCenterHorizontal
textColor = "#363A37".toColorInt()
textSize = 16f
text = "温馨提示"
})
messageView = add(lParams(width = matchParent) { verticalMargin = 16.dp }, typefaceTextView {
gravity = gravityCenterHorizontal
textColor = "#97755F".toColorInt()
textSize = 14f
setLineSpacing(dp(4f), 1f)
})
add(lParams(width = matchParent, height = 44.dp) { topMargin = 20.dp }, horizontalLayout {
negativeView = add(lParams(width = 0, weight = 1f, height = matchParent) { endMargin = 7.dp }, typefaceTextView {
typefaceScale = TypefaceScale.MEDIUM_SMALL
background = rectangleShape(radius = dp(4f), color = Color.WHITE) {
setStroke(1.dp, "#B4B4B4".toColorInt())
}
gravity = gravityCenter
setTextColor(enabledColorDrawable("#CC363A37".toColorInt(), "#363A37".toColorInt()))
textSize = 14f
text = "暂不领取"
})
positiveView = add(lParams(width = 0, weight = 1f, height = matchParent) { startMargin = 7.dp }, typefaceTextView {
background = enabledDrawable(radius = 4f.dp, disabledColor = "#CC3C3733", defaultColor = "#3C3733")
typefaceScale = TypefaceScale.MEDIUM_SMALL
gravity = gravityCenter
textColor = "#F9E6D4".toColorInt()
textSize = 14f
text = "免费领取"
})
})
})
add(lParams(width = wrapContent, gravity = gravityCenterHorizontal), imageView {
imageResource = R.drawable.icon_discount_alert
})
}

/**
* 内容文字
*/
fun setMessage(message: CharSequence) {
messageView.text = message
if (message is SpannedString) {
messageView.movementMethod = LinkMovementMethod.getInstance()
}
}

/**
* 消极按钮
*/
fun setNegativeButton(negativeText: CharSequence, listener: OnClickListener?) {
negativeView.text = negativeText
negativeView.setOnClick(listener)
}

/**
* 积极按钮
*/
fun setPositiveButton(positiveText: CharSequence, listener: OnClickListener?) {
positiveView.text = positiveText
positiveView.setOnClick(listener)
}

}

扩展函数

View、TextView (基本UI)

@file:Suppress("NOTHING_TO_INLINE", "unused")

package xx.views

import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.annotation.Px

val matchParent: Int = ViewGroup.LayoutParams.MATCH_PARENT

val wrapContent: Int = ViewGroup.LayoutParams.WRAP_CONTENT

val View.inflater: LayoutInflater
get() = LayoutInflater.from(context)

var View.backgroundResource: Int
@Deprecated(NO_GETTER, level = DeprecationLevel.HIDDEN) get() = noGetter
set(value) {
setBackgroundResource(value)
}

inline var View.backgroundColor: Int
@Deprecated(NO_GETTER, level = DeprecationLevel.HIDDEN) get() = noGetter
set(@ColorInt colorInt) = setBackgroundColor(colorInt)

inline fun View.onLongClick(consume: Boolean = true, crossinline block: () -> Unit) = setOnLongClickListener { block(); consume }

inline var ImageView.imageResource: Int
@Deprecated(NO_GETTER, level = DeprecationLevel.HIDDEN) get() = noGetter
set(@DrawableRes value) = setImageResource(value)

inline var ImageView.imageDrawable: Drawable?
get() = drawable
set(value) = setImageDrawable(value)

inline var ImageView.imageBitmap: Bitmap
@Deprecated(NO_GETTER, level = DeprecationLevel.HIDDEN) get() = noGetter
set(value) = setImageBitmap(value)

// TextView
inline var TextView.lines: Int
@Deprecated(NO_GETTER, level = DeprecationLevel.HIDDEN) get() = noGetter
set(value) = setLines(value)

fun TextView.centerText() {
textAlignment = View.TEXT_ALIGNMENT_CENTER
gravity = Gravity.CENTER
}

fun TextView.alignTextToStart() {
textAlignment = View.TEXT_ALIGNMENT_VIEW_START
gravity = Gravity.START
}

fun TextView.alignTextToEnd() {
textAlignment = View.TEXT_ALIGNMENT_VIEW_END
gravity = Gravity.END
}

fun TextView.setCompoundDrawables(start: Drawable? = null, top: Drawable? = null, end: Drawable? = null, bottom: Drawable? = null, intrinsicBounds: Boolean = false) {
if (intrinsicBounds) {
setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom)
} else {
setCompoundDrawables(start, top, end, bottom)
}
}

inline fun TextView.setCompoundDrawables(@DrawableRes start: Int = 0, @DrawableRes top: Int = 0, @DrawableRes end: Int = 0, @DrawableRes bottom: Int = 0) {
setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom)
}

inline fun TextView.clearCompoundDrawables() = setCompoundDrawables(null, null, null, null)

SpannableStringBuilder 自身(富文本)

根据效果,我仅加入了字体大小的扩展函数

fun SpannableStringBuilder.appendRelativeSize(
proportion: Float,
text: CharSequence,
): SpannableStringBuilder = inSpans(RelativeSizeSpan(proportion)) { append(text) }

fun SpannableStringBuilder.appendAbsoluteSize(
proportion: Int,
text: CharSequence,
): SpannableStringBuilder = inSpans(AbsoluteSizeSpan(proportion,true)) { append(text) }

项目内较全的 SpannableStringBuilder 扩展函数

package xx
import android.text.SpannableStringBuilder
import android.text.style.AbsoluteSizeSpan
import android.text.style.ForegroundColorSpan
import android.text.style.RelativeSizeSpan
import android.text.style.StrikethroughSpan
import android.view.View
import androidx.annotation.ColorInt
import androidx.core.text.inSpans

inline fun SpannableStringBuilder.mediumSmall(
builderAction: SpannableStringBuilder.() -> Unit
) = inSpans(MediumSmallSpan(), builderAction = builderAction)

inline fun SpannableStringBuilder.linearGradient(
colors: IntArray,
orientation: Int = LinearGradientFontSpan.HORIZONTAL,
builderAction: SpannableStringBuilder.() -> Unit
): SpannableStringBuilder {
return inSpans(LinearGradientFontSpan(colors, orientation), builderAction = builderAction)
}

inline fun SpannableStringBuilder.clickable(
listener: View.OnClickListener,
builderAction: SpannableStringBuilder.() -> Unit
): SpannableStringBuilder {
return inSpans(ClickableSpan(listener), builderAction = builderAction)
}

//
fun SpannableStringBuilder.appendClickable(
text: CharSequence,
listener: View.OnClickListener
): SpannableStringBuilder = inSpans(ClickableSpan(listener)) { append(text) }

fun SpannableStringBuilder.appendColor(
@ColorInt color: Int,
text: CharSequence,
): SpannableStringBuilder = inSpans(ForegroundColorSpan(color)) { append(text) }

fun SpannableStringBuilder.appendStrikeThrough(
text: CharSequence,
): SpannableStringBuilder = inSpans(StrikethroughSpan()) { append(text) }

fun SpannableStringBuilder.appendRelativeSize(
proportion: Float,
text: CharSequence,
): SpannableStringBuilder = inSpans(RelativeSizeSpan(proportion)) { append(text) }

fun SpannableStringBuilder.appendAbsoluteSize(
proportion: Int,
text: CharSequence,
): SpannableStringBuilder = inSpans(AbsoluteSizeSpan(proportion,true)) { append(text) }
//
fun SpannableStringBuilder.appendMore(
@ColorInt color: Int? = null,
text: CharSequence,
listener: View.OnClickListener? = null,
): SpannableStringBuilder {
val spans = mutableListOf()
color?.also { spans.add(ForegroundColorSpan(color)) }
listener?.also { spans.add(ClickableSpan(listener)) }
inSpans(*spans.toTypedArray()) { append(text) }
return this
}

fun SpannableStringBuilder.appendLabel(
@ColorInt color: Int? = null,
text: String,
listener: View.OnClickListener? = null,
): SpannableStringBuilder {
val splits = text.replace("", "").split("")
splits.forEachIndexed { index, s ->
if (index % 2 == 1) {
appendMore(color = color, s, listener)
} else {
append(s)
}
}
return this
}

SpannableStringBuilder 引用

MediumSmallSpan

package xx.spannable

import android.graphics.Paint
import android.text.TextPaint
import android.text.style.MetricAffectingSpan

class MediumSmallSpan : MetricAffectingSpan() {

override fun updateDrawState(paint: TextPaint?) = apply(paint)

override fun updateMeasureState(paint: TextPaint) = apply(paint)

private fun apply(paint: Paint?) {
if (paint == null) return
paint.strokeWidth = 0.6f
paint.style = Paint.Style.FILL_AND_STROKE
}

}

LinearGradientFontSpan

package xx.spannable

import android.graphics.Canvas
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Shader
import android.text.style.ReplacementSpan
import android.widget.LinearLayout

class LinearGradientFontSpan(val colors: IntArray, val orientation: Int = HORIZONTAL) : ReplacementSpan() {
companion object {
const val HORIZONTAL = LinearLayout.HORIZONTAL // 水平渐变方向
const val VERTICAL = LinearLayout.VERTICAL // 垂直渐变方向
}

private var mMeasureTextWidth = 0 // 测量的文本宽度

override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fontMetricsInt: Paint.FontMetricsInt?): Int {
mMeasureTextWidth = paint.measureText(text ?: "", start, end).toInt()
// 这段不可以去掉,字体高度没设置,会出现 draw 方法没有被调用的问题
// 详情请见:https://stackoverflow.com/questions/20069537/replacementspans-draw-method-isnt-called
val metrics = paint.fontMetrics
fontMetricsInt?.top = metrics.top.toInt()
fontMetricsInt?.ascent = metrics.ascent.toInt()
fontMetricsInt?.descent = metrics.descent.toInt()
fontMetricsInt?.bottom = metrics.bottom.toInt()
return mMeasureTextWidth
}

override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
if (text.isNullOrEmpty()) return
val linearGradient = if (orientation == VERTICAL) {
LinearGradient(0f, 0f, 0f, paint.descent() - paint.ascent(), colors, null, Shader.TileMode.REPEAT)
} else {
LinearGradient(x, 0f, x + mMeasureTextWidth, 0f, colors, null, Shader.TileMode.REPEAT)
}
val shader = paint.shader
val alpha = paint.alpha
//
paint.shader = linearGradient
paint.alpha = 255 // 如果是则设置不透明
//
canvas.drawText(text, start, end, x, y.toFloat(), paint)
//绘制完成之后将画笔的透明度还原回去
paint.shader = shader
paint.alpha = alpha
}

}

ClickableSpan

package xx.spannable

import android.text.TextPaint
import android.view.View
import xx.OnClickWrapListener

class ClickableSpan(val listener: View.OnClickListener) : android.text.style.ClickableSpan() {

override fun onClick(widget: View) {
OnClickWrapListener(listener).onClick(widget)
}

override fun updateDrawState(ds: TextPaint) {
ds.isUnderlineText = false
}
}

OnClickWrapListener

//private var clickTime = 0L

class OnClickWrapListener(val listener: View.OnClickListener) : View.OnClickListener {
companion object {
private val clickTime = AtomicLong(0)
}

override fun onClick(v: View?) {
val currentTime = System.currentTimeMillis()
if (currentTime - clickTime.get() > 500) {
clickTime.set(currentTime)
listener.onClick(v)
}
}
}

调用方式

纯净版

override suspend fun checkDiscountIntercept(activity: BaseActivity): Boolean = suspendCancellableCoroutine { coroutine ->
val discountAlertDialogUi = DiscountAlertDialogUi(activity)
val alertDialog = AlertDialog.Builder(activity).setView(discountAlertDialogUi).create()
alertDialog.window?.setBackgroundDrawableResource(R.color.transparent)
alertDialog.setCanceledOnTouchOutside(false)
discountAlertDialogUi.setMessage(
buildSpannedString {
append("您还未领取")
appendAbsoluteSize(17, "0折购基")
append("打折卡,无法享受0折购基的优惠,是否现在去领取?")
}
}
discountAlertDialogUi.setNegativeButton("暂不领取") {
alertDialog.dismiss()
coroutine.resume(false)
}
discountAlertDialogUi.setPositiveButton("免费领取") {
alertDialog.dismiss()
toDiscount(activity)
coroutine.resume(true)
}
}

项目版(因项目版有业务场景,仅做记录,方便日后回顾

override suspend fun checkDiscountIntercept(activity: BaseActivity, state: Int): Boolean = suspendCancellableCoroutine { coroutine ->
if (state == 0 || state == 3) {
val discountAlertDialogUi = DiscountAlertDialogUi(activity)
val alertDialog = AlertDialog.Builder(activity).setView(discountAlertDialogUi).create()
alertDialog.window?.setBackgroundDrawableResource(R.color.transparent)
alertDialog.setCanceledOnTouchOutside(false)
discountAlertDialogUi.setMessage(if (state == 0) {
buildSpannedString {
append("您还未领取")
appendAbsoluteSize(17, "0折购基")
append("打折卡,无法享受0折购基的优惠,是否现在去领取?")
}
} else {
"您的0折购基打折卡已过期,无法享受0折购基的优惠,是否现在去续期?"
})
discountAlertDialogUi.setNegativeButton(if (state == 0) "暂不领取" else "暂不续期") {
alertDialog.dismiss()
coroutine.resume(false)
}
discountAlertDialogUi.setPositiveButton(if (state == 0) "免费领取" else "免费续期") {
alertDialog.dismiss()
toDiscount(activity)
coroutine.resume(true)
}
alertDialog.show()
} else {
coroutine.resume(false)
}
}

posted @ 2023-04-26 16:14  cps666  阅读(535)  评论(0)    收藏  举报