概述
前言
在电商项目中,通常会遇到商品价格多样化的需求;如:显示±符号,货币符号,金额数字要使用千分位,小数位要四舍五入,货币符号与金额数字要大小不一样等需求,使用 TextView + Spanned 也可以实现预期效果,但是相对来说处理起来比较麻烦,要灵活多变,个人觉得还是自定义一个View比较方便。
效果图
自定义AmountTextView
- AmountTextView 组合:±符号+货币符号+整数位+小数位
- 使用DecimalFormat进行格式化及四舍五入计算
- 提供 ***fun setCurrency(locale: Locale)***函数,可根据国家自动获取对象货币符号
class AmountTextView : View {
companion object {
// 使用正则去验证传入的金额是否有效,防止格式化非数字意外的内容
private const val NUMBER_CONSTRAINTS = "^[-+]?(([0-9]+)([.]([0-9]+))?|([.]([0-9]+))?)$"
private const val MIN_PADDING = 2f
}
private var totalWidth = 0
private var totalHeight = 0
private var textPaintRoomSize = 0f
private val symbolSection by lazy { Section() }
private val integerSection by lazy { Section() }
private val decimalSection by lazy { Section() }
private val positiveNegativeSection by lazy { Section() }
private val linePaint by lazy { Paint() }
private val textPaint by lazy { TextPaint(Paint.ANTI_ALIAS_FLAG) }
/**
* 设置金额
*/
var amount = 0.0
set(value) {
field = if (checkAmountValid("$value")) value else 0.0
requestLayout()
}
@ColorInt
var textColor = Color.parseColor("#8A000000")
set(value) {
field = value
integerSection.color = value
postInvalidate()
}
var textSize = dpToPx(14f)
set(value) {
field = value
integerSection.textSize = value
requestLayout()
}
/**
* 是否使用 ± 符号,默认不使用
*/
var positiveNegativeEnable = false
set(value) {
field = value
requestLayout()
}
var positiveNegativePadding = 4f
set(value) {
field = value
requestLayout()
}
var positiveNegativeTextSize = dpToPx(14f)
set(value) {
field = value
positiveNegativeSection.textSize = value
requestLayout()
}
var positiveNegativeTextColor = textColor
set(value) {
field = value
positiveNegativeSection.color = value
postInvalidate()
}
/**
* 是否使用千分位标记
*/
var groupingUsed = false
set(value) {
field = value
requestLayout()
}
/**
* 四舍五入计算,默认只舍不入
*/
var roundingMode = RoundingMode.DOWN
set(value) {
field = value
requestLayout()
}
/**
* 货币符号, 默认使用 ¥ 符号
* 这里做个适配,直接写 ¥ 符号,在国产部分手机上 ¥ 符号中间的横杠会显示一条
*/
var symbol = "${fromHtml("¥")}"
set(value) {
field = value
symbolSection.text = value
requestLayout()
}
/**
* 符号位置
*/
var symbolGravity = Gravity.START
set(value) {
field = value
postInvalidate()
}
/**
* 货币符号颜色
*/
@ColorInt
var symbolTextColor = textColor
set(value) {
field = value
symbolSection.color = value
postInvalidate()
}
/**
* 货币符号字体尺寸
*/
var symbolTextSize = textSize
set(value) {
field = value
symbolSection.textSize = value
requestLayout()
}
var symbolPadding = 4f
set(value) {
field = value
requestLayout()
}
/**
* 设置保留的小数位数
*/
var decimalDigits = 0
set(value) {
field = value
requestLayout()
}
/**
* 小数位位置,支持 top 与 bottom
*/
var decimalGravity = Gravity.BOTTOM
set(value) {
field = value
postInvalidate()
}
/**
* 小数位与整数部分边距
*/
var decimalPadding = 4f
set(value) {
field = value
requestLayout()
}
/**
* 小数位文本大小
*/
var decimalTextSize = textSize
set(value) {
field = value
decimalSection.textSize = value
requestLayout()
}
/**
* 小数位文本颜色
*/
var decimalTextColor = textColor
set(value) {
field = value
decimalSection.color = value
postInvalidate()
}
/**
* 删除线
*/
var strikeThroughLineEnable = false
set(value) {
field = value
postInvalidate()
}
var strikeThroughLineColor = Color.BLACK
set(value) {
field = value
postInvalidate()
}
var strikeThroughLineSize = 1f
set(value) {
field = value
requestLayout()
}
/**
* 下划线
*/
var underlineEnable = false
set(value) {
field = value
postInvalidate()
}
var underlineColor = Color.BLACK
set(value) {
field = value
postInvalidate()
}
var underlineSize = 1f
set(value) {
field = value
requestLayout()
}
constructor(@NonNull context: Context) : this(context, null)
constructor(@NonNull context: Context, @Nullable attrs: AttributeSet?) : this(context, attrs, 0)
constructor(
@NonNull context: Context,
@Nullable attrs: AttributeSet?,
defStyleAttr: Int
) : this(context, attrs, defStyleAttr, 0)
constructor(
@NonNull context: Context,
@Nullable attrs: AttributeSet?,
defStyleAttr: Int,
defStyleRes: Int
) : super(
context, attrs, defStyleAttr, defStyleRes
) {
initAttribute(context, attrs, defStyleAttr, defStyleRes)
}
private fun initAttribute(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int,
defStyleRes: Int
) {
textPaintRoomSize = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
textPaint.density,
resources.displayMetrics
)
val typedArray = context.theme.obtainStyledAttributes(
attrs,
R.styleable.AmountTextView,
defStyleAttr,
defStyleRes
)
try {
// positive negative
positiveNegativeEnable = typedArray.getBoolean(R.styleable.AmountTextView_positiveNegative, positiveNegativeEnable)
positiveNegativePadding = typedArray.getDimensionPixelSize(R.styleable.AmountTextView_positiveNegativePadding, positiveNegativePadding.toInt()).toFloat()
positiveNegativeSection.textSize = typedArray.getDimension(R.styleable.AmountTextView_positiveNegativeTextSize, positiveNegativeTextSize)
positiveNegativeSection.color = typedArray.getColor(R.styleable.AmountTextView_positiveNegativeTextColor, positiveNegativeTextColor)
// symbol
symbol = typedArray.getString(R.styleable.AmountTextView_symbol) ?: symbol
symbolGravity = typedArray.getInt(R.styleable.AmountTextView_symbolGravity, symbolGravity)
symbolPadding = typedArray.getDimensionPixelSize(R.styleable.AmountTextView_symbolPadding, symbolPadding.toInt()).toFloat()
symbolSection.textSize = typedArray.getDimension(R.styleable.AmountTextView_symbolTextSize, symbolTextSize)
symbolSection.color = typedArray.getInt(R.styleable.AmountTextView_symbolTextColor, symbolTextColor)
// amount
if (typedArray.hasValue(R.styleable.AmountTextView_android_text)) {
val text = typedArray.getString(R.styleable.AmountTextView_android_text) ?: "0"
amount = if (checkAmountValid(text)) text.toDouble() else 0.0
}
integerSection.textSize = typedArray.getDimension(R.styleable.AmountTextView_android_textSize, textSize)
integerSection.color = typedArray.getInt(R.styleable.AmountTextView_android_textColor, textColor)
groupingUsed = typedArray.getBoolean(R.styleable.AmountTextView_groupingUsed, groupingUsed)
roundingMode = getRoundingMode(typedArray.getInt(R.styleable.AmountTextView_roundingMode, 0))
// decimal
decimalDigits = typedArray.getInteger(R.styleable.AmountTextView_decimalDigits, decimalDigits)
decimalGravity = typedArray.getInt(R.styleable.AmountTextView_decimalGravity, decimalGravity)
decimalPadding = typedArray.getDimensionPixelSize(R.styleable.AmountTextView_decimalPadding, decimalPadding.toInt()).toFloat()
decimalSection.textSize = typedArray.getDimension(R.styleable.AmountTextView_decimalTextSize, decimalTextSize)
decimalSection.color = typedArray.getInt(R.styleable.AmountTextView_decimalTextColor, decimalTextColor)
// style
val textStyle = typedArray.getInt(R.styleable.AmountTextView_android_textStyle, Typeface.DEFAULT.style)
textPaint.typeface = Typeface.defaultFromStyle(textStyle)
// line
strikeThroughLineEnable = typedArray.getBoolean(R.styleable.AmountTextView_strikeThroughLine, strikeThroughLineEnable)
strikeThroughLineColor = typedArray.getColor(R.styleable.AmountTextView_strikeThroughLineColor, strikeThroughLineColor)
strikeThroughLineSize = typedArray.getDimension(R.styleable.AmountTextView_strikeThroughLineSize, strikeThroughLineSize)
underlineEnable = typedArray.getBoolean(R.styleable.AmountTextView_underline, underlineEnable)
underlineColor = typedArray.getColor(R.styleable.AmountTextView_underlineColor, underlineColor)
underlineSize = typedArray.getDimension(R.styleable.AmountTextView_underlineSize, underlineSize)
} finally {
typedArray.recycle()
}
if (strikeThroughLineEnable || underlineEnable) {
linePaint.isDither = true
linePaint.isAntiAlias = true
linePaint.style = Paint.Style.FILL_AND_STROKE
}
}
/**
* 检测数字是否有效
*/
fun checkAmountValid(text: String): Boolean {
return Pattern.compile(NUMBER_CONSTRAINTS).matcher(text).matches()
}
/**
* @return 返回四舍五入计算模式
*/
private fun getRoundingMode(mode: Int): RoundingMode {
return when(mode) {
1 -> RoundingMode.UP
2 -> RoundingMode.DOWN
3 -> RoundingMode.CEILING
4 -> RoundingMode.FLOOR
5 -> RoundingMode.HALF_UP
6 -> RoundingMode.HALF_DOWN
7 -> RoundingMode.HALF_EVEN
8 -> RoundingMode.UNNECESSARY
else -> RoundingMode.DOWN
}
}
/**
* 设置 Currency 后 symbol 会跟随设置的国家变化。
*/
fun setCurrency(locale: Locale) {
val currency = Currency.getInstance(locale)
this.symbol = currency.symbol
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
setPadding(
getMinPadding(paddingStart), getMinVerticalPadding(paddingTop),
getMinPadding(paddingEnd), getMinVerticalPadding(paddingBottom)
)
createTextFromAmount()
calculateBounds(widthMeasureSpec, heightMeasureSpec)
calculatePositions()
setMeasuredDimension(totalWidth, totalHeight)
}
private fun getMinPadding(padding: Int): Int {
val density = resources.displayMetrics.density
return if (padding == 0) (MIN_PADDING * density).toInt() else padding
}
private fun getMinVerticalPadding(padding: Int): Int {
val maxTextSize = max(positiveNegativeSection.textSize, max(symbolSection.textSize, max(integerSection.textSize, decimalSection.textSize)))
textPaint.textSize = maxTextSize
val maximumDistanceLowestGlyph = textPaint.fontMetrics.bottom
return if (padding < maximumDistanceLowestGlyph) maximumDistanceLowestGlyph.toInt() else padding
}
private fun createTextFromAmount() {
val positiveNegative = if (positiveNegativeEnable) if (amount > -1) "+" else "-" else ""
positiveNegativeSection.text = positiveNegative
symbolSection.text = symbol
// 奖金额格式化,添加千分符,进行四舍五入计算。
val amountFormat = formatAmount(abs(amount), decimalDigits, groupingUsed, roundingMode)
integerSection.text = amountFormat.split(".").firstOrNull() ?: "0"
val decimalValue = (amountFormat.split(".").lastOrNull() ?: "0").toLong()
val decimalFormat = if (decimalDigits > 0) ".${decimalValue}" else ""
decimalSection.text = decimalFormat
decimalPadding = if (decimalValue > 0) decimalPadding else 0f
}
/**
* @return 返回格式化后的金额文本
*/
fun getAmountText(): String {
val positiveNegative = if (positiveNegativeEnable) if (amount > -1) "+" else "-" else ""
val amountFormat = formatAmount(abs(amount), decimalDigits, groupingUsed, roundingMode)
val amountValue = amountFormat.split(".").firstOrNull() ?: "0"
val decimalValue = (amountFormat.split(".").lastOrNull() ?: "0").toLong()
val decimalFormat = if (decimalDigits > 0) ".${decimalValue}" else ""
return positiveNegative + symbol + amountValue + decimalFormat
}
private fun calculateBounds(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
symbolSection.calculateBounds(textPaint)
integerSection.calculateBounds(textPaint)
decimalSection.calculateBounds(textPaint)
positiveNegativeSection.calculateBounds(textPaint)
decimalSection.calculateNumbersHeight(textPaint)
integerSection.calculateNumbersHeight(textPaint)
positiveNegativeSection.calculateNumbersHeight(textPaint)
when (widthMode) {
MeasureSpec.EXACTLY -> totalWidth = widthSize
MeasureSpec.AT_MOST, MeasureSpec.UNSPECIFIED -> {
val positiveNegativeWidth = if (positiveNegativeEnable) positiveNegativeSection.width + positiveNegativePadding.toInt() else 0
val symbolWidth = symbolSection.width + symbolPadding
val amountWidth = integerSection.width
val decimalWidth = decimalSection.width + decimalPadding
totalWidth = (paddingStart + positiveNegativeWidth + symbolWidth + amountWidth + decimalWidth + paddingEnd).toInt()
}
}
when (heightMode) {
MeasureSpec.EXACTLY -> totalHeight = heightSize
MeasureSpec.AT_MOST, MeasureSpec.UNSPECIFIED -> {
totalHeight = paddingTop + paddingBottom + max(integerSection.height, max(decimalSection.height, symbolSection.height))
}
}
}
/**
* 内容排版
*/
private fun calculatePositions() {
val positiveNegativeWidth = if (positiveNegativeEnable) positiveNegativeSection.width + positiveNegativePadding.toInt() else 0
// ±
if (positiveNegativeEnable) {
positiveNegativeSection.x = paddingStart
positiveNegativeSection.y = calculatePositiveNegativeX()
} else {
positiveNegativeSection.x = 0
positiveNegativeSection.y = 0
}
// symbol
symbolSection.x = calculateSymbolX(positiveNegativeWidth)
symbolSection.y = calculateSymbolY()
// amount
integerSection.x = calculateAmountX(positiveNegativeWidth)
integerSection.y = totalHeight - paddingBottom
// decimal
decimalSection.x = calculateDecimalX(positiveNegativeWidth)
decimalSection.y = calculateDecimalY()
}
private fun calculatePositiveNegativeX(): Int {
val textPaintHeight = textPaint.fontMetrics.bottom - textPaint.fontMetrics.top
return (totalHeight.div(2f) + textPaintHeight.div(2f) - textPaint.fontMetrics.bottom - textPaintRoomSize.div(2f)).toInt()
}
private fun calculateSymbolX(positiveNegativeWidth: Int): Int {
return if (symbolGravity == Gravity.END
|| symbolGravity == Gravity.END or Gravity.TOP
|| symbolGravity == Gravity.END or Gravity.BOTTOM
) {
(paddingStart + positiveNegativeWidth + integerSection.width + decimalPadding + decimalSection.width + symbolPadding).toInt()
} else paddingStart + positiveNegativeWidth
}
private fun calculateSymbolY(): Int {
return when(symbolGravity) {
Gravity.START or Gravity.TOP,
Gravity.END or Gravity.TOP -> paddingTop + symbolSection.height
Gravity.START or Gravity.BOTTOM, Gravity.END or Gravity.BOTTOM -> totalHeight - paddingBottom
else -> {
val maxHeight = max(positiveNegativeSection.height, max(symbolSection.height, max(integerSection.height, decimalSection.height)))
totalHeight.div(2) + maxHeight.div(2) - textPaintRoomSize.div(2f).toInt()
}
}
}
private fun calculateAmountX(positiveNegativeWidth: Int): Int {
return if (symbolGravity == Gravity.END
|| symbolGravity == Gravity.END or Gravity.TOP
|| symbolGravity == Gravity.END or Gravity.BOTTOM
) {
paddingStart + positiveNegativeWidth
} else paddingStart + positiveNegativeWidth + symbolSection.width + symbolPadding.toInt()
}
private fun calculateDecimalX(positiveNegativeWidth: Int): Int {
return if (symbolGravity == Gravity.START
|| symbolGravity == Gravity.START or Gravity.TOP
|| symbolGravity == Gravity.START or Gravity.BOTTOM
) {
paddingStart + positiveNegativeWidth + symbolSection.width + symbolPadding.toInt() + integerSection.width + decimalPadding.toInt()
} else if (symbolGravity == Gravity.END
|| symbolGravity == Gravity.END or Gravity.TOP
|| symbolGravity == Gravity.END or Gravity.BOTTOM
) {
paddingStart + positiveNegativeWidth + integerSection.width + decimalPadding.toInt()
} else {
paddingStart + positiveNegativeWidth + symbolSection.width + symbolPadding.toInt() + integerSection.width + decimalPadding.toInt()
}
}
private fun calculateDecimalY(): Int {
val baseline = if (groupingUsed && abs(amount) > 1000) textPaint.fontMetrics.descent.toInt() else 0
return if(decimalGravity == Gravity.TOP) paddingTop + decimalSection.height + baseline else totalHeight - paddingBottom
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
drawSection(canvas, positiveNegativeSection)
drawSection(canvas, symbolSection)
drawSection(canvas, integerSection)
drawSection(canvas, decimalSection)
drawDecorationLine(canvas)
}
private fun drawSection(canvas: Canvas, section: Section) {
textPaint.textSize = section.textSize
textPaint.color = section.color
canvas.drawText(section.text, section.x - textPaintRoomSize.times(2f), section.y - textPaintRoomSize.div(2f), textPaint)
}
/**
* 绘制装饰线条
*/
private fun drawDecorationLine(canvas: Canvas) {
val lineMaxWidth = positiveNegativeSection.width + positiveNegativePadding + symbolSection.width + symbolPadding + integerSection.width + decimalPadding + decimalSection.width
// 删除线
if (strikeThroughLineEnable) {
linePaint.color = strikeThroughLineColor
linePaint.strokeWidth = strikeThroughLineSize
val strikeThroughLineY = totalHeight.div(2f)
canvas.drawLine(paddingStart.toFloat(), strikeThroughLineY, lineMaxWidth, strikeThroughLineY, linePaint)
}
// 下划线
if (underlineEnable) {
linePaint.color = underlineColor
linePaint.strokeWidth = underlineSize
val underlineY = (totalHeight - paddingBottom).toFloat()
canvas.drawLine(paddingStart.toFloat(), underlineY, lineMaxWidth, underlineY, linePaint)
}
}
private fun fromHtml(content: String): Spanned {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Html.fromHtml(content, Html.FROM_HTML_MODE_LEGACY)
} else {
Html.fromHtml(content)
}
}
private fun formatAmount(
number: Number,
decimalDigits: Int = 0,
groupingUsed: Boolean = true,
rounding: RoundingMode = RoundingMode.DOWN
): String {
return DecimalFormat.getNumberInstance().apply {
maximumFractionDigits = decimalDigits
isGroupingUsed = groupingUsed
roundingMode = rounding
}.format(number)
}
private inner class Section {
var x = 0
var y = 0
var bounds: Rect = Rect()
var text = ""
var textSize = 0f
var color = Color.BLACK
var width = 0
var height = 0
fun calculateBounds(paint: TextPaint) {
paint.textSize = textSize
paint.getTextBounds(text, 0, text.length, bounds)
width = bounds.width()
height = bounds.height()
}
fun calculateNumbersHeight(paint: TextPaint) {
val numbers = text.replace("[^0-9]", "")
paint.textSize = textSize
paint.getTextBounds(numbers, 0, numbers.length, bounds)
height = bounds.height()
}
}
}
AmountTextView属性
<declare-styleable name="AmountTextView">
<!--金额-->
<attr name="android:text" />
<attr name="android:textColor" />
<attr name="android:textSize" />
<attr name="android:textStyle" />
<!--是否启用千分符-->
<attr name="groupingUsed" format="boolean" />
<!--四舍五入计算模式-->
<attr name="roundingMode" format="enum">
<enum name="up" value="1" />
<enum name="down" value="2" />
<enum name="ceiling" value="3" />
<enum name="floor" value="4" />
<enum name="half_up" value="5" />
<enum name="half_down" value="6" />
<enum name="half_even" value="7" />
<enum name="unnecessary" value="8" />
</attr>
<!--金额 ± 符号,默认不使用-->
<attr name="positiveNegative" format="boolean" />
<attr name="positiveNegativePadding" format="dimension" />
<attr name="positiveNegativeTextSize" format="dimension" />
<attr name="positiveNegativeTextColor" format="color" />
<!--货币符号-->
<attr name="symbol" format="string" />
<!--货币符号与金额间距-->
<attr name="symbolPadding" format="dimension" />
<!--货币符号位置-->
<attr name="symbolGravity">
<flag name="start" value="0x00800003" />
<flag name="top" value="0x30" />
<flag name="end" value="0x00800005" />
<flag name="bottom" value="0x50" />
</attr>
<!--货币符号字体大小-->
<attr name="symbolTextSize" format="dimension" />
<!--货币符号字体颜色-->
<attr name="symbolTextColor" format="color" />
<!--保留的小数位数-->
<attr name="decimalDigits" format="integer" />
<!--小数位与整数位间距-->
<attr name="decimalPadding" format="dimension" />
<!--小数位字体大小-->
<attr name="decimalTextSize" format="dimension" />
<!--小数位字体颜色-->
<attr name="decimalTextColor" format="color" />
<!--小数位位置-->
<attr name="decimalGravity" format="enum">
<enum name="top" value="0x30" />
<enum name="bottom" value="0x50" />
</attr>
<!--删除线-->
<attr name="strikeThroughLine" format="boolean" />
<attr name="strikeThroughLineColor" format="color" />
<attr name="strikeThroughLineSize" format="dimension" />
<!--下划线-->
<attr name="underline" format="boolean" />
<attr name="underlineColor" format="color" />
<attr name="underlineSize" format="dimension" />
</declare-styleable>
针对商品价格存在多种实现方式,也许存在更优雅的处理方式,这里仅分享个人开发中的一些思考,有兴趣的同学可以联系我,一起交流交流。
最后
以上就是忧心母鸡为你收集整理的自定义商品价格AmountTextView的全部内容,希望文章能够帮你解决自定义商品价格AmountTextView所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复