package global.xfinite.coinbox
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import androidx.core.content.ContextCompat
import global.xfinite.library.R
import kotlin.math.max
import kotlin.math.min
class SwipeToSendView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// 画笔
private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#F5F6F8")
style = Paint.Style.FILL
}
private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#44D564")
style = Paint.Style.FILL
}
private val thumbPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#44D564")
style = Paint.Style.FILL
}
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#222222")
textSize = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
16f,
resources.displayMetrics
)
textAlign = Paint.Align.CENTER
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) // 使用系统默认粗体
}
private lateinit var arrowBitmap: Bitmap
// 尺寸
private var cornerRadius = 0f
private var thumbRadius = 0f
private var thumbPosition = 0f
private var verticalPadding = 0f // 2dp 的垂直间距
// 状态
private var isDragging = false
private var isVerified = false
private var lastX = 0f
// 文字
private val text = "Swipe to send"
private val textBounds = Rect().also {
textPaint.getTextBounds(text, 0, text.length, it)
}
// 轨迹区域
private val trackRect = RectF()
private val clipPath = Path()
init {
verticalPadding = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
2f,
resources.displayMetrics
)
setLayerType(LAYER_TYPE_HARDWARE, null) // 启用硬件加速
arrowBitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_arrow_cps_go_night)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
cornerRadius = (height - verticalPadding * 2) / 2f
thumbRadius = (height - verticalPadding * 2) / 2f
thumbPosition = thumbRadius
// 初始化裁剪路径
clipPath.reset()
clipPath.addRoundRect(
0f, verticalPadding,
width.toFloat(), height - verticalPadding,
cornerRadius, cornerRadius,
Path.Direction.CW
)
// 缩放 arrowBitmap(如果单独使用)
val arrowSize = (thumbRadius * 0.8f).toInt()
arrowBitmap = Bitmap.createScaledBitmap(
arrowBitmap,
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
9f,
resources.displayMetrics
).toInt(),
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
18f,
resources.displayMetrics
).toInt(),
true
)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 1. 绘制背景(带垂直间距)
canvas.drawRoundRect(
0f, verticalPadding,
width.toFloat(), height - verticalPadding,
cornerRadius, cornerRadius, bgPaint
)
// 4. 绘制文字(居中)
val textY = height / 2f - (textBounds.top + textBounds.bottom) / 2f
canvas.drawText(text, width / 2f, textY, textPaint)
// 2. 绘制轨迹(使用矩形而不是路径提高性能)
if (thumbPosition > thumbRadius) {
trackRect.set(
0f, verticalPadding,
thumbPosition + thumbRadius, height - verticalPadding
)
canvas.drawRoundRect(
trackRect,
cornerRadius, cornerRadius,
trackPaint
)
}
// 3. 绘制滑块
canvas.drawCircle(
thumbPosition,
height / 2f,
thumbRadius,
thumbPaint
)
// 4. 绘制箭头(如果单独使用)
canvas.drawBitmap(
arrowBitmap,
thumbPosition - arrowBitmap.width / 2f, // x 居中
height / 2f - arrowBitmap.height / 2f, // y 居中
null
)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 检查是否点击了滑块
val touchX = event.x
val touchY = event.y
val distance = Math.sqrt(
Math.pow((touchX - thumbPosition).toDouble(), 2.0) +
Math.pow((touchY - height / 2f).toDouble(), 2.0)
)
if (distance <= thumbRadius) {
isDragging = true
lastX = event.x
return true
}
}
MotionEvent.ACTION_MOVE -> {
if (isDragging && !isVerified) {
val deltaX = event.x - lastX
thumbPosition += deltaX
// 限制滑块位置
thumbPosition = max(thumbRadius, min(thumbPosition, width - thumbRadius))
lastX = event.x
// 只调用invalidate()而不是postInvalidate()以提高性能
invalidate()
return true
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
if (isDragging) {
isDragging = false
// 检查是否滑动到最右边
if (thumbPosition >= width - thumbRadius - 10) {
isVerified = true
listener?.onVerified()
} else {
// 未到最右边,回弹动画
startReboundAnimation()
}
return true
}
}
}
return super.onTouchEvent(event)
}
private fun startReboundAnimation() {
val startTime = System.currentTimeMillis()
val startPosition = thumbPosition
val duration = 300L // 动画时长300ms
post(object : Runnable {
override fun run() {
val elapsed = System.currentTimeMillis() - startTime
if (elapsed < duration) {
// 使用插值器实现平滑回弹
val interpolation = Math.sin((elapsed / duration.toFloat()) * Math.PI / 2).toFloat()
thumbPosition = thumbRadius + (startPosition - thumbRadius) * (1 - interpolation)
invalidate()
postDelayed(this, 16)
} else {
// 确保最终位置正确
thumbPosition = thumbRadius
invalidate()
}
}
})
}
// 验证回调接口
interface OnVerifiedListener {
fun onVerified()
}
private var listener: OnVerifiedListener? = null
fun setOnVerifiedListener(listener: OnVerifiedListener) {
this.listener = listener
}
// 重置验证状态
fun reset() {
isVerified = false
thumbPosition = thumbRadius
invalidate()
}
}
优化版本可以设置颜色
package global.xfinite.conso.view
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import androidx.core.content.ContextCompat
import global.xfinite.conso.library.R
import kotlin.math.max
import kotlin.math.min
class SwipeToSendView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// 画笔
private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
}
private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
}
private val thumbPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
}
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#222222")
textSize = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
16f,
resources.displayMetrics
)
textAlign = Paint.Align.CENTER
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) // 使用系统默认粗体
}
private var arrowBitmap: Bitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_arrow_cps_go_night)
// 尺寸
private var cornerRadius = 0f
private var thumbRadius = 0f
private var thumbPosition = 0f
private var verticalPadding = 0f // 2dp 的垂直间距
// 状态
private var isDragging = false
private var isVerified = false
private var lastX = 0f
// 文字
private val text = "Swipe to send"
private val textBounds = Rect().also {
textPaint.getTextBounds(text, 0, text.length, it)
}
// 轨迹区域
private val trackRect = RectF()
private val clipPath = Path()
// 可配置的颜色
var backColor: Int = Color.parseColor("#F5F6F8")
set(value) {
field = value
bgPaint.color = value
invalidate() // 触发重绘
}
var trackColor: Int = Color.parseColor("#44D564")
set(value) {
field = value
trackPaint.color = value
invalidate() // 触发重绘
}
var thumbColor: Int = Color.parseColor("#44D564")
set(value) {
field = value
thumbPaint.color = value
invalidate() // 触发重绘
}
var textColor: Int = Color.parseColor("#222222")
set(value) {
field = value
textPaint.color = value
invalidate() // 触发重绘
}
init {
verticalPadding = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
2f,
resources.displayMetrics
)
setLayerType(LAYER_TYPE_HARDWARE, null) // 启用硬件加速
// 初始化画笔颜色
bgPaint.color = backColor
trackPaint.color = trackColor
thumbPaint.color = thumbColor
textPaint.color = textColor
arrowBitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_arrow_cps_go_night)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
cornerRadius = (height - verticalPadding * 2) / 2f
thumbRadius = (height - verticalPadding * 2) / 2f
thumbPosition = thumbRadius
// 初始化裁剪路径
clipPath.reset()
clipPath.addRoundRect(
0f, verticalPadding,
width.toFloat(), height - verticalPadding,
cornerRadius, cornerRadius,
Path.Direction.CW
)
// 缩放 arrowBitmap(如果单独使用)
val arrowSize = (thumbRadius * 0.8f).toInt()
arrowBitmap = Bitmap.createScaledBitmap(
arrowBitmap,
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
9f,
resources.displayMetrics
).toInt(),
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
18f,
resources.displayMetrics
).toInt(),
true
)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 1. 绘制背景(带垂直间距)
canvas.drawRoundRect(
0f, verticalPadding,
width.toFloat(), height - verticalPadding,
cornerRadius, cornerRadius, bgPaint
)
// 4. 绘制文字(居中)
val textY = height / 2f - (textBounds.top + textBounds.bottom) / 2f
canvas.drawText(text, width / 2f, textY, textPaint)
// 2. 绘制轨迹(使用矩形而不是路径提高性能)
if (thumbPosition > thumbRadius) {
trackRect.set(
0f, verticalPadding,
thumbPosition + thumbRadius, height - verticalPadding
)
canvas.drawRoundRect(
trackRect,
cornerRadius, cornerRadius,
trackPaint
)
}
// 3. 绘制滑块
canvas.drawCircle(
thumbPosition,
height / 2f,
thumbRadius,
thumbPaint
)
// 4. 绘制箭头(如果单独使用)
canvas.drawBitmap(
arrowBitmap,
thumbPosition - arrowBitmap.width / 2f, // x 居中
height / 2f - arrowBitmap.height / 2f, // y 居中
null
)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 检查是否点击了滑块
val touchX = event.x
val touchY = event.y
val distance = Math.sqrt(
Math.pow((touchX - thumbPosition).toDouble(), 2.0) +
Math.pow((touchY - height / 2f).toDouble(), 2.0)
)
if (distance <= thumbRadius) {
isDragging = true
lastX = event.x
return true
}
}
MotionEvent.ACTION_MOVE -> {
if (isDragging && !isVerified) {
val deltaX = event.x - lastX
thumbPosition += deltaX
// 限制滑块位置
thumbPosition = max(thumbRadius, min(thumbPosition, width - thumbRadius))
lastX = event.x
// 只调用invalidate()而不是postInvalidate()以提高性能
invalidate()
return true
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
if (isDragging) {
isDragging = false
// 检查是否滑动到最右边
if (thumbPosition >= width - thumbRadius - 10) {
isVerified = true
listener?.onVerified()
} else {
// 未到最右边,回弹动画
startReboundAnimation()
}
return true
}
}
}
return super.onTouchEvent(event)
}
private fun startReboundAnimation() {
val startTime = System.currentTimeMillis()
val startPosition = thumbPosition
val duration = 300L // 动画时长300ms
post(object : Runnable {
override fun run() {
val elapsed = System.currentTimeMillis() - startTime
if (elapsed < duration) {
// 使用插值器实现平滑回弹
val interpolation = Math.sin((elapsed / duration.toFloat()) * Math.PI / 2).toFloat()
thumbPosition = thumbRadius + (startPosition - thumbRadius) * (1 - interpolation)
invalidate()
postDelayed(this, 16)
} else {
// 确保最终位置正确
thumbPosition = thumbRadius
invalidate()
}
}
})
}
// 验证回调接口
interface OnVerifiedListener {
fun onVerified()
}
private var listener: OnVerifiedListener? = null
fun setOnVerifiedListener(listener: OnVerifiedListener) {
this.listener = listener
}
// 重置验证状态
fun reset() {
isVerified = false
thumbPosition = thumbRadius
invalidate()
}
// // 设置背景颜色
// fun setBackgroundColor1(color: Int) {
// backgroundColor = color
// }
//
// // 设置轨迹颜色
// fun setTrackColor(color: Int) {
// trackColor = color
// }
//
// // 设置滑块颜色
// fun setThumbColor(color: Int) {
// thumbColor = color
// }
//
// // 设置文字颜色
// fun setTextColor(color: Int) {
// textColor = color
// }
// 设置箭头位图
fun setArrowBitmap(bitmap: Bitmap) {
arrowBitmap = bitmap
invalidate() // 触发重绘
}
// 设置箭头资源ID
fun setArrowResource(resId: Int) {
arrowBitmap = BitmapFactory.decodeResource(resources, resId)
invalidate() // 触发重绘
}
}