扩展EditText
自带清空按钮 显示/隐藏密码按钮(右图标)
支持IconFont图标
/**
* 创建者: TomCat0916
* 创建时间: 2020/10/25
* 功能描述: 可配置显示/隐藏按钮(设置密码的显示隐藏) 或 配置清除按钮(清空输入框) 三者同时配置优先清空模式
* 支持IconFont
*
* 示例一:图片模式
* ```
*
* ```
*示例二:IconFont模式
* ```
*
* ```
* 属性设置参考[package.R.styleable.ClearEditTextView2]
*/
class ClearEditTextView2 @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = android.R.attr.editTextStyle
) : androidx.appcompat.widget.AppCompatEditText(context, attrs, defStyleAttr),
OnFocusChangeListener, TextWatcher, LifecycleObserver {
private var mClearDrawable: Drawable? = null
private var visibleDrawable: Drawable? = null
private var invisibleDrawable: Drawable? = null
private var onTouchListener: OnTouchListener? = null
private var translateAnimation: Animation? = null
private var invisibleText: CharSequence? = null
private var clearText: CharSequence? = null
private var leftIconText: CharSequence? = null
private var visibleText: CharSequence? = null
private var iconFontTextSize = 0
private var iconFontTextColor = Color.BLACK
private var iconFontBackgroundColor = Color.TRANSPARENT
private var iconFontPath: String? = null
private var leftIconFontTextLayout: Layout? = null
private var rightIconFontTextLayout: Layout? = null
private var hasData = false
private var oldPaddingLeft = 0
private var oldPaddingTop = 0
private var oldPaddingRight = 0
private var oldPaddingBottom = 0
private var isLeftIconDraw = false
private var isRightIconDraw = false
private var iconLeftPadding = 0f
private var iconRightPadding = 0f
private var leftIconW = 0
private var rightIconW = 0
private var leftRecFBg: RectF? = null
private var rightRecFBg: RectF? = null
private var iconFontH = 0f
private var iconFontDrawPadding = 0f
private var lastVisibleState = true
private var lastClearShowState = false
private var maxRightTextW = 0
private val textMap by lazy {
HashMap()
}
private val iconFontPaint: TextPaint by lazy {
TextPaint().apply {
isAntiAlias = false
color = iconFontTextColor
textSize = iconFontTextSize.toFloat()
style = Paint.Style.FILL
}
}
private val bgPaint: Paint by lazy {
Paint().apply {
isAntiAlias = false
color = iconFontBackgroundColor
style = Paint.Style.FILL
}
}
private companion object {
private const val CLEAR_HIDE_MODEL = 0
private const val CLEAR_SHOW_MODEL = 1
private const val INVISIBLE_MODEL = 2
private const val VISIBLE_MODEL = 3
}
private var rightIconModel = CLEAR_HIDE_MODEL//0清理按钮隐藏 1清理按钮显示 2 明文显示按钮 3明文关闭按钮
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
onFocusChangeListener = null
removeTextChangedListener(this)
mClearDrawable = null
visibleDrawable = null
invisibleDrawable = null
onTouchListener = null
if (translateAnimation != null) {
translateAnimation!!.cancel()
translateAnimation = null
}
if (context is LifecycleOwner) {
(context as LifecycleOwner).lifecycle.removeObserver(this)
}
leftIconFontTextLayout = null
rightIconFontTextLayout = null
leftRecFBg = null
rightRecFBg = null
textMap.clear()
}
init {
if (context is LifecycleOwner) {
(context as LifecycleOwner).lifecycle.addObserver(this)
}
val array =
context.obtainStyledAttributes(attrs, R.styleable.ClearEditTextView2)
if (array != null) {
mClearDrawable = array.getDrawable(R.styleable.ClearEditTextView2_DrawableClear)
visibleDrawable = array.getDrawable(R.styleable.ClearEditTextView2_DrawableVisible)
invisibleDrawable = array.getDrawable(R.styleable.ClearEditTextView2_DrawableInvisible)
leftIconText = array.getText(R.styleable.ClearEditTextView2_IconFontLeft)
clearText = array.getText(R.styleable.ClearEditTextView2_IconFontClear)
visibleText = array.getText(R.styleable.ClearEditTextView2_IconFontVisible)
invisibleText = array.getText(R.styleable.ClearEditTextView2_IconFontInvisible)
iconFontPath = array.getString(R.styleable.ClearEditTextView2_IconFontPath)
iconFontTextColor =
array.getColor(R.styleable.ClearEditTextView2_IconFontTextColor, paint.color)
iconFontBackgroundColor =
array.getColor(
R.styleable.ClearEditTextView2_IconFontBackground,
iconFontBackgroundColor
)
iconFontTextSize = array.getDimensionPixelSize(
R.styleable.ClearEditTextView2_IconFontTextSize, paint.textSize.toInt()
)
array.recycle()
}
setSingleLine()
init()
}
private fun init() {
if (isIconFontModel()) {//优先IconFont模式
//设置IconFont文本字体样式
setIconFontPath(iconFontPath ?: BaseApplication.getInstance().globalIconFontPath)
if (clearText?.isNotEmpty() == true) {
lastClearShowState = false
//默认隐藏清除按钮状态 (优先)
rightIconModel = CLEAR_HIDE_MODEL
//初始化清除按钮IconFont
addIconFontText(clearText?.toString(), true)
} else if (visibleText?.isNotEmpty() == true
&& invisibleText?.isNotEmpty() == true
) {
//初始化显示按钮IconFont
addIconFontText(visibleText?.toString(), true)
//初始化隐藏按钮IconFont
addIconFontText(invisibleText?.toString(), true)
lastVisibleState = true
//默认设置隐藏状态
inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD
rightIconModel = INVISIBLE_MODEL
setVisibleIcon()
}
//记录原本padding距离
oldPaddingLeft = paddingLeft
oldPaddingTop = paddingTop
oldPaddingRight = paddingRight
oldPaddingBottom = paddingBottom
if (leftIconText?.isNotEmpty() == true) {
//初始化左边IconFont
addIconFontText(leftIconText?.toString(), false)
}
} else if (mClearDrawable != null) {
//设置删除按钮的边界
mClearDrawable!!.setBounds(
0,
0,
mClearDrawable!!.intrinsicWidth,
mClearDrawable!!.intrinsicHeight
)
rightIconModel = CLEAR_HIDE_MODEL
//默认隐藏删除按钮
setClearIcon(false)
//监听EditText焦点变化,以根据text长度控制删除按钮的显示、隐藏
onFocusChangeListener = this
//监听文本内容变化
addTextChangedListener(this)
} else if (visibleDrawable != null && invisibleDrawable != null) {
inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD
//设置显示按钮的边界
visibleDrawable!!.setBounds(
0,
0,
visibleDrawable!!.intrinsicWidth,
visibleDrawable!!.intrinsicHeight
)
//设置隐藏按钮的边界
invisibleDrawable!!.setBounds(
0,
0,
invisibleDrawable!!.intrinsicWidth,
invisibleDrawable!!.intrinsicHeight
)
rightIconModel = INVISIBLE_MODEL
lastVisibleState = true
setVisibleIcon()
}
}
/**
* 根据状态获取右边IconFont内容文字
*/
private fun getRightIcon(): CharSequence? {
return when (rightIconModel) {
VISIBLE_MODEL -> visibleText
INVISIBLE_MODEL -> invisibleText
CLEAR_SHOW_MODEL -> clearText
else -> ""
}
}
private fun isIconFontModel(): Boolean {
return (leftIconText?.isNotEmpty() == true
|| clearText?.isNotEmpty() == true
|| visibleText?.isNotEmpty() == true
|| invisibleText?.isNotEmpty() == true
|| iconFontPath?.isNotEmpty() == true)
}
/**
* 设置IconFont字体样式文件(优先assets下字体文件)
*
* @param path 文件路径
*/
@Suppress("UNCHECKED")
fun setIconFontPath(path: String?) {
Intrinsics.checkNotNull(path)
if (!TextUtils.isEmpty(path)) {
val assets = context.assets
if (assets != null) {
try {
assets.open(path!!).use {
iconFontPaint.typeface = Typeface.createFromAsset(assets, path)
}
} catch (e: IOException) {
setIconFontPathFromFile(path)
}
} else {
setIconFontPathFromFile(path)
}
}
}
/**
* 设置IconFont字体样式文件
*
* @param path 文件路径
*/
@Suppress("UNCHECKED")
fun setIconFontPathFromFile(path: String?) {
Intrinsics.checkNotNull(path)
if (!TextUtils.isEmpty(path)) {
setIconFontPathFromFile(File(path))
}
}
/**
* 设置IconFont字体样式文件
*
* @param fontFile 字体样式文件
*/
@Suppress("UNCHECKED")
fun setIconFontPathFromFile(fontFile: File) {
Intrinsics.checkNotNull(fontFile)
if (fontFile.exists()) {
iconFontPaint.typeface = Typeface.createFromFile(fontFile)
}
}
/**
* 控制EditText右边制眼睛按钮的显示、隐藏
*/
private fun setVisibleIcon() {
val isVisible = rightIconModel == VISIBLE_MODEL
if (lastVisibleState != isVisible) {
lastVisibleState = isVisible
if (isIconFontModel()) {
this.transformationMethod =
if (isVisible) HideReturnsTransformationMethod.getInstance() else PasswordTransformationMethod.getInstance()
setRightText()
invalidate()
requestLayout()
} else {
val visibleIcon = if (isVisible) visibleDrawable else invisibleDrawable
setCompoundDrawables(
compoundDrawables[0], compoundDrawables[1],
visibleIcon, compoundDrawables[3]
)
setCompoundDrawablesWithIntrinsicBounds(
compoundDrawables[0], compoundDrawables[1],
visibleIcon, compoundDrawables[3]
)
this.transformationMethod =
if (isVisible) HideReturnsTransformationMethod.getInstance() else PasswordTransformationMethod.getInstance()
setSelection(length())
}
setSelection(text?.length ?: 0)
}
}
/**
* 控制EditText右边制删除按钮的显示、隐藏
*/
private fun setClearIcon(isShow: Boolean) {
if (isShow != lastClearShowState) {
lastClearShowState = isShow
rightIconModel = if (isShow) CLEAR_SHOW_MODEL else CLEAR_HIDE_MODEL
if (isIconFontModel()) {
setRightText()
invalidate()
requestLayout()
} else if (mClearDrawable != null) {
val rightDrawable = if (isShow) mClearDrawable else null
setCompoundDrawables(
compoundDrawables[0], compoundDrawables[1],
rightDrawable, compoundDrawables[3]
)
}
}
}
private fun isClearModel() =
rightIconModel == CLEAR_SHOW_MODEL || rightIconModel == CLEAR_HIDE_MODEL
/**
* 有焦点,并文本长度大于0则显示删除按钮
*/
override fun onFocusChange(v: View, hasFocus: Boolean) {
if (isClearModel()) {
if (hasFocus) {
setClearIcon(!text.isNullOrEmpty())
} else {
setClearIcon(false)
}
}
}
/**
* 文本内容变化时回调
* 当文本长度大于0时显示删除按钮, 否则隐藏
*/
override fun onTextChanged(
text: CharSequence,
start: Int,
lengthBefore: Int,
lengthAfter: Int
) {
super.onTextChanged(text, start, lengthBefore, lengthAfter)
if (hasData && text.isEmpty() && lengthAfter == 0) {
startShake(3)
}
hasData = text.isNotEmpty()
if (isClearModel()) {
setClearIcon(hasData)
}
}
override fun beforeTextChanged(
s: CharSequence,
start: Int,
count: Int,
after: Int
) {
}
override fun afterTextChanged(s: Editable) {}
/**
* 通过手指的触摸位置模式删除按钮的点击事件
*/
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (compoundDrawables[2] != null || isRightIconDraw) {
if (event.action == MotionEvent.ACTION_UP) {
if (isClearModel()) {
if ((isClickDrawable(event, mClearDrawable)
|| isClickRecF(event, rightRecFBg))
) {
setText("")
return true
}
} else if (isClickDrawable(event, visibleDrawable)
|| isClickDrawable(event, invisibleDrawable)
|| isClickRecF(event, rightRecFBg)
) {
rightIconModel =
if (rightIconModel == VISIBLE_MODEL) INVISIBLE_MODEL else VISIBLE_MODEL
setVisibleIcon()
return true
}
}
}
return if (onTouchListener != null && onTouchListener!!.onTouch(this, event)) {
true
} else super.onTouchEvent(event)
}
override fun setOnTouchListener(l: OnTouchListener) {
onTouchListener = l
}
/**
* 是否在右边图标范围内点击
*/
private fun isClickDrawable(event: MotionEvent, drawable: Drawable?): Boolean {
if (drawable == null) return false
val xTouchable =
(event.x > width - paddingRight - drawable.intrinsicWidth
&& event.x < width - paddingRight)
val yTouchable =
(event.y > (height - drawable.intrinsicHeight) shr 1
&& event.y < (height + drawable.intrinsicHeight) shr 1)
return xTouchable && yTouchable
}
/**
* 是否在右边iconFont范围内点击
*/
private fun isClickRecF(event: MotionEvent, rectF: RectF?): Boolean {
if (rectF == null) return false
val xTouchable =
(event.x > right - paddingRight
&& event.x < right)
val yTouchable =
(event.y > rectF.top
&& event.y < rectF.bottom)
return xTouchable && yTouchable
}
/**
* EditText抖动
*/
@Suppress("UNCHECKED")
fun startShake(counts: Int) {
if (translateAnimation == null) {
synchronized(this) {
if (translateAnimation == null) {
translateAnimation = TranslateAnimation(0f, 10f, 0f, 0f)
translateAnimation?.interpolator = CycleInterpolator(counts.toFloat())
translateAnimation?.duration = 500
}
}
}
startAnimation(translateAnimation)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (isIconFontModel()) {
iconFontDrawPadding = compoundDrawablePadding.toFloat()
val fontMetrics = iconFontPaint.fontMetrics
leftRecFBg = RectF()
rightRecFBg = RectF()
iconFontH = fontMetrics.top.absoluteValue + fontMetrics.bottom.absoluteValue//字体高度
val textBean = textMap[leftIconText]
leftIconFontTextLayout = textBean?.layout
leftIconW = textBean?.with ?: 0
isLeftIconDraw = leftIconW > 0
iconLeftPadding = 2 * iconFontDrawPadding + leftIconW//内容文本左偏移距离
setRightText()
iconRightPadding = 2 * iconFontDrawPadding + maxRightTextW//内容文本右偏移距离
setPadding(oldPaddingLeft, oldPaddingTop, oldPaddingRight, oldPaddingBottom)
}
}
private fun setRightText() {
val textBean = textMap[getRightIcon()?.toString()]
rightIconFontTextLayout = textBean?.layout
rightIconW = textBean?.with ?: 0
isRightIconDraw = rightIconW > 0
}
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
oldPaddingLeft = left
oldPaddingTop = top
oldPaddingRight = right
oldPaddingBottom = bottom
super.setPadding(
left + (iconLeftPadding).toInt(),
top,
right + (iconRightPadding).toInt(),
bottom
)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (isLeftIconDraw || isRightIconDraw) {
val compoundPaddingTop = compoundPaddingTop
val compoundPaddingBottom = compoundPaddingBottom
val mScrollX: Int = scrollX//横向滚动距离 防止IconFont跟随内容滚动
val mScrollY: Int = scrollY//纵向滚动距离 防止IconFont跟随内容滚动
//iconFont文字顶部位置
val mTop =
mScrollY + compoundPaddingTop + ((bottom - top - compoundPaddingBottom - compoundPaddingTop - iconFontH) / 2f)
//iconFont文字底部位置
val mBottom = mTop + iconFontH
if (isLeftIconDraw) {
setLeftRecF(mScrollX, mTop, mBottom)
//绘制左边IconFont背景
drawIconFontBg(canvas, leftRecFBg)
//绘制左边IconFont
drawIconFont(canvas, leftRecFBg, leftIconFontTextLayout)
}
if (isRightIconDraw) {
setRightRecF(mScrollX, mTop, mBottom)
//绘制右边IconFont背景
drawIconFontBg(canvas, rightRecFBg)
//绘制右边IconFont
drawIconFont(canvas, rightRecFBg, rightIconFontTextLayout)
}
}
}
private fun setLeftRecF(mScrollX: Int, mTop: Float, mBottom: Float) {
val mLeft = (mScrollX + iconFontDrawPadding)
leftRecFBg?.left = mLeft
leftRecFBg?.right = mLeft + leftIconW
leftRecFBg?.top = mTop
leftRecFBg?.bottom = mBottom
}
private fun setRightRecF(mScrollX: Int, mTop: Float, mBottom: Float) {
val left = mScrollX + right - left - rightIconW - iconFontDrawPadding
rightRecFBg?.left = left
rightRecFBg?.right = left + rightIconW
rightRecFBg?.top = mTop
rightRecFBg?.bottom = mBottom
}
private fun drawIconFont(canvas: Canvas?, rectF: RectF?, layout: Layout?) {
canvas?.save()
canvas?.translate(rectF?.left ?: 0f, rectF?.top ?: 0f)
layout?.draw(canvas)
canvas?.restore()
}
@Suppress("DEPRECATION")
private fun addIconFontText(source: String?, isRight: Boolean) {
if (!source.isNullOrEmpty()) {
val rect = Rect()
iconFontPaint.getTextBounds(source, 0, source.length, rect)
val width = (rect.width() + iconFontPaint.textSize / 2).toInt()
// 1.需要分行的字符串
// 4.画笔对象
// 5.layout的宽度,字符串超出宽度时自动换行。
// 6.layout的对其方式,有ALIGN_CENTER, ALIGN_NORMAL, ALIGN_OPPOSITE 三种。
// 7.相对行间距,相对字体大小,1.5f表示行间距为1.5倍的字体高度。
// 8.在基础行距上添加多少 实际行间距等于这两者的和。
// 9.设置是否在字体的上升和下降之外包括额外的空间(避免在某些语言(例如阿拉伯语和卡纳达语)中进行剪切是必需的*)。 *默认值为{@code true}
val textLayout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
StaticLayout.Builder
.obtain(source, 0, source.length, iconFontPaint, width)
.setAlignment(Layout.Alignment.ALIGN_CENTER)
.setLineSpacing(0f, 1f)
.build()
} else {
StaticLayout(
source,
iconFontPaint,
width,
Layout.Alignment.ALIGN_CENTER,
1f,
0f,
true
)
}
if (textLayout != null) {
textMap[source] = TextBean(textLayout, width)
}
if (isRight) {
maxRightTextW = max(maxRightTextW, width)
}
}
}
private fun drawIconFontBg(canvas: Canvas?, bgRectF: RectF?) {
if (bgRectF != null) {
canvas?.save()
canvas?.drawRoundRect(
bgRectF,
0f,
0f,
bgPaint
)
canvas?.restore()
}
}
private data class TextBean(
val layout: Layout? = null,
val with: Int? = null
)
}